scrapy分页抓取网页(WebCrawler如何调度针对不同站点的网络爬虫)
优采云 发布时间: 2021-10-23 17:18scrapy分页抓取网页(WebCrawler如何调度针对不同站点的网络爬虫)
网络爬虫(Spider)是一种在互联网上爬行的机器人。当然,它通常不是物理机器人,因为网络本身也是一个虚拟的东西,所以这个“机器人”其实就是一个程序,不是爬行,而是有一定的用途,爬行的时候会采集. 一些信息。例如,谷歌有很多爬虫采集网页内容和它们之间的链接信息;另一个例子是别有用心的爬虫在互联网上采集诸如foo [at] bar [dot] com之类的东西。此外,还有一些定制的爬虫,专门针对某个网站。比如JavaEye的Robbin前段时间写了几篇专门对付恶意爬虫的博客(原链接好像已经过期了,所以不行),还有网站 小众软件、LinuxToy、酷琴网等经常被整个站点爬下来,挂在另一个名字下。其实,爬虫的基本原理非常简单。只要能上网,能分析网页,现在大部分语言都有方便的Http客户端库可以抓取网页,最简单的HTML分析可以直接使用正则规则。表达式来做,所以做一个最简单的网络爬虫其实是一件很简单的事情。但是,实现一个高质量的蜘蛛是非常困难的。现在大多数语言都有方便的Http客户端库可以抓取网页,最简单的HTML分析可以直接使用正则规则。表达式来做,所以做一个最简单的网络爬虫其实是一件很简单的事情。但是,实现一个高质量的蜘蛛是非常困难的。现在大多数语言都有方便的Http客户端库可以抓取网页,最简单的HTML分析可以直接使用正则规则。表达式来做,所以做一个最简单的网络爬虫其实是一件很简单的事情。但是,实现一个高质量的蜘蛛是非常困难的。
爬虫的两部分是下载网页。有很多问题需要考虑,比如如何最大限度地利用本地带宽,如何调度不同站点的Web请求以减少其他服务器的负担。在高性能的Web Crawler系统中,DNS查询也将成为亟待优化的瓶颈。此外,还有一些“配置文件”需要遵循(例如,robots.txt)。获取网页后的分析过程也很复杂。网上奇奇怪怪的东西很多,各种HTML页面也有各种错误。几乎不可能清楚地分析所有这些。另外,随着AJAX的普及,如何获取Javascript动态生成的内容成为了一个大问题;此外,互联网上有各种有意或无意出现的蜘蛛陷阱。如果一味的跟着超链接走,就会被困在陷阱里。例如,这个网站据说是谷歌之前宣布互联网上Unique URL的数量已经达到1万亿,所以这个人很自豪地宣布第二万亿。
然而,实际上并没有多少人需要像谷歌这样的通用爬虫。通常我们构建一个爬虫来爬取某个特定的或者某类网站,所谓知己知彼,百战不死,我们可以提前爬取对网站做一些分析网站 结构,事情变得容易多了。通过分析和选择有价值的链接进行跟踪,我们可以避免很多不必要的链接或蜘蛛陷阱。如果网站的结构允许选择合适的路径,我们可以把感兴趣的东西按照一定的顺序重新爬上去,这样就连URL重复的判断都可以省略了。
比如我们要爬下pongba博客中的博客文字,通过观察,很容易发现我们对其中的两个页面感兴趣:
文章列表页面,比如首页,或者URL为/page/\d+/的页面,通过Firebug可以看到每个文章链接都在h1下的一个标签中(应该是注意到Firebug的HTML面板中看到的HTML代码可能与View Source中看到的有些不同,如果网页中有动态修改DOM树的Javascript,则前者是修改后的版本,Firebug是正则化的,对于比如属性有引号等等,后者通常是你的蜘蛛爬取的原创内容,如果用正则表达式分析页面或者使用的HTML Parser和Firefox有些不同,(需要特别注意)。另外,一个div里面有指向不同列表页的链接,类是wp-pagenavi 文章 内容页,每个博客都有这样一个页面,比如/2008/09/11/machine-learning-and-ai-resources/,收录了文章的完整内容,这是我们感觉感兴趣的内容。
因此,我们从首页开始,利用 wp-pagenavi 中的链接来获取其他 文章 列表页面。特别是我们定义了一个路径:只跟随Next Page的链接,这样我们就可以从头到尾按顺序走一遍,免去了判断和重复爬行的麻烦。另外,文章列表页上链接到具体文章的链接对应的页面就是我们真正要保存的数据页。
这样,它实用的脚本语言写一个ad hoc Crawler来完成这个任务并不难,但是今天的主角是Scrapy,它是一个用Python编写的Crawler Framework,简单轻量,非常方便,官网说已经在实际生产中使用过,所以不是玩具级别的东西。不过目前还没有Release版本,你可以直接使用他们的Mercurial仓库获取安装源码。不过这个东西也可以不用安装直接使用,方便随时更新。文档很详细,不再赘述。
Scrapy 使用异步网络库 Twisted 来处理网络通信。它结构清晰,收录各种中间件接口,可以灵活满足各种需求。整体架构如下图所示:
绿线是数据流。首先从初始URL开始,Scheduler将其交给Downloader下载,下载完成后交给Spider进行分析。Spider分析的结果有两种:一种是需要进一步爬取的链接。比如之前分析过的“下一页”的链接,这些东西会被发回给Scheduler;另一个是需要保存的数据,发送到Item Pipeline,就是数据的后处理(详细分析、过滤、存储等)。此外,可以在数据流通道中安装各种中间件来进行必要的处理。
它看起来很复杂,但使用起来非常简单。就像Rails一样,首先新建一个项目:
scrapy-admin.py startproject blog_crawl
会创建一个blog_crawl目录,里面有一个scrapy-ctl.py是整个项目的控制脚本,代码全部放在子目录blog_crawl下。为了能够爬取,我们在spiders目录下新建了一个mindhacks_spider.py,定义我们的Spider如下:
from scrapy.spider import BaseSpider
class MindhacksSpider(BaseSpider):
domain_name = "mindhacks.cn"
start_urls = ["http://mindhacks.cn/"]
def parse(self, response):
return []
SPIDER = MindhacksSpider()
我们的MindhacksSpider继承自BaseSpider(通常是直接继承自scrapy.contrib.spiders.CrawlSpider,它更通用,更方便,但为了展示数据是如何解析的,这里使用了BaseSpider),变量domain_name和start_urls很容易理解是什么意思,parse方法就是我们需要定义的回调函数。默认请求在得到响应后会调用这个回调函数。我们这里需要解析页面,返回两个结果(需要进一步爬取链接,需要保存Data),让我觉得有点奇怪的是,它的接口定义中的两个结果实际上是在一个混合列表中返回的。目前尚不清楚为什么会出现这种设计。最后不是要分开吗?总之,这里我们先写一个空函数,它只返回一个空列表。另外,定义一个“全局”变量 SPIDER,当 Scrapy 导入这个模块时会实例化,并且会被 Scrapy 引擎自动找到。所以你可以先运行爬虫试试:
./scrapy-ctl.py crawl mindhacks.cn
会有一堆输出,可以看到爬取了,因为这是初始的URL,但是因为我们在parse函数中没有返回需要进一步爬取的URL,所以整个爬取过程只爬到了首页就结束了. 下一步是分析页面。Scrapy 提供了一个非常方便的 Shell(需要 IPython),可以让我们进行实验。使用以下命令启动 Shell:
./scrapy-ctl.py shell http://mindhacks.cn
它会启动爬虫,抓取命令行指定的页面,然后进入shell。根据提示,我们有很多现成的变量可以使用。其中之一是 hxs,它是一个 HtmlXPathSelector。mindhacks 的 HTML 页面更加标准化。直接用XPath分析非常方便。通过Firebug可以看到每个博客文章的链接都在h1下,所以在Shell中使用这个XPath表达式测试:
In [1]: hxs.x('//h1/a/@href').extract()
Out[1]:
[u'http://mindhacks.cn/2009/07/06/why-you-should-do-it-yourself/',
u'http://mindhacks.cn/2009/05/17/seven-years-in-nju/',
u'http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/',
u'http://mindhacks.cn/2009/03/15/preconception-explained/',
u'http://mindhacks.cn/2009/03/09/first-principles-of-programming/',
u'http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now/',
u'http://mindhacks.cn/2009/02/09/writing-is-better-thinking/',
u'http://mindhacks.cn/2009/02/07/better-explained-conflicts-in-intimate-relationship/',
u'http://mindhacks.cn/2009/02/07/independence-day/',
u'http://mindhacks.cn/2009/01/18/escape-from-your-shawshank-part1/']
这正是我们需要的 URL。另外可以找到“下一页”的链接,和其他几个页面的链接放在一个div里,但是“下一页”的链接没有title属性,所以写XPath
//div[@class="wp-pagenavi"]/a[not(@title)]
但是,如果你往回翻一页,你会发现“上一页”其实是一样的,所以你需要确定链接上的文字是下一页的箭头u'\xbb',这可能有是用 XPath 写的。去,不过好像这本身就是一个unicode转义字符,由于编码原因不清楚,直接在外面判断,最终解析函数如下:
def parse(self, response):
items = []
hxs = HtmlXPathSelector(response)
posts = hxs.x('//h1/a/@href').extract()
items.extend([self.make_requests_from_url(url).replace(callback=self.parse_post)
for url in posts])
page_links = hxs.x('//div[@class="wp-pagenavi"]/a[not(@title)]')
for link in page_links:
if link.x('text()').extract()[0] == u'\xbb':
url = link.x('@href').extract()[0]
items.append(self.make_requests_from_url(url))
return items
前半部分是解析需要爬取的博客正文的链接,后半部分是给出“下一页”的链接。需要注意的是,这里返回的列表并非都是字符串格式的URL。Scrapy希望得到Request对象,它可以携带比字符串格式的URL更多的东西,比如cookies或者回调。功能等。可以看到我们在创建博客正文的请求时替换了回调函数,因为默认的回调函数parse是专门用来解析文章列表等页面的,parse_post定义如下:
def parse_post(self, response):
item = BlogCrawlItem()
item.url = unicode(response.url)
item.raw = response.body_as_unicode()
return [item]
这很简单。返回一个 BlogCrawlItem 并将捕获的数据放入其中。你可以在这里做一些分析。比如可以通过XPath解析文本和标题,但是我倾向于后期做这些事情,比如Item Pipeline或者Later Offline stage。BlogCrawlItem 是 Scrapy 自动为我们定义的一个空类,继承自 ScrapedItem。在 items.py 中,我在这里添加了一些内容:
from scrapy.item import ScrapedItem
class BlogCrawlItem(ScrapedItem):
def __init__(self):
ScrapedItem.__init__(self)
self.url = ''
def __str__(self):
return 'BlogCrawlItem(url: %s)' % self.url
定义了__str__函数,只给出了URL,因为默认的__str__函数会显示所有的数据,所以看到爬取的时候,控制台日志会输出一些东西,就是把爬取到的网页的内容输出出来。-.-bb
这样,数据就被取出来了,最后只剩下存储数据的功能了。我们通过添加流水线来实现它。由于Python在标准库中自带Sqlite3支持,所以我使用Sqlite数据库来存储数据。将 pipelines.py 的内容替换为以下代码:
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
import sqlite3
from os import path
from scrapy.core import signals
from scrapy.xlib.pydispatch import dispatcher
class SQLiteStorePipeline(object):
filename = 'data.sqlite'
def __init__(self):
self.conn = None
dispatcher.connect(self.initialize, signals.engine_started)
dispatcher.connect(self.finalize, signals.engine_stopped)
def process_item(self, domain, item):
self.conn.execute('insert into blog values(?,?,?)',
(item.url, item.raw, unicode(domain)))
return item
def initialize(self):
if path.exists(self.filename):
self.conn = sqlite3.connect(self.filename)
else:
self.conn = self.create_table(self.filename)
def finalize(self):
if self.conn is not None:
self.conn.commit()
self.conn.close()
self.conn = None
def create_table(self, filename):
conn = sqlite3.connect(filename)
conn.execute("""create table blog
(url text primary key, raw text, domain text)""")
conn.commit()
return conn
在__init__函数中,使用dispatcher将两个信号连接到指定函数,用于初始化和关闭数据库连接。Dd-.-) 丢失。当数据通过管道时,将调用 process_item 函数。这里我们将原创数据直接存入数据库,不做任何处理。如有必要,您可以添加额外的管道来提取和过滤数据,但我不会在这里详细介绍。
最后,在 settings.py 中列出我们的管道:
ITEM_PIPELINES = ['blog_crawl.pipelines.SQLiteStorePipeline']
再次运行爬虫就OK了!最后总结一下:一个高质量的爬虫是一个极其复杂的项目,但是如果你有一个好的工具,做一个专用的爬虫是相对容易的。Scrapy是一个非常轻量级的爬虫框架,极大的简化了爬虫的开发过程。另外,Scrapy的文档也很详细。如果觉得我的介绍省略了一些不清楚的地方,推荐阅读他的Tutorial。
本文来自: