8.2 启动项目
安装好Scrapy以后,我们可以运行startproject 命令生成该项目的默认结构。具体步骤为:打开终端进入想要存储Scrapy项目的目录,然后运行scrapy startproject <project name> 。这里我们使用example 作为项目名。
$ scrapy startproject example $ cd example
下面是scrapy 命令生成的文件结构。
scrapy.cfg example/ __init__.py items.py pipelines.py settings.py spiders/ __init__.py
其中,在本章比较重要的几个文件如下所示。
items.py: 该文件定义了待抓取域的模型。
settings.py :该文件定义了一些设置,如用户代理、爬取延时等。
spiders/ :该目录存储实际的爬虫代码。
另外,Scrapy使用scrapy.cfg 设置项目配置,使用pipelines.py 处理要抓取的域,不过在本例中无须修改这两个文件。
8.2.1 定义模型
默认情况下,example/items.py 文件包含如下代码。
import scrapy class ExampleItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() pass
ExampleItem 类是一个模板,需要将其中的内容替换为爬虫运行时想要存储的待抓取国家信息。为了更好地聚焦Scrapy的执行过程,接下来我们只会抓取国家名称和人口数量,而不是抓取国家的所有信息。下面是修改后支持该功能的模型代码。
class ExampleItem(scrapy.Item): name = scrapy.Field() population = scrapy.Field()
定义模型的详细文档可以参考`http://doc.scrapy.org/en/latest/topics/ items.html`。
8.2.2 创建爬虫
现在,我们要开始编写真正的爬虫代码了,在Scrapy里又被称为spider 。通过genspider 命令,传入爬虫名、域名以及可选的模板参数,就可以生成初始模板。
$ scrapy genspider country example.webscraping.com --template=crawl
这里使用内置的crawl 模板,可以生成更接近我们想要的国家爬虫的初始版本。运行genspider 命令之后,下面的代码将会在example/spiders/country.py 中自动生成。
import scrapy from scrapy.contrib.linkextractors import LinkExtractor from scrapy.contrib.spiders import CrawlSpider, Rule from example.items import ExampleItem class CountrySpider(CrawlSpider): name = 'country' start_urls = ['http://www.example.webscraping.com/'] allowed_domains = ['example.webscraping.com'] rules = ( Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), ) def parse_item(self, response): i = ExampleItem() #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract() #i['name'] = response.xpath('//div[@id="name"]').extract() #i['description'] = response.xpath('//div[@id="description"]').extract() return i
最开始几行导入了后面会用到的Scrapy库,包括8.2.1节中定义的ExampleItem 模型。然后创建了一个爬虫类,该类包括如下类属性。
name :该属性为定义爬虫名称的字符串。
start_urls :该属性定义了爬虫起始URL列表。不过,start_urls 的默认值与我们想要的不一样,在example.webscraping.com 域名之前多了www 前缀。
allowed_domains :该属性定义了可以爬取的域名列表。如果没有定义该属性,则表示可以爬取任何域名。
rules :该属性为一个正则表达式集合,用于告知爬虫需要跟踪哪些链接。
rules 属性还有一个callback 函数,用于解析下载得到的响应,而parse_item() 示例方法给我们提供了一个从响应中获取数据的例子。
Scrapy是一个高级框架,因此即使只有这几行代码,也还有很多需要了解的知识。官方文档中包含了创建爬虫相关的更多细节,其网址为http://doc. scrapy.org/en/latest/topics/spiders.html 。
1.优化设置
在运行前面生成的爬虫之前,需要更新Scrapy的设置,避免爬虫被封禁。默认情况下,Scrapy对同一域名允许最多8个并发下载,并且两次下载之间没有延时,这样就会比真实用户浏览时的速度快很多,所以很容易被服务器检测到。在前言中我们提到,当下载速度持续高于每秒一个请求时,我们抓取的示例网站会暂时封禁爬虫,也就是说使用默认配置会造成我们的爬虫被封禁。除非你在本地运行示例网站,否则我建议在example/settings.py 文件中添加如下几行,使爬虫同时只能对每个域名发起一个请求,并且每两次请求之间存在延时:
CONCURRENT_REQUESTS_PER_DOMAIN = 1 DOWNLOAD_DELAY = 5
请注意,Scrapy在两次请求之间的延时并不是精确的,这是因为精确的延时同样会造成爬虫容易被检测到,然后被封禁。而Scrapy实际使用的方法是在两次请求之间的延时上添加随机的偏移量。要想了解更多关于上述设置和其他设置的细节,可以参考http://doc.scrapy.org/en/latest/ topics/settings.html 。
2.测试爬虫
想要从命令行运行爬虫,需要使用crawl 命令,并且带上爬虫的名称。
$ scrapy crawl country -s LOG_LEVEL=ERROR [country] ERROR: Error downloading com/>: DNS lookup failed: address 'www.example.webscraping.com' not found: [Errno -5] No address associated with hostname.
和预期一样,默认的爬虫代码运行失败了,这是因为http://www. example.webscraping.com 并不存在。此外,你还会注意到命令中有一个-s LOG_LEVEL=ERROR 标记,这是一个Scrapy设置,等同于在settings.py 文件中定义LOG_LEVEL = 'ERROR' 。默认情况下,Scrapy会在终端上输出所有日志信息,而这里是将日志级别提升至只显示错误 信息。
下面的代码更正了爬虫的起始URL,并且设定了要爬取的网页。
start_urls = ['http://example.webscraping.com/'] rules = ( Rule(LinkExtractor(allow='/index/'), follow=True), Rule(LinkExtractor(allow='/view/'), callback='parse_item') )
第一条规则爬取索引页并跟踪其中的链接,而第二条规则爬取国家页面并将下载响应传给callback 函数用于抓取。下面让我们把日志级别设为DEBUG 以显示所有信息,来看下爬虫是如何运行的。
$ scrapy crawl country -s LOG_LEVEL=DEBUG ... [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/> [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/index/1> [country] DEBUG: Filtered duplicate request: < GET http://example.webscraping. com/ index/1> - no more duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates) [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/view/Antigua-and-Barbuda-10> [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/user/login?_next=%2Findex%2F1> [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/user/register?_next=%2Findex%2F1> ...
输出的日志信息显示,索引页和国家页都可以正确爬取,并且已经过滤了重复链接。但是,我们还会发现爬虫浪费了很多资源来爬取每个网页上的登录和注册表单链接,因为它们也匹配rules 里的正则表达式。前面命令中的登录URL以_next=%2Findex%2F1 结尾,也就是_next=/index/1 经过URL编码后的结果,其目的是让服务器端获取用户登录后的跳转地址。要想避免爬取这些URL,我们可以使用规则的deny 参数,该参数同样需要一个正则表达式,用于匹配所有不想爬取的URL。下面对之前的代码进行了修改,通过避免URL包含/user/ 来防止爬取用户登录和注册表单。
rules = ( Rule(LinkExtractor(allow='/index/', deny='/user/'), follow=True), Rule(LinkExtractor(allow='/view/', deny='/user/'), callback='parse_item') )
想要进一步了解如何使用该类,可以参考其文档,网址为`http://doc.scrapy.org/en/latest/topics/linkextractors.html`。
8.2.3 使用shell命令抓取
现在Scrapy已经可以爬取国家页面了,下面还需要定义要抓取哪些数据。为了帮助测试如何从网页中抽取数据,Scrapy提供了一个很方便的命令——shell ,可以下载URL并在Python解释器中给出结果状态。下面是爬取某个示例国家时的结果。
$ scrapy shell http://example.webscraping.com/view/United-Kingdom-239 [s] Available Scrapy objects: [s] crawler < scrapy.crawler.Crawler object at 0x7f1475da5390> [s] item {} [s] request < GET http://example.webscraping.com/view/United-Kingdom-239> [s] response < 200 http://example.webscraping.com/view/United-Kingdom-239> [s] settings < scrapy.settings.Settings object at 0x7f147c1fb490> [s] spider < Spider 'default' at 0x7f147552eb90> [s] Useful shortcuts: [s] shelp() Shell help (print this help) [s] fetch(req_or_url) Fetch request (or URL) and update local objects [s] view(response) View response in a browser
现在我们可以查询这些对象,检查哪些数据可以使用。
In [1]: response.url 'http://example.webscraping.com/view/United-Kingdom-239' In [2]: response.status 200
Scrapy使用lxml 抓取数据,所以我们仍然可以使用第2章中用过的CSS选择器。
In [3]: response.css('tr#places_country__row td.w2p_fw::text') [<Selector xpath=u"descendant-or-self:: tr[@id = 'places_country__row']/descendant-or-self:: */td[@class and contains( concat(' ', normalize-space(@class), ' '), ' w2p_fw ')]/text()" data=u'United Kingdom'>]
该方法返回一个lxml 选择器,要想使用它,还需要调用extract() 方法。
In [4]: name_css = 'tr#places_country__row td.w2p_fw::text' In [5]: response.css(name_css).extract() [u'United Kingdom'] In [6]: pop_css = 'tr#places_population__row td.w2p_fw::text' In [7]: response.css(pop_css).extract() [u'62,348,447']
然后,可以在先前生成的example/spiders/cou``n``try.py 文件的parse_item() 方法中使用这些CSS选择器。
def parse_item(self, response): item = ExampleItem() name_css = 'tr#places_country__row td.w2p_fw::text' item['name'] = response.css(name_css).extract() pop_css = 'tr#places_population__row td.w2p_fw::text' item['population'] = response.css(pop_css).extract() return item
8.2.4 检查结果
下面是该爬虫的完整代码。
class CountrySpider(CrawlSpider): name = 'country' start_urls = ['http://example.webscraping.com/'] allowed_domains = ['example.webscraping.com'] rules = ( Rule(LinkExtractor(allow='/index/', deny='/user/'), follow=True), Rule(LinkExtractor(allow='/view/', deny='/user/'), callback='parse_item') ) def parse_item(self, response): item = ExampleItem() name_css = 'tr#places_country__row td.w2p_fw::text' item['name'] = response.css(name_css).extract() pop_css = 'tr#places_population__row td.w2p_fw::text' item['population'] = response.css(pop_css).extract() return item
要想保存结果,我们可以在parse_item() 方法中添加额外的代码,用于写入已抓取的国家数据,或是定义管道。不过,这一操作并不是必需的,因为Scrapy还提供了一个更方便的--output 选项,用于自动保存已抓取的条目,可选格式包括CSV、JSON和XML。下面是该爬虫的最终版运行时的结果,该结果将会输出到一个CSV文件中,此外该爬虫的日志级别被设定为INFO 以过滤不重要的信息。
$ scrapy crawl country --output=countries.csv -s LOG_LEVEL=INFO [scrapy] INFO: Scrapy 0.24.4 started (bot: example) [country] INFO: Spider opened [country] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) [country] INFO: Crawled 10 pages (at 10 pages/min), scraped 9 items (at 9 items/min) ... [country] INFO: Crawled 264 pages (at 10 pages/min), scraped 238 items (at 9 items/min) [country] INFO: Crawled 274 pages (at 10 pages/min), scraped 248 items (at 10 items/min) [country] INFO: Closing spider (finished) [country] INFO: Stored csv feed (252 items) in: countries.csv [country] INFO: Dumping Scrapy stats: {'downloader/request_bytes': 155001, 'downloader/request_count': 279, 'downloader/request_method_count/GET': 279, 'downloader/response_bytes': 943190, 'downloader/response_count': 279, 'downloader/response_status_count/200': 279, 'dupefilter/filtered': 61, 'finish_reason': 'finished', 'item_scraped_count': 252, 'log_count/INFO': 36, 'request_depth_max': 26, 'response_received_count': 279, 'scheduler/dequeued': 279, 'scheduler/dequeued/memory': 279, 'scheduler/enqueued': 279, 'scheduler/enqueued/memory': 279} [country] INFO: Spider closed (finished)
在爬取过程的最后阶段,Scrapy会输出一些统计信息,给出爬虫运行的一些指标。从统计结果中,我们可以了解到爬虫总共爬取了279个网页,并抓取到其中的252个条目,这与数据库中的国家数量一致,因此我们知道爬虫已经找到了所有国家数据。
要想验证抓取的这些国家信息正确与否,我们可以检查countries.csv 文件中的内容。
name,population Afghanistan,"29,121,286" Antigua and Barbuda,"86,754" Antarctica,0 Anguilla,"13,254" Angola,"13,068,161" Andorra,"84,000" American Samoa,"57,881" Algeria,"34,586,184" Albania,"2,986,952" Aland Islands,"26,711" ...
和预期一样,表格中包含了每个国家的名称和人口数量。抓取这些数据所要编写的代码比第2章中的原始爬虫要少很多,这是因为Scrapy提供了很多高级功能。在下一节中,我们将使用Portia重新实现该爬虫,而且要编写的代码更少。
8.2.5 中断与恢复爬虫
在抓取网站时,暂停爬虫并于稍后恢复而不是重新开始,有时会很有用。比如,软件更新后重启计算机,或是要爬取的网站出现错误需要稍后继续爬取时,都可能会中断爬虫。非常方便的是,Scrapy内置了对暂停与恢复爬取的支持,这样我们就不需要再修改示例爬虫了。要开启该功能,我们只需要定义用于保存爬虫当前状态目录的JOBDIR 设置即可。需要注意的是,多个爬虫的状态需要保存在不同的目录当中。下面是在我们的爬虫中使用该功能的示例。
$ scrapy crawl country -s LOG_LEVEL=DEBUG -s JOBDIR=crawls/country ... [country] DEBUG: Scraped from < 200 http://example.webscraping.com/view/Afghanistan-1> { 'name': [u'Afghanistan'], 'population': [u'29,121,286'] } ^C [scrapy] INFO: Received SIGINT, shutting down gracefully. Send again to force [country] INFO: Closing spider (shutdown) [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/view/Antigua-and-Barbuda-10> (referer: http://example.webscraping.com/) [country] DEBUG: Scraped from < 200 http://example.webscraping.com/view/Antigua-and-Barbuda-10> { 'name': [ u'Antigua and Barbuda' ], 'population': [u'86,754'] } [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/view/Antarctica-9> (referer: http://example.webscraping.com/) [country] DEBUG: Scraped from < 200 http://example.webscraping.com/view/Antarctica-9> { 'name': [ u'Antarctica' ], 'population': [ u'0' ] } ... [ country ] INFO: Spider closed (shutdown)
从上述执行过程可以看出,我们使用^C (Ctrl+_C)发送终止信号,然后爬虫又完成了几个条目的处理之后才终止。想要Scrapy保存爬虫状态,就必须等待它正常结束,而不能经受不住诱惑再次按下Ctrl +_C强行立即终止!现在,爬虫状态保存在crawls/country 目录中,之后可以运行同样的命令恢复爬虫运行。
$ scrapy crawl country -s LOG_LEVEL=DEBUG -s JOBDIR=crawls/country ... [country] INFO: Resuming crawl (12 requests scheduled) [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/view/Anguilla-8> (referer: http://example.webscraping.com/) [country] DEBUG: Scraped from < 200 http://example.webscraping.com/view/Anguilla-8> { 'name': [u'Anguilla' ], 'population': [ u'13,254' ] } [country] DEBUG: Crawled (200) < GET http://example.webscraping.com/view/Angola-7> (referer: http://example.webscraping.com/) [country] DEBUG: Scraped from < 200 http://example.webscraping.com/view/Angola-7> { 'name': [ u'Angola' ], 'population': [ u'13,068,161' ] } ...
此时,爬虫从刚才暂停的地方恢复运行,和正常启动一样继续进行爬取。该功能对于我们的示例网站而言用处不大,因为要下载的页面数量非常小。不过,对于那些需要爬取几个月的大型网站而言,能够暂停和恢复爬虫就非常方便了。
需要注意的是,有一些边界情况在这里没有覆盖,可能会在恢复爬取时产生问题,比如cookie过期等,此类问题可以从Scrapy的官方文档中进行详细了解,其网址为http://doc.scrapy.org/en/latest/topics/jobs.html 。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论