在网页抓取实践中,许多页面的关键信息并不是一开始就写在服务器返回的 HTML 中,而是通过异步请求在浏览器中二次加载,再由前端脚本插入到文档对象模型里。BeautifulSoup 擅长解析已经拿到的 HTML 文本,但它本身不会执行脚本,也不会主动发起浏览器内部的异步请求。因此,当目标数据由 AJAX 生成时,单纯解析初始响应往往只能得到骨架、占位符或加载提示。要解决这个问题,需要根据页面加载机制选择合适策略,而不是机械地重复解析静态源码。

理解 AJAX 动态加载与 BeautifulSoup 的解析边界
从原理上看,AJAX 动态内容通常分为几类。第一类是后端接口返回 JSON,前端拿到数据后拼接成列表、卡片或表格。第二类是后端接口直接返回 HTML 片段,前端将其插入已有节点。第三类是页面依赖复杂脚本、事件触发或用户交互,才会把数据渲染到可见区域。不同类型对应不同抓取入口,也决定了 BeautifulSoup 的介入时机。
BeautifulSoup 的角色应当放在整个抓取流程中理解。它负责把结构化或半结构化的 HTML 转成可查询的对象,支持选择器、属性过滤和文本提取。但它并不承担网络分析、脚本执行和事件模拟职责。因此,面对动态页面时,更稳妥的思路是先确认数据最终出现的位置,再决定是在接口层获取,还是在渲染后的页面源码层解析。
判断数据位置时,可以从页面表现入手。如果页面刚打开就出现内容,但源码中没有对应文本,说明内容大概率来自脚本执行后的网络请求。如果内容需要点击、翻页、滚动才出现,则可能涉及事件触发接口。如果页面嵌套了 <iframe>,还需要先确认目标内容是否位于子文档中。只有把加载链路梳理清楚,后续选择 BeautifulSoup 解析接口片段还是完整渲染结果,才会更有针对性。
- 观察初始响应中是否已经包含目标文本。
- 观察浏览器异步请求是否返回 JSON 或 HTML 片段。
- 观察目标内容是否需要点击、滚动或切换分页才会出现。
优先分析网络请求:直接请求 AJAX 接口
在多数情况下,直接分析网络请求是优先级最高的方案。因为动态内容最终往往来自一个或多个数据接口,接口响应可能非常干净,例如 JSON 数组、分页对象或 HTML 片段。跳过页面渲染过程,直接请求接口,可以减少脚本执行开销,也能避免被复杂前端结构干扰。对于需要批量翻页、稳定采集的任务,这种方式通常更容易控制节奏和异常。
分析接口时,可以借助浏览器开发者工具观察异步请求。重点关注请求方法、查询参数、请求头、Cookie、Referer 以及响应格式。如果接口只依赖简单分页参数,直接模拟请求即可。如果接口带有签名、时间戳、设备标识或加密令牌,则需要评估能否在 Python 中稳定复现生成逻辑。若无法复现,就不宜强行逆向,而应考虑浏览器自动化方案。
当接口返回 JSON 时,BeautifulSoup 不一定参与核心解析,因为 JSON 本身已有明确结构。只有当接口返回 HTML 片段,或者返回内容中仍夹杂需要二次提取的标签结构时,BeautifulSoup 才继续发挥作用。下面示例展示如何请求一个带分页参数的接口,并根据响应类型分别处理 JSON 与 HTML 片段。
import requests
from bs4 import BeautifulSoup
# 模拟常见浏览器请求头,降低被拦截的概率
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
api_url = 'https://api.ipipp.com/list'
params = {'page': 1}
response = requests.get(api_url, headers=headers, params=params, timeout=10)
response.raise_for_status()
content_type = response.headers.get('Content-Type', '')
# 如果接口返回JSON,则直接按JSON处理
if 'application/json' in content_type:
data = response.json()
print(data)
else:
# 如果接口返回HTML片段,则交给BeautifulSoup解析
soup = BeautifulSoup(response.text, 'html.parser')
for item in soup.select('.list-item'):
print(item.get_text(strip=True))
实际使用时,还需要注意响应状态、超时、重试和空数据分支。接口可能返回空列表、错误码或需要登录的提示页,单纯假设返回格式固定会导致程序中断。将请求层与解析层分离,先校验响应,再交给 BeautifulSoup 或 JSON 解析器处理,可以提高脚本健壮性。
使用 Selenium 驱动真实浏览器并交给 BeautifulSoup 解析
当接口难以直接模拟时,Selenium 是更稳妥的替代方案。它可以驱动真实浏览器加载页面,让脚本、样式、异步请求和事件机制按正常流程执行。等到目标元素出现在文档中以后,再读取完整页面源码,交给 BeautifulSoup 做后续解析。这样既利用了浏览器环境处理复杂前端逻辑,又保留了 BeautifulSoup 在节点查询和文本清洗上的便利。
使用 Selenium 时,应尽量避免只靠固定睡眠等待内容。页面加载速度、网络延迟和脚本执行顺序都可能变化,固定等待时间太短会漏数据,太长会拖慢任务。显式等待更可靠,例如等待某个类名、属性或文本出现。对于列表内容,可以等待第一个列表项出现,再决定是否继续滚动或翻页。
如果动态内容依赖滚动加载,还需要模拟用户滚动行为。很多页面并不会一次性请求全部数据,而是在接近底部时继续调用下一页接口。此时可以先等待首个内容块出现,再多次滚动到底部,让浏览器持续触发加载。下面示例分别展示基础等待解析,以及滚动加载后的 BeautifulSoup 提取过程。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://www.ipipp.com/dynamic-page')
# 等待动态内容出现,最多等待10秒
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'dynamic-content')))
# 浏览器渲染完成后,再取出完整页面源码
soup = BeautifulSoup(driver.page_source, 'html.parser')
for content in soup.select('.dynamic-content'):
print(content.get_text(strip=True))
finally:
driver.quit()
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
try:
driver.get('https://www.ipipp.com/infinite-scroll-page')
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'ajax-loaded-item')))
# 模拟多次滚动到底部,触发后续异步加载
for _ in range(3):
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
time.sleep(2)
soup = BeautifulSoup(driver.page_source, 'html.parser')
for item in soup.select('.ajax-loaded-item'):
print(item.get_text(strip=True))
finally:
driver.quit()
在 Selenium 场景中,浏览器资源占用较高,因此要及时关闭浏览器实例,并尽量使用无头模式减少界面开销。同时,页面结构可能在加载过程中发生变化,解析前应确认目标节点已经稳定。若页面存在懒加载图片、骨架屏或占位文本,也可以结合属性判断,避免把未完成渲染的中间状态当作最终数据。
轻量渲染方案、策略对比与实践建议
除了 Selenium,也可以使用更轻量的渲染型请求库完成部分动态页面抓取。requests-html 将普通请求与脚本渲染能力结合在一起,适合页面逻辑不复杂、无需大量交互的场景。它先获取初始响应,再在内部执行页面脚本,待渲染完成后输出 HTML。此时再把结果交给 BeautifulSoup,就可以提取原本不在初始源码中的内容。
from requests_html import HTMLSession
from bs4 import BeautifulSoup
session = HTMLSession()
try:
response = session.get('https://www.ipipp.com/dynamic-page')
# 执行页面脚本,等待动态内容渲染
response.html.render(timeout=10)
soup = BeautifulSoup(response.html.html, 'html.parser')
for item in soup.select('.ajax-loaded-item'):
print(item.get_text(strip=True))
finally:
session.close()
不同策略的选择,本质上是在效率、稳定性和复杂度之间取得平衡。直接请求接口速度最快,但前提是接口可被稳定复现。Selenium 适应性最强,但资源消耗和运行成本更高。轻量渲染方案介于两者之间,使用简单,却不一定能覆盖所有复杂脚本。实际项目中,通常建议先尝试接口分析,失败后再逐步升级到浏览器方案。
| 策略 | 优势 | 局限 | 适用情况 |
|---|---|---|---|
| 直接请求接口 | 速度快,资源占用低,便于批量翻页 | 需要分析请求参数和响应结构,复杂签名较难复现 | 接口清晰、参数简单、返回 JSON 或 HTML 片段 |
| Selenium 渲染 | 能执行脚本,支持等待、点击、滚动等交互 | 启动浏览器成本较高,速度相对较慢 | 接口校验严格、页面逻辑复杂或需要模拟用户行为 |
| 轻量渲染请求 | 使用较简单,比完整浏览器更轻 | 对复杂脚本和反制机制支持有限 | 动态逻辑不复杂且无需大量交互的页面 |
在实施过程中,还应重视合规与稳定性。抓取前应查看目标网站的 robots.txt,确认采集范围是否允许。请求频率要控制在合理区间,避免短时间内大量访问造成服务器压力。对于需要登录、地域限制或会话保持的页面,应妥善管理 Cookie 和请求头。若动态内容位于 <iframe> 中,需要先获取其来源地址或切换到对应文档上下文,再决定解析方式。
综合来看,BeautifulSoup 并不是不能处理 AJAX 页面,而是需要放在正确的流程节点上使用。它可以解析接口返回的 HTML 片段,也可以解析浏览器渲染完成后的页面源码。真正决定抓取效果的是对加载机制的判断:优先寻找数据接口,必要时借助浏览器执行脚本,最后用合适的解析器完成结构化提取。掌握这套思路后,面对动态加载页面时就能更从容地设计方案,而不是在静态源码中反复寻找不存在的数据。
BeautifulSoupAJAX动态加载requestsselenium数据抓取修改时间:2026-07-01 00:21:43