3. 解析 HTML
这是什么:上一章抓回来的 r.text 是一整坨 HTML 文本,数据混在几万个字符里;BeautifulSoup(下称 bs4)把这段文本解析成「标签树」,然后你就能用选择器像点菜一样把数据挑出来。
为什么用它:手动 str.find() 截取网页是自虐——标签嵌套、属性顺序、空格换行都会变。bs4 按标签结构定位,页面格式小变也不容易挂,是 Python 解析 HTML 的入门标配。
如何用:先十分钟看懂 HTML 树,再学 find 和 select 两套查找 API,最后解决动态页面的「数据不在我这」问题。
十分钟看懂 HTML 树
HTML 是一对对标签(tag)嵌出来的树:
<div class="quote">
<span class="text">“The world as we have created it…”</span>
<small class="author">Albert Einstein</small>
<div class="tags">
<a class="tag" href="/tag/change/">change</a>
</div>
</div>
| 术语 | 例子 | 说明 |
|---|---|---|
| 标签 | <span>...</span> |
树上的节点,成对出现 |
| 属性 | class="text"、href="/tag/change/" |
挂在标签上的键值对 |
| 文本 | Albert Einstein |
标签包着的内容 |
| 父子关系 | div.quote 是 span.text 的父亲 |
选择器靠层级定位 |
装 bs4 并解析第一棵树
pip install beautifulsoup4
from bs4 import BeautifulSoup
html = """
<div class="quote">
<span class="text">“The world as we have created it…”</span>
<small class="author">Albert Einstein</small>
</div>
"""
soup = BeautifulSoup(html, "html.parser") # 用标准库解析器,零额外依赖
span = soup.find("span", class_="text") # 找第一个匹配的标签
print(span.get_text()) # 标签里的文本
print(span["class"]) # 标签的属性(像字典)
注意:Python 的 class 是关键字,bs4 里写 class_(带下划线)——入门第一个坑。
find 与 find_all
soup.find("small", class_="author") # 第一个匹配 → 返回标签对象;没有返回 None
soup.find_all("a") # 全部匹配 → 返回列表;没有返回空列表
| 方法 | 返回 | 拿不到时 |
|---|---|---|
find() |
单个标签 | None(所以先判空再用) |
find_all() |
列表 | [](所以直接 for 循环安全) |
select:用 CSS 选择器挑数据
bs4 还支持 CSS 选择器(前端 CSS 那套语法),层级关系写起来更顺手:
quotes = soup.select("div.quote") # 所有 class 为 quote 的 div
first_text = soup.select_one("span.text") # 只取第一个(对应 find)
tags = soup.select("div.tags a.tag") # 空格 = 「里面的」层级
选择器速查表(这张表 Scrapy 也用,第 5 章直接复用):
| 写法 | 选中什么 |
|---|---|
span |
所有 <span> 标签 |
.text |
class 为 text 的元素 |
#main |
id 为 main 的元素 |
div.quote |
class 为 quote 的 div |
div.tags a |
div.tags 里面的 a(子孙) |
a::attr(href)* |
a 标签的 href 属性值(*bs4 用 a["href"],Scrapy 用这种写法) |
实战:抓一整页名言
把第 2 章的请求和今天的解析接起来,存成 parse_quotes.py:
import requests
from bs4 import BeautifulSoup
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0"}
r = requests.get("https://quotes.toscrape.com/", headers=headers, timeout=10)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
for quote in soup.select("div.quote"):
text = quote.select_one("span.text").get_text()
author = quote.select_one("small.author").get_text()
tags = [a.get_text() for a in quote.select("a.tag")]
print(f"{author}:{text} 标签:{tags}")
你会看到:一页 10 条名言整齐打印。回想一下流程——请求(第 2 章)+ 解析(本章)= 一页数据的完整采集。
怎么知道该选 div.quote、span.text?在浏览器里按 F12,点左上角箭头图标,再点页面上你想抓的内容,开发者工具会跳到对应源码位置——看标签名和 class,照抄进选择器。这是爬虫最核心的「定位」手艺。
动态页面:数据不在 HTML 里怎么办
按 Ctrl+U 搜不到数据时(第 1 章说的动态页面),别解析空壳——直接调它背后的 JSON 接口:
- F12 打开 Network 标签 → 刷新页面 → 在请求列表里找
Fetch/XHR类型、名字像list/data/api的请求 - 点开看 Response:是不是 JSON?数据在不在里面?
- 右键该请求 → Copy → Copy URL,用 requests 请求它
import requests
r = requests.get("https://httpbin.org/json", timeout=10) # 一个返回 JSON 的演示接口
data = r.json() # 响应是 JSON 时,一步转成 Python 字典/列表
print(data["slideshow"]["title"]) # 像操作字典一样取数据
r.json() 一步到位,连解析都省了——能找到接口就别解析页面,接口返回的数据干净、稳定、还带翻页参数。
常见翻车现场
| 症状 | 原因 | 修复 |
|---|---|---|
AttributeError: 'NoneType' |
find/select_one 没找到,返回 None 还继续 .get_text() |
先 if x is not None 再用 |
KeyError: 'class' |
写成了 soup.find("span", class="text") |
是 class_ 带下划线 |
| 拿到的文本带一堆空格换行 | HTML 源码里本来就有缩进 | get_text(strip=True) |
| 页面上有但 select 不到 | 数据是 JS 后渲染的 | 回到 Ctrl+U 验证,走 JSON 接口路线 |
| 解析正常但字段对不上 | 页面结构和你以为的不一样 | F12 重新核对标签层级 |
动手改
- 把循环里的
tags打印改成只打印第一个标签,体会 select 返回列表的用法 - 抓
https://books.toscrape.com/首页,提取每本书的书名和价格(书名在h3 a的 title 属性里,价格在p.price_color) - 用 F12 找一个你常用网站的列表接口,用
r.json()取回第一页数据打印
上一章:requests 抓第一个网页 · 下一章:数据存储与去重 🐸