🐸 青蛙编程网
首页 / Python / 爬虫 / 3. 解析 HTML

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 接口:

  1. F12 打开 Network 标签 → 刷新页面 → 在请求列表里找 Fetch/XHR 类型、名字像 list/data/api 的请求
  2. 点开看 Response:是不是 JSON?数据在不在里面?
  3. 右键该请求 → 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 重新核对标签层级

动手改

  1. 把循环里的 tags 打印改成只打印第一个标签,体会 select 返回列表的用法
  2. 抓 https://books.toscrape.com/ 首页,提取每本书的书名和价格(书名在 h3 a 的 title 属性里,价格在 p.price_color)
  3. 用 F12 找一个你常用网站的列表接口,用 r.json() 取回第一页数据打印

上一章:requests 抓第一个网页 · 下一章:数据存储与去重 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。