Day 127 — BeautifulSoup / lxml 解析:图解
1. 解析流程
┌──────────────────────────────────────────────────────────────┐
│ HTML 解析流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 原始HTML │───▶│ 解析器选择│───▶│ 构建DOM树 │ │
│ │ 字符串 │ │ BS4/lxml │ │ │ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 提取数据 │◀───│ 查找元素 │◀───│ 遍历DOM树│ │
│ │ text/attr│ │ find/xpath│ │ │ │
│ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 清洗数据 │───▶│ 存储数据 │ │
│ │ 去空格 │ │ JSON/CSV │ │
│ └──────────┘ └──────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
2. BeautifulSoup 查找方法对比
┌──────────────────────────────────────────────────────────────┐
│ BeautifulSoup 查找方法对比 │
├──────────────────────────────────────────────────────────────┤
│ │
│ find() — 返回第一个匹配 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ soup.find("h1") → <h1>标题</h1> │ │
│ │ soup.find("h1", class_="main") │ │
│ │ soup.find("a", href="/about") │ │
│ │ soup.find("div", attrs={"id": "content"}) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ find_all() — 返回所有匹配 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ soup.find_all("a") → [<a>, <a>, <a>] │ │
│ │ soup.find_all("a", limit=3) → 最多3个 │ │
│ │ soup.find_all(["h1", "h2"]) → 所有h1和h2 │ │
│ │ soup.find_all("p", class_=re.compile("post")) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ CSS 选择器 — 更简洁的语法 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ soup.select("h1") → [<h1>] │ │
│ │ soup.select(".main") → [<h1 class="main">] │ │
│ │ soup.select("div > p") → div 的直接子 p │ │
│ │ soup.select("div p") → div 的所有后代 p │ │
│ │ soup.select("a[href]") → 有 href 的所有 a │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
3. XPath 语法速查
┌──────────────────────────────────────────────────────────────┐
│ XPath 语法速查 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 路径表达式 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ //div → 所有 div 元素 │ │
│ │ /html/body → 绝对路径 │ │
│ │ //div/p → div 的直接子 p │ │
│ │ //div//p → div 的所有后代 p │ │
│ │ .. → 父元素 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 谓语(条件) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ //div[@class="main"] → class="main" 的 div │ │
│ │ //a[@href] → 有 href 的 a │ │
│ │ //p[contains(text(),"Python")] → 包含 "Python" 的 p │ │
│ │ //li[1] → 第一个 li │ │
│ │ //li[last()] → 最后一个 li │ │
│ │ //li[position()<3] → 前两个 li │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 提取 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ //h1/text() → h1 的文本内容 │ │
│ │ //a/@href → 所有 a 的 href 属性 │ │
│ │ //div/@class → 所有 div 的 class 属性 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 组合 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ //h1 | //h2 → h1 或 h2 │ │
│ │ //div[p] → 包含 p 的 div │ │
│ │ //div[p and a] → 同时包含 p 和 a 的 div │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
4. 解析器选择决策树
┌──────────────────────────────────────────────────────────────┐
│ 解析器选择决策树 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 需要解析 HTML? │
│ │ │
│ ├── 需要最高性能? │
│ │ │ │
│ │ ├── 是 → 使用 lxml XPath │
│ │ │ │
│ │ └── 否 → 需要简单 API? │
│ │ │ │
│ │ ├── 是 → 使用 BeautifulSoup │
│ │ │ │
│ │ └── 否 → 使用 lxml │
│ │ │
│ ├── HTML 很脏(未闭合标签等)? │
│ │ │ │
│ │ ├── 是 → 使用 html5lib 或 BeautifulSoup │
│ │ │ │
│ │ └── 否 → lxml 或 html.parser 都可以 │
│ │ │
│ ├── 需要提取文本节点? │
│ │ │ │
│ │ ├── 是 → 使用 lxml XPath │
│ │ │ (XPath 的 /text() 是独有能力) │
│ │ │ │
│ │ └── 否 → BeautifulSoup 也支持 │
│ │ │
│ └── 不能安装第三方库? │
│ │ │
│ └── 使用 html.parser (Python 内置) │
│ │
└──────────────────────────────────────────────────────────────┘
5. Mermaid 流程图
graph TD
A[获取 HTML 内容] --> B{选择解析器}
B -->|简单场景| C[html.parser]
B -->|性能优先| D[lxml]
B -->|脏HTML| E[html5lib]
C --> F[构建 DOM 树]
D --> F
E --> F
F --> G{查找方式}
G -->|CSS 选择器| H[select / select_one]
G -->|方法调用| I[find / find_all]
G -->|XPath| J[xpath]
H --> K[提取数据]
I --> K
J --> K
K --> L{数据类型}
L -->|文本| M[.text / text()]
L -->|属性| N[.get / @attr]
L -->|HTML| O[.prettify]
M --> P[清洗数据]
N --> P
O --> P
P --> Q[存储数据]
Q --> R[JSON / CSV / 数据库]
graph TD
A[获取 HTML 内容] --> B{选择解析器}
B -->|简单场景| C[html.parser]
B -->|性能优先| D[lxml]
B -->|脏HTML| E[html5lib]
C --> F[构建 DOM 树]
D --> F
E --> F
F --> G{查找方式}
G -->|CSS 选择器| H[select / select_one]
G -->|方法调用| I[find / find_all]
G -->|XPath| J[xpath]
H --> K[提取数据]
I --> K
J --> K
K --> L{数据类型}
L -->|文本| M[.text / text()]
L -->|属性| N[.get / @attr]
L -->|HTML| O[.prettify]
M --> P[清洗数据]
N --> P
O --> P
P --> Q[存储数据]
Q --> R[JSON / CSV / 数据库]
6. CSS 选择器 vs XPath 对比
┌──────────────────────────────────────────────────────────────┐
│ CSS 选择器 vs XPath 对比 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 操作 CSS 选择器 XPath │
│ ────────────────────────────────────────────────────────── │
│ 选择所有 h1 h1 //h1 │
│ │
│ 按类查找 .classname //*[@class="c"] │
│ │
│ 按 ID 查找 #id //*[@id="id"] │
│ │
│ 直接子元素 div > p //div/p │
│ │
│ 后代元素 div p //div//p │
│ │
│ 属性选择 a[href] //a[@href] │
│ │
│ 属性值匹配 [href="url"] //a[@href="url"] │
│ │
│ 模糊匹配 [class*="part"] //*[contains(@c,"p")] │
│ │
│ 索引选择 :nth-child(2) //li[2] │
│ │
│ 最后一个 :last-child //*[last()] │
│ │
│ 提取文本 N/A (需 .text) //a/text() │
│ │
│ 提取属性 N/A (需 .get) //a/@href │
│ │
│ 父元素 N/A (不支持) //a/.. │
│ │
│ 兄弟选择 + (相邻) following-sibling:: │
│ │
│ 组合查询 .a, .b //a | //b │
│ │
└──────────────────────────────────────────────────────────────┘