搜尋
首頁後端開發Python教學深入剖析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 的 p 里有到不同列表页面的链接。
文章内容页面,每篇 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 这个异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。整体架构如下图所示:

2016120120500435.png (550×388)

绿线是数据流向,首先从初始 URL 开始,Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,Spider 分析出来的结果有两种:一种是需要进一步抓取的链接,例如之前分析的“下一页”的链接,这些东西会被传回 Scheduler ;另一种是需要保存的数据,它们则被送到 Item Pipeline 那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。另外,在数据流动的通道里还可以安装各种中间件,进行必要的处理。

具体的内容在最后的附属中还会介绍。

看起来好像很复杂,其实用起来很简单,就如同 Rails 一样,首先新建一个工程:

scrapy-admin.py startproject blog_crawl

会创建一个 blog_crawl 目录,里面有个 scrapy-ctl.py 是整个项目的控制脚本,而代码全都放在子目录 blog_crawl 里面。为了能抓取 mindhacks.cn ,我们在 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 要方便一些,不过为了展示数据是如何 parse 的,这里还是使用 BaseSpider 了),变量 domain_name 和 start_urls 都很容易明白是什么意思,而 parse 方法是我们需要定义的回调函数,默认的 request 得到 response 之后会调用这个回调函数,我们需要在这里对页面进行解析,返回两种结果(需要进一步 crawl 的链接和需要保存的数据),让我感觉有些奇怪的是,它的接口定义里这两种结果竟然是混杂在一个 list 里返回的,不太清楚这里为何这样设计,难道最后不还是要费力把它们分开?总之这里我们先写一个空函数,只返回一个空列表。另外,定义一个“全局”变量 SPIDER ,它会在 Scrapy 导入这个 module 的时候实例化,并自动被 Scrapy 的引擎找到。这样就可以先运行一下 crawler 试试了:

./scrapy-ctl.py crawl mindhacks.cn

会有一堆输出,可以看到抓取了 http://mindhacks.cn ,因为这是初始 URL ,但是由于我们在 parse 函数里没有返回需要进一步抓取的 URL ,因此整个 crawl 过程只抓取了主页便结束了。接下来便是要对页面进行分析,Scrapy 提供了一个很方便的 Shell (需要 IPython )可以让我们做实验,用如下命令启动 Shell :

./scrapy-ctl.py shell http://mindhacks.cn

它会启动 crawler ,把命令行指定的这个页面抓取下来,然后进入 shell ,根据提示,我们有许多现成的变量可以用,其中一个就是 hxs ,它是一个 HtmlXPathSelector ,mindhacks 的 HTML 页面比较规范,可以很方便的直接用 XPath 进行分析。通过 Firebug 可以看到,到每篇 blog 文章的链接都是在 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 ,另外,还可以找到“下一页”的链接所在,连同其他几个页面的链接一同在一个 p 里,不过“下一页”的链接没有 title 属性,因此 XPath 写作

//p[@class="wp-pagenavi"]/a[not(@title)]

不过如果向后翻一页的话,会发现其实“上一页”也是这样的,因此还需要判断该链接上的文字是那个下一页的箭头 u'\xbb' ,本来也可以写到 XPath 里面去,但是好像这个本身是 unicode escape 字符,由于编码原因理不清楚,直接放到外面判断了,最终 parse 函数如下:

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('//p[@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

前半部分是解析需要抓取的 blog 正文的链接,后半部分则是给出“下一页”的链接。需要注意的是,这里返回的列表里并不是一个个的字符串格式的 URL 就完了,Scrapy 希望得到的是 Request 对象,这比一个字符串格式的 URL 能携带更多的东西,诸如 Cookie 或者回调函数之类的。可以看到我们在创建 blog 正文的 Request 的时候替换掉了回调函数,因为默认的这个回调函数 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 或者更后面的 Offline 阶段。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__ 函数会把所有的数据都显示出来,因此会看到 crawl 的时候控制台 log 狂输出东西,那是把抓取到的网页内容输出出来了。-.-bb

这样一来,数据就取到了,最后只剩下存储数据的功能,我们通过添加一个 Pipeline 来实现,由于 Python 在标准库里自带了 Sqlite3 的支持,所以我使用 Sqlite 数据库来存储数据。用如下代码替换 pipelines.py 的内容:

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 将两个信号连接到指定的函数上,分别用于初始化和关闭数据库连接(在 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返回给引擎,引擎将它们通过下载中间件发送到下载器。
当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎。
引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。
蜘蛛处理响应并返回爬取到的项目,然后给引擎发送新的请求。
引擎将抓取到的项目项目管道,并向调度发送请求。
系统重复第二部后面的操作,直到调度中没有请求,然后断开引擎与域之间的联系。

以上就是深入剖析Python的爬虫框架Scrapy的结构与运作流程的内容,更多相关内容请关注PHP中文网(www.php.cn)!


陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
學習Python:2小時的每日學習是否足夠?學習Python:2小時的每日學習是否足夠?Apr 18, 2025 am 12:22 AM

每天學習Python兩個小時是否足夠?這取決於你的目標和學習方法。 1)制定清晰的學習計劃,2)選擇合適的學習資源和方法,3)動手實踐和復習鞏固,可以在這段時間內逐步掌握Python的基本知識和高級功能。

Web開發的Python:關鍵應用程序Web開發的Python:關鍵應用程序Apr 18, 2025 am 12:20 AM

Python在Web開發中的關鍵應用包括使用Django和Flask框架、API開發、數據分析與可視化、機器學習與AI、以及性能優化。 1.Django和Flask框架:Django適合快速開發複雜應用,Flask適用於小型或高度自定義項目。 2.API開發:使用Flask或DjangoRESTFramework構建RESTfulAPI。 3.數據分析與可視化:利用Python處理數據並通過Web界面展示。 4.機器學習與AI:Python用於構建智能Web應用。 5.性能優化:通過異步編程、緩存和代碼優

Python vs.C:探索性能和效率Python vs.C:探索性能和效率Apr 18, 2025 am 12:20 AM

Python在開發效率上優於C ,但C 在執行性能上更高。 1.Python的簡潔語法和豐富庫提高開發效率。 2.C 的編譯型特性和硬件控制提升執行性能。選擇時需根據項目需求權衡開發速度與執行效率。

python在行動中:現實世界中的例子python在行動中:現實世界中的例子Apr 18, 2025 am 12:18 AM

Python在現實世界中的應用包括數據分析、Web開發、人工智能和自動化。 1)在數據分析中,Python使用Pandas和Matplotlib處理和可視化數據。 2)Web開發中,Django和Flask框架簡化了Web應用的創建。 3)人工智能領域,TensorFlow和PyTorch用於構建和訓練模型。 4)自動化方面,Python腳本可用於復製文件等任務。

Python的主要用途:綜合概述Python的主要用途:綜合概述Apr 18, 2025 am 12:18 AM

Python在數據科學、Web開發和自動化腳本領域廣泛應用。 1)在數據科學中,Python通過NumPy、Pandas等庫簡化數據處理和分析。 2)在Web開發中,Django和Flask框架使開發者能快速構建應用。 3)在自動化腳本中,Python的簡潔性和標準庫使其成為理想選擇。

Python的主要目的:靈活性和易用性Python的主要目的:靈活性和易用性Apr 17, 2025 am 12:14 AM

Python的靈活性體現在多範式支持和動態類型系統,易用性則源於語法簡潔和豐富的標準庫。 1.靈活性:支持面向對象、函數式和過程式編程,動態類型系統提高開發效率。 2.易用性:語法接近自然語言,標準庫涵蓋廣泛功能,簡化開發過程。

Python:多功能編程的力量Python:多功能編程的力量Apr 17, 2025 am 12:09 AM

Python因其簡潔與強大而備受青睞,適用於從初學者到高級開發者的各種需求。其多功能性體現在:1)易學易用,語法簡單;2)豐富的庫和框架,如NumPy、Pandas等;3)跨平台支持,可在多種操作系統上運行;4)適合腳本和自動化任務,提升工作效率。

每天2小時學習Python:實用指南每天2小時學習Python:實用指南Apr 17, 2025 am 12:05 AM

可以,在每天花費兩個小時的時間內學會Python。 1.制定合理的學習計劃,2.選擇合適的學習資源,3.通過實踐鞏固所學知識,這些步驟能幫助你在短時間內掌握Python。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前By尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。