>>> from env_helper import info; info()
页面更新时间: 2020-02-22 11:31:01
操作系统/OS: Linux-4.19.0-8-amd64-x86_64-with-debian-10.3 ;Python: 3.7.3

3.1. 网页抓取

网页抓取是通过程序下载网页并从中提取信息的过程。这种技术很有用,在网页中有你要在 程序中使用的信息时,就可使用它。当然,如果网页是动态的,即随时间而变化,这就更有用了。 如果网页不是动态的,你可手工下载一次并提取其中的信息。(当然,最理想的情况是,可通过 Web服务来获取这些信息,这将在本章后面讨论。)

从概念上说,这种技术非常简单:下载数据并对其进行分析。例如,你可使用urllib来获取网页的HTML代码,再使用正则表达式或其他技术从中提取信息。例如,假设你要从Python Job Board(http://python.org/jobs )提取招聘单位的名称和网站。 通过查看该网页的源 代码,你发现可在类似于下面的链接中找到名称和URL:

Python Engineer

代码清单15-1所示的示例程序使用urllib和re来提取所需的信息。

3.1.1. 代码清单15-1 简单的网页抓取程序

>>> from urllib.request import urlopen
>>> import re
>>>
>>> p = re.compile('<a href="(/jobs/\\d+)/">(.*?)</a>')
>>> text = urlopen('http://python.org/jobs').read().decode()
>>>
>>> for url, name in p.findall(text):
>>>     print('{} ({})'.format(name, url))
ERP5 Python Developer for Automotive (/jobs/4428)
Python PyData and Javascript Project Developer (/jobs/4427)
Remote Senior Python Developer (/jobs/4426)
Edge Computing for 4G/5G Telco (/jobs/4425)
Python Out Of Core Numpy Developer (/jobs/4424)
Open Source Robot / Industrial Automation (/jobs/4423)
Artificial Optical Inspection (AOI) (/jobs/4422)
Jupyter Lab Mass Deployment (/jobs/4421)
Senior Python Developer (/jobs/4419)
Embedded and Back End Software Engineer (/jobs/4418)
Collections and Information Developer (/jobs/4410)
Dev Ops Engineer (Remote) (/jobs/4409)
Senior-Level Python Developer (/jobs/4407)
Junior Backend Developer Part-time and Remote, Continuous demand (/jobs/4406)
Python Developer (/jobs/4405)
Full Stack Python Developer [SuperDesk] (/jobs/4404)
Senior Engineer (/jobs/4370)
Senior Full Stack Engineer (/jobs/4368)
Python  with C++  AND VC++ Experts  - Bangalore / Noida (/jobs/4367)
Python / Deep Learning Instructor (/jobs/4366)
Senior Python ETL Developer (/jobs/4365)
Life Adventure: Philippines + 40 Existing Devs (/jobs/4364)
Tech Lead Python Machine Learning Engineer (/jobs/4361)
Python/Django Full Stack Developer (/jobs/4360)
Software Engineer (/jobs/4359)

这些代码当然有改进的空间,但已经做得非常出色了。然而,这种方法至少存在3个缺点。

  • 正则表达式一点都不容易理解。如果HTML代码和查询都更复杂,正则表达式将更难以理 解和维护。

  • 它对付不了独特的HTML内容,如CDATA部分和字符实体(如&)。遇到这样的东西 时,这个程序很可能束手无策。

正则表达式依赖于HTML代码的细节,而不是更抽象的结构。这意味着只要网页的结构发生 细微的变化,这个程序可能就不管用(等你阅读本书时,它可能已经不管用了)。

针对基于正则表达式的方法存在的问题,接下来将讨论两种可能的解决方案。一是结合使用 程序Tidy(一个Python库)和XHTML解析;二是使用专为网页抓取而设计的Beautiful Soup库。

注意 还有其他Python网页抓取工具。例如,你可能想查看Ka-Ping Yee的scrape.py(http://zesty.ca/python )。

Tidy 和 XHTML 解析

Python标准库为解析HTML和XML等结构化格式提供了强大的支持(参见“Python库参考手 册”中的Structured Markup Processing Tools部分)。XML和XML解析将在第22章更深入地讨论, 这里只介绍处理XHTML所需的工具。XHTML是HTML 5规范描述的两种具体语法之一,也是一 种XML格式。这里介绍的大部分内容也适用于HTML。

如果每个网页包含的XHTML都正确而有效,解析工作将非常简单。问题是较老的HTML方言不那么严谨,虽然有人指责这些不严谨的方言,但有些人对这些指责置若罔闻。原因可能在于 大多数Web浏览器都非常宽容,即便面对的是最混乱、最无意义的HTML,也会尽最大努力将其 渲染出来。这为网页制作者提供了方便,可能让他们感到满意,却让网页抓取工作变得难得多。

标准库提供的通用的HTML解析方法是基于事件的:你编写事件处理程序,供解析程序处理 数据时调用。标准库模块html.parser让你能够以这种方式对极不严谨的HTML进行解析,但要基 于文档结构来提取数据(如第二个二级标题后面的第一项),在存在标签缺失的情况下恐怕就只 能靠猜了。如果你愿意,当然可以这样做,但还有另一种方式——使用Tidy。

3.1.2. Tidy是什么

Tidy是用于对格式不正确且不严谨的HTML进行修复的工具。它非常聪明,能够修复很多常 见的错误,从而完成大量你不愿意做的工作。它还提供了极大的配置空间,让你能够开/关各种校正。

下面是一个错误百出的HTML文件——有些过时的HTML代码,还有些明显的错误(你能找出所有的问题吗):

>>> ugly_html = '''
>>> <h1>Pet Shop
>>> <h2>Complaints</h3>
>>> <p>There is <b>no <i>way</b> at all</i> we can accept returned
>>> parrots.
>>> <h1><i>Dead Pets</h1>
>>> <p>Our pets may tend to rest at times, but rarely die within the
>>> warranty period.
>>> <i><h2>News</h2></i>
>>> <p>We have just received <b>a really nice parrot.
>>> <p>It's really nice.</b>
>>> <h3><hr>The Norwegian Blue</h3>
>>> <h4>Plumage and <hr>pining behavior</h4>
>>> <a href="#norwegian-blue">More information<a>
>>> <p>Features:
>>> <body>
>>> <li>Beautiful plumage
>>> '''
>>> with open('/tmp/ugly.html', 'w') as fo:
>>>     fo.write(ugly_html)

下面是Tidy修复后的版本:

当然,Tidy并不能修复HTML文件存在的所有问题,但确实能够确保文件是格式良好的(即 所有元素都嵌套正确),这让解析工作容易得多。

3.1.3. 获取Tidy

有多个用于Python的Tidy库包装器,至于哪个最新并非固定不变的。可像下面这样使用pip 来找出可供使用的包装器:

$ pip search tidy

一个不错的选择是PyTidyLib,可像下面这样安装它:

$ pip install pytidylib

然而,并非一定要安装Tidy库包装器。如果你使用的是UNIX或Linux系统,很可能已安装了 命令行版Tidy。 另外,不管你使用的是哪种操作系统,都可从Tidy网站(http://html-tidy.org )获取可执行的二进制版本。 有了二进制版本后,就可使用模块subprocess(或其他包含popen函数的 模块)来运行Tidy程序了。 例如,假设你有一个混乱的HTML文件(messy.html),且在执行路径 中包含命令行版Tidy,

$ sudo apt install tidy

下面的程序将对这个文件运行Tidy并将结果打印出来:

>>> from subprocess import Popen, PIPE
>>>
>>> text = open('/tmp/ugly.html').read()
>>> tidy = Popen('tidy', stdin=PIPE, stdout=PIPE, stderr=PIPE)
>>>
>>> tidy.stdin.write(text.encode())
>>> tidy.stdin.close()
>>>
>>> print(tidy.stdout.read().decode())
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.6.0">
<title></title>
</head>
<body>
<h1>Pet Shop</h1>
<h2>Complaints</h2>
<p>There is <b>no <i>way</i></b> <i>at all</i> we can accept
returned parrots.</p>
<h1><i>Dead Pets</i></h1>
<p><i>Our pets may tend to rest at times, but rarely die within the
warranty period. </i></p>
<h2><i>News</i></h2>
<p>We have just received <b>a really nice parrot.</b></p>
<p><b>It's really nice.</b></p>
<hr>
<h3>The Norwegian Blue</h3>
<h4>Plumage and</h4>
<hr>
<h4>pining behavior</h4>
<a href="#norwegian-blue">More information</a>
<p>Features:</p>
<li>Beautiful plumage</li>
</body>
</html>

如果Popen找不到tidy,可能需要提供这个可执行文件的完整路径。 在实际工作中,你很可能不会打印结果,而是从中提取一些有用的信息,这将在接下来的几小节中演示。

3.1.4. 为何使用XHTML

XHTML和旧式HTML的主要区别在于,XHTML非常严格,要求显式地结束所有的元素(至 少就我们当前的目标而言如此)。因此,在HTML中,可通过(使用标签<p>)开始另一个段落来 结束当前段落,但在XHTML中,必须先(使用标签</p>)显式地结束当前段落。这让XHTML 解析起来容易得多,因为你能清楚地知道何时进入或离开各种元素。XHTML的另一个优点是, 它是一种XML方言,可使用各种出色的工具(如XPath)来处理,但本章不会利用这一点。有关 XML的详细信息,请参阅第22章。有关如何使用XPath的详细信息,请参阅http://www.w3schools.com/xml/xml:xpath.asp 。

要对Tidy生成的格式良好的XHTML进行解析,一种非常简单的方式是使用标准库模块html.parser中的HTMLParser类。

3.1.5. 使用HTMLParser

使用HTMLParser意味着继承它,并重写各种事件处理方法,如handle_starttag和handle_data。 表15-1概述了相关的方法以及解析器在什么时候自动调用它们。

表 HTMLParser中的回调方法

回调方法 | 何时被调用
---------|-----------
handle_starttag(tag, attrs) | 遇到开始标签时调用。attrs是一个由形如(name, value)的元组组成的序列
handle_startendtag(tag, attrs) | 遇到空标签时调用。默认分别处理开始标签和结束标签
handle_endtag(tag) | 遇到结束标签时调用
handle_data(data) | 遇到文本数据时调用
handle_charref(ref) | 遇到形如&#ref;的字符引用时调用
handle_entityref(name) | 遇到形如&name;的实体引用时调用
handle_comment(data) |  遇到注释时;只对注释内容调用
handle_decl(decl) | 遇到形如<!...>的声明时调用
handle_pi(data) | 用于处理指令
unknown_decl(data) | 遇到未知声明时调用

就网页抓取而言,通常无需实现所有的解析器回调方法(事件处理程序),也可能无需创建 整个文档的抽象表示(如文档树)就能找到所需的内容。只需跟踪找到目标内容所需的信息就可 以了。(有关这个主题的更详细信息,请参阅第22章;该章讨论了如何使用SAX来解析XML。) 代码所示程序解决的问题与前面相同,但使用的是 HTMLParser

代码: 使用模块 HTMLParser 的网页抓取程序

>>> from urllib.request import urlopen
>>> from html.parser import HTMLParser
>>>
>>> def isjob(url):
>>>     try:
>>>         a, b, c, d = url.split('/')
>>>     except ValueError:
>>>         return False
>>>     return a == d == '' and b == 'jobs' and c.isdigit()
>>>
>>> class Scraper(HTMLParser):
>>>     in_link = False
>>>
>>>     def handle_starttag(self, tag, attrs):
>>>         attrs = dict(attrs)
>>>         url = attrs.get('href', '')
>>>
>>>         if tag == 'a' and isjob(url):
>>>             self.url = url
>>>             self.in_link = True
>>>             self.chunks = []
>>>
>>>     def handle_data(self, data):
>>>         if self.in_link:
>>>             self.chunks.append(data)
>>>
>>>     def handle_endtag(self, tag):
>>>         if tag == 'a' and self.in_link:
>>>             print('{} ({})'.format(''.join(self.chunks), self.url))
>>>             self.in_link = False
>>>
>>> text = urlopen('http://python.org/jobs').read().decode()
>>> parser = Scraper()
>>> parser.feed(text)
>>> parser.close()
ERP5 Python Developer for Automotive (/jobs/4428/)
Python PyData and Javascript Project Developer (/jobs/4427/)
Remote Senior Python Developer (/jobs/4426/)
Edge Computing for 4G/5G Telco (/jobs/4425/)
Python Out Of Core Numpy Developer (/jobs/4424/)
Open Source Robot / Industrial Automation (/jobs/4423/)
Artificial Optical Inspection (AOI) (/jobs/4422/)
Jupyter Lab Mass Deployment (/jobs/4421/)
Senior Python Developer (/jobs/4419/)
Embedded and Back End Software Engineer (/jobs/4418/)
Collections and Information Developer (/jobs/4410/)
Dev Ops Engineer (Remote) (/jobs/4409/)
Senior-Level Python Developer (/jobs/4407/)
Junior Backend Developer Part-time and Remote, Continuous demand (/jobs/4406/)
Python Developer (/jobs/4405/)
Full Stack Python Developer [SuperDesk] (/jobs/4404/)
Senior Engineer (/jobs/4370/)
Senior Full Stack Engineer (/jobs/4368/)
Python  with C++  AND VC++ Experts  - Bangalore / Noida (/jobs/4367/)
Python / Deep Learning Instructor (/jobs/4366/)
Senior Python ETL Developer (/jobs/4365/)
Life Adventure: Philippines + 40 Existing Devs (/jobs/4364/)
Tech Lead Python Machine Learning Engineer (/jobs/4361/)
Python/Django Full Stack Developer (/jobs/4360/)
Software Engineer (/jobs/4359/)

有几点需要注意。首先,这里没有使用Tidy,因为这个网页的HTML格式足够良好。 如果你 运气好,可能发现并不需要使用Tidy。 另外,我使用了一个布尔状态变量(属性)来跟踪自己是 否位于相关的链接中。 在事件处理程序中,我检查并更新这个属性。其次,handle_starttag的参 数是一个由形如(key, value)的元组组成的列表,因此我使用dict将它们转换为字典,以便管理。

方法 handle_data (和属性 chunks )可能需要稍做说明。 它使用的技术在基于事件的结构化标 记(如HTML和XML)解析中很常见: 不是假定通过调用handle_data一次就能获得所需的所有 文本,而是假定这些文本分成多个块,需要多次调用handle_data才能获得。 导致这种情况的原 因有多个——缓冲、字符实体、忽略的标记等,因此需要确保获取所有的文本。 接下来,为了(在 方法handle_endtag中)输出结果,我将所有的文本块合并在一起。 为运行这个解析器,调用其 方法feed将并text作为参数,然后调用其方法close。

在有些情况下,这样的解决方案比使用正则表达式更健壮——应对输入数据变化的能力更 强。 然而,你可能持反对意见,理由是与使用正则表达式相比,这种解决方案的代码更繁琐,还 可能不那么清晰易懂。 面对更复杂的提取任务时,支持这种解决方案的论据可能更有说服力,但即便如此,还是让人依稀觉得一定有更好的办法。 如果你不介意多安装一个模块,确实有更佳的办法,下面就来介绍。

Beautiful Soup

Beautiful Soup是一个小巧而出色的模块,用于解析你在Web上可能遇到的不严谨且格式糟糕 的HTML。

下载并安装Beautiful Soup易如反掌。与大多数包一样,你可使用pip来完成这种任务。

$ pip install beautifulsoup4

你可能想使用pip进行搜索,看看是否有更新的版本。安装Beautiful Soup,编写从Python Job Board提取Python职位的程序非常容易,且代码很容易理解,如代码清单15-3所示。这个程序不检 查网页的内容,而是在文档结构中导航。

代码: 使用Beautiful Soup的网页抓取程序

>>> from urllib.request import urlopen
>>> from bs4 import BeautifulSoup
>>>
>>> text = urlopen('http://python.org/jobs').read()
>>> soup = BeautifulSoup(text, 'html.parser')
>>>
>>> jobs = set()
>>> for job in soup.body.section('h2'):
>>>     jobs.add('{} ({})'.format(job.a.string, job.a['href']))
>>>
>>> print('\n'.join(sorted(jobs, key=str.lower)))
Artificial Optical Inspection (AOI) (/jobs/4422/)
Collections and Information Developer (/jobs/4410/)
Dev Ops Engineer (Remote) (/jobs/4409/)
Edge Computing for 4G/5G Telco (/jobs/4425/)
Embedded and Back End Software Engineer (/jobs/4418/)
ERP5 Python Developer for Automotive (/jobs/4428/)
Full Stack Python Developer [SuperDesk] (/jobs/4404/)
Junior Backend Developer Part-time and Remote, Continuous demand (/jobs/4406/)
Jupyter Lab Mass Deployment (/jobs/4421/)
Life Adventure: Philippines + 40 Existing Devs (/jobs/4364/)
Open Source Robot / Industrial Automation (/jobs/4423/)
Python  with C++  AND VC++ Experts  - Bangalore / Noida (/jobs/4367/)
Python / Deep Learning Instructor (/jobs/4366/)
Python Developer (/jobs/4405/)
Python Out Of Core Numpy Developer (/jobs/4424/)
Python PyData and Javascript Project Developer (/jobs/4427/)
Python/Django Full Stack Developer (/jobs/4360/)
Remote Senior Python Developer (/jobs/4426/)
Senior Engineer (/jobs/4370/)
Senior Full Stack Engineer (/jobs/4368/)
Senior Python Developer (/jobs/4419/)
Senior Python ETL Developer (/jobs/4365/)
Senior-Level Python Developer (/jobs/4407/)
Software Engineer (/jobs/4359/)
Tech Lead Python Machine Learning Engineer (/jobs/4361/)

我使用要从中抓取文本的HTML代码实例化BeautifulSoup类,然后用各种机制来提取解析树 的不同部分。例如,使用soup.body来获取文档体,再访问其中的第一个section。使用参数’h2’ 调用返回的对象,这与使用其方法find_all等效——返回其中的所有h2元素。每个h2元素都表示 一个职位,而我感兴趣的是它包含的第一个链接job.a。属性string是链接的文本内容,而a[‘href’] 为属性href。你肯定注意到了,在代码清单15-3中,我使用了set和sorted(通过将参数key设置 为一个函数以忽略大小写)。这些与Beautiful Soup毫无关系,旨在消除重复的职位并按字母顺序 打印它们,从而让这个程序更有用。

如果你要抓取(本章后面将讨论的)RSS feed,可使用另一个与Beautiful Soup相关的工具, 名为 ScrapeNFeedhttp://crummy.com/software/ScrapeNFeed )。