## 用Python解析HTML页面
在前面的课程中,我们讲到了使用`request`三方库获取网络资源,还介绍了一些前端的基础知识。接下来,我们继续探索如何解析 HTML 代码,从页面中提取出有用的信息。之前,我们尝试过用正则表达式的捕获组操作提取页面内容,但是写出一个正确的正则表达式也是一件让人头疼的事情。为了解决这个问题,我们得先深入的了解一下 HTML 页面的结构,并在此基础上研究另外的解析页面的方法。
### HTML 页面的结构
我们在浏览器中打开任意一个网站,然后通过鼠标右键菜单,选择“显示网页源代码”菜单项,就可以看到网页对应的 HTML 代码。
![image-20210822094218269](https://gitee.com/jackfrued/mypic/raw/master/20210822094218.png)
代码的第`1`行是文档类型声明,第`2`行的``标签是整个页面根标签的开始标签,最后一行是根标签的结束标签``。``标签下面有两个子标签`
`和``,放在``标签下的内容会显示在浏览器窗口中,这部分内容是网页的主体;放在``标签下的内容不会显示在浏览器窗口中,但是却包含了页面重要的元信息,通常称之为网页的头部。HTML 页面大致的代码结构如下所示。
```HTML
```
标签、层叠样式表(CSS)、JavaScript 是构成 HTML 页面的三要素,其中标签用来承载页面要显示的内容,CSS 负责对页面的渲染,而 JavaScript 用来控制页面的交互式行为。要实现 HTML 页面的解析,可以使用 XPath 的语法,它原本是 XML 的一种查询语法,可以根据 HTML 标签的层次结构提取标签中的内容或标签属性;此外,也可以使用 CSS 选择器来定位页面元素,就跟用 CSS 渲染页面元素是同样的道理。
### XPath 解析
XPath 是在 XML(eXtensible Markup Language)文档中查找信息的一种语法,XML 跟 HTML 类似也是一种用标签承载数据的标签语言,不同之处在于 XML 的标签是可扩展的,可以自定义的,而且 XML 对语法有更严格的要求。XPath 使用路径表达式来选取 XML 文档中的节点或者节点集,这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。下面我们通过一个例子来说明如何使用 XPath 对页面进行解析。
```XML
Harry Potter
29.99
Learning XML
39.95
```
对于上面的 XML 文件,我们可以用如下所示的 XPath 语法获取文档中的节点。
| 路径表达式 | 结果 |
| --------------- | ------------------------------------------------------------ |
| `/bookstore` | 选取根元素 bookstore。**注意**:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
| `//book` | 选取所有 book 子元素,而不管它们在文档中的位置。 |
| `//@lang` | 选取名为 lang 的所有属性。 |
| `/bookstore/book[1]` | 选取属于 bookstore 子元素的第一个 book 元素。 |
| `/bookstore/book[last()]` | 选取属于 bookstore 子元素的最后一个 book 元素。 |
| `/bookstore/book[last()-1]` | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
| `/bookstore/book[position()<3]` | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
| `//title[@lang]` | 选取所有拥有名为 lang 的属性的 title 元素。 |
| `//title[@lang='eng']` | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
| `/bookstore/book[price>35.00]` | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
| `/bookstore/book[price>35.00]/title` | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
XPath还支持通配符用法,如下所示。
| 路径表达式 | 结果 |
| -------------- | --------------------------------- |
| `/bookstore/*` | 选取 bookstore 元素的所有子元素。 |
| `//*` | 选取文档中的所有元素。 |
| `//title[@*]` | 选取所有带有属性的 title 元素。 |
如果要选取多个节点,可以使用如下所示的方法。
| 路径表达式 | 结果 |
| ---------------------------------- | ------------------------------------------------------------ |
| `//book/title \| //book/price` | 选取 book 元素的所有 title 和 price 元素。 |
| `//title \| //price` | 选取文档中的所有 title 和 price 元素。 |
| `/bookstore/book/title \| //price` | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
> **说明**:上面的例子来自于“菜鸟教程”网站上的 [XPath 教程](),有兴趣的读者可以自行阅读原文。
当然,如果不理解或不熟悉 XPath 语法,可以在浏览器的开发者工具中按照如下所示的方法查看元素的 XPath 语法,下图是在 Chrome 浏览器的开发者工具中查看豆瓣网电影详情信息中影片标题的 XPath 语法。
![](https://gitee.com/jackfrued/mypic/raw/master/20210822093707.png)
实现 XPath 解析需要三方库`lxml` 的支持,可以使用下面的命令安装`lxml`。
```Bash
pip install lxml
```
下面我们用 XPath 解析方式改写之前获取豆瓣电影 Top250的代码,如下所示。
```Python
from lxml import etree
import requests
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
headers={'User-Agent': 'BaiduSpider'}
)
tree = etree.HTML(resp.text)
# 通过XPath语法从页面中提取电影标题
title_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]')
# 通过XPath语法从页面中提取电影评分
rank_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]')
for title_span, rank_span in zip(title_spans, rank_spans):
print(title_span.text, rank_span.text)
```
### CSS 选择器解析
对于熟悉 CSS 选择器和 JavaScript 的开发者来说,通过 CSS 选择器获取页面元素可能是更为简单的选择,因为浏览器中运行的 JavaScript 本身就可以`document`对象的`querySelector()`和`querySelectorAll()`方法基于 CSS 选择器获取页面元素。在 Python 中,我们可以利用三方库`beautifulsoup4`或`pyquery`来做同样的事情。Beautiful Soup 可以用来解析 HTML 和 XML 文档,修复含有未闭合标签等错误的文档,通过为待解析的页面在内存中创建一棵树结构,实现对从页面中提取数据操作的封装。可以用下面的命令来安装 Beautiful Soup。
```Python
pip install beautifulsoup4
```
下面是使用`bs4`改写的获取豆瓣电影Top250电影名称的代码。
```Python
import bs4
import requests
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
headers={'User-Agent': 'BaiduSpider'}
)
# 创建BeautifulSoup对象
soup = bs4.BeautifulSoup(resp.text, 'lxml')
# 通过CSS选择器从页面中提取包含电影标题的span标签
title_spans = soup.select('div.info > div.hd > a > span:nth-child(1)')
# 通过CSS选择器从页面中提取包含电影评分的span标签
rank_spans = soup.select('div.info > div.bd > div > span.rating_num')
for title_span, rank_span in zip(title_spans, rank_spans):
print(title_span.text, rank_span.text)
```
关于 BeautifulSoup 更多的知识,可以参考它的[官方文档](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)。
### 简单的总结
下面我们对三种解析方式做一个简单比较。
| 解析方式 | 对应的模块 | 速度 | 使用难度 |
| -------------- | ---------------- | ------ | -------- |
| 正则表达式解析 | `re` | 快 | 困难 |
| XPath 解析 | `lxml` | 快 | 一般 |
| CSS 选择器解析 | `bs4`或`pyquery` | 不确定 | 简单 |