3
网页解析:BS4 / lxml / 正则
Parsing HTML
拿到 HTML 字符串只是第一步,怎么从一堆标签里精准抠出你要的内容?这章讲三个工具:BeautifulSoup(入门友好)、lxml/XPath(快且强)、正则(处理文本)。
BeautifulSoup4 基础
pip install beautifulsoup4 lxml
from bs4 import BeautifulSoup
html = '<div class="item"><h2 class="title">Python入门</h2><p>作者:小明</p></div>'
soup = BeautifulSoup(html, "lxml") # 用 lxml 解析器,比 html.parser 快
# find:找第一个匹配的
title = soup.find("h2", class_="title")
print(title.text) # Python入门
# find_all:找所有
for p in soup.find_all("p"):
print(p.text)
# CSS 选择器 select:更灵活
soup.select("div.item h2.title") # 后代选择器
soup.select("a[href='xxx']") # 属性选择器
标签对象的常用属性
| 属性 | 拿什么 |
|---|---|
.text / .get_text() | 标签里所有文本(含子标签) |
.name | 标签名("div") |
.attrs | 所有属性字典 |
tag["href"] | 单个属性 |
.parent / .parents | 父节点 |
.children / .contents | 子节点 |
.find_next_sibling() | 下一个兄弟 |
遍历 HTML 树
# 从一个标签出发,往上/下/旁边走
soup.find("a").parent # 父标签
soup.find("div").contents # 直接子节点列表(含文本节点)
soup.find("div").children # 子节点迭代器
soup.find("div").descendants # 所有子孙
elem.find_next_sibling() # 下一个兄弟
elem.find_previous_sibling() # 上一个兄弟
# decompose() 删标签,extract() 摘标签
soup.find("script").decompose() # 把 script 删掉
常用 CSS 选择器速查
| 选择器 | 选什么 |
|---|---|
.item | class 为 item 的所有标签 |
#id1 | id 为 id1 |
div.item | class 为 item 的 div |
div > a | div 的直接子 a |
div a | div 里所有 a(含孙子) |
a[href] | 带 href 属性的 a |
a[href*="detail"] | href 含 detail |
.item:nth-child(2) | 第二个 .item |
lxml 与 XPath
论XPath 比 BS4 快
lxml 是 C 写的解析器,比 BS4 纯 Python 快几倍。XPath 是"路径表达式",像文件系统路径一样定位节点://div[@class="item"]/h2/text()。大规模爬虫用 lxml + XPath,小脚本用 BS4 够了。
from lxml import etree
html = '<div><ul><li>A</li><li>B</li></ul></div>'
tree = etree.HTML(html)
# // 所有子孙,@ 属性,text() 文本,[] 谓语
items = tree.xpath("//li/text()")
print(items) # ['A', 'B']
titles = tree.xpath("//div[@class='item']/h2/text()")
links = tree.xpath("//a/@href") # 取属性
XPath 常用语法速查
| XPath | 含义 |
|---|---|
//div | 所有 div |
//div[@class="item"] | class 为 item 的 div |
//a/@href | 所有 a 的 href 属性 |
//h2/text() | 所有 h2 的文本 |
//li[1] | 第一个 li(从1开始) |
//a[contains(@href, "detail")] | href 含 detail 的 a |
//div[position()<=3] | 前三个 div |
//*[@class] | 所有带 class 属性的标签 |
正则表达式 re
import re
text = "电话:13812345678,备用:13987654321"
# findall:找所有匹配
phones = re.findall(r"1\d{10}", text)
print(phones) # ['13812345678', '13987654321']
# search:找第一个
m = re.search(r"(\d{3})-(\d{4})", "电话 021-1234")
if m:
print(m.group(1), m.group(2)) # 021 1234
# 常用元字符:.任意 *重复0+ +重复1+ ?0或1 []字符集 ()分组 \d数字 \w字母数字 \s空白
# 贪婪 vs 非贪婪:.*? 比 .* 少匹配,爬虫里几乎都用 .*?
常用正则模式速查
| 目标 | 正则 |
|---|---|
| 手机号 | 1[3-9]\d{9} |
| 邮箱 | [\w.-]+@[\w.-]+\.\w+ |
| URL | https?://[^\s"<>]+ |
| 价格 | ¥?\d+\.?\d* |
| 数字 | \d+(\.\d+)? |
| 中文 | [\u4e00-\u9fa5]+ |
JSON 解析:现在网站多是 JSON 接口
import requests, json
# 很多网站数据藏在 JSON 接口里,比 HTML 好爬多了
resp = requests.get("https://api.example.com/news")
data = resp.json() # 直接转 Python dict/list
for item in data["articles"]:
print(item["title"], item["url"])
完整案例:爬新闻网站文章列表
# news_spider.py —— BS4 + lxml 解析新闻列表
import requests
from bs4 import BeautifulSoup
headers = {"User-Agent": "Mozilla/5.0 ..."}
resp = requests.get("https://news.example.com/", headers=headers)
soup = BeautifulSoup(resp.text, "lxml")
articles = []
for item in soup.select(".news-item"):
title = item.select_one(".title a").text.strip()
link = item.select_one(".title a")["href"]
date = item.select_one(".date").text
summary = item.select_one(".summary").text.strip()
articles.append({"title": title, "link": link,
"date": date, "summary": summary})
for a in articles:
print(f"{a['date']} {a['title']}")
本章面试题 · 网页解析
1.(概念题)BeautifulSoup 的 find() 和 select() 有什么区别?
查看答案
答案:find() 按标签名+属性找,简单直观;select() 用 CSS 选择器,能写复杂层级。功能上差不多,select 更灵活,find 更易读。
2.(概念题)XPath 的 // 和 / 有什么区别?
查看答案
答案:/ 只找直接子节点,// 找所有子孙节点。爬虫里几乎都用 //,因为你不知道层级有多深。
3.(概念题)什么时候用正则,什么时候用解析库?
查看答案
答案:HTML 结构用 BS4/lxml,正则用来提取文本里的特定模式(电话、邮箱、价格)。别用正则解析 HTML——标签嵌套会把你逼疯。
4.(思考题)为什么现在很多网站"爬 JSON 接口比爬 HTML 容易"?
查看答案
答案:现代网站前端框架渲染,HTML 里经常没数据,数据靠 JS 调 JSON 接口填充。找到那个接口直接调,省得解析 HTML 还能跳过前端反爬。