第三章–开始爬行吧
到目前为止,本书例子覆盖了单一的静态网页,含有些人工修饰的例子。本章中我们将面对真实世界的问题,让爬虫穿越在多个网页甚至多个网站上。
其之所以叫做网络爬虫,是因为它们在网络上爬行。爬虫的核心是循环(递归)。从一个URL获取网页内容,检查网页内容从中获取另一个新的URL链接,再去这个新的URL上去获取网页内容,如此循环。
然而需要注意的是,你可以爬取网页,但不意味你总是应当这样做。前一章的例子中爬虫在一个网页上爬取所有数据,这是中很少见的情况。在使用网络爬虫的时候,你必须非常小心爬虫占用多少带宽,并且努力探索是否有让目标服务器更容易加载的方法。
单一域名内的爬取
即使你没有听说过“六度维基百科”,你或许很确定听说其同名的”Six Degrees of Kevin Bacon”. 在两个游戏中,都可以将两个完全不相干的事务用总数不超过六个(包括起始结束两个主题)节点的链联系起来(第一个例子是Wikipadia文章,第二个例子是出现在电影里面的不同的人)。
例如,Eric Idle 和 Brendan Fraser 都出现在电影 Dudley Do-Right 中,而Brendan Fraser 和 Kevin Bacon出现在电影 The Air I Breathe 中。在这个例子中,从Eric Idle到Kevin Bacon的链上只有三个节点。在这一节中,我们将实现一个“Six Degrees of Wikipedia”解决方案的搜寻器。这个搜寻器能够找出从Eric Idle 页面到Kevin Bacon页面最少的链接点击数。
但是维基百科的服务器负载如何呢?
根据维基百科的上层组织---------维基媒体基金会-------的数据,大约每秒接受2500次查询(本书出版于2015年,所引用数据可能还要早得多),其中99%以上的数据都是指向维基百科网站。
鉴于如此大的数据量,你的网络爬虫不太可能对维基百科服务器产生显著影响。但是,如果你需要更广泛的按照本书示例运行代码,或者搭建你自己的项目来爬取维基百科,我鼓励你捐助维基媒体基金会(这也可减免税款)-----------即使几美元也可以,用来补偿服务器的负载,也可以帮助其他人更好的获取教育资源。
你应该知道怎样去写一个能检索任一Wiki百科的页面和页面上的任何产品的Python脚本。
from urllib.request import urlopen
from bs4 import BeautifulSoup
html=urlopen("https://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html,'lxml')
for link in bsObj.findAll('a'):
if 'href' in link.attrs:
print(link.attrs['href'])
如果查看产生的链接,你会注意到你希望获取的主题类似于“Apollo 13”、“Philadelphia”、“Primetime Emmy Award”。有些不是我们需要的,比如说:
//wikimediafoundation.org/wiki/Privacy_policy
//en.wikipedia.org/wiki/Wikipedia:Contact_us
实际上,维基百科网页上充满了工具条、侧边栏、页脚、页眉等链接,这些链接没有主题(article)。同时也存在分类页、会话页、和其他没有什么不同的链接,这些链接也没有主题(article),也不是我们需要的,又比如:
/wiki/Category:Articles_with_unsourced_statements_from_April_2014
/wiki/Talk:Kevin_Bacon
现在我一个朋友,就在写一个维基百科的爬虫,他写了个很大的超过100行的函数用来识别一个维基百科的链接是否是一个主题链接。不幸的是他没有花时间去寻找“article link”和“other link”在模式上的区别,他本应该发现这些小把戏的。当你检查这些指向article页面的链接(和指向其他内部链接对比),指向article页面的链接有三点共同处:
- 这些链接都在属性id=”bodyContent”的div标签之中。
- 这些article的URL中不含冒号(原文是 semicolon 分号,但是从上下文描述及代码推断作者所言应是colon,故翻译为冒号)。
- 这些URL以 /wiki/ 开头。
我们可以依照以上三条规则,稍微修改代码就可以获取期望的链接了:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = opener.open("https://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html, 'lxml')
for link in bsObj.find("div",{"id":"bodyContent"}).findAll("a", href = re.compile("^(/wiki/)((?!:).)*$")):
if 'href' in link.attrs:
print(link.attrs['href'])
如果运行这个,将会输出Kevin Bacon上所有维基百科的主题的链接。当然,通过脚本,在一个硬编码的维基百科主题上,找到该页面的所有主题链接,很有趣,同时实际上是毫无用处的。我们需要将这个代码,转化成更加类似以下形式的代码:
- 一个函数getLinks,接受格式为/wiki/<Article_Name>的形式的维基百科URL(常用正则表达式),并且返回一个符合格式的所有主题链接的列表。
- 一个主函数,从某一主题开始调用getLinks函数,从所返回的主题链接列表中,随机选择一个链接,再次调用getLinks函数,直至我们主动停止程序或者在新的页面上没有找到主题链接。完成后代码如下:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re
random.seed(datetime.datetime.now())
def getLinks(articleUrl):
html = urlopen("https://en.wikipedia.org" + articleUrl)
bsObj = BeautifulSoup(html, 'lxml')
return bsObj.find("div",{"id":"bodyContent"}).findAll("a", href = re.compile("^(/wiki/)((?!:).)*$"))
links = getLinks("/wiki/Kevin_Bacon")
while(len(links) > 0):
newArticle = links[random.randint(0, len(links) - 1)].attrs["href"]
print(newArticle)
links = getLinks(newArticle)
首先,程序导入必要的python库,用当前系统时间设置随机数种子。这事实上保证了每次运行程序,都能产生一个新的、有趣的、随机的路径,去获取维基百科主题。
伪随机数和随机种子
在上一个例子中,我用Python随机数发生器随机选择页面上的主题,在维基百科上产生一个随机且可持续的爬取路径。然而,使用随机数应当警惕。
计算机擅长于计算正确答案,而不是伪造。基于这一原因,产生随机数是个挑战。大多数随机数算法产生一个均匀分布的且难以预测的数列,但是都需要给这些算法一个“种子”作为起始。完全相同的种子会产生出完全相同的随机数序列,所以我用系统时钟,来作为产生一个新的随机数序列的起始条件,也因此爬取新的随机主题序列。这让运行这个程序有一点小激动,因为每次都不一样。
Python的伪随机数发生器是基于梅森缠绕算法(Mersenne Twister algorithm)。当它产生随机数的时候,是较难预测且是正态分布的。所以,真正的随机数这种东西并不容易获取。
接下来,定义了getLinks函数,这个函数接受形似/wiki/…格式的主题的URL,预先考虑到维基百科的域名:http://en.wikipedia.org,然后在该域名上读取HTML页面得到BeautifulSoup对象。然后按照前面所述的参数条件,抽取出主题链接的一个列表,并且返回这个列表。主程序的起始处,就用初始页面(https://en.wikipedia.org/wiki/Kevin_Bacon)上所含的链接列表,去赋值给主题链接tag的列表(links变量)。然后进入循环,在页面上找到一个随机的主题链接,抽取其href属性,打印,并且获取到一个新的URL列表。当然,解决“六度维基百科”问题,比起写一个爬虫从一个页面到另一个页面的爬取,要困难一些。我们必须能否存储和分析结果。这将在第5章予以解决。
处理异常
在例子中,为了简略了大部分异常处理,但是需要注意的是有许多潜在危险可能发生:如果维基百科改变了bodyContent的tag名呢?(提示:程序会崩溃)
尽管这些脚本作为例子在严密关注下运行,但是自主研发的代码需要更多的异常处理,这超出了本书所提供的示例。可以回看第一章获取更多相关信息。
爬取整个网站
之前的章节中,我们在一个网站中用随机路径,从一个链接到另一个链接。但是如果你需要网站的系统分类或者搜索每一个页面呢?爬取整个网站,特别是对于大网站来说,是一个内存密集型(memory-intensive)的过程,最好使用程序很容易的访问数据库来存储结果。然而,我们将会探寻这些类型的程序行为,而不会真正的完全大规模的运行。如果需要更多的了解用程序访问数据库,请参考第五章。
Dark web和Deep web
你或许听说过deep web,dark web或者 hidden web,它们是什么意思呢?
Deep web是互联网中不属于表面网络(surface web)之外的所有web,约占互联网总量的90%。迈克尔·伯格曼将当今互联网上的搜索服务比喻为像在地球的海洋表面的拉起一个大网的搜索,巨量的表面信息固然可以通过这种方式被查找得到,可是还有相当大量的信息由于隐藏在深处而被搜索引擎错失掉。绝大部分这些隐藏的信息是须通过动态请求产生的网页信息,而标准的搜索引擎却无法对其进行查找。传统的搜索引擎“看”不到,也获取不了这些存在于深网的内容,除非通过特定的搜查这些页面才会动态产生。于是相对的,深网就隐藏了起来。据估计,深网要比表面网站大几个数量级。
Dark web,也叫做Darknet或者Dark Internet,完全是野兽。其在现有的网络架构上运行,但是需要使用在HTTP承载之上的应用协议的Tor客户端访问,以求在安全通道中交换信息。虽然也可以爬取,但是不在本书范围之内。
Deep Web是指所有搜索引擎无法触及的网站的集合,它除了包括Dark Web还包括更多普通的内容,比如注册网站账号时弹出的需要填写的表单页面和动态创建的页面。Dark Web占整个互联网的比重非常小,可能连0.1%都不到。
和Dark web不同,爬取Deep web要相对容易一些。在本书中有很多工具,用来告诉你怎样爬取那些Google爬虫无法获取的Dark web信息。
下面这个图是译者从baidu百科截取的,希望会加深你对以上概念的了解。
什么时候爬取整个网站是有益的,什么时候是有害的呢?网络爬虫穿过整个网站有许多好处,包括:
- 生成网站地图。
几年前,我遇到一个问题。一个重要的客户需要评估其网站,以便重新设计网站,但是不想给我公司提供访问他们当前内容管理系统的权限,也没有可公开获取的网站地图。我采用爬虫覆盖了它们整个网站,收集内部链接,并且将网页组织成文件夹格式的结构,以展示他们网站的结构。这允许我迅速的找到那些不曾注意的节点,并且精确的计算需要设计的网页数目,以及有多少内容需要从旧页面上被迁移到新的网页。 - 收集信息。
另一个客户希望收集主题信息(故事、博客、新闻、等等),以便于做过一个特别的搜索平台原型。虽然这些网站爬虫不需要做到完全详尽爬取,但是最好能更广范围爬取(对于那些我们感兴趣的网站)。我让爬虫在那些网站间穿梭,只收集那些感兴趣的主题信息。
详尽爬取一个网站的一般方法,是从一个最顶的页面(主页)开始,搜索这个页面的内部链接。然后爬取所有内部链接,找到每一个这些链接网页上的所有内部链接,再执行下一轮爬取。很明显,这种情况下任务会爆炸似的增长。如果每个页面有10个内部链接,一个网站假设有5层(对于中型大小的网站),其需要爬取的页面数量就很快会达到10的5次方,即100000个页面。说来也奇怪,一个典型的“5层深和每个页面10个链接”的网站,很少能有100000个或者以上的页面。究其原因,是绝大多数内部链接都是重复的。为了避免爬取重复的页面,很重要的是发现的所有内部链接都归一化处理,并添加到爬取列表中便于查询。只有那些新的链接才会被爬取和在其中搜索额外的链接:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages=set()
def getLinks(pageUrl):
global pages
html = urlopen("https://en.wikipedia.org" + pageUrl)
bsObj = BeautifulSoup(html, 'lxml')
for link in bsObj.findAll("a", href = re.compile("^(/wiki/)")):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
# We have encountered a new page
newPage = link.attrs['href']
print(newPage)
pages.add(newPage)
getLinks(newPage)
getLinks("")
为了完全展示爬虫工作的效果,我在这放松了上一个例子中的限制。不是去爬取主题列表,而是搜寻所有的以/wiki/开头的内部链接,也不管其是否含有冒号(colons)。记住,主题页不含冒号,但是文档上传页,对话页,是含有冒号的。最初getLinks函数接受一个空字符串的URL。这就是维基百科的主页。然后,在这个主页上的每个链接被循环访问,并且检查其是否在pages这个全局集合变量里面(这个集合包含那些已经遇到的网页)。如果没有遇到过,则会将其加入到列表中,输出到屏幕,再次递归调用getLinks函数。
一个关于递归的警告
在很多软件书上,很少提到这个警告,但是我还是想提醒大家:如果运行太长时间,这个程序会奔溃的。Python有一个默认为1000的递归限制(一个程序能递归调用他自己的次数)。
所以象维基百科这样非常大的网站,程序最终可能会达到递归限制而停止运行,除非你用递归计数器或者其他的方法来阻止其发生。对于一个扁平的网站,远没有1000的深度,这个函数一般会运行的很好,当然很可能附带一些不常见的异常。
例如,我遇到一个网站有条产生博客链接的规则。其对于当前页面,总是附加一个/blog/title_of_blog.php来一个博客链接。问题就在于对于已经在一个含有/blog/的URL中,其也会在后面附加/blog/title_of_blog.php。
到最后,我的爬虫访问了类似/blog/blog/blog/blog/blog/blog/blog/blog/blog.../blog/title_of_blog.php的页面。最终我添加了一个检查,来避免这极度可笑的,不断添加重复字节的情况发生。
如果不检查的整天运行,程序是很可能崩溃的。
从全站收集信息
如果网络爬虫能做的只是从一个网页跳到另一个网页,那是相当无聊的。为了使得他们更有用,我们需要在所爬取的网页上做些什么。让我们看看,怎样写一个可以收集诸如标题、正文第一段内容、以及页面编辑链接(如果有的话)的爬虫。如同平常一样,第一步应当决定怎样做好这件事:查看一些页面,归纳出一些模式。查看了一些维基百科页面(既有主题页,也有非主体页比如隐私政策页面),如下的事情应该清晰了:
- 所有标题(在所有页面上,不管他们的状态是主题页面、编辑历史页面、或者其他页面)都在h1标签下,并且一个页面只有一对h1标签。
- 如前所述,所有的body对应文本都在div#bodyContent这个标签之下。然而,如果我们只要某些特定的比如第一段文本,或许更好的是用 div#mw-content-text -> p 来获取(只选择第一段的标签)。除不含段落内容的文件页面外,这种方法对其他的页面都是有效的。
- 编辑链接只在主题页面上存在。如果存在,可以通过 li#ca-edit 标签来获取,其路径为 li#ca-edit -> span -> a。
通过修改最基本的爬虫代码,我们可以写出一个结合爬虫/数据收集(或者说至少是数据打印)的程序:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages=set()
def getLinks(pageUrl):
global pages
html = urlopen("https://en.wikipedia.org" + pageUrl)
bsObj = BeautifulSoup(html, 'lxml')
try:
print(bsObj.h1.get_text())
print(bsObj.find("div", id="mw-content-text").findAll("p")[0].get_text())
print(bsObj.find(id="ca-edit").find("span").find("a").attrs['href'])
except AttributeError:
print("This page is missing something! No worries though!")
for link in bsObj.findAll("a", href = re.compile("^(/wiki/)")):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
# We have encountered a new page
newPage = link.attrs['href']
print("---------------\n"+newPage)
pages.add(newPage)
getLinks(newPage)
getLinks("")
上面程序中的for循环本质上和以前程序中的原始爬虫是一样的(这里用破折号分开打印内容,更清晰一些)。因为我们不能保证每个页面上都有这些待打印的信息,所以按照它们出现的可能性来打印。<h1>标签出现在每个页面上,所以我们第一个获取。段落内容出现在大多页面上(除了文件页面),所以排在第二来获取。有edit按钮的是在标题和文本都存在的页面才可能出现,但不一定会出现在所有含有标题和文本的页面上。
针对不同需要,使用不同模式
很明显,将几行可能都发生异常的代码,打包写在一个异常处理中是危险的。
当一个异常发生时,你不知道到底是哪一行抛出异常。
而且,如果一个页面有edit按钮但是没有标题,则在上面的程序中这个edit的内容永远不会被记录。
然而,对于许多页面的例子,这种顺序都能够满足,而非故意的丢弃或保存一些数据就也不是问题。
你或许已经注意到,在这个及以前的例子中,我们并没有收集数据,顶多是打印了它。显然,数据在终端是比较难处理的。我们将在第五章进一步讨论存储数据和创建数据库。
在互联网上爬行
无论何时我谈起网络爬行,总有人会问:“怎样才能制造一个Google(那么多的数据)”。我的回答总是两方面的:“要么,你可以有百亿美元能够买到这个世界上最大的数据库并把它隐藏在世界各地。要么,你可以创造一个网络爬虫。”
当Google从1994年创立的时候,仅仅是两个斯坦福的研究生,用一台很旧的服务器和一个Python网络爬虫。现在的你,可以从官方获取你需要的工具,来成为下一个基于技术的百万富翁。
严肃地说,网络爬虫是驱动很多现代网络技术的核心,而且你并不需要拥有且使用一个很大的数据库。做任何跨领域的数据研究,你只需要搭建爬虫,并且从因特网上的无数不同网页上解释和储存数据即可。
和前面的例子相似,我们将要搭建的网络爬虫也会一页一页的爬取,直到生成一个网络地图。但是这一次,网络爬虫不会忽略外部链接,它们会顺着爬下去。作为额外的挑战,我们将看看是否可以记录爬虫访问的每个页面的一些种类的信息。这比爬取单一域名的工作更有难度——因为网站有完全不同的页面布局。这意味着,对那些我们寻找的信息,以及怎样去找到它们,我们的爬虫必须有足够的灵活性。
未知水域
从这节开始,我们的代码可以在互联网上任意爬行。
如果我们了解过“六度维基百科”就知道,“六度维基百科”认为可以从一个网站(例如:http://www.sesamestreet.org/)通过少许几跳就可以得到你想得到的内容(一些开胃菜)。
未成年人在运行这些代码之前需告知父母。对于那些限制了敏感信息或者有宗教禁忌的家庭来说,可能需要禁止看到色情网站上的文本。
可以遵从你所阅读的代码,但是运行的时候还需要特别小心。
不管三七二十一,当你开始写一个爬虫访问外部链路的时候,你需要问你自己几个问题:
- 我需要收集什么信息?这些信息能从预定的网站上爬取完成么(从预订网站上爬取,几乎是最简单的选项),还是说爬虫需要从我不知道的新网站上收集信息?
- 当爬虫遇到一个特殊的网站,他会立即沿着外部链接到另一个网站,还是顺着当前的网站往下爬?
- 有条件去识别那些我不想爬取的网站吗?我是否在意非英语的内容?
- 当我的网络爬虫在爬取网站时,收到网络管理员的警告,我该如何保护自己免遭法律诉讼?(请参考附录C以获取更多信息)
一些很容易实现的Python函数(每个函数都少于50行代码)结合起来,去执行不同类型的网站爬取工作。
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import datetime
import random
from urllib.error import *
pages=set()
random.seed(datetime.datetime.now())
# Retrieves a list of all Internal links found on a page
def getInternalLinks(bsObj, includeUrl):
internalLinks = []
# Find all links that begin with a "/"
for link in bsObj.findAll("a", href=re.compile("^(/|.*" + includeUrl + ")")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in internalLinks:
internalLinks.append(link.attrs['href'])
return internalLinks
# Retrieves a list of all external links found on a page
def getExternalLinks(bsObj, excludeUrl):
externalLinks = []
# Find all links that start with "http" or "www" that do
# not contain the current URL
for link in bsObj.findAll("a", href=re.compile("^(http|www)((?!" + excludeUrl + ").)*$")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in externalLinks:
externalLinks.append(link.attrs['href'])
return externalLinks
def splitAddress(address):
# HTTP = re.compile("(http|https)://")
addressParts = address.replace("http://", "").split("/")
return addressParts
def getRandomExternalLink(startingPage):
html = urlopen(startingPage)
bsObj = BeautifulSoup(html, 'lxml')
externalLinks = getExternalLinks(bsObj, splitAddress(startingPage)[0])
if len(externalLinks) == 0:
internalLinks = getInternalLinks(startingPage)
return getRandomExternalLink(internalLinks[random.randint(0, len(externalLinks)-1)])
else:
return externalLinks[random.randint(0, len(externalLinks)-1)]
def followExternalOnly(startingSite):
externalLink = getRandomExternalLink("http://oreilly.com")
print("Random external link is: "+ externalLink)
followExternalOnly(externalLink)
followExternalOnly("http://oreilly.com")
这个程序从 http://oreilly.com 开始,随机的在外部链接之间跳转。这里有一个输出的例子:
Random external link is: https://www.safaribooksonline.com/static/corp/safari_transformation_whitepaper.pdf
Random external link is: https://www.safaribooksonline.com/static/corp/safari_transformation_whitepaper.pdf
Random external link is: http://fb.co/OReilly
Random external link is: https://www.linkedin.com/company/o%27reilly-media
Random external link is: https://www.linkedin.com/company/o%27reilly-media
因为并不总是能在网站首页上找到外部链接。所以在本例中,为了找到外部链接,一个函数被用来在网站向下寻找直到找到一个外部链接。
注意:
上面的程序很容易达到Python的递归上限。例如,可以加计数器,在1000后报错:
Random external link 1000 is: https://www.linkedin.com/company/o%27reilly-media
Fatal Python error: Cannot recover from stack overflow.
图3-1展示其流程
提醒:不要将试验程序用于产品
我始终提醒,由于缺乏必要的检查和异常处理,试验程序可能达到Python递归上限。
所以用于产品等严肃的目的之前,需要保证有足够的检查去处理潜在的失败。
将任务分解为简单的函数,例如“找到页面上所有的外部链接”的好处是,代码容易重构用于另外的爬虫任务。例如,目标是爬取整个网站的外部链接,并且对每一个进行标注,我们可以添加如下函数:
# Collects a list of a external URLs found on the site
allExtLinks = set()
allIntLinks = set()
def getAllExternalLinks(siteUrl):
html=urlopen(siteUrl)
bsObj=BeautifulSoup(html,"html.parser")
internalLinks=getInternalLinks(bsObj,splitAddress(siteUrl)[0])
externalLinks=getExternalLinks(bsObj,splitAddress(siteUrl)[0])
for link in externalLinks:
if link not in allExtLinks:
allExtLinks.add(link)
print(link)
for link in internalLinks:
if link not in allIntLinks:
print("About to get link: " + link)
allIntLinks.add(link)
getAllExternalLinks(link)
getAllExternalLinks("http://oreilly.com")
这部分代码可看成两个循环——一个用于收集内部链接,一个用于收集外部链接——两个协同工作。流程图如3-2:
在写代码之前,画草图或者流程图是很好的习惯,这能够节约你大量的时间,也会减少因你的爬虫复杂度的产生的挫折感。
处理重定位
重定位允许在不同的域名下查看同样的内容。重定位有两种情况:
1,服务器端重定位,在页面加载之前就完成了URL的重定位。
2,客户端重定位,有时候在页面加载之前,看到的“You will be directed in 10 seconds...”的信息。
这一节将要处理服务器端的重定位。对于客户端的重定位,主要是用JavaScript或者HTML来实现的,请参考Chapter 10
对于服务器端的重定位,一般来说无须担心。
如果使用Python 3.X中的urllib库,其会自动处理重定位。只需要意识到,某些情况下,你所正在爬取的URL并不是你点击访问的URL。
用Scrapy库来爬取
写爬虫的一个挑战是,你需要一遍又一遍的重复执行相同的任务:在页面找寻链接,评估内部链接和外部链接的不同,爬取新的网页。知道这些基本模式是有用的,能帮助你从无到有的写爬虫,但这里也有能帮你处理这些细节的其他选项。
Scrapy是一个Python库,能够处理许多纷繁复杂的任务,比如毫不费力的查找链接和评估链接、爬取域名或者列出域名。不幸的是,Scrapy并没有在Python 3.x上发布相应的版本,而是兼容在Python 2.7(本书出版的时间是2015年,但现在是2017年,哦耶)。好消息是在同样的机器上安装多个Python版本(Python 2.7 和 3.4)常常能兼容运行得很好。如果你要使用Scrapy,又想使用Python 3.4的话,不会存在什么问题。可以从 https://scrapy.org/download/ 下载 Scrapy,也可以通过第三方库安装管理工具比如 pip 来安装Scrapy。需要记住的是,你需要用Python 2.7来安装 Scrapy(Scrapy不兼容 2.6 或者 3.x ),并且运行Scrapy的程序也需要用Python 2.7。(从 Scrapy网站来的好消息:What Python versions does Scrapy support? Scrapy is supported under Python 2.7 and Python 3.3+. Python 2.6 support was dropped starting at Scrapy 0.20. Python 3 support was added in Scrapy 1.1. Note: Python 3 is not yet supported on Windows。所以我们不用担心Python 3.3+了,直接去下载相应的Scrapy库安装吧。虽然你会在windows系统上遇到某些安装依赖,但是不用担心,google去解决吧。提示:可以安装Visual Studio来解决依赖。)
尽管写一个Scrapy的爬虫相对简单,但是对于每个爬虫来说,仍需要先完成一些设置。在当前文件夹下创建一个新的Scrapy项目,执行命令行:
$ scrapy startproject wikiSpider
wikiSpider是新项目名。该命令行会在当前目录下,创建一个新的叫做wikiSpider的目录。该目录下的结构如下:
- scrapy.cfg
wikiSpider
- __init.py__
- items.py
- pipelines.py
- setting.py
- spiders
- __init.py__
为了创建一个爬虫,我们添加一个文件 wikiSpider/wikiSpider/spiders/articleSpider.py 来调用 items.py , 我们在 items.py 中定义了一个新的类 Article 。
你的items.py文件应该是这样的(关于生成的Scrapy注释,可以保留也可以随意删除)
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html
from scrapy import Item, Field
class Article(Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = Field()
每一个Scrapy Item对象代表网站上的一个单独页面。很显然,你可以定义足够多的fields(url,content,header,image,等),但现在在这里,我只收集每个页面的title。
在你新建的articleSpider.py文件中,有如下代码:
from scrapy.selector import Selector
from scrapy import Spider
from ..items import Article
class ArticleSpider(Spider):
name = "article"
allowed_domains = ["en.wikipedia.org"]
start_urls = ["http://en.wikipedia.org/wiki/Main_Page","http://en.wikipedia.org/wiki/Python_%28programming_language%29"]
def parse(self, response):
item = Article()
title = response.xpath('//h1/text()')[0].extract()
print("Title is: " + title)
item['title'] = title
return item
这个对象名(ArticleSpider)和目录名(wikiSpider)不同,其意义在于,在wikiSpider这个项目中,这个类是仅仅用于爬取主题页面。对于有很多内容的大型网站,你应当将Scrapy元素逐一分开(博客公告、新闻稿、主题页,等等),每一类都用不同的名字,但是都在同一个Scrapy工程下运行。
你可以在主目录wikiSpider下面输入如下命令,来运行ArticleSpider:
$ scrapy crawl article
该命令通过article这个名字(是在ArticleSpider类中定义的 name=”article”,而非类名ArticleSpider或文件名articleSpider),来调用爬虫。以下信息会连同一些调试信息,打印出来:
Title is: Main Page
Title is: Python (programming language)
爬虫将这两个网页作为start_urls,收集信息,然后结束。上面这个爬虫例子并没有什么用,但是如果你有一些URL需要爬取,使用Scrapy能帮助你。将其扩展为一个完整成熟的爬虫,你需要定义一系列规则,Scrapy能用这些规则在遇到的页面中检索新的URL。
from scrapy.contrib.spiders import CrawlSpider, Rule
from ..items import Article
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
class ArticleSpider(CrawlSpider):
name = "article"
allowed_domain = ["en.wikipedia.org"]
start_urls = ["http://en.wikipedia.org/wiki/Python_%28programming_language%29"]
rules = [Rule(SgmlLinkExtractor(allow=('(/wiki/)((?!:).)*$'),), callback="parse_item", follow=True)]
def parse(self, response):
item = Article()
title = response.xpath('//h1/text()')[0].extract()
print("Title is: " + title)
item['title'] = title
return item
注意:使用Python 3.5.2可能报错:
File "..\scrapy\linkextractors\sgml.py", line 7, in <module>
from sgmllib import SGMLParser
ImportError: No module named 'sgmllib'
原因是sgmllib是2.6以后引入python,在3.0以后这个库被移除了。如果你的python版本<2.6或者>=3.0就找不到这个module。
如果你要使用已有的、依赖sgmllib的代码,安装python2.7等合适的版本。如果要迁移到3.0环境,需要移植代码,可以在sgml.py使用html.parser.HTMLParser来替代。
解决方法一:
# from sgmllib import SGMLParser #注释掉该行
from html.parser import HTMLParser as SGMLParser #增加一行
解决方法二:
try:
from sgmllib import SGMLParser
except:
from html.parser import HTMLParser as SGMLParser
和前一个一样,用命令行运行该爬虫,但是这个爬虫不会结束(至少在很长时间内),除非你用Crtl+C或者关闭终端来结束它。
记录Scrapy
Scrapy的调试信息很有用,但是有时候太冗长了。你可以在setting.py文件中,加入一行,来调整日记记录等级。例如:
LOG_LEVEL = 'ERROR'
Scrapy有5个日志等级,顺序如下:
- CRITICAL
- ERROR
- WARNING
- DEBUG
- INFO
如果等级设置为ERROR,则只有CRITICAL和ERROR的日志会显示出来。如果等级设置为INFO,则所有的日志都会显示。如此类推。
将日志从终端输出重定位到一个单独的日志文件,可以在运行命令行中简单的定义一个日志文件:
$ scrapy crawl article -s LOG_FILE=wiki.log
如果日志文件不存在,这条命令将在执行命令的当前目录创建一个新的日志文件,并将所有的日志和状态打印输出到该文件。
Scrapy用Item对象来决定需要从其访问的页面中保存哪些信息。这些信息可被Scrapy存储成很多种格式的文件,例如CSV,JSON,或者XML文件:
$ scrapy crawl article -o articles.csv -t csv
$ scrapy crawl article -o articles.json -t json
$ scrapy crawl article -o articles.xml -t xml
当然,仅需要在解析函数里面加入合适的代码,你就可以随意使用Item对象,将其写入到你期望的文件或者数据库中。
Scrapy是一个非常强大的网络爬虫。它能自动的收集URL,与预先定义的规则进行对比,保证所有的URL都是唯一的,将需要的URL标准化,递归的深入到更多页面。
这一节只简单介绍了Scrapy是什么,所以我强烈推荐你查阅Scrapy的文档,或者从线上获取其他资料。Scrapy是一个极其大型的且不断扩张的库,其有很多特性。如果你想用Scrapy来实现的工作,这里没有提到,那么一定有一种(或者数种)方法通过Scrapy来实现它。