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

简介: 本文详解网页采集两大坑:一是编码问题,解析响应时`resp.text`易乱码,推荐优先用`apparent_encoding`,再手动指定GBK/UTF-8;二是BeautifulSoup不执行JS,无法获取动态渲染内容,需配合Selenium等工具。

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

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

# 不靠谱:依赖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()补一手:

import html
text = html.unescape(tag.get_text())

BeautifulSoup不执行JavaScript

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

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

# 方案一:找接口
# 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里大多能自动处理。不同解析器容错能力不同:

# 一段残缺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。批量采集中最常见的崩溃原因就是这个。

# 危险写法
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"

批量循环里更要注意:

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)时会比较慢,内存占用也高。几个实际经验:

# 只需要部分数据时,先截取再解析
# 比如只需要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要反复查询,考虑把解析结果缓存:

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

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

小结

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

相关文章
人工智能 缓存 前端开发
8582 32
人工智能 JavaScript 开发工具
3609 8
开发工具 Swift git
1363 2
缓存 JavaScript Shell
1679 2
Shell API 调度
926 3
人工智能 JavaScript 测试技术
1087 0
安全 机器人 API
719 2
|
16天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1911 13
|
15天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
2193 121
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考