
深入剖析Python的爬虫框架Scrapy的结构与运作流程
网络爬虫(Web Crawler, Spider)就是一个在网络上乱爬的机器人。当然它通常并不是一个实体的机器人,因为网络本身也是虚拟的东西,所以这个“机器人”其实也就是一段程序,并且它也不是乱爬,而是有一定目的的,并且在爬行的时候会搜集一些信息。例如 Google 就有一大堆爬虫会在 Internet 上搜集网页内容以及它们之间的链接等信息;又比如一些别有用心的爬虫会在 Internet 上搜集诸如 foo@bar.com 或者 foo [at] bar [dot] com 之类的东西。除此之外,还有一些定制的爬虫,专门针对某一个网站,例如前一阵子 JavaEye 的 Robbin 就写了几篇专门对付恶意爬虫的 blog (原文链接似乎已经失效了,就不给了),还有诸如小众软件或者 LinuxToy 这样的网站也经常被整个站点 crawl 下来,换个名字挂出来。其实爬虫从基本原理上来讲很简单,只要能访问网络和分析 Web 页面即可,现在大部分语言都有方便的 Http 客户端库可以抓取 Web 页面,而 HTML 的分析最简单的可以直接用正则表达式来做,因此要做一个最简陋的网络爬虫实际上是一件很简单的事情。不过要实现一个高质量的 spider 却是非常难的。
爬虫的两部分,一是下载 Web 页面,有许多问题需要考虑,如何最大程度地利用本地带宽,如何调度针对不同站点的 Web 请求以减轻对方服务器的负担等。一个高性能的 Web Crawler 系统里,DNS 查询也会成为急需优化的瓶颈,另外,还有一些“行规”需要遵循(例如 robots.txt)。而获取了网页之后的分析过程也是非常复杂的,Internet 上的东西千奇百怪,各种错误百出的 HTML 页面都有,要想全部分析清楚几乎是不可能的事;另外,随着 AJAX 的流行,如何获取由 Javascript 动态生成的内容成了一大难题;除此之外,Internet 上还有有各种有意或无意出现的 Spider Trap ,如果盲目的跟踪超链接的话,就会陷入 Trap 中万劫不复了,例如这个网站,据说是之前 Google 宣称 Internet 上的 Unique URL 数目已经达到了 1 trillion 个,因此这个人 is proud to announce the second trillion 。 :D
不过,其实并没有多少人需要做像 Google 那样通用的 Crawler ,通常我们做一个 Crawler 就是为了去爬特定的某个或者某一类网站,所谓知己知彼,百战不殆,我们可以事先对需要爬的网站结构做一些分析,事情就变得容易多了。通过分析,选出有价值的链接进行跟踪,就可以避免很多不必要的链接或者 Spider Trap ,如果网站的结构允许选择一个合适的路径的话,我们可以按照一定顺序把感兴趣的东西爬一遍,这样以来,连 URL 重复的判断也可以省去。
举个例子,假如我们想把 pongba 的 blog mindhacks.cn 里面的 blog 文字爬下来,通过观察,很容易发现我们对其中的两种页面感兴趣:
文章列表页面,例如首页,或者 URL 是 /page/\d+/ 这样的页面,通过 Firebug 可以看到到每篇文章的链接都是在一个 h1
下的 a 标签里的(需要注意的是,在 Firebug 的 HTML 面板里看到的 HTML 代码和 View Source
所看到的也许会有些出入,如果网页中有 Javascript 动态修改 DOM 树的话,前者是被修改过的版本,并且经过 Firebug
规则化的,例如 attribute 都有引号扩起来等,而后者通常才是你的 spider
爬到的原始内容。如果是使用正则表达式对页面进行分析或者所用的 HTML Parser 和 Firefox
的有些出入的话,需要特别注意),另外,在一个 class 为 wp-pagenavi 的 div 里有到不同列表页面的链接。
文章内容页面,每篇 blog 有这样一个页面,例如 /2008/09/11/machine-learning-and-ai-resources/ ,包含了完整的文章内容,这是我们感兴趣的内容。
因此,我们从首页开始,通过 wp-pagenavi 里的链接来得到其他的文章列表页面,特别地,我们定义一个路径:只 follow Next
Page
的链接,这样就可以从头到尾按顺序走一遍,免去了需要判断重复抓取的烦恼。另外,文章列表页面的那些到具体文章的链接所对应的页面就是我们真正要保存的数据页面了。
这样以来,其实用脚本语言写一个 ad hoc 的 Crawler 来完成这个任务也并不难,不过今天的主角是 Scrapy ,这是一个用 Python 写的 Crawler Framework ,简单轻巧,并且非常方便,并且官网上说已经在实际生产中在使用了,因此并不是一个玩具级别的东西。不过现在还没有 Release 版本,可以直接使用他们的 Mercurial 仓库里抓取源码进行安装。不过,这个东西也可以不安装直接使用,这样还方便随时更新,文档里说得很详细,我就不重复了。
Scrapy 使用 Twisted 这个异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。整体架构如下图所示:
绿线是数据流向,首先从初始 URL 开始,Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,Spider 分析出来的结果有两种:一种是需要进一步抓取的链接,例如之前分析的“下一页”的链接,这些东西会被传回 Scheduler ;另一种是需要保存的数据,它们则被送到 Item Pipeline 那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。另外,在数据流动的通道里还可以安装各种中间件,进行必要的处理。
具体的内容在最后的附属中还会介绍。
看起来好像很复杂,其实用起来很简单,就如同 Rails 一样,首先新建一个工程:
会创建一个 blog_crawl 目录,里面有个 scrapy-ctl.py 是整个项目的控制脚本,而代码全都放在子目录 blog_crawl 里面。为了能抓取 mindhacks.cn ,我们在 spiders 目录里新建一个mindhacks_spider.py ,定义我们的 Spider 如下:
我们的 MindhacksSpider 继承自 BaseSpider (通常直接继承自功能更丰富的 scrapy.contrib.spiders.CrawlSpider 要方便一些,不过为了展示数据是如何 parse 的,这里还是使用 BaseSpider 了),变量 domain_name 和 start_urls 都很容易明白是什么意思,而 parse 方法是我们需要定义的回调函数,默认的 request 得到 response 之后会调用这个回调函数,我们需要在这里对页面进行解析,返回两种结果(需要进一步 crawl 的链接和需要保存的数据),让我感觉有些奇怪的是,它的接口定义里这两种结果竟然是混杂在一个 list 里返回的,不太清楚这里为何这样设计,难道最后不还是要费力把它们分开?总之这里我们先写一个空函数,只返回一个空列表。另外,定义一个“全局”变量 SPIDER ,它会在 Scrapy 导入这个 module 的时候实例化,并自动被 Scrapy 的引擎找到。这样就可以先运行一下 crawler 试试了:
会有一堆输出,可以看到抓取了 http://mindhacks.cn ,因为这是初始 URL ,但是由于我们在 parse 函数里没有返回需要进一步抓取的 URL ,因此整个 crawl 过程只抓取了主页便结束了。接下来便是要对页面进行分析,Scrapy 提供了一个很方便的 Shell (需要 IPython )可以让我们做实验,用如下命令启动 Shell :
它会启动 crawler ,把命令行指定的这个页面抓取下来,然后进入 shell ,根据提示,我们有许多现成的变量可以用,其中一个就是 hxs ,它是一个 HtmlXPathSelector ,mindhacks 的 HTML 页面比较规范,可以很方便的直接用 XPath 进行分析。通过 Firebug 可以看到,到每篇 blog 文章的链接都是在 h1 下的,因此在 Shell 中使用这样的 XPath 表达式测试:
这正是我们需要的 URL ,另外,还可以找到“下一页”的链接所在,连同其他几个页面的链接一同在一个 div 里,不过“下一页”的链接没有 title 属性,因此 XPath 写作
不过如果向后翻一页的话,会发现其实“上一页”也是这样的,因此还需要判断该链接上的文字是那个下一页的箭头 u'\xbb' ,本来也可以写到 XPath 里面去,但是好像这个本身是 unicode escape 字符,由于编码原因理不清楚,直接放到外面判断了,最终 parse 函数如下:
前半部分是解析需要抓取的 blog 正文的链接,后半部分则是给出“下一页”的链接。需要注意的是,这里返回的列表里并不是一个个的字符串格式的 URL 就完了,Scrapy 希望得到的是 Request 对象,这比一个字符串格式的 URL 能携带更多的东西,诸如 Cookie 或者回调函数之类的。可以看到我们在创建 blog 正文的 Request 的时候替换掉了回调函数,因为默认的这个回调函数 parse 是专门用来解析文章列表这样的页面的,而 parse_post 定义如下:
很简单,返回一个 BlogCrawlItem ,把抓到的数据放在里面,本来可以在这里做一点解析,例如,通过 XPath 把正文和标题等解析出来,但是我倾向于后面再来做这些事情,例如 Item Pipeline 或者更后面的 Offline 阶段。BlogCrawlItem 是 Scrapy 自动帮我们定义好的一个继承自 ScrapedItem 的空类,在 items.py 中,这里我加了一点东西:
定义了 __str__ 函数,只给出 URL ,因为默认的 __str__ 函数会把所有的数据都显示出来,因此会看到 crawl 的时候控制台 log 狂输出东西,那是把抓取到的网页内容输出出来了。-.-bb
这样一来,数据就取到了,最后只剩下存储数据的功能,我们通过添加一个 Pipeline 来实现,由于 Python 在标准库里自带了 Sqlite3 的支持,所以我使用 Sqlite 数据库来存储数据。用如下代码替换 pipelines.py 的内容:
在 __init__ 函数中,使用 dispatcher 将两个信号连接到指定的函数上,分别用于初始化和关闭数据库连接(在 close 之前记得 commit ,似乎是不会自动 commit 的,直接 close 的话好像所有的数据都丢失了 dd-.-)。当有数据经过 pipeline 的时候,process_item 函数会被调用,在这里我们直接讲原始数据存储到数据库中,不作任何处理。如果需要的话,可以添加额外的 pipeline ,对数据进行提取、过滤等,这里就不细说了。
最后,在 settings.py 里列出我们的 pipeline :
ITEM_PIPELINES = ['blog_crawl.pipelines.SQLiteStorePipeline']
再跑一下 crawler ,就 OK 啦!
PS1:Scrapy的组件
1.Scrapy Engine(Scrapy引擎)
Scrapy引擎是用来控制整个系统的数据处理流程,并进行事务处理的触发。更多的详细内容可以看下面的数据处理流程。
2.Scheduler(调度程序)
调度程序从Scrapy引擎接受请求并排序列入队列,并在Scrapy引擎发出请求后返还给它们。
3.Downloader(下载器)
下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)。
4.Spiders(蜘蛛)
蜘蛛是有Scrapy用户自己定义用来解析网页并抓取制定URL返回的内容的类,每个蜘蛛都能处理一个域名或一组域名。换句话说就是用来定义特定网站的抓取和解析规则。
5.Item Pipeline(项目管道)
项目管道的主要责任是负责处理有蜘蛛从网页中抽取的项目,它的主要任务是清晰、验证和存储数据。当页面被蜘蛛解析后,将被发送到项目管道,并经过几个特定的次序处理数据。每个项目管道的组件都是有一个简单的方法组成的Python类。它们获取了项目并执行它们的方法,同时还需要确定的是是否需要在项目管道中继续执行下一步或是直接丢弃掉不处理。
项目管道通常执行的过程有:
清洗HTML数据 验证解析到的数据(检查项目是否包含必要的字段) 检查是否是重复数据(如果重复就删除) 将解析到的数据存储到数据库中
6.Middlewares(中间件)
中间件是介于Scrapy引擎和其他组件之间的一个钩子框架,主要是为了提供一个自定义的代码来拓展Scrapy的功能。
PS2:Scrapy的数据处理流程
Scrapy的整个数据处理流程有Scrapy引擎进行控制,其主要的运行方式为:
引擎打开一个域名,时蜘蛛处理这个域名,并让蜘蛛获取第一个爬取的URL。
引擎从蜘蛛那获取第一个需要爬取的URL,然后作为请求在调度中进行调度。
引擎从调度那获取接下来进行爬取的页面。
调度将下一个爬取的URL返回给引擎,引擎将它们通过下载中间件发送到下载器。
当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎。
引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。
蜘蛛处理响应并返回爬取到的项目,然后给引擎发送新的请求。
引擎将抓取到的项目项目管道,并向调度发送请求。
系统重复第二部后面的操作,直到调度中没有请求,然后断开引擎与域之间的联系。
数据分析咨询请扫描二维码
若不方便扫码,搜微信号:CDAshujufenxi
在人工智能领域,“大模型” 已成为近年来的热点标签:从参数超 1750 亿的 GPT-3,到万亿级参数的 PaLM,再到多模态大模型 GPT-4 ...
2025-10-22在 MySQL 数据库的日常运维与开发中,“更新数据是否会影响读数据” 是一个高频疑问。这个问题的答案并非简单的 “是” 或 “否 ...
2025-10-22在企业数据分析中,“数据孤岛” 是制约分析深度的核心瓶颈 —— 用户数据散落在注册系统、APP 日志、客服记录中,订单数据分散 ...
2025-10-22在神经网络设计中,“隐藏层个数” 是决定模型能力的关键参数 —— 太少会导致 “欠拟合”(模型无法捕捉复杂数据规律,如用单隐 ...
2025-10-21在特征工程流程中,“单变量筛选” 是承上启下的关键步骤 —— 它通过分析单个特征与目标变量的关联强度,剔除无意义、冗余的特 ...
2025-10-21在数据分析全流程中,“数据读取” 常被误解为 “简单的文件打开”—— 双击 Excel、执行基础 SQL 查询即可完成。但对 CDA(Cert ...
2025-10-21在实际业务数据分析中,我们遇到的大多数数据并非理想的正态分布 —— 电商平台的用户消费金额(少数用户单次消费上万元,多数集 ...
2025-10-20在数字化交互中,用户的每一次操作 —— 从电商平台的 “浏览商品→加入购物车→查看评价→放弃下单”,到内容 APP 的 “点击短 ...
2025-10-20在数据分析的全流程中,“数据采集” 是最基础也最关键的环节 —— 如同烹饪前需备好新鲜食材,若采集的数据不完整、不准确或不 ...
2025-10-20在数据成为新时代“石油”的今天,几乎每个职场人都在焦虑: “为什么别人能用数据驱动决策、升职加薪,而我面对Excel表格却无从 ...
2025-10-18数据清洗是 “数据价值挖掘的前置关卡”—— 其核心目标是 “去除噪声、修正错误、规范格式”,但前提是不破坏数据的真实业务含 ...
2025-10-17在数据汇总分析中,透视表凭借灵活的字段重组能力成为核心工具,但原始透视表仅能呈现数值结果,缺乏对数据背景、异常原因或业务 ...
2025-10-17在企业管理中,“凭经验定策略” 的传统模式正逐渐失效 —— 金融机构靠 “研究员主观判断” 选股可能错失收益,电商靠 “运营拍 ...
2025-10-17在数据库日常操作中,INSERT INTO SELECT是实现 “批量数据迁移” 的核心 SQL 语句 —— 它能直接将一个表(或查询结果集)的数 ...
2025-10-16在机器学习建模中,“参数” 是决定模型效果的关键变量 —— 无论是线性回归的系数、随机森林的树深度,还是神经网络的权重,这 ...
2025-10-16在数字化浪潮中,“数据” 已从 “辅助决策的工具” 升级为 “驱动业务的核心资产”—— 电商平台靠用户行为数据优化推荐算法, ...
2025-10-16在大模型从实验室走向生产环境的过程中,“稳定性” 是决定其能否实用的关键 —— 一个在单轮测试中表现优异的模型,若在高并发 ...
2025-10-15在机器学习入门领域,“鸢尾花数据集(Iris Dataset)” 是理解 “特征值” 与 “目标值” 的最佳案例 —— 它结构清晰、维度适 ...
2025-10-15在数据驱动的业务场景中,零散的指标(如 “GMV”“复购率”)就像 “散落的零件”,无法支撑系统性决策;而科学的指标体系,则 ...
2025-10-15在神经网络模型设计中,“隐藏层层数” 是决定模型能力与效率的核心参数之一 —— 层数过少,模型可能 “欠拟合”(无法捕捉数据 ...
2025-10-14