Scrapy框架(二) 抽取更多的URL

一、爬取更多的URL

到目前为止,我们使用的只是设置在爬虫的start_urls属性中的单一URL。而该属性实际为一个列表,我们可以硬编码写入更多的URL,如下所示:

start_urls = [
   ‘https://jcoffeezph.top/‘,
   ‘http://qzmvc1.top/‘,
   ···
]

这种写法可能不会让你太激动。不过我们还可以使用文件作为URL的源,写法如下所示:

start_urls = [ i.strip() for i in open(‘xxx.txt’,’r’).readlines()]

这种写法其实也不那么令人激动,但它确实管用。更经常发生的情况是感兴趣的网站中包含一些索引页以及内容页。


一个典型的索引页会包含许多到博客的链接,以及一个能够让你从一个索引页前往另一个索引页的分页系统。

因此,一个典型的爬虫会向两个方向移动:

  • 横向——从一个索引页到另一个索引页;
  • 纵向——从一个索引页到内容页并抽取Item。

我们将前者称为水平爬取,因为这种情况下是在同一层级下爬取页面;而将后者成为垂直爬取,因为该方式是从一个更高的层级到一个更低的层级。

实际上,它比听起来更加容易。我们所有需要做的事情就是再增加两个Xpath表达式。对于第一个表达式,右键单击 Next 按钮打开审查元素,可以注意到URL包含在一个链接中,如图所示:

因此我们只需要使用一个使用的Xpath表达式 //*[@class="extend next"]//@href就可以运行了。

对于第二个表达式,右键单击页面中的列表标题打开审查元素,如图所示:

Xpath表达式: //*[@class="article-title"]/@href
可以运行Scrapy Shell观察上述表达式是否有效。


二、使用爬虫实现双向爬取

该做的分析、工作差不多都做完了,下面我们上代码然后慢慢解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding: utf-8 -*-
import scrapy
from Demo.items import DemoItem
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose,Join
from urllib.parse import urljoin
from scrapy.http import Request
class DemoSpider(scrapy.Spider):
name = 'demo'
allowed_domains = ['web']
start_urls = ['https://jcoffeezph.top/']

def parse(self, response):
URLS = response.xpath('//*[@class="extend next"]//@href').extract()
for url in URLS:
#Request()函数没有给callback赋值,所以默认回调函数就是parse函数
yield Request(urljoin(response.url,url),dont_filter=True)

content_urls = response.xpath('//*[@class="article-title"]/@href').extract()
for url in content_urls:
yield Request(urljoin(response.url,url),callback=self.parse_item,dont_filter=True)

def parse_item(self,response):
l = ItemLoader(item=DemoItem(),response=response)
l.add_xpath('title', '//*[@class="article-title"]/text()',MapCompose(str.strip))
l.add_xpath('author', '//*[@id="header"]/hgroup/h1/a/text()',MapCompose(str.strip))
l.add_xpath('time', '//*[@itemprop="datePublished"]/text()',MapCompose(str.strip))
l.add_xpath('link', '//*[@itemprop="name"]/a/@href',MapCompose(lambda i:urljoin(response.url,i)))
return l.load_item()
代码解释:
程序从start_urls处开始运行,然后用创建好项目就有的parse()默认解析函数对start_urls进行解析,我们在parse()中抓取了横向和纵向的所有URL。对于横向的URL,我们只需要用scrapy.http中的Request对象不断请求就行,而对于内容页,我们需要请求并进行解析。我们在parse()下方定义了一个新的解析函数,并把它命名为parse_item(),然后在Request中用callback属性进行调用,别忘了将dont_filter设置为true,具体原因可以参考我的博客:Scrapy框架之细数到目前为止我遇到的坑)。

什么是回调函数?

我们绕点远路来回答这个问题。

编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。
     
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
   
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。
      
可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。                 

关于yield:
yield与return在某种意义上来说有些相似,都是将返回值提供给调用者。不过,和return不同的是,yield不会退出函数,而是继续执行for循环。

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
2018-11-19 11:02:57 [scrapy.core.engine] INFO: Spider opened
2018-11-19 11:02:57 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2018-11-19 11:02:57 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2018-11-19 11:02:58 [scrapy.core.engine] DEBUG: Crawled (404) <GET https://jcoffeezph.top/robots.txt> (referer: None)
2018-11-19 11:02:59 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/> (referer: None)
2018-11-19 11:02:59 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/page/2/> (referer: https://jcoffeezph.top/)
2018-11-19 11:02:59 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0Set%E6%8E%A5%E5%8F%A3%E7%9A%84HashSet%E3%80%81TreeSet%E7%94%A8%E6%B3%95%E7%AE%80%E6%9E%90/> (referer: https://jcoffeezph.top/)
2018-11-19 11:02:59 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/11/01/MySQL%E7%9A%84%E5%B8%B8%E7%94%A8%E7%AE%80%E5%8D%95%E6%93%8D%E4%BD%9C/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0%E9%A1%BA%E5%BA%8F%E8%A1%A8/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/11/02/JDBC%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/11/01/MySQL%E7%9A%84%E5%B8%B8%E7%94%A8%E7%AE%80%E5%8D%95%E6%93%8D%E4%BD%9C/>
{'author': ['ForMe'], 'time': ['2018-11-01'], 'title': ['MySQL的常用简单操作']}
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0Set%E6%8E%A5%E5%8F%A3%E7%9A%84HashSet%E3%80%81TreeSet%E7%94%A8%E6%B3%95%E7%AE%80%E6%9E%90/>
{'author': ['ForMe'],
'time': ['2018-10-29'],
'title': ['java实现Set接口的HashSet、TreeSet用法简析']}
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/11/14/JSP%E8%AE%BF%E9%97%AEmysql%E6%95%B0%E6%8D%AE%E5%BA%93/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0%E9%A1%BA%E5%BA%8F%E8%A1%A8/>
{'author': ['ForMe'], 'time': ['2018-10-29'], 'title': ['java实现顺序表']}
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/11/02/JDBC%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/>
{'author': ['ForMe'], 'time': ['2018-11-02'], 'title': ['JDBC的简单使用']}
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/11/14/JSP%E8%AE%BF%E9%97%AEmysql%E6%95%B0%E6%8D%AE%E5%BA%93/>
{'author': ['ForMe'], 'time': ['2018-11-14'], 'title': ['JSP访问mysql数据库']}
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/26/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%92%8C%E5%93%88%E5%A4%AB%E6%9B%BC%E6%A0%91%E7%9A%84%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/> (referer: https://jcoffeezph.top/page/2/)
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/11/07/%E6%95%A3%E5%88%97%E8%A1%A8%E7%AE%80%E6%9E%90/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/25/%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3synchronized%E7%94%A8%E6%B3%95/> (referer: https://jcoffeezph.top/page/2/)
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/26/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%92%8C%E5%93%88%E5%A4%AB%E6%9B%BC%E6%A0%91%E7%9A%84%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/>
{'author': ['ForMe'], 'time': ['2018-10-26'], 'title': ['二叉树和哈夫曼树的简单实现']}
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/11/07/%E6%95%A3%E5%88%97%E8%A1%A8%E7%AE%80%E6%9E%90/>
{'author': ['ForMe'], 'time': ['2018-11-07'], 'title': ['散列表及HashMap简析']}
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/25/%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3synchronized%E7%94%A8%E6%B3%95/>
{'author': ['ForMe'], 'time': ['2018-10-25'], 'title': ['简单理解synchronized用法']}
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/11/05/jsp%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%8F%8A%E5%86%85%E7%BD%AE%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/27/ArrayList%E5%92%8CLinkedList%E7%94%A8%E6%B3%95%E7%AE%80%E6%9E%90/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:00 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/11/05/jsp%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%8F%8A%E5%86%85%E7%BD%AE%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D/>
{'author': ['ForMe'], 'time': ['2018-11-05'], 'title': ['jsp基本语法及内置对象的简单介绍']}
2018-11-19 11:03:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/27/ArrayList%E5%92%8CLinkedList%E7%94%A8%E6%B3%95%E7%AE%80%E6%9E%90/>
{'author': ['ForMe'],
'time': ['2018-10-27'],
'title': ['ArrayList和LinkedList用法简析']}
2018-11-19 11:03:01 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0%E9%93%BE%E8%A1%A8%E7%9A%84%E7%AE%80%E5%8D%95%E6%93%8D%E4%BD%9C/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/29/java%E5%AE%9E%E7%8E%B0%E9%93%BE%E8%A1%A8%E7%9A%84%E7%AE%80%E5%8D%95%E6%93%8D%E4%BD%9C/>
{'author': ['ForMe'], 'time': ['2018-10-29'], 'title': ['java实现链表的简单操作']}
2018-11-19 11:03:01 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://jcoffeezph.top/2018/10/30/java-IO%E5%AD%97%E7%AC%A6%E6%B5%81%E4%B8%8E%E5%AD%97%E8%8A%82%E6%B5%81%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/> (referer: https://jcoffeezph.top/)
2018-11-19 11:03:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://jcoffeezph.top/2018/10/30/java-IO%E5%AD%97%E7%AC%A6%E6%B5%81%E4%B8%8E%E5%AD%97%E8%8A%82%E6%B5%81%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/>
{'author': ['ForMe'], 'time': ['2018-10-30'], 'title': ['java IO字符流与字节流简单使用示例']}
2018-11-19 11:03:01 [scrapy.core.engine] INFO: Closing spider (finished)
2018-11-19 11:03:01 [scrapy.extensions.feedexport] INFO: Stored json feed (12 items) in: scrapy.json
2018-11-19 11:03:01 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 4772,
'downloader/request_count': 15,
'downloader/request_method_count/GET': 15,
'downloader/response_bytes': 635577,
'downloader/response_count': 15,
'downloader/response_status_count/200': 14,
'downloader/response_status_count/404': 1,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2018, 11, 19, 3, 3, 1, 726464),
'item_scraped_count': 12,
'log_count/DEBUG': 28,
'log_count/INFO': 8,
'request_depth_max': 2,
'response_received_count': 15,
'scheduler/dequeued': 14,
'scheduler/dequeued/memory': 14,
'scheduler/enqueued': 14,
'scheduler/enqueued/memory': 14,
'start_time': datetime.datetime(2018, 11, 19, 3, 2, 57, 810300)}
2018-11-19 11:03:01 [scrapy.core.engine] INFO: Spider closed (finished)

当内容页过多时,我们可以输入如下命令限制爬取数量:

scrapy crawl demo -s CLOSESPIDER_ITEMCOUNT=20


二、使用CrawlSpider实现双向爬取

如果感觉上面的双向爬取有些冗长,则说明你确实发现了关键问题。Scrapy尝试简化此类通用情况,以使其编码更加简单。最简单的实现同样结果的方式是使用CrawlSpider,这是一个能够更容易地实现这种爬取的类。为了实现它,我们需要使用genspider命令,并设置 -t crawl 参数,以使用crawl爬虫模板创建一个爬虫。
scrapy genspider -t crawl demo web

现在文件包含如下内容:

当阅读这段自动生成的代码时,会发现它和之前的爬虫有些相似,不过在此处类的声明中,会发现爬虫是继承自 CrawlSpider ,而不再是Spider。CrawlSpider提供了一个使用rules变量实现的parse()方法,这与我们之前例子中手工实现的功能一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from Demo.items import DemoItem
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose,Join
from urllib.parse import urljoin

class CrawldemoSpider(CrawlSpider):
name = 'crawldemo'
allowed_domains = ['web']
start_urls = ['https://jcoffeezph.top/']

rules = (
Rule(LinkExtractor(restrict_xpaths='//*[@class="extend next"]')),
Rule(LinkExtractor(restrict_xpaths='//*[@class="article-title"]'),callback='parse_item')
)

def parse_item(self, response):
l = ItemLoader(item=DemoItem(), response=response)
l.add_xpath('title', '//*[@class="article-title"]/text()', MapCompose(str.strip))
l.add_xpath('author', '//*[@id="header"]/hgroup/h1/a/text()', MapCompose(str.strip))
l.add_xpath('time', '//*[@itemprop="datePublished"]/text()', MapCompose(str.strip))
l.add_xpath('link', '//*[@itemprop="name"]/a/@href', MapCompose(lambda i: urljoin(response.url, i)))
return l.load_item()

这两条规则使用的是和我们之前手工实现的示例中相同的Xpath表达式,不过这是没有了a 或 href 的限制。顾名思义,LinkExtractor正是专门用于抽取链接的,因此在默认情况下,他们会去查找a (及area) href属性。你可以通过设置它的 tags 和 attrs 参数来进行自定义。需要注意的是,回调函数目前是包含回调方法名称的字符串(比如 ‘parse_item’),而不是方法引用,如Request(self.parse_item)。最后,除非设置了callback参数,否则Rule将跟踪已经抽取的URL,也就是说他会扫描目标页面以获取额外的链接并跟踪他们。如果设置了callback,Rule将不会跟踪目标页面的链接。如果你希望它跟踪链接,应在callback方法中使用return或yield返回它们,或将Rule()的follow参数设置为True。

——这里有个坑,爬取博客时,CrawlSpider可能会碰到域名过滤问题,这里又没有Request中的dont_filter 参数,怎么办呢?我们只需要把allowed_domains去掉就好啦~

CrawlSpider很强大,但此处先不做过多阐述,以后遇到了再慢慢学~~我超懒的!

本章完