互联网包含了迄今为止最有用的数据集,并且大部分可以免费公开访问。但是,这些数据难以复用。它们被嵌入在网站的结构和样式当中,需要抽取出来才能使用。从网页中抽取数据的过程又称为网络爬虫,随着越来越多的信息被发布到网络上,网络爬虫也变得越来越有用。
今天介绍的这一本书《用Python写网络爬虫(第2版)》是Python网络爬虫畅销图书全新升级版,上一版年度畅销近4万册,而本书针对Python 3.x编写,提供示例完整源码和实例网站搭建源码,确保你可以在本地成功复现爬取网站环境,并保障网站的稳定性与可靠性以及代码运行结果的可再现性。 书中使用的所有代码均已使用Python 3.4+测试通过,并且可以在异步社区下载到。
点击封面查看更多详情
“网络爬虫何时有用”
假设我有一个鞋店,并且想要及时了解竞争对手的价格。我可以每天访问他们的网站,与我店铺中鞋子的价格进行对比。但是,如果我店铺中的鞋类品种繁多,或是希望能够更加频繁地查看价格变化的话,就需要花费大量的时间,甚至难以实现。再举一个例子,我看中了一双鞋,想等到它促销时再购买。我可能需要每天访问这家鞋店的网站来查看这双鞋是否降价,也许需要等待几个月的时间,我才能如愿盼到这双鞋促销。上述这两个重复性的手工流程,都可以利用本书介绍的网络爬虫技术实现自动化处理。
在理想状态下,网络爬虫并不是必需品,每个网站都应该提供API,以结构化的格式共享它们的数据。然而在现实情况中,虽然一些网站已经提供了这种API,但是它们通常会限制可以抓取的数据,以及访问这些数据的频率。另外,网站开发人员可能会变更、移除或限制其后端API。总之,我们不能仅仅依赖于API去访问我们所需的在线数据,而是应该学习一些网络爬虫技术的相关知识。
“本书基于Python 3”
在本书中,我们将完全使用Python 3进行开发。Python软件基金会已经宣布Python 2将会被逐步淘汰,并且只支持到2020年;出于该原因,我们和许多其他Python爱好者一样,已经将开发转移到对Python 3的支持当中,在本书中我们将使用3.6版本。本书代码将兼容Python 3.4+的版本。
如果你熟悉Python Virtual Environments
或Anaconda
的使用,那么你可能已经知道如何在一个新环境中创建Python 3了。如果你希望以全局形式安装Python 3,那么我们推荐你搜索自己使用的操作系统的特定文档。就我而言,我会直接使用Virtual Environment Wrapper,这样就可以很容易地对不同项目和Python版本使用多个不同的环境了。使用Conda环境或虚拟环境是最为推荐的,这样你就可以轻松变更基于项目需求的依赖,而不会影响到你正在做的其他工作了。对于初学者来说,我推荐使用Conda,因为其需要的安装工作更少一些。
“编写第一个网络爬虫”
为了抓取网站,我们首先需要下载包含有感兴趣数据的网页,该过程一般称为爬取(crawling)。爬取一个网站有很多种方法,而选用哪种方法更加合适,则取决于目标网站的结构。本文中,我们首先会探讨如何安全地下载网页,然后会介绍如下3种爬取网站的常见方法:
爬取网站地图;
使用数据库ID遍历每个网页;
跟踪网页链接。
到目前为止,我们交替使用了抓取和爬取这两个术语,接下来让我们先来定义这两种方法的相似点和不同点。
1.1抓取与爬取的对比
根据你所关注的信息以及站点内容和结构的不同,你可能需要进行网络抓取或是网站爬取。那么它们有什么区别呢?
网络抓取通常针对特定网站,并在这些站点上获取指定信息。网络抓取用于访问这些特定的页面,如果站点发生变化或者站点中的信息位置发生变化的话,则需要进行修改。例如,你可能想要通过网络抓取查看你喜欢的当地餐厅的每日特色菜,为了实现该目的,你需要抓取其网站中日常更新该信息的部分。
与之不同的是,网络爬取通常是以通用的方式构建的,其目标是一系列顶级域名的网站或是整个网络。爬取可以用来收集更具体的信息,不过更常见的情况是爬取网络,从许多不同的站点或页面中获取小而通用的信息,然后跟踪链接到其他页面中。
除了爬取和抓取外,我们还会在第8章中介绍网络爬虫。爬虫可以用来爬取指定的一系列网站,或是在多个站点甚至整个互联网中进行更广泛的爬取。
一般来说,我们会使用特定的术语反映我们的用例。在你开发网络爬虫时,可能会注意到它们在你想要使用的技术、库和包中的区别。在这些情况下,你对不同术语的理解,可以帮助你基于所使用的术语选择适当的包或技术(例如,是否只用于抓取?是否也适用于爬虫?)。
1.2下载网页
要想抓取网页,我们首先需要将其下载下来。下面的示例脚本使用Python的urllib
模块下载URL。
1import urllib.request
2def download(url):
3 return urllib.request.urlopen(url).read()
当传入URL参数时,该函数将会下载网页并返回其HTML。不过,这个代码片段存在一个问题,即当下载网页时,我们可能会遇到一些无法控制的错误,比如请求的页面可能不存在。此时,urllib
会抛出异常,然后退出脚本。安全起见,下面再给出一个更稳建的版本,可以捕获这些异常。
1import urllib.request
2from urllib.error import URLError, HTTPError, ContentTooShortError
3
4def download(url):
5 print('Downloading:', url)
6 try:
7 html = urllib.request.urlopen(url).read()
8 except (URLError, HTTPError, ContentTooShortError) as e:
9 print('Download error:', e.reason)
10 html = None
11 return html
现在,当出现下载或URL错误时,该函数能够捕获到异常,然后返回None
。
下载时遇到的错误经常是临时性的,比如服务器过载时返回的503 Service Unavailable
错误。对于此类错误,我们可以在短暂等待后尝试重新下载,因为这个服务器问题现在可能已经解决。不过,我们不需要对所有错误都尝试重新下载。如果服务器返回的是404 Not Found
这种错误,则说明该网页目前并不存在,再次尝试同样的请求一般也不会出现不同的结果。
互联网工程任务组(Internet Engineering Task Force)定义了HTTP错误的完整列表,从中可以了解到4xx
错误发生在请求存在问题时,而5xx
错误则发生在服务端存在问题时。所以,我们只需要确保download
函数在发生5xx
错误时重试下载即可。下面是支持重试下载功能的新版本代码。
1def download(url, num_retries=2):
2 print('Downloading:', url)
3 try:
4 html = urllib.request.urlopen(url).read()
5 except (URLError, HTTPError, ContentTooShortError) as e:
6 print('Download error:', e.reason)
7 html = None
8 if num_retries > 0:
9 if hasattr(e, 'code') and 500 <= e.code < 600:
10 # recursively retry 5xx HTTP errors
11 return download(url, num_retries - 1)
12 return html
现在,当download
函数遇到5xx
错误码时,将会递归调用函数自身进行重试。此外,该函数还增加了一个参数,用于设定重试下载的次数,其默认值为两次。我们在这里限制网页下载的尝试次数,是因为服务器错误可能暂时还没有恢复。想要测试该函数,可以尝试下载http://httpstat.us/500
,该网址会始终返回500
错误码。
1 >>> download('http://httpstat.us/500')
2Downloading: http://httpstat.us/500
3Download error: Internal Server Error
4Downloading: http://httpstat.us/500
5Download error: Internal Server Error
6Downloading: http://httpstat.us/500
7Download error: Internal Server Error
从上面的返回结果可以看出,download
函数的行为和预期一致,先尝试下载网页,在接收到500
错误后,又进行了两次重试才放弃。
默认情况下,urllib
使用Python-urllib/``3.x
作为用户代理下载网页内容,其中3.x
是环境当前所用Python的版本号。如果能使用可辨识的用户代理则更好,这样可以避免我们的网络爬虫碰到一些问题。此外,也许是因为曾经历过质量不佳的Python网络爬虫造成的服务器过载,一些网站还会封禁这个默认的用户代理。
因此,为了使下载网站更加可靠,我们需要控制用户代理的设定。下面的代码对download
函数进行了修改,设定了一个默认的用户代理‘wswp’
(即Web Scraping with Python的首字母缩写)。
1def download(url, user_agent='wswp', num_retries=2):
2 print('Downloading:', url)
3 request = urllib.request.Request(url)
4 request.add_header('User-agent', user_agent)
5 try:
6 html = urllib.request.urlopen(request).read()
7 except (URLError, HTTPError, ContentTooShortError) as e:
8 print('Download error:', e.reason)
9 html = None
10 if num_retries > 0:
11 if hasattr(e, 'code') and 500 <= e.code < 600:
12 # recursively retry 5xx HTTP errors
13 return download(url, num_retries - 1)
14 return html
现在,如果你再次尝试访问meetup.com
,就能够看到一个合法的HTML了。我们的下载函数可以在后续代码中得到复用,该函数能够捕获异常、在可能的情况下重试网站以及设置用户代理。
1.3网站地图爬虫
在第一个简单的爬虫中,我们将使用示例网站robots.txt
文件中发现的网站地图来下载所有网页。为了解析网站地图,我们将会使用一个简单的正则表达式,从<loc>
标签中提取出URL。
我们需要更新代码以处理编码转换,因为我们目前的download
函数只是简单地返回了字节。下面是该示例爬虫的代码。
1import re
2
3def download(url, user_agent='wswp', num_retries=2, charset='utf-8'):
4 print('Downloading:', url)
5 request = urllib.request.Request(url)
6 request.add_header('User-agent', user_agent)
7 try:
8 resp = urllib.request.urlopen(request)
9 cs = resp.headers.get_content_charset()
10 if not cs:
11 cs = charset
12 html = resp.read().decode(cs)
13 except (URLError, HTTPError, ContentTooShortError) as e:
14 print('Download error:', e.reason)
15 html = None
16 if num_retries > 0:
17 if hasattr(e, 'code') and 500 <= e.code < 600:
18 # recursively retry 5xx HTTP errors
19 return download(url, num_retries - 1)
20 return html
21
22def crawl_sitemap(url):
23 # download the sitemap file
24 sitemap = download(url)
25 # extract the sitemap links
26 links = re.findall('(.*?) ', sitemap)
27 # download each link
28 for link in links:
29 html = download(link)
30 # scrape html here
31 # ...
现在,运行网站地图爬虫,从示例网站中下载所有国家或地区页面。
1 >>> crawl_sitemap('http://example.python-scraping.com/sitemap.xml')
2Downloading: http://example.python-scraping.com/sitemap.xml
3Downloading: http://example.python-scraping.com/view/Afghanistan-1
4Downloading: http://example.python-scraping.com/view/Aland-Islands-2
5Downloading: http://example.python-scraping.com/view/Albania-3
6...
正如上面代码中的download
方法所示,我们必须更新字符编码才能利用正则表达式处理网站响应。Python的read
方法返回字节,而正则表达式期望的则是字符串。我们的代码依赖于网站维护者在响应头中包含适当的字符编码。如果没有返回字符编码头部,我们将会把它设置为默认值UTF-8,并抱有最大的希望。当然,如果返回头中的编码不正确,或是编码没有设置并且也不是UTF-8的话,则会抛出错误。还有一些更复杂的方式用于猜测编码(参见https://pypi.python.org/pypi/chardet
),该方法非常容易实现。
到目前为止,网站地图爬虫已经符合预期。不过正如前文所述,我们无法依靠Sitemap
文件提供每个网页的链接。下一节中,我们将会介绍另一个简单的爬虫,该爬虫不再依赖于Sitemap
文件。
本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家(或地区)的URL。
http://example.python-scraping.com/view/Afghanistan-1
http://example.python-scraping.com/view/Australia-2
http://example.python-scraping.com/view/Brazil-3
可以看出,这些URL只在URL路径的最后一部分有所区别,包括国家(或地区)名(作为页面别名)和ID。在URL中包含页面别名是非常普遍的做法,可以对搜索引擎优化起到帮助作用。一般情况下,Web服务器会忽略这个字符串,只使用ID来匹配数据库中的相关记录。下面我们将其移除,查看http://example.python-scraping.com/view/1
,测试示例网站中的链接是否仍然可用。测试结果如图1.1所示。
图1.1
从图1.1中可以看出,网页依然可以加载成功,也就是说该方法是有用的。现在,我们就可以忽略页面别名,只利用数据库ID来下载所有国家(或地区)的页面了。下面是使用了该技巧的代码片段。
1import itertools
2
3def crawl_site(url):
4 for page in itertools.count(1):
5 pg_url = '{}{}'.format(url, page)
6 html = download(pg_url)
7 if html is None:
8 break
9 # success - can scrape the result
现在,我们可以使用该函数传入基础URL。
1>>> crawl_site('http://example.python-scraping.com/view/-')
2Downloading: http://example.python-scraping.com/view/-1
3Downloading: http://example.python-scraping.com/view/-2
4Downloading: http://example.python-scraping.com/view/-3
5Downloading: http://example.python-scraping.com/view/-4
6[...]
在这段代码中,我们对ID进行遍历,直到出现下载错误时停止,我们假设此时抓取已到达最后一个国家(或地区)的页面。不过,这种实现方式存在一个缺陷,那就是某些记录可能已被删除,数据库ID之间并不是连续的。此时,只要访问到某个间隔点,爬虫就会立即退出。下面是这段代码的改进版本,在该版本中连续发生多次下载错误后才会退出程序。
1def crawl_site(url, max_errors=5):
2 for page in itertools.count(1):
3 pg_url = '{}{}'.format(url, page)
4 html = download(pg_url)
5 if html is None:
6 num_errors += 1
7 if num_errors == max_errors:
8 # max errors reached, exit loop
9 break
10 else:
11 num_errors = 0
12 # success - can scrape the result
上面代码中实现的爬虫需要连续5次下载错误才会停止遍历,这样就很大程度上降低了遇到记录被删除或隐藏时过早停止遍历的风险。
在爬取网站时,遍历ID是一个很便捷的方法,但是和网站地图爬虫一样,这种方法也无法保证始终可用。比如,一些网站会检查页面别名是否在URL中,如果不是,则会返回404 Not Found
错误。而另一些网站则会使用非连续大数作为ID,或是不使用数值作为ID,此时遍历就难以发挥其作用了。例如,Amazon使用ISBN作为可用图书的ID,这种编码包含至少10位数字。使用ID对ISBN进行遍历需要测试数十亿次可能的组合,因此这种方法肯定不是抓取该站内容最高效的方法。
正如你一直关注的那样,你可能已经注意到一些TOO MANY REQUESTS
下载错误信息。现在无须担心它,我们将会在1.5节的“高级功能”部分中介绍更多处理该类型错误的方法。
1.5链接爬虫
到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有已发布的国家(或地区)页面。只要这两种技术可用,就应当使用它们进行爬取,因为这两种方法将需要下载的网页数量降至最低。不过,对于另一些网站,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。
通过跟踪每个链接的方式,我们可以很容易地下载整个网站的页面。但是,这种方法可能会下载很多并不需要的网页。例如,我们想要从一个在线论坛中抓取用户账号详情页,那么此时我们只需要下载账号页,而不需要下载讨论贴的页面。本文使用的链接爬虫将使用正则表达式来确定应当下载哪些页面。下面是这段代码的初始版本。
1import re
2
3def link_crawler(start_url, link_regex):
4 """ Crawl from the given start URL following links matched by
5link_regex
6 """
7 crawl_queue = [start_url]
8 while crawl_queue:
9 url = crawl_queue.pop()
10 html = download(url)
11 if html is not None:
12 continue
13 # filter for links matching our regular expression
14 for link in get_links(html):
15 if re.match(link_regex, link):
16 crawl_queue.append(link)
17
18def get_links(html):
19 """ Return a list of links from html
20 """
21 # a regular expression to extract all links from the webpage
22 webpage_regex = re.compile("""]+href=["'](.*?)["']""",
23re.IGNORECASE)
24 # list of all links from the webpage
25 return webpage_regex.findall(html)
要运行这段代码,只需要调用link_crawler
函数,并传入两个参数:要爬取的网站URL以及用于匹配你想跟踪的链接的正则表达式。对于示例网站来说,我们想要爬取的是国家(或地区)列表索引页和国家(或地区)页面。
我们查看站点可以得知索引页链接遵循如下格式:
http://example.python-scraping.com/index/1
http://example.python-scraping.com/index/2
国家(或地区)页遵循如下格式:
http://example.python-scraping.com/view/Afghanistan-1
http://example.python-scraping.com/view/Aland-Islands-2
因此,我们可以用/(index|view)/
这个简单的正则表达式来匹配这两类网页。当爬虫使用这些输入参数运行时会发生什么呢?你会得到如下所示的下载错误。
1>>> link_crawler('http://example.python-scraping.com', '/(index|view)/')
2Downloading: http://example.python-scraping.com
3Downloading: /index/1
4Traceback (most recent call last):
5 ...
6ValueError: unknown url type: /index/1
可以看出,问题出在下载/index/1
时,该链接只有网页的路径部分,而没有协议和服务器部分,也就是说这是一个相对链接。由于浏览器知道你正在浏览哪个网页,并且能够采取必要的步骤处理这些链接,因此在浏览器浏览时,相对链接是能够正常工作的。但是,urllib
并没有上下文。为了让urllib
能够定位网页,我们需要将链接转换为绝对链接的形式,以便包含定位网页的所有细节。如你所愿,Python的urllib
中有一个模块可以用来实现该功能,该模块名为parse
。下面是link_crawler
的改进版本,使用了urljoin
方法来创建绝对路径。
1from urllib.parse import urljoin
2
3def link_crawler(start_url, link_regex):
4 """ Crawl from the given start URL following links matched by
5link_regex
6 """
7 crawl_queue = [start_url]
8 while crawl_queue:
9 url = crawl_queue.pop()
10 html = download(url)
11 if not html:
12 continue
13 for link in get_links(html):
14 if re.match(link_regex, link):
15 abs_link = urljoin(start_url, link)
16 crawl_queue.append(abs_link)
当你运行这段代码时,会看到虽然下载了匹配的网页,但是同样的地点总是会被不断下载到。产生该行为的原因是这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲又链接回了澳大利亚,此时爬虫就会继续将这些URL放入队列,永远不会到达队列尾部。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的link_crawler
函数,具备了存储已发现URL的功能,可以避免重复下载。
1def link_crawler(start_url, link_regex):
2 crawl_queue = [start_url]
3 # keep track which URL's have seen before
4 seen = set(crawl_queue)
5 while crawl_queue:
6 url = crawl_queue.pop()
7 html = download(url)
8 if not html:
9 continue
10 for link in get_links(html):
11 # check if link matches expected regex
12 if re.match(link_regex, link):
13 abs_link = urljoin(start_url, link)
14 # check if have already seen this link
15 if abs_link not in seen:
16 seen.add(abs_link)
17 crawl_queue.append(abs_link)
当运行该脚本时,它会爬取所有地点,并且能够如期停止。最终,我们得到了一个可用的链接爬虫!
本文摘自《用Python写网络爬虫(第2版)》
《用Python写网络爬虫》
史上首本Python网络爬虫图书全新升级版
针对Python 3.x编写
提供示例完整源码和实例网站搭建源码
本书目录
(滑动手机查看)
第1章 网络爬虫简介 1
1.1 网络爬虫何时有用 1
1.2 网络爬虫是否合法 2
1.3 Python 3 3
1.4 背景调研 4
1.4.1 检查robots.txt 4
1.4.2 检查网站地图 5
1.4.3 估算网站大小 6
1.4.4 识别网站所用技术 7
1.4.5 寻找网站所有者 9
1.5 编写第一个网络爬虫 11
1.5.1 抓取与爬取的对比 11
1.5.2 下载网页 12
1.5.3 网站地图爬虫 15
1.5.4 ID遍历爬虫 17
1.5.5 链接爬虫 19
1.5.6 使用requests库 28
1.6 本章小结 30
第2章 数据抓取 31
2.1 分析网页 32
2.2 3种网页抓取方法 34
2.2.1 正则表达式 35
2.2.2 Beautiful Soup 37
2.2.3 Lxml 39
2.3 CSS选择器和浏览器控制台 41
2.4 XPath选择器 43
2.5 LXML和家族树 46
2.6 性能对比 47
2.7 抓取结果 49
2.7.1 抓取总结 50
2.7.2 为链接爬虫添加抓取回调 51
2.8 本章小结 55
第3章 下载缓存 56
3.1 何时使用缓存 57
3.2 为链接爬虫添加缓存支持 57
3.3 磁盘缓存 60
3.3.1 实现磁盘缓存 62
3.3.2 缓存测试 64
3.3.3 节省磁盘空间 65
3.3.4 清理过期数据 66
3.3.5 磁盘缓存缺点 68
3.4 键值对存储缓存 69
3.4.1 键值对存储是什么 69
3.4.2 安装Redis 70
3.4.3 Redis概述 71
3.4.4 Redis缓存实现 72
3.4.5 压缩 74
3.4.6 测试缓存 75
3.4.7 探索requests-cache 76
3.5 本章小结 78
第4章 并发下载 79
4.1 100万个网页 79
4.2 串行爬虫 82
4.3 多线程爬虫 83
4.4 线程和进程如何工作 83
4.4.1 实现多线程爬虫 84
4.4.2 多进程爬虫 87
4.5 性能 91
4.6 本章小结 94
第5章 动态内容 95
5.1 动态网页示例 95
5.2 对动态网页进行逆向工程 98
5.3 渲染动态网页 104
5.3.1 PyQt还是PySide 105
5.3.2 执行JavaScript 106
5.3.3 使用WebKit与网站交互 108
5.4 渲染类 111
5.5 本章小结 117
第6章 表单交互 119
6.1 登录表单 120
6.2 支持内容更新的登录脚本扩展 128
6.3 使用Selenium实现自动化表单处理 132
6.4 本章小结 135
第7章 验证码处理 136
7.1 注册账号 137
7.2 光学字符识别 140
7.3 处理复杂验证码 144
7.4 使用验证码处理服务 144
7.4.1 9kw入门 145
7.4.2 报告错误 150
7.4.3 与注册功能集成 151
7.5 验证码与机器学习 153
7.6 本章小结 153
第8章 Scrapy 154
8.1 安装Scrapy 154
8.2 启动项目 155
8.2.1 定义模型 156
8.2.2 创建爬虫 157
8.3 不同的爬虫类型 162
8.4 使用shell命令抓取 163
8.4.1 检查结果 165
8.4.2 中断与恢复爬虫 167
8.5 使用Portia编写可视化爬虫 170
8.5.1 安装 170
8.5.2 标注 172
8.5.3 运行爬虫 176
8.5.4 检查结果 176
8.6 使用Scrapely实现自动化抓取 177
8.7 本章小结 178
第9章 综合应用 179
9.1 Google搜索引擎 179
9.2 Facebook 184
9.2.1 网站 184
9.2.2 Facebook API 186
9.3 Gap 188
9.4 宝马 192
9.5 本章小结 196
京东预售
当当预售
今日互动
学会爬虫,你最想做的第一件事儿是什么?为什么?截止时间7月21日17时,留言+转发本活动到朋友圈,小编将抽奖选出2名读者赠送纸书2本,文末留言点赞最多的自动获得图书1本。
每天与你分享IT好文。
在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程
点击阅读原文,购买《用Python写网络爬虫(第2版)》