网络爬虫by pluskid

时间:2022-09-02 11:46:42

网络爬虫(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 。 网络爬虫by pluskid

不过,其实并没有多少人需要做像 Google 那样通用的 Crawler ,通常我们做一个 Crawler 就是为了去爬特定的某个或者某一类网站,所谓知己知彼,百战不殆,我们可以事先对需要爬的网站结构做一些分析,事情就变得容易多了。通过分析,选出有价值的链接进行跟踪,就可以避免很多不必要的链接或者 Spider Trap ,如果网站的结构允许选择一个合适的路径的话,我们可以按照一定顺序把感兴趣的东西爬一遍,这样以来,连 URL 重复的判断也可以省去。

举个例子,假如我们想把 pongba 的 blog mindhacks.cn 里面的 blog 文字爬下来,通过观察,很容易发现我们对其中的两种页面感兴趣:

  1. 文章列表页面,例如首页,或者 URL 是 /page/\d+/ 这样的页面,通过 Firebug 可以看到到每篇文章的链接都是在一个 h1 下的 a 标签里的(需要注意的是,在 Firebug 的 HTML 面板里看到的 HTML 代码和 View Source 所看到的也许会有些出入,如果网页中有 Javascript 动态修改 DOM 树的话,前者是被修改过的版本,并且经过 Firebug 规则化的,例如 attribute 都有引号扩起来等,而后者通常才是你的 spider 爬到的原始内容。如果是使用正则表达式对页面进行分析或者所用的 HTML Parser 和 Firefox 的有些出入的话,需要特别注意),另外,在一个 class 为 wp-pagenavi 的 div 里有到不同列表页面的链接。
  2. 文章内容页面,每篇 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 这个异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。整体架构如下图所示:

网络爬虫by pluskid

绿线是数据流向,首先从初始 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 ,另外,还可以找到“下一页”的链接所在,连同其他几个页面的链接一同在一个 div 里,不过“下一页”的链接没有 title 属性,因此 XPath 写作

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

前半部分是解析需要抓取的 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 的内容:

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 将两个信号连接到指定的函数上,分别用于初始化和关闭数据库连接(在 close 之前记得 commit ,似乎是不会自动 commit 的,直接 close 的话好像所有的数据都丢失了 dd-.-)。当有数据经过 pipeline 的时候,process_item 函数会被调用,在这里我们直接讲原始数据存储到数据库中,不作任何处理。如果需要的话,可以添加额外的 pipeline ,对数据进行提取、过滤等,这里就不细说了。

最后,在 settings.py 里列出我们的 pipeline :

ITEM_PIPELINES = ['blog_crawl.pipelines.SQLiteStorePipeline']

再跑一下 crawler ,就 OK 啦! 网络爬虫by pluskid 最后,总结一下:一个高质量的 crawler 是极其复杂的工程,但是如果有好的工具的话,做一个专用的 crawler 还是比较容易的。Scrapy 是一个很轻便的爬虫框架,极大地简化了 crawler 开发的过程。另外,Scrapy 的文档也是十分详细的,如果觉得我的介绍省略了一些东西不太清楚的话,推荐看他的 Tutorial 。

网络爬虫by pluskid的更多相关文章

  1. Python初学者之网络爬虫(二)

    声明:本文内容和涉及到的代码仅限于个人学习,任何人不得作为商业用途.转载请附上此文章地址 本篇文章Python初学者之网络爬虫的继续,最新代码已提交到https://github.com/octans ...

  2. 网络爬虫:使用Scrapy框架编写一个抓取书籍信息的爬虫服务

      上周学习了BeautifulSoup的基础知识并用它完成了一个网络爬虫( 使用Beautiful Soup编写一个爬虫 系列随笔汇总 ), BeautifulSoup是一个非常流行的Python网 ...

  3. 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(3): 抓取amazon.com价格

    通过上一篇随笔的处理,我们已经拿到了书的书名和ISBN码.(网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(2): 抓取allitebooks.com书籍信息 ...

  4. 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(2): 抓取allitebooks.com书籍信息及ISBN码

    这一篇首先从allitebooks.com里抓取书籍列表的书籍信息和每本书对应的ISBN码. 一.分析需求和网站结构 allitebooks.com这个网站的结构很简单,分页+书籍列表+书籍详情页. ...

  5. 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(1): 基础知识Beautiful Soup

    开始学习网络数据挖掘方面的知识,首先从Beautiful Soup入手(Beautiful Soup是一个Python库,功能是从HTML和XML中解析数据),打算以三篇博文纪录学习Beautiful ...

  6. Atitit.数据检索与网络爬虫与数据采集的原理概论

    Atitit.数据检索与网络爬虫与数据采集的原理概论 1. 信息检索1 1.1. <信息检索导论>((美)曼宁...)[简介_书评_在线阅读] - dangdang.html1 1.2. ...

  7. Java 网络爬虫获取页面源代码

    原博文:http://www.cnblogs.com/xudong-bupt/archive/2013/03/20/2971893.html 1.网络爬虫是一个自动提取网页的程序,它为搜索引擎从万维网 ...

  8. &lbrack;Search Engine&rsqb; 搜索引擎技术之网络爬虫

    随着互联网的大力发展,互联网称为信息的主要载体,而如何在互联网中搜集信息是互联网领域面临的一大挑战.网络爬虫技术是什么?其实网络爬虫技术就是指的网络数据的抓取,因为在网络中抓取数据是具有关联性的抓取, ...

  9. &lbrack;Python&rsqb; 网络爬虫和正则表达式学习总结

    以前在学校做科研都是直接利用网上共享的一些数据,就像我们经常说的dataset.beachmark等等.但是,对于实际的工业需求来说,爬取网络的数据是必须的并且是首要的.最近在国内一家互联网公司实习, ...

随机推荐

  1. squid清除缓存

    客户经常要求清除缓存  由于CDN后台只能支持单个url的 所以目录级别的只能用脚本 example:清除 www.123.com 下所以的缓存 #!/bin/bash TODAY=`date +%Y ...

  2. FileUpload1上传控件

    <asp:FileUpload ID="FileUpload1" runat="server" /> string fn = System.IO.P ...

  3. run a Freight robot &lpar;3&rpar;

    5.Logging In Once the robot is turned on and the robot is on the network, ssh into the computer of t ...

  4. Oracle的体系结构

    前言 这个章节主要想说的是Oracle的体系结构,这个也是理论强些.还有一些比较理论的知识点(比如表空间啊),就暂时先不写了,下一章节开始进入Oracle的操作阶段,比如表的查询啊.插入以及重点是和S ...

  5. 【matlab】matalb生成dll给Cpp用

    http://blog.csdn.net/scudz/article/details/13628917 这篇文章写得很好,我跟据这个,操作了一下,发现,好用,精简版总结如下 1. matlab打开一个 ...

  6. C&num;使用DataSet类、DataTable类、DataRow类、OleDbConnection类、OleDbDataAdapter类编写简单数据库应用

    //注意:请使用VS2010打开以下的源代码. //源代码地址:http://pan.baidu.com/s/1j9WVR using System; using System.Collections ...

  7. C&plus;&plus;采用模板实现栈的方法

    今天又看了遍<effective C++>,手动实现了一下条款42中的栈,贴出来当博客的处女贴. 首先栈的声明如下,采用了模板传入类型,而栈的底层采用是个链表. // stack.h // ...

  8. flask开发表单

    from flask import Flask from flask import render_template from flask import request from flask impor ...

  9. 2025战略,中秋送福利!免费开源ERP Odoo Windows 一键傻瓜式安装版发布

    概述 为了帮助更多的小白,能够快速的体验Odoo的强大功能,针对国内大多数小白用户无法快速直接体验Odoo的尴尬,开源智造呕心沥血,历经数百次的测试和整合终于赶在中秋节这天将Odoo所需要的复杂运行环 ...

  10. mysql中主键和唯一键的区别

    区别项 primary key(主键) unique(唯一键约束) 唯一性 可以 可以 是否可以为空 不可以 可以 允许个数 只能有1个 允许多个 是否允许多列组合 允许 允许