网页抓取是通过程序下载网页并从中提取信息的过程。 这种技术很有用,在网页中有需要在程序中使用的信息时,就可使用它。 当然,如果网页是动态的,即随时间而变化,这就更有用了。 如果网页不是动态的,可手工下载一次并提取其中的信息。 (当然,最理想的情况是,可通过 Web服务来获取这些信息,这将在本章后面讨论。)
从概念上说,这种技术非常简单:下载数据并对其进行分析。
例如,可使用urllib
来获取网页的HTML代码,再使用正则表达式或其他技术从中提取信息。
例如,假设要从Python Job Board(http://python.org/jobs )提取招聘单位的名称和网站。
通过查看该网页的源代码,发现可在类似于下面的链接中找到名称和URL:
下面示例程序使用urllib
和re
来提取所需的信息。
import requests
import re
response = requests.get('http://python.org/jobs')
response.raise_for_status()
p = re.compile('<a href="(/jobs/\\d+)/">(.*?)</a>')
for url, name in p.findall(response.text):
print(f"{name} (https://python.org{url})")
Research Engineer (https://python.org/jobs/7854) Backend Python Developer (Django/DRF) (https://python.org/jobs/7853) CTO (Python Expert & Technical Leader) – Equity/Stock Options Available (https://python.org/jobs/7852) Senior Python SDR Software Engineer (https://python.org/jobs/7851) Senior Financial Analyst (https://python.org/jobs/7849) Senior Software Engineer (LATAM) (https://python.org/jobs/7848) Scientific Software Engineer (https://python.org/jobs/7847) Senior Scientific Software Engineer (https://python.org/jobs/7846) Core Tech AI Engineer at CodeFlash.ai (https://python.org/jobs/7844) Python Software Engineer (Docker required) (https://python.org/jobs/7843) Senior Data Analyst (https://python.org/jobs/7842) Senior Python Engineer (https://python.org/jobs/7841) Senior Full Stack Web Developer (Python/Django + CMS) (https://python.org/jobs/7840) Python Software Engineer (https://python.org/jobs/7836) Senior Platform Engineer (https://python.org/jobs/7835) Drone Python Programmer/Software Engineer (https://python.org/jobs/7834) Senior Back-End Engineer (Python) (https://python.org/jobs/7832) Senior Software Engineer (https://python.org/jobs/7830) Senior Python Full Stack Developer in Canada. (100% Remote) (https://python.org/jobs/7829) Python React Developer (https://python.org/jobs/7828) Full Stack Engineer (Django) (https://python.org/jobs/7827) Python/C++ Developer (https://python.org/jobs/7826) Senior Python Backend Developer with Blockchain Experience (https://python.org/jobs/7824) Python/MATLAB Programmer (https://python.org/jobs/7823) Senior Python Backend Engineer (https://python.org/jobs/7821)
这些代码当然有改进的空间,但已经做得非常出色了。 不过这种方法至少存在3个缺点。
- 正则表达式一点都不容易理解。如果HTML代码和查询都更复杂, 正则表达式将更难以理解和维护。
- 无法处理独特的HTML内容,如CDATA部分和字符实体(如&)。 遇到这种情况时,程序很可能会令人束手无策。
正则表达式依赖于HTML代码的细节,而不是更抽象的结构。 这意味着只要网页的结构发生细微的变化,这个程序可能就不管用(等阅读本书时,它可能已经不管用了)。
针对基于正则表达式的方法存在的问题,接下来将讨论两种可能的解决方案。 一是结合使用程序Tidy(一个Python库)和XHTML解析; 二是使用专为网页抓取而设计的Beautiful Soup库。
注意:还有其他Python网页抓取工具。例如,
可能想查看 Ka-Ping Yee
的scrape.py
(http://zesty.ca/python )。
Python标准库为解析HTML和XML等结构化格式提供了强大的支持。 XML和XML解析将在后面更深入地讨论, 这里只介绍处理XHTML所需的工具。 XHTML是HTML 规范描述的两种具体语法之一, 也是一 种XML格式。 这里介绍的大部分内容也适用于HTML。
如果每个网页包含的XHTML都正确而有效,解析工作将非常简单。 问题是较老的HTML方言不那么严谨,虽然有人指责这些不严谨的方言, 但有些人对这些指责置若罔闻。原因可能在于大多数Web浏览器都非常宽容, 即便面对的是最混乱、最无意义的HTML,也会尽最大努力将其渲染出来。 这为网页制作者提供了方便,可能让他们感到满意,却让网页抓取工作变得难得多。
标准库提供的通用的HTML解析方法是基于事件的:
编写事件处理程序,供解析程序处理 数据时调用。
标准库模块html.parser
能够以这种方式对极不严谨的HTML进行解析,
但要基于文档结构来提取数据(如第二个二级标题后面的第一项),
在存在标签缺失的情况下恐怕就只能靠猜了。如果愿意,
当然可以这样做,但还有另一种方式:使用Tidy。
Tidy是用于对格式不正确且不严谨的HTML进行修复的工具。 它非常聪明,能够修复很多常 见的错误,从而完成大量不愿意做的工作。 它还提供了极大的配置空间,能够开/关各种校正。
下面是一个错误百出的HTML文件,有些过时的HTML代码, 还有些明显的错误(能找出所有的问题吗):
下面是Tidy修复后的版本: 当然,Tidy并不能修复HTML文件存在的所有问题, 但确实能够确保文件是格式良好的(即所有元素都嵌套正确), 这让解析工作容易得多。
有多个用于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。
# pip install pytidylib
下面的程序将对这个文件运行Tidy并将结果打印出来:
如果Popen找不到tidy,可能需要提供这个可执行文件的完整路径。 在实际工作中,很可能不会打印结果,而是从中提取一些有用的信息, 这将在接下来的几小节中演示。
XHTML和旧式HTML的主要区别在于,XHTML非常严格,
要求显式地结束所有的元素(至少就当前的目标而言如此)。
因此,在HTML中,可通过(使用标签<p>
)开始另一个段落来结束当前段落,
但在XHTML中,必须先(使用标签</p>
)显式地结束当前段落。
这让XHTML 解析起来容易得多,因为能清楚地知道何时进入或离开各种元素。
XHTML的另一个优点是, 它是一种XML方言,可使用各种出色的工具(如XPath)来处理,
但本章不会利用这一点。
要对Tidy生成的格式良好的XHTML进行解析,
一种非常简单的方式是使用标准库模块html.parser
中的HTMLParser类。
使用HTMLParser意味着继承它,并重写各种事件处理方法,如 handle_starttag
和 handle_data
。
下表概述了相关的方法以及解析器在什么时候自动调用它们。以下是 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) |
遇到未知声明时调用 |
就网页抓取而言,通常无需实现所有的解析器回调方法(事件处理程序), 也可能无需创建整个文档的抽象表示(如文档树)就能找到所需的内容。 只需跟踪找到目标内容所需的信息就可以了。
功能说明
链接提取:自动收集网页中所有的标签的href属性
特定内容提取:查找具有特定class的
元素并提取其文本内容可扩展性:可以通过添加更多handle_*方法处理其他HTML元素
使用建议
对于简单任务,HTMLParser足够使用且轻量
对于复杂页面解析,BeautifulSoup通常更方便
可以结合正则表达式进行更复杂的内容匹配
from html.parser import HTMLParser
import urllib.request
# 自定义HTML解析器类
class MyHTMLParser(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
self.current_data = ""
self.recording = False
self.target_class = "some-class" # 设置要查找的class名
def handle_starttag(self, tag, attrs):
# 处理链接
if tag == 'a':
for (attr, value) in attrs:
if attr == 'href':
self.links.append(value)
# 处理特定class的内容
if tag == 'div':
for (attr, value) in attrs:
if attr == 'class' and value == self.target_class:
self.recording = True
def handle_data(self, data):
if self.recording:
self.current_data += data
def handle_endtag(self, tag):
if tag == 'div' and self.recording:
print("找到内容:", self.current_data.strip())
self.current_data = ""
self.recording = False
# 获取网页内容
url = "https://example.com" # 替换为目标网址
response = urllib.request.urlopen(url)
html_content = response.read().decode('utf-8')
# 创建解析器并解析内容
parser = MyHTMLParser()
parser.feed(html_content)
# 输出所有链接
print("\n网页中的所有链接:")
for link in parser.links:
print(link)
网页中的所有链接: https://www.iana.org/domains/example
高级用法示例
class AdvancedParser(HTMLParser):
def __init__(self):
super().__init__()
self.inside_target = False
self.depth = 0
self.result = []
def handle_starttag(self, tag, attrs):
if tag == 'div' and ('id', 'content') in attrs:
self.inside_target = True
if self.inside_target:
self.depth += 1
def handle_endtag(self, tag):
if self.inside_target:
self.depth -= 1
if self.depth == 0:
self.inside_target = False
def handle_data(self, data):
if self.inside_target:
self.result.append(data.strip())
# 使用示例
parser = AdvancedParser()
parser.feed(html_content)
print("提取的内容:", ' '.join(parser.result))
提取的内容:
有几点需要注意。首先,这里没有使用Tidy,因为这个网页的HTML格式足够良好。
如果运气好,可能发现并不需要使用Tidy。
另外,使用了一个布尔状态变量(属性)来跟踪自己是 否位于相关的链接中。
在事件处理程序中,检查并更新这个属性。
其次,handle_starttag
的参 数是一个由形如(key, value)的元组组成的列表,
因此使用dict
将它们转换为字典,以便管理。
方法 handle_data
(和属性 chunks
)可能需要稍做说明。
它使用的技术在基于事件的结构化标记(如HTML和XML)解析中很常见:
不是假定通过调用handle_data
一次就能获得所需的所有 文本,
而是假定这些文本分成多个块,需要多次调用handle_data
才能获得。
导致这种情况的原因有多个——缓冲、字符实体、忽略的标记等,
因此需要确保获取所有的文本。
接下来,为了(在方法handle_endtag
中)输出结果,
将所有的文本块合并在一起。
为运行这个解析器,调用其方法feed将并text作为参数,
然后调用其方法close。
在有些情况下,这样的解决方案比使用正则表达式更健壮——应对输入数据变化的能力更强。 然而,可能持反对意见,理由是与使用正则表达式相比,这种解决方案的代码更繁琐, 还 可能不那么清晰易懂。 面对更复杂的提取任务时,支持这种解决方案的论据可能更有说服力, 但即便如此,还是让人依稀觉得一定有更好的办法。 如果不介意多安装一个模块,确实有更佳的办法,下面就来介绍。
$ pip install beautifulsoup4
可能想使用pip
进行搜索,看看是否有更新的版本。安装Beautiful Soup,
编写从Python Job Board提取Python职位的程序非常容易,且代码很容易理解,如下面代码所示。
这个程序不检查网页的内容,而是在文档结构中导航。
from bs4 import BeautifulSoup
import requests
# 1. 获取网页内容
url = "https://example.com" # 替换为你想抓取的网址
response = requests.get(url)
html_content = response.text
# 2. 创建BeautifulSoup对象
soup = BeautifulSoup(html_content, 'html.parser')
# 3. 提取数据示例
# 提取所有链接
print("网页中的所有链接:")
for link in soup.find_all('a'):
print(link.get('href'))
# 提取标题
title = soup.title.string
print(f"\n网页标题: {title}")
# 提取特定class的内容
print("\n特定class的内容:")
for item in soup.find_all(class_='some-class'): # 替换'some-class'为实际class名
print(item.text.strip())
# 4. 更复杂的提取示例
# 提取表格数据
print("\n表格数据:")
for table in soup.find_all('table'):
for row in table.find_all('tr'):
cells = [cell.text.strip() for cell in row.find_all('td')]
print(cells)
网页中的所有链接: https://www.iana.org/domains/example 网页标题: Example Domain 特定class的内容: 表格数据:
对于需要抓取RSS feed(本章后续将详细探讨)的场景, 推荐使用Beautiful Soup生态中的专用工具ScrapeNFeed(官方地址:http://crummy.com/software/ScrapeNFeed)。