首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >BeautifulSoup采集踩坑实录:编码、动态页面和异常处理

BeautifulSoup采集踩坑实录:编码、动态页面和异常处理

原创
作者头像
阿秋数据采集
发布2026-08-20 15:15:35
发布2026-08-20 15:15:35
910
举报

编码问题:最常见的第一个坑

requests拿到响应后,resp.text的解码行为取决于resp.encoding。默认值来自HTTP响应头的Content-Type字段里的charset声明。问题在于,不少站不返回charset,或者返回的charset和实际编码不一致。

代码语言:javascript
复制
# 不靠谱:依赖HTTP头
resp = requests.get(url)
text = resp.text  # 可能乱码
​
# 稍好:用chardet嗅探
resp.encoding = resp.apparent_encoding
text = resp.text
​
# 最稳:已知目标站编码时直接指定
resp.encoding = "gbk"
text = resp.text

apparent_encoding底层用chardet,准确率大约九成以上,但碰到短文本或混编码页面会判错。我在采集老站时习惯先试apparent_encoding,看到乱码就手动指定GBK或GB2312。大部分国内老站用GBK,新站用UTF-8。

还有一种隐蔽的编码问题:页面编码正确但HTML实体没有解码。& 中这类实体在.text里会被自动转义成对应字符,但偶尔碰到不规范的自定义实体(比如&xxx不是标准实体),BeautifulSoup会原样保留。这种情况用html.unescape()补一手:

代码语言:javascript
复制
import html
text = html.unescape(tag.get_text())

BeautifulSoup不执行JavaScript

这是新手最容易误解的一点:BeautifulSoup是HTML解析器,不是浏览器引擎。它拿到什么HTML就解析什么,不会执行页面里的<script>。如果目标数据是JS渲染后插入DOM的,BeautifulSoup拿到的HTML里对应位置是空的。

判断方法:浏览器里查看页面源代码(右键→查看网页源代码),如果源代码里有你要的数据,说明是服务端渲染,BeautifulSoup能拿到。如果源代码里没有但页面显示有,说明是JS动态渲染,需要换方案。

代码语言:javascript
复制
# 方案一:找接口
# F12 Network面板里找XHR/Fetch请求,直接请求API拿JSON
api_url = "https://example.com/api/list?page=1"
resp = requests.get(api_url, headers=headers)
data = resp.json()  # 多数现代站返回JSON
​
# 方案二:Selenium/Playwright渲染后解析
from selenium import webdriver
from bs4 import BeautifulSoup
​
driver = webdriver.Chrome()
driver.get("https://example.com")
driver.implicitly_wait(5)  # 给JS执行时间
soup = BeautifulSoup(driver.page_source, "lxml")
# 后续正常用BeautifulSoup提取
driver.quit()

方案一优先,直接请求API最干净,速度也快。Playwright比Selenium新,API更稳定,但Selenium的生态更成熟。我们工作室两者都备着,按目标站情况选。

残缺HTML的容错

真实世界的HTML不保证规范。标签未闭合、嵌套混乱、属性值没引号——这些在BeautifulSoup里大多能自动处理。不同解析器容错能力不同:

代码语言:javascript
复制
# 一段残缺HTML
broken = "<p>段落一<p>段落二<div>未闭合"
​
# lxml:尝试修复结构
BeautifulSoup(broken, "lxml")
# <html><body><p>段落一</p><p>段落二<div>未闭合</div></p></body></html>
​
# html5lib:按浏览器标准容错
BeautifulSoup(broken, "html5lib")
# 修复结果更接近浏览器实际渲染
​
# html.parser:标准库自带
BeautifulSoup(broken, "html.parser")
# 容错中等,介于两者之间

lxml和html5lib对同一段残缺HTML的修复结果可能不同。碰到提取结果和浏览器显示不一致时,先换解析器试。html5lib最接近浏览器行为,但速度最慢,只在排查问题时临时用。

NoneType异常的防护

BeautifulSoup找不到元素时返回None。对None调用.text.get()会抛AttributeError。批量采集中最常见的崩溃原因就是这个。

代码语言:javascript
复制
# 危险写法
price = soup.find("span", class_="price").text
# 如果没有匹配元素,None.text直接报错
​
# 安全写法
price_tag = soup.find("span", class_="price")
price = price_tag.get_text(strip=True) if price_tag else "N/A"

批量循环里更要注意:

代码语言:javascript
复制
for item in soup.select("div.item"):
    name = item.select_one(".name")
    price = item.select_one(".price")
​
    # 任一为None就跳过这条
    if not name or not price:
        continue
​
    results.append({
        "name": name.get_text(strip=True),
        "price": price.get_text(strip=True),
    })

解析性能注意

BeautifulSoup解析大HTML文档(超过5MB)时会比较慢,内存占用也高。几个实际经验:

代码语言:javascript
复制
# 只需要部分数据时,先截取再解析
# 比如只需要table部分,用正则先切出来
import re
table_html = re.search(r'<table.*?</table>', html, re.S)
if table_html:
    soup = BeautifulSoup(table_html.group(), "lxml")

用lxml解析器比html.parser快三到五倍,这是最简单的提速手段。如果同一段HTML要反复查询,考虑把解析结果缓存:

代码语言:javascript
复制
soup = BeautifulSoup(html, "lxml")  # 只解析一次
# 后续所有find/select都复用这个soup对象

不要在循环里反复构造BeautifulSoup对象。同一页面解析一次就够了。

小结

采集踩坑无非几类:编码不对、JS渲染没处理、None没判空、解析器选错。编码和None是最高频的两个,编码问题排查靠apparent_encoding加手动指定,None防护靠每个取值点判空。碰到诡异问题先换解析器试一把,经常能省下大半排查时间。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 编码问题:最常见的第一个坑
  • BeautifulSoup不执行JavaScript
  • 残缺HTML的容错
  • NoneType异常的防护
  • 解析性能注意
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档