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,甚至采取更进一步的法律行动。这就要求下载请求的速度需要限定在一个合理值之内,并且还需要设定一个专属的用户代理来标识自己。在下面的小节中我们将会对这些实践进行具体介绍。
在深入讨论爬取一个网站之前,我们首先需要对目标站点的规模和结构进行一定程度的了解。网站自身的robots.txt和Sitemap文件都可以为我们提供一定的帮助,此外还有一些能提供更详细信息的外部工具,比如Google搜索和WHOIS。
3.1 检查robots.txt
在section 1中,robots.txt文件禁止用户代理为BadCrawler的爬虫爬取该网站,不过这种写法可能无法起到应有的作用,因为恶意爬虫根本不会遵从robots.txt的要求。本章后面的一个例子将会展示如何让爬虫自动遵守robots.txt的要求。
section 3定义了一个Sitemap文件,我们将在下一节中了解如何检查该文件。
3.2 检查网站地图
网站地图提供了所有网页的链接,我们会在后面的小节中使用这些信息,用于创建我们的第一个爬虫。虽然Sitemap文件提供了一种爬取网站的有效方式,但是我们仍需对其谨慎处理,因为该文件经常存在缺失、过期或不完整的问题。
3.3 估算网站大小
估算网站大小的一个简便方法是检查Google爬虫的结果,因为Google很可能已经爬取过我们感兴趣的网站。我们可以通过Google搜索的site关键词过滤域名结果,从而获取该信息。我们可以从http://www.google.com/advanced_search了解到该接口及其他高级搜索参数的用法。
从图1中可以看出,此时Google估算该网站拥有202个网页,这和实际情况差不多。不过对于更大型的网站,我们会发现Google的估算并不十分准确。
这种附加的过滤条件非常有用,因为在理想情况下,你只希望爬取网站中包含有用数据的部分,而不是爬取网站的每个页面。
3.4 识别网站所用技术
该模块将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']}对于一些网站,我们可能会关心其所有者是谁。比如,我们已知网站的所有者会封禁网络爬虫,那么我们最好把下载速度控制得更加保守一些。为了找到网站的所有者,我们可以使用WHOIS协议查询域名的注册者是谁。Python中有一个针对该协议的封装库,其文档地址为https://pypi.python.org/pypi/python-whois,我们可以通过pip进行安装。
pip install python-whois从结果中可以看出该域名归属于Google,实际上也确实如此。该域名是用于Google App Engine服务的。当我们爬取该域名时就需要十分小心,因为Google经常会阻断网络爬虫,尽管实际上其自身就是一个网络爬虫业务。
4. 编写第一个网络爬虫
要想爬取网页,我们首先需要将其下载下来。下面的示例脚本使用Python的urllib2模块下载URL。
import urllib2 def download(url): return urllib2.urlopen(url).read()现在,当出现下载错误时,该函数能够捕获到异常,然后返回None。
1.重试下载
互联网工程任务组(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函数的行为和预期一致,先尝试下载网页,在接收到500错误后,又进行了两次重试才放弃。
2.设置用户代理
nerror="javascript:errorimg.call(this);">
图3
现在,我们拥有了一个灵活的下载函数,可以在后续示例中得到复用。该函数能够捕获异常、重试下载并设置用户代理。
4.2 网站地图爬虫
现在,运行网站地图爬虫,从示例网站中下载所有国家页面。
>>> 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...本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家的URL。
- http://example.webscraping.com/view/Afghanistan-1
- http://example.webscraping.com/view/Australia-2
- http://example.webscraping.com/view/Brazil-3
从图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上面代码中实现的爬虫需要连续5次下载错误才会停止遍历,这样就很大程度上降低了遇到被删除记录时过早停止遍历的风险。
到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有的国家页面。只要这两种技术可用,就应当使用其进行爬取,因为这两种方法最小化了需要下载的网页数量。不过,对于另一些网站,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。
要运行这段代码,只需要调用link_crawler函数,并传入两个参数:要爬取的网站URL和用于跟踪链接的正则表达式。对于示例网站,我们想要爬取的是国家列表索引页和国家页面。其中,索引页链接格式如下。
- http://example.webscraping.com/index/1
- http://example.webscraping.com/index/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当你运行这段代码时,会发现虽然网页下载没有出现错误,但是同样的地点总是会被不断下载到。这是因为这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲也存在到澳大利亚的链接,此时爬虫就会在它们之间不断循环下去。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的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
robotparser模块首先加载robots.txt文件,然后通过can_fetch()函数确定指定的用户代理是否允许访问网页。在本例中,当用户代理设置为 BadCrawler 时,robotparser模块会返回结果表明无法获取网页,这和示例网站robots.txt的定义一样。
有时我们需要使用代理访问某个网站。比如,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)如果我们爬取网站的速度过快,就会面临被封禁或是造成服务器过载的风险。为了降低这些风险,我们可以在两次下载之间添加延时,从而对爬虫限速。下面是实现了该功能的类的代码。
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()目前,我们的爬虫会跟踪所有之前没有访问过的链接。但是,一些网站会动态生成页面内容,这样就会出现无限多的网页。比如,网站有一个在线日历功能,提供了可以访问下个月和下一年的链接,那么下个月的页面中同样会包含访问再下个月的链接,这样页面就会无止境地链接下去。这种情况被称为爬虫陷阱。
现在有了这一功能,我们就有信心爬虫最终一定能够完成。如果想要禁用该功能,只需将max_depth设为一个负数即可,此时当前深度永远不会与之相等。
最终版本
现在,让我们使用默认的用户代理,并将最大深度设置为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写网络爬虫》
nerror="javascript:errorimg.call(this);">
本书适合有一定Python编程经验,而且对爬虫技术感兴趣的读者阅读。
ntent='{"new_thumb_url": "http://p1.toutiaoimg.com/img/pgc-image/a15453196503429488ea24784ab0de41", "title": "python\u6838\u5fc3\u6280\u672f\u5b9e\u6218", "url": "", "price": 199, "column_id": "6825860383811567886", "content": "", "author_description": "\u5f02\u6b65\u793e\u533a", "share_price": 0, "thumb_url": "http://p1.toutiaoimg.com/large/pgc-image/a15453196503429488ea24784ab0de41", "sold": 0}'>

