@程序员,想要基于 Python 3.4 玩爬虫该看些什么?

x
用微信扫描二维码
分享至好友和朋友圈

  

  互联网包含了迄今为止最有用的数据集,并且大部分可以免费公开访问。但是,这些数据难以复用。它们被嵌入在网站的结构和样式当中,需要抽取出来才能使用。从网页中抽取数据的过程又称为网络爬虫,随着越来越多的信息被发布到网络上,网络爬虫也变得越来越有用。

  

  今天介绍的这一本书《用Python写网络爬虫(第2版)》是 Python 网络爬虫畅销图书全新升级版,上一版年度畅销近 4 万册,而本书针对 Python 3.x 编写,提供示例完整源码和实例网站搭建源码,确保你可以在本地成功复现爬取网站环境,并保障网站的稳定性与可靠性以及代码运行结果的可再现性。

  

  “网络爬虫何时有用”

  假设我有一个鞋店,并且想要及时了解竞争对手的价格。我可以每天访问他们的网站,与我店铺中鞋子的价格进行对比。但是,如果我店铺中的鞋类品种繁多,或是希望能够更加频繁地查看价格变化的话,就需要花费大量的时间,甚至难以实现。再举一个例子,我看中了一双鞋,想等到它促销时再购买。我可能需要每天访问这家鞋店的网站来查看这双鞋是否降价,也许需要等待几个月的时间,我才能如愿盼到这双鞋促销。上述这两个重复性的手工流程,都可以利用本书介绍的网络爬虫技术实现自动化处理。

  在理想状态下,网络爬虫并不是必需品,每个网站都应该提供 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 遍历每个网页;

  跟踪网页链接。

  到目前为止,我们交替使用了抓取和爬取这两个术语,接下来让我们先来定义这两种方法的相似点和不同点。

  抓取与爬取的对比

  根据你所关注的信息以及站点内容和结构的不同,你可能需要进行网络抓取或是网站爬取。那么它们有什么区别呢?

  网络抓取通常针对特定网站,并在这些站点上获取指定信息。网络抓取用于访问这些特定的页面,如果站点发生变化或者站点中的信息位置发生变化的话,则需要进行修改。例如,你可能想要通过网络抓取查看你喜欢的当地餐厅的每日特色菜,为了实现该目的,你需要抓取其网站中日常更新该信息的部分。

  与之不同的是,网络爬取通常是以通用的方式构建的,其目标是一系列顶级域名的网站或是整个网络。爬取可以用来收集更具体的信息,不过更常见的情况是爬取网络,从许多不同的站点或页面中获取小而通用的信息,然后跟踪链接到其他页面中。

  除了爬取和抓取外,我们还会在第 8 章中介绍网络爬虫。爬虫可以用来爬取指定的一系列网站,或是在多个站点甚至整个互联网中进行更广泛的爬取。

  一般来说,我们会使用特定的术语反映我们的用例。在你开发网络爬虫时,可能会注意到它们在你想要使用的技术、库和包中的区别。在这些情况下,你对不同术语的理解,可以帮助你基于所使用的术语选择适当的包或技术(例如,是否只用于抓取?是否也适用于爬虫?)。

  下载网页

  要想抓取网页,我们首先需要将其下载下来。下面的示例脚本使用 Python 的 urllib 模块下载 URL。

  1importurllib.request
2defdownload(url):
3returnurllib.request.urlopen(url).read()

  当传入 URL 参数时,该函数将会下载网页并返回其 HTML。不过,这个代码片段存在一个问题,即当下载网页时,我们可能会遇到一些无法控制的错误,比如请求的页面可能不存在。此时,urllib 会抛出异常,然后退出脚本。安全起见,下面再给出一个更稳建的版本,可以捕获这些异常。

  1importurllib.request
2fromurllib.errorimportURLError,HTTPError,ContentTooShortError
3
4defdownload(url):
5print('Downloading:',url)
6try:
7html=urllib.request.urlopen(url).read()
8except(URLError,HTTPError,ContentTooShortError)ase:
9print('Downloaderror:',e.reason)
10html=None
11returnhtml

  现在,当出现下载或 URL 错误时,该函数能够捕获到异常,然后返回 None。

  ① 重试下载

  下载时遇到的错误经常是临时性的,比如服务器过载时返回的 503 Service Unavailable 错误。对于此类错误,我们可以在短暂等待后尝试重新下载,因为这个服务器问题现在可能已经解决。不过,我们不需要对所有错误都尝试重新下载。如果服务器返回的是 404 Not Found 这种错误,则说明该网页目前并不存在,再次尝试同样的请求一般也不会出现不同的结果。

  互联网工程任务组(Internet Engineering Task Force)定义了 HTTP 错误的完整列表,从中可以了解到 4xx 错误发生在请求存在问题时,而 5xx 错误则发生在服务端存在问题时。所以,我们只需要确保 download 函数在发生 5xx 错误时重试下载即可。下面是支持重试下载功能的新版本代码。

  1defdownload(url,num_retries=2):
2print('Downloading:',url)
3try:
4html=urllib.request.urlopen(url).read()
5except(URLError,HTTPError,ContentTooShortError)ase:
6print('Downloaderror:',e.reason)
7html=None
8ifnum_retries>0:
9ifhasattr(e,'code')and500<=e.code<600:
10#recursivelyretry5xxHTTPerrors
11returndownload(url,num_retries-1)
12returnhtml

  现在,当 download 函数遇到 5xx 错误码时,将会递归调用函数自身进行重试。此外,该函数还增加了一个参数,用于设定重试下载的次数,其默认值为两次。我们在这里限制网页下载的尝试次数,是因为服务器错误可能暂时还没有恢复。想要测试该函数,可以尝试下载 http://httpstat.us/500,该网址会始终返回 500 错误码。

  1>>>download('http://httpstat.us/500')
2Downloading:http://httpstat.us/500
3Downloaderror:InternalServerError
4Downloading:http://httpstat.us/500
5Downloaderror:InternalServerError
6Downloading:http://httpstat.us/500
7Downloaderror:InternalServerError

  从上面的返回结果可以看出,download 函数的行为和预期一致,先尝试下载网页,在接收到 500 错误后,又进行了两次重试才放弃。

  ② 设置用户代理

  默认情况下,urllib 使用 Python-urllib/``3.x 作为用户代理下载网页内容,其中 3.x 是环境当前所用 Python 的版本号。如果能使用可辨识的用户代理则更好,这样可以避免我们的网络爬虫碰到一些问题。此外,也许是因为曾经历过质量不佳的 Python 网络爬虫造成的服务器过载,一些网站还会封禁这个默认的用户代理。

  因此,为了使下载网站更加可靠,我们需要控制用户代理的设定。下面的代码对 download 函数进行了修改,设定了一个默认的用户代理 ‘wswp’(即 Web Scraping with Python 的首字母缩写)。

  1defdownload(url,user_agent='wswp',num_retries=2):
2print('Downloading:',url)
3request=urllib.request.Request(url)
4request.add_header('User-agent',user_agent)
5try:
6html=urllib.request.urlopen(request).read()
7except(URLError,HTTPError,ContentTooShortError)ase:
8print('Downloaderror:',e.reason)
9html=None
10ifnum_retries>0:
11ifhasattr(e,'code')and500<=e.code<600:
12#recursivelyretry5xxHTTPerrors
13returndownload(url,num_retries-1)
14returnhtml

  现在,如果你再次尝试访问 meetup.com,就能够看到一个合法的 HTML 了。我们的下载函数可以在后续代码中得到复用,该函数能够捕获异常、在可能的情况下重试网站以及设置用户代理。

  网站地图爬虫

  在第一个简单的爬虫中,我们将使用示例网站 robots.txt 文件中发现的网站地图来下载所有网页。为了解析网站地图,我们将会使用一个简单的正则表达式,从 <loc> 标签中提取出 URL。

  我们需要更新代码以处理编码转换,因为我们目前的 download 函数只是简单地返回了字节。下面是该示例爬虫的代码。

  1importre
2
3defdownload(url,user_agent='wswp',num_retries=2,charset='utf-8'):
4print('Downloading:',url)
5request=urllib.request.Request(url)
6request.add_header('User-agent',user_agent)
7try:
8resp=urllib.request.urlopen(request)
9cs=resp.headers.get_content_charset()
10ifnotcs:
11cs=charset
12html=resp.read().decode(cs)
13except(URLError,HTTPError,ContentTooShortError)ase:
14print('Downloaderror:',e.reason)
15html=None
16ifnum_retries>0:
17ifhasattr(e,'code')and500<=e.code<600:
18#recursivelyretry5xxHTTPerrors
19returndownload(url,num_retries-1)
20returnhtml
21
22defcrawl_sitemap(url):
23#downloadthesitemapfile
24sitemap=download(url)
25#extractthesitemaplinks
26links=re.findall('(.*?)',sitemap)
27#downloadeachlink
28forlinkinlinks:
29html=download(link)
30#scrapehtmlhere
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 文件。

  ID 遍历爬虫

  本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家(或地区)的 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,测试示例网站中的链接是否仍然可用。测试结果如下图所示。

  

  从上图中可以看出,网页依然可以加载成功,也就是说该方法是有用的。现在,我们就可以忽略页面别名,只利用数据库 ID 来下载所有国家(或地区)的页面了。下面是使用了该技巧的代码片段。

  1importitertools
2
3defcrawl_site(url):
4forpageinitertools.count(1):
5pg_url='{}{}'.format(url,page)
6html=download(pg_url)
7ifhtmlisNone:
8break
9#success-canscrapetheresult

  现在,我们可以使用该函数传入基础 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 之间并不是连续的。此时,只要访问到某个间隔点,爬虫就会立即退出。下面是这段代码的改进版本,在该版本中连续发生多次下载错误后才会退出程序。

  1defcrawl_site(url,max_errors=5):
2forpageinitertools.count(1):
3pg_url='{}{}'.format(url,page)
4html=download(pg_url)
5ifhtmlisNone:
6num_errors+=1
7ifnum_errors==max_errors:
8#maxerrorsreached,exitloop
9break
10else:
11num_errors=0
12#success-canscrapetheresult

  上面代码中实现的爬虫需要连续 5 次下载错误才会停止遍历,这样就很大程度上降低了遇到记录被删除或隐藏时过早停止遍历的风险。

  在爬取网站时,遍历 ID 是一个很便捷的方法,但是和网站地图爬虫一样,这种方法也无法保证始终可用。比如,一些网站会检查页面别名是否在 URL 中,如果不是,则会返回 404 Not Found 错误。而另一些网站则会使用非连续大数作为 ID,或是不使用数值作为 ID,此时遍历就难以发挥其作用了。例如,Amazon 使用 ISBN 作为可用图书的 ID,这种编码包含至少 10 位数字。使用 ID 对 ISBN 进行遍历需要测试数十亿次可能的组合,因此这种方法肯定不是抓取该站内容最高效的方法。

  正如你一直关注的那样,你可能已经注意到一些 TOO MANY REQUESTS 下载错误信息。现在无须担心它,我们将会在 1.5 节的“高级功能”部分中介绍更多处理该类型错误的方法。

  链接爬虫

  到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有已发布的国家(或地区)页面。只要这两种技术可用,就应当使用它们进行爬取,因为这两种方法将需要下载的网页数量降至最低。不过,对于另一些网站,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。

  通过跟踪每个链接的方式,我们可以很容易地下载整个网站的页面。但是,这种方法可能会下载很多并不需要的网页。例如,我们想要从一个在线论坛中抓取用户账号详情页,那么此时我们只需要下载账号页,而不需要下载讨论贴的页面。本文使用的链接爬虫将使用正则表达式来确定应当下载哪些页面。下面是这段代码的初始版本。

  1importre
2
3deflink_crawler(start_url,link_regex):
4"""CrawlfromthegivenstartURLfollowinglinksmatchedby
5link_regex
6"""
7crawl_queue=[start_url]
8whilecrawl_queue:
9url=crawl_queue.pop()
10html=download(url)
11ifhtmlisnotNone:
12continue
13#filterforlinksmatchingourregularexpression
14forlinkinget_links(html):
15ifre.match(link_regex,link):
16crawl_queue.append(link)
17
18defget_links(html):
19"""Returnalistoflinksfromhtml
20"""
21#aregularexpressiontoextractalllinksfromthewebpage
22webpage_regex=re.compile("""]+href=["'](.*?)["']""",
23re.IGNORECASE)
24#listofalllinksfromthewebpage
25returnwebpage_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/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(mostrecentcalllast):
5...
6ValueError:unknownurltype:/index/1

  可以看出,问题出在下载 /index/1 时,该链接只有网页的路径部分,而没有协议和服务器部分,也就是说这是一个相对链接。由于浏览器知道你正在浏览哪个网页,并且能够采取必要的步骤处理这些链接,因此在浏览器浏览时,相对链接是能够正常工作的。但是,urllib 并没有上下文。为了让 urllib 能够定位网页,我们需要将链接转换为绝对链接的形式,以便包含定位网页的所有细节。如你所愿,Python 的 urllib 中有一个模块可以用来实现该功能,该模块名为 parse。下面是 link_crawler 的改进版本,使用了 urljoin 方法来创建绝对路径。

  1fromurllib.parseimporturljoin
2
3deflink_crawler(start_url,link_regex):
4"""CrawlfromthegivenstartURLfollowinglinksmatchedby
5link_regex
6"""
7crawl_queue=[start_url]
8whilecrawl_queue:
9url=crawl_queue.pop()
10html=download(url)
11ifnothtml:
12continue
13forlinkinget_links(html):
14ifre.match(link_regex,link):
15abs_link=urljoin(start_url,link)
16crawl_queue.append(abs_link)

  当你运行这段代码时,会看到虽然下载了匹配的网页,但是同样的地点总是会被不断下载到。产生该行为的原因是这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲又链接回了澳大利亚,此时爬虫就会继续将这些 URL 放入队列,永远不会到达队列尾部。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的 link_crawler 函数,具备了存储已发现 URL 的功能,可以避免重复下载。

  1deflink_crawler(start_url,link_regex):
2crawl_queue=[start_url]
3#keeptrackwhichURL'shaveseenbefore
4seen=set(crawl_queue)
5whilecrawl_queue:
6url=crawl_queue.pop()
7html=download(url)
8ifnothtml:
9continue
10forlinkinget_links(html):
11#checkiflinkmatchesexpectedregex
12ifre.match(link_regex,link):
13abs_link=urljoin(start_url,link)
14#checkifhavealreadyseenthislink
15ifabs_linknotinseen:
16seen.add(abs_link)
17crawl_queue.append(abs_link)

  当运行该脚本时,它会爬取所有地点,并且能够如期停止。最终,我们得到了一个可用的链接爬虫!

  本文摘自《用Python写网络爬虫(第2版)》

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

跟贴 跟贴 4 参与 7
© 1997-2020 网易公司版权所有 About NetEase | 公司简介 | 联系方法 | 招聘信息 | 客户服务 | 隐私政策 | 广告服务 | 侵权投诉 Reporting Infringements | 不良信息举报

CSDN

成就一亿技术人

头像

CSDN

成就一亿技术人

17161

篇文章

207781

人关注

列表加载中...
请登录后再关注
x

用户登录

网易通行证/邮箱用户可以直接登录:
忘记密码