「Python」教你编写网络爬虫(用python写网络爬虫)
1.网络爬虫何时有用
假设我有一个鞋店,并且想要及时了解竞争对手的价格。我可以每天访问他们的网站,与我店铺中鞋子的价格进行对比。但是,如果我店铺中的鞋类品种繁多,或是希望能够更加频繁地查看价格变化的话,就需要花费大量的时间,甚至难以实现。再举一个例子,我看中了一双鞋,想等它促销时再购买。我可能需要每天访问这家鞋店的网站来查看这双鞋是否降价,也许需要等待几个月的时间,我才能如愿盼到这双鞋促销。上述这 两个重复性的手工流程,都可以利用网络爬虫技术实现自动化处理。
理想状态下,网络爬虫并不是必须品,每个网站都应该提供API,以结构化的格式共享它们的数据。然而现实情况中,虽然一些网站已经提供了这种API,但是它们通常会限制可以抓取的数据,以及访问这些数据的频率。另外,对于网站的开发者而言,维护前端界面比维护后端API接口优先级更高。总之,我们不能仅仅依赖于API去访问我们所需的在线数据,而是应该学习一些网络爬虫技术的相关知识。
2. 网络爬虫是否合法
网络爬虫目前还处于早期的蛮荒阶段,“允许哪些行为”这种基本秩序还处于建设之中。从目前的实践来看,如果抓取数据的行为用于个人使用,则不存在问题;而如果数据用于转载,那么抓取的数据类型就非常关键了。
世界各地法院的一些案件可以帮助我们确定哪些网络爬虫行为是允许的。在Feist Publications, Inc.起诉Rural Telephone Service Co.的案件中,美国联邦最高法院裁定抓取并转载真实数据(比如,电话清单)是允许的。而在澳大利亚,Telstra Corporation Limited起诉Phone Directories Company Pty Ltd这一类似案件中,则裁定只有拥有明确作者的数据,才可以获得版权。此外,在欧盟的ofir.dk起诉home.dk一案中,最终裁定定期抓取和深度链接是允许的。
这些案件告诉我们,当抓取的数据是现实生活中的真实数据(比如,营业地址、电话清单)时,是允许转载的。但是,如果是原创数据(比如,意见和评论),通常就会受到版权限制,而不能转载。
无论如何,当你抓取某个网站的数据时,请记住自己是该网站的访客,应当约束自己的抓取行为,否则他们可能会封禁你的IP,甚至采取更进一步的法律行动。这就要求下载请求的速度需要限定在一个合理值之内,并且还需要设定一个专属的用户代理来标识自己。在下面的小节中我们将会对这些实践进行具体介绍。
关于上述几个法律案件的更多信息可以参考下述地址:
- http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vol=499&invol=340
- http://www.austlii.edu.au/au/cases/cth/FCA/2010/44.html
- http://www.bvhd.dk/uploads/tx_mocarticles/S_og_Handelsrettens_afg_relse_i_Ofir-sagen.pdf
3. 背景调研
在深入讨论爬取一个网站之前,我们首先需要对目标站点的规模和结构进行一定程度的了解。网站自身的robots.txt和Sitemap文件都可以为我们提供一定的帮助,此外还有一些能提供更详细信息的外部工具,比如Google搜索和WHOIS。
3.1 检查robots.txt
大多数网站都会定义robots.txt文件,这样可以让爬虫了解爬取该网站时存在哪些限制。这些限制虽然仅仅作为建议给出,但是良好的网络公民都应当遵守这些限制。在爬取之前,检查robots.txt文件这一宝贵资源可以最小化爬虫被封禁的可能,而且还能发现和网站结构相关的线索。关于robots.txt协议的更多信息可以参见http://www.robotstxt.org。下面的代码是我们的示例文件robots.txt中的内容,可以访问http://example.webscraping.com/robots.txt获取。
# section 1 User-agent: BadCrawler Disallow: / # section 2 User-agent: * Crawl-delay: 5 Disallow: /trap # section 3 Sitemap: http://example.webscraping.com/sitemap.xml
在section 1中,robots.txt文件禁止用户代理为BadCrawler的爬虫爬取该网站,不过这种写法可能无法起到应有的作用,因为恶意爬虫根本不会遵从robots.txt的要求。本章后面的一个例子将会展示如何让爬虫自动遵守robots.txt的要求。
section 2规定,无论使用哪种用户代理,都应该在两次下载请求之间给出5秒的抓取延迟,我们需要遵从该建议以避免服务器过载。这里还有一个/trap链接,用于封禁那些爬取了不允许链接的恶意爬虫。如果你访问了这个链接,服务器就会封禁你的IP一分钟!一个真实的网站可能会对你的IP封禁更长时间,甚至是永久封禁。不过如果这样设置的话,我们就无法继续这个例子了。
section 3定义了一个Sitemap文件,我们将在下一节中了解如何检查该文件。
3.2 检查网站地图
网站提供的Sitemap文件(即网站地图)可以帮助爬虫定位网站最新的内容,而无须爬取每一个网页。如果想要了解更多信息,可以从http://www.sitemaps.org/protocol.html获取网站地图标准的定义。下面是在robots.txt文件中发现的Sitemap文件的内容。
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>http://example.webscraping.com/view/Afghanistan-1 </loc></url> <url><loc>http://example.webscraping.com/view/Aland-Islands-2 </loc></url> <url><loc>http://example.webscraping.com/view/Albania-3</loc> </url> ... </urlset>
网站地图提供了所有网页的链接,我们会在后面的小节中使用这些信息,用于创建我们的第一个爬虫。虽然Sitemap文件提供了一种爬取网站的有效方式,但是我们仍需对其谨慎处理,因为该文件经常存在缺失、过期或不完整的问题。
3.3 估算网站大小
目标网站的大小会影响我们如何进行爬取。如果是像我们的示例站点这样只有几百个URL的网站,效率并没有那么重要;但如果是拥有数百万个网页的站点,使用串行下载可能需要持续数月才能完成,这时就需要使用第4章中介绍的分布式下载来解决了。
估算网站大小的一个简便方法是检查Google爬虫的结果,因为Google很可能已经爬取过我们感兴趣的网站。我们可以通过Google搜索的site关键词过滤域名结果,从而获取该信息。我们可以从http://www.google.com/advanced_search了解到该接口及其他高级搜索参数的用法。
图1所示为使用site关键词对我们的示例网站进行搜索的结果,即在Google中搜索site:example.webscraping.com。
从图1中可以看出,此时Google估算该网站拥有202个网页,这和实际情况差不多。不过对于更大型的网站,我们会发现Google的估算并不十分准确。
在域名后面添加URL路径,可以对结果进行过滤,仅显示网站的某些部分。图2所示为搜索site:example.webscraping.com/view的结果。该搜索条件会限制Google只搜索国家页面。
图1
图2
这种附加的过滤条件非常有用,因为在理想情况下,你只希望爬取网站中包含有用数据的部分,而不是爬取网站的每个页面。
3.4 识别网站所用技术
构建网站所使用的技术类型也会对我们如何爬取产生影响。有一个十分有用的工具可以检查网站构建的技术类型——builtwith模块。该模块的安装方法如下。
pip install builtwith
该模块将URL作为参数,下载该URL并对其进行分析,然后返回该网站使用的技术。下面是使用该模块的一个例子。
>>> import builtwith >>> builtwith.parse('http://example.webscraping.com') {u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'], u'programming-languages': [u'Python'], u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'], u'web-servers': [u'Nginx']}
从上面的返回结果中可以看出,示例网站使用了Python的Web2py框架,另外还使用了一些通用的JavaScript库,因此该网站的内容很有可能是嵌入在HTML中的,相对而言比较容易抓取。而如果改用AngularJS构建该网站,此时的网站内容就很可能是动态加载的。另外,如果网站使用了ASP.NET,那么在爬取网页时,就必须要用到会话管理和表单提交了。
3.5 寻找网站所有者
对于一些网站,我们可能会关心其所有者是谁。比如,我们已知网站的所有者会封禁网络爬虫,那么我们最好把下载速度控制得更加保守一些。为了找到网站的所有者,我们可以使用WHOIS协议查询域名的注册者是谁。Python中有一个针对该协议的封装库,其文档地址为https://pypi.python.org/pypi/python-whois,我们可以通过pip进行安装。
pip install python-whois
下面是使用该模块对appspot.com这个域名进行WHOIS查询时的返回结果。
>>> import whois >>> print whois.whois('appspot.com') { ... "name_servers": [ "NS1.GOOGLE.COM", "NS2.GOOGLE.COM", "NS3.GOOGLE.COM", "NS4.GOOGLE.COM", "ns4.google.com", "ns2.google.com", "ns1.google.com", "ns3.google.com" ], "org": "Google Inc.", "emails": [ "abusecomplaints@markmonitor.com", "dns-admin@google.com" ] }
从结果中可以看出该域名归属于Google,实际上也确实如此。该域名是用于Google App Engine服务的。当我们爬取该域名时就需要十分小心,因为Google经常会阻断网络爬虫,尽管实际上其自身就是一个网络爬虫业务。
4. 编写第一个网络爬虫
为了抓取网站,我们首先需要下载包含有感兴趣数据的网页,该过程一般被称为爬取(crawling)。爬取一个网站有很多种方法,而选用哪种方法更加合适,则取决于目标网站的结构。我们首先会探讨如何安全地下载网页,然后会介绍如下3种爬取网站的常见方法:
- 爬取网站地图;
- 遍历每个网页的数据库ID;
- 跟踪网页链接。
4.1 下载网页
要想爬取网页,我们首先需要将其下载下来。下面的示例脚本使用Python的urllib2模块下载URL。
import urllib2 def download(url): return urllib2.urlopen(url).read()
当传入URL参数时,该函数将会下载网页并返回其HTML。不过,这个代码片段存在一个问题,即当下载网页时,我们可能会遇到一些无法控制的错误,比如请求的页面可能不存在。此时,urllib2会抛出异常,然后退出脚本。安全起见,下面再给出一个更健壮的版本,可以捕获这些异常。
import urllib2 def download(url): print 'Downloading:', url try: html = urllib2.urlopen(url).read() except urllib2.URLError as e: print 'Download error:', e.reason html = None return html
现在,当出现下载错误时,该函数能够捕获到异常,然后返回None。
1.重试下载
下载时遇到的错误经常是临时性的,比如服务器过载时返回的503 Service Unavailable错误。对于此类错误,我们可以尝试重新下载,因为这个服务器问题现在可能已解决。不过,我们不需要对所有错误都尝试重新下载。如果服务器返回的是404 Not Found这种错误,则说明该网页目前并不存在,再次尝试同样的请求一般也不会出现不同的结果。
互联网工程任务组(Internet Engineering Task Force)定义了HTTP错误的完整列表,详情可参考https://tools.ietf.org/html/rfc7231#section-6。从该文档中,我们可以了解到4xx错误发生在请求存在问题时,而5xx错误则发生在服务端存在问题时。所以,我们只需要确保download函数在发生5xx错误时重试下载即可。下面是支持重试下载功能的新版本 代码。
def download(url, num_retries=2): print 'Downloading:', url try: html = urllib2.urlopen(url).read() except urllib2.URLError as e: print 'Download error:', e.reason html = None if num_retries > 0: if hasattr(e, 'code') and 500 <= e.code < 600: # recursively retry 5xx HTTP errors return download(url, num_retries-1) return html
现在,当download函数遇到5xx错误码时,将会递归调用函数自身进行重试。此外,该函数还增加了一个参数,用于设定重试下载的次数,其默认值为两次。我们在这里限制网页下载的尝试次数,是因为服务器错误可能暂时还没有解决。想要测试该函数,可以尝试下载http://httpstat.us/500,该网址会始终返回500错误码。
>>> download('http://httpstat.us/500')Downloading: http://httpstat.us/500Download error: Internal Server ErrorDownloading: http://httpstat.us/500Download error: Internal Server ErrorDownloading: http://httpstat.us/500Download error: Internal Server Error
从上面的返回结果可以看出,download函数的行为和预期一致,先尝试下载网页,在接收到500错误后,又进行了两次重试才放弃。
2.设置用户代理
默认情况下,urllib2使用Python-urllib/2.7作为用户代理下载网页内容,其中2.7是Python的版本号。如果能使用可辨识的用户代理则更好,这样可以避免我们的网络爬虫碰到一些问题。此外,也许是因为曾经历过质量不佳的Python网络爬虫造成的服务器过载,一些网站还会封禁这个默认的用户代理。比如,在使用Python默认用户代理的情况下,访问http://www.meetup.com/,目前会返回如图3所示的访问拒绝提示。
图3
因此,为了下载更加可靠,我们需要控制用户代理的设定。下面的代码对download函数进行了修改,设定了一个默认的用户代理“wswp”(即Web Scraping with Python的首字母缩写)。
def download(url, user_agent='wswp', num_retries=2): print 'Downloading:', url headers = {'User-agent': user_agent} request = urllib2.Request(url, headers=headers) try: html = urllib2.urlopen(request).read() except urllib2.URLError as e: print 'Download error:', e.reason html = None if num_retries > 0: if hasattr(e, 'code') and 500 <= e.code < 600: # retry 5XX HTTP errors return download(url, user_agent, num_retries-1) return html
现在,我们拥有了一个灵活的下载函数,可以在后续示例中得到复用。该函数能够捕获异常、重试下载并设置用户代理。
4.2 网站地图爬虫
在第一个简单的爬虫中,我们将使用示例网站robots.txt文件中发现的网站地图来下载所有网页。为了解析网站地图,我们将会使用一个简单的正则表达式,从<loc>标签中提取出URL。下面是该示例爬虫的代码。
def crawl_sitemap(url): # download the sitemap file sitemap = download(url) # extract the sitemap links links = re.findall('<loc>(.*?)</loc>', sitemap) # download each link for link in links: html = download(link) # scrape html here # ...
现在,运行网站地图爬虫,从示例网站中下载所有国家页面。
>>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')Downloading: http://example.webscraping.com/sitemap.xmlDownloading: http://example.webscraping.com/view/Afghanistan-1Downloading: http://example.webscraping.com/view/Aland-Islands-2Downloading: http://example.webscraping.com/view/Albania-3...
可以看出,上述运行结果和我们的预期一致,不过正如前文所述,我们无法依靠Sitemap文件提供每个网页的链接。下面我们将会介绍另一个简单的爬虫,该爬虫不再依赖于Sitemap文件。
4.3 ID遍历爬虫
本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家的URL。
- http://example.webscraping.com/view/Afghanistan-1
- http://example.webscraping.com/view/Australia-2
- http://example.webscraping.com/view/Brazil-3
可以看出,这些URL只在结尾处有所区别,包括国家名(作为页面别名)和ID。在URL中包含页面别名是非常普遍的做法,可以对搜索引擎优化起到帮助作用。一般情况下,Web服务器会忽略这个字符串,只使用ID来匹配数据库中的相关记录。下面我们将其移除,加载http://example.webscraping.com/view/1,测试示例网站中的链接是否仍然可用。测试结果如图4所示。
图4
从图4中可以看出,网页依然可以加载成功,也就是说该方法是有用的。现在,我们就可以忽略页面别名,只遍历ID来下载所有国家的页面。下面是使用了该技巧的代码片段。
import itertools for page in itertools.count(1): url = 'http://example.webscraping.com/view/-%d' % page html = download(url) if html is None: break else: # success - can scrape the result pass
在这段代码中,我们对ID进行遍历,直到出现下载错误时停止,我们假设此时已到达最后一个国家的页面。不过,这种实现方式存在一个缺陷,那就是某些记录可能已被删除,数据库ID之间并不是连续的。此时,只要访问到某个间隔点,爬虫就会立即退出。下面是这段代码的改进版本,在该版本中连续发生多次下载错误后才会退出程序。
# maximum number of consecutive download errors allowed max_errors = 5 # current number of consecutive download errors num_errors = 0 for page in itertools.count(1): url = 'http://example.webscraping.com/view/-%d' % page html = download(url) if html is None: # received an error trying to download this webpage num_errors = 1 if num_errors == max_errors: # reached maximum number of # consecutive errors so exit break else: # success - can scrape the result # ... num_errors = 0
上面代码中实现的爬虫需要连续5次下载错误才会停止遍历,这样就很大程度上降低了遇到被删除记录时过早停止遍历的风险。
在爬取网站时,遍历ID是一个很便捷的方法,但是和网站地图爬虫一样,这种方法也无法保证始终可用。比如,一些网站会检查页面别名是否满足预期,如果不是,则会返回404 Not Found错误。而另一些网站则会使用非连续大数作为ID,或是不使用数值作为ID,此时遍历就难以发挥其作用了。例如,Amazon使用ISBN作为图书ID,这种编码包含至少10位数字。使用ID对Amazon的图书进行遍历需要测试数十亿次,因此这种方法肯定不是抓取该站内容最高效的方法。
4.4 链接爬虫
到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有的国家页面。只要这两种技术可用,就应当使用其进行爬取,因为这两种方法最小化了需要下载的网页数量。不过,对于另一些网站,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。
通过跟踪所有链接的方式,我们可以很容易地下载整个网站的页面。但是,这种方法会下载大量我们并不需要的网页。例如,我们想要从一个在线论坛中抓取用户账号详情页,那么此时我们只需要下载账号页,而不需要下载讨论贴的页面。本文中的链接爬虫将使用正则表达式来确定需要下载哪些页面。下面是这段代码的初始版本。
import re def link_crawler(seed_url, link_regex): """Crawl from the given seed URL following links matched by link_regex """ crawl_queue = [seed_url] while crawl_queue: url = crawl_queue.pop() html = download(url) # filter for links matching our regular expression for link in get_links(html): if re.match(link_regex, link): crawl_queue.append(link) def get_links(html): """Return a list of links from html """ # a regular expression to extract all links from the webpage webpage_regex = re.compile('<a[^>] href=["'](.*?)["']', re.IGNORECASE) # list of all links from the webpage return webpage_regex.findall(html)
要运行这段代码,只需要调用link_crawler函数,并传入两个参数:要爬取的网站URL和用于跟踪链接的正则表达式。对于示例网站,我们想要爬取的是国家列表索引页和国家页面。其中,索引页链接格式如下。
- http://example.webscraping.com/index/1
- http://example.webscraping.com/index/2
国家页链接格式如下。
- http://example.webscraping.com/view/Afghanistan-1
- http://example.webscraping.com/view/Aland-Islands-2
因此,我们可以用/(index|view)/这个简单的正则表达式来匹配这两类网页。当爬虫使用这些输入参数运行时会发生什么呢?你会发现我们得到了如下的下载错误。
>>> link_crawler('http://example.webscraping.com', '/(index|view)') Downloading: http://example.webscraping.com Downloading: /index/1 Traceback (most recent call last): ... ValueError: unknown url type: /index/1
可以看出,问题出在下载/index/1时,该链接只有网页的路径部分,而没有协议和服务器部分,也就是说这是一个相对链接。由于浏览器知道你正在浏览哪个网页,所以在浏览器浏览时,相对链接是能够正常工作的。但是,urllib2是无法获知上下文的。为了让urllib2能够定位网页,我们需要将链接转换为绝对链接的形式,以便包含定位网页的所有细节。如你所愿,Python中确实有用来实现这一功能的模块,该模块称为urlparse。下面是link_crawler的改进版本,使用了urlparse模块来创建绝对路径。
import urlparse def link_crawler(seed_url, link_regex): """Crawl from the given seed URL following links matched by link_regex """ crawl_queue = [seed_url] while crawl_queue: url = crawl_queue.pop() html = download(url) for link in get_links(html): if re.match(link_regex, link): link = urlparse.urljoin(seed_url, link) crawl_queue.append(link)
当你运行这段代码时,会发现虽然网页下载没有出现错误,但是同样的地点总是会被不断下载到。这是因为这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲也存在到澳大利亚的链接,此时爬虫就会在它们之间不断循环下去。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的link_crawler函数,已具备存储已发现URL的功能,可以避免重复下载。
def link_crawler(seed_url, link_regex): crawl_queue = [seed_url] # keep track which URL's have seen before seen = set(crawl_queue) while crawl_queue: url = crawl_queue.pop() html = download(url) for link in get_links(html): # check if link matches expected regex if re.match(link_regex, link): # form absolute link link = urlparse.urljoin(seed_url, link) # check if have already seen this link if link not in seen: seen.add(link) crawl_queue.append(link)
当运行该脚本时,它会爬取所有地点,并且能够如期停止。最终,我们得到了一个可用的爬虫!
高级功能
现在,让我们为链接爬虫添加一些功能,使其在爬取其他网站时更加有用。
解析robots.txt
首先,我们需要解析robots.txt文件,以避免下载禁止爬取的URL。使用Python自带的robotparser模块,就可以轻松完成这项工作,如下面的代码所示。
>>> import robotparser>>> rp = robotparser.RobotFileParser()>>> rp.set_url('http://example.webscraping.com/robots.txt')>>> rp.read()>>> url = 'http://example.webscraping.com'>>> user_agent = 'BadCrawler'>>> rp.can_fetch(user_agent, url)False>>> user_agent = 'GoodCrawler'>>> rp.can_fetch(user_agent, url)True
robotparser模块首先加载robots.txt文件,然后通过can_fetch()函数确定指定的用户代理是否允许访问网页。在本例中,当用户代理设置为 BadCrawler 时,robotparser模块会返回结果表明无法获取网页,这和示例网站robots.txt的定义一样。
为了将该功能集成到爬虫中,我们需要在crawl循环中添加该检查。
... while crawl_queue: url = crawl_queue.pop() # check url passes robots.txt restrictions if rp.can_fetch(user_agent, url): ... else: print 'Blocked by robots.txt:', url
支持代理
有时我们需要使用代理访问某个网站。比如,Netflix屏蔽了美国以外的大多数国家。使用urllib2支持代理并没有想象中那么容易(可以尝试使用更友好的Python HTTP模块requests来实现该功能,其文档地址为http://docs.python-requests.org/)。下面是使用urllib2支持代理的代码。
proxy = ... opener = urllib2.build_opener() proxy_params = {urlparse.urlparse(url).scheme: proxy} opener.add_handler(urllib2.ProxyHandler(proxy_params)) response = opener.open(request)
下面是集成了该功能的新版本download函数。
def download(url, user_agent='wswp', proxy=None, num_retries=2): print 'Downloading:', url headers = {'User-agent': user_agent} request = urllib2.Request(url, headers=headers) opener = urllib2.build_opener() if proxy: proxy_params = {urlparse.urlparse(url).scheme: proxy} opener.add_handler(urllib2.ProxyHandler(proxy_params)) try: html = opener.open(request).read() except urllib2.URLError as e: print 'Download error:', e.reason html = None if num_retries > 0: if hasattr(e, 'code') and 500 <= e.code < 600: # retry 5XX HTTP errors html = download(url, user_agent, proxy, num_retries-1) return html
下载限速
如果我们爬取网站的速度过快,就会面临被封禁或是造成服务器过载的风险。为了降低这些风险,我们可以在两次下载之间添加延时,从而对爬虫限速。下面是实现了该功能的类的代码。
class Throttle: """Add a delay between downloads to the same domain """ def __init__(self, delay): # amount of delay between downloads for each domain self.delay = delay # timestamp of when a domain was last accessed self.domains = {} def wait(self, url): domain = urlparse.urlparse(url).netloc last_accessed = self.domains.get(domain) if self.delay > 0 and last_accessed is not None: sleep_secs = self.delay - (datetime.datetime.now() - last_accessed).seconds if sleep_secs > 0: # domain has been accessed recently # so need to sleep time.sleep(sleep_secs) # update the last accessed time self.domains[domain] = datetime.datetime.now()
Throttle类记录了每个域名上次访问的时间,如果当前时间距离上次访问时间小于指定延时,则执行睡眠操作。我们可以在每次下载之前调用Throttle对爬虫进行限速。
throttle = Throttle(delay) ... throttle.wait(url) result = download(url, headers, proxy=proxy, num_retries=num_retries)
避免爬虫陷阱
目前,我们的爬虫会跟踪所有之前没有访问过的链接。但是,一些网站会动态生成页面内容,这样就会出现无限多的网页。比如,网站有一个在线日历功能,提供了可以访问下个月和下一年的链接,那么下个月的页面中同样会包含访问再下个月的链接,这样页面就会无止境地链接下去。这种情况被称为爬虫陷阱。
想要避免陷入爬虫陷阱,一个简单的方法是记录到达当前网页经过了多少个链接,也就是深度。当到达最大深度时,爬虫就不再向队列中添加该网页中的链接了。要实现这一功能,我们需要修改seen变量。该变量原先只记录访问过的网页链接,现在修改为一个字典,增加了页面深度的记录。
def link_crawler(..., max_depth=2): max_depth = 2 seen = {} ... depth = seen[url] if depth != max_depth: for link in links: if link not in seen: seen[link] = depth 1 crawl_queue.append(link)
现在有了这一功能,我们就有信心爬虫最终一定能够完成。如果想要禁用该功能,只需将max_depth设为一个负数即可,此时当前深度永远不会与之相等。
最终版本
这个高级链接爬虫的完整源代码可以在https://bitbucket.org/ wswp/code/src/tip/chapter01/link_crawler3.py下载得到。要测试这段代码,我们可以将用户代理设置为BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那个用户代理。从下面的运行结果中可以看出,爬虫果然被屏蔽了,代码启动后马上就会结束。
>>> seed_url = 'http://example.webscraping.com/index'>>> link_regex = '/(index|view)'>>> link_crawler(seed_url, link_regex, user_agent='BadCrawler')Blocked by robots.txt: http://example.webscraping.com/
现在,让我们使用默认的用户代理,并将最大深度设置为1,这样只有主页上的链接才会被下载。
>>> link_crawler(seed_url, link_regex, max_depth=1)Downloading: http://example.webscraping.com//indexDownloading: http://example.webscraping.com/index/1Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10Downloading: http://example.webscraping.com/view/Antarctica-9Downloading: http://example.webscraping.com/view/Anguilla-8Downloading: http://example.webscraping.com/view/Angola-7Downloading: http://example.webscraping.com/view/Andorra-6Downloading: http://example.webscraping.com/view/American-Samoa-5Downloading: http://example.webscraping.com/view/Algeria-4Downloading: http://example.webscraping.com/view/Albania-3Downloading: http://example.webscraping.com/view/Aland-Islands-2Downloading: http://example.webscraping.com/view/Afghanistan-1
和预期一样,爬虫在下载完国家列表的第一页之后就停止了。
本文节选自《用Python写网络爬虫》
本书讲解了如何使用Python来编写网络爬虫程序,内容包括网络爬虫简介,从页面中抓取数据的三种方法,提取缓存中的数据,使用多个线程和进程来进行并发抓取,如何抓取动态页面中的内容,与表单进行交互,处理页面中的验证码问题,以及使用Scarpy和Portia来进行数据抓取,并在最后使用本书介绍的数据抓取技术对几个真实的网站进行了抓取,旨在帮助读者活学活用书中介绍的技术。
本书适合有一定Python编程经验,而且对爬虫技术感兴趣的读者阅读。