前置说明:本篇为上篇《Pandas 读取 HTML 表格》的进阶内容,聚焦静态
read_html搞不定的场景:JS动态渲染、分页表格、合并单元格清洗、多表批量抓取、编码乱码、反爬、异步加载表格、表格数据校验。
依赖包:pandas lxml requests playwright openpyxl
pip install pandas lxml requests playwright openpyxl
playwright install chromium
1. 动态渲染表格标准方案:Playwright + read_html
pd.read_html 只能解析原始响应 HTML,页面由 JS / Axios / Vue / React 渲染出来的表格直接读不到。思路:浏览器加载完页面 → 获取渲染后完整源码 → 交给 pandas 解析。
1.1 基础同步版
import pandas as pd
from playwright.sync_api import sync_playwright
url = "目标动态表格页面地址"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # headless=False 可弹出浏览器查看
page = browser.new_page(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")
page.goto(url, timeout=30000)
# 等待表格元素加载,根据实际修改选择器
page.wait_for_selector("table", timeout=15000)
html = page.content()
df_list = pd.read_html(html)
print(f"共解析到 {len(df_list)} 个表格")
df = df_list[0]
print(df.head())
browser.close()
1.2 需要登录 / Cookie 保持会话
适合需要登录才能查看的后台表格:
from playwright.sync_api import sync_playwright
import pandas as pd
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("登录页地址")
# 这里手动输入账号密码登录,登录完成后跳转表格页
input("登录完成后按回车继续...")
page.goto("表格页面")
page.wait_for_selector("table")
html = page.content()
df_list = pd.read_html(html)
browser.close()
2. 分页表格抓取(循环翻页 + 数据拼接)
网页分页(下一页按钮、页码组件),核心逻辑:循环点击下一页,每页提取表格,append 到总 DataFrame。
import pandas as pd
from playwright.sync_api import sync_playwright
all_df = pd.DataFrame()
url = "分页表格地址"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, timeout=30000)
while True:
page.wait_for_selector("table")
html = page.content()
df_list = pd.read_html(html)
current_df = df_list[0]
all_df = pd.concat([all_df, current_df], ignore_index=True)
# 判断下一页按钮是否可点击,根据网站修改选择器
next_btn = page.locator("text=下一页")
if next_btn.is_disabled():
break
next_btn.click()
page.wait_for_timeout(1500) # 等待页面刷新,可替换成等待元素
browser.close()
print(f"全部数据行数:{len(all_df)}")
all_df.to_excel("分页汇总数据.xlsx", index=False)
如果是接口分页(page 参数),优先 requests 请求接口 JSON,比操作浏览器翻页稳定很多。
3. 合并单元格数据清洗(高频坑)
网页里 rowspan / colspan 合并单元格,read_html 读取后合并区域只有第一格有值,其余为空,需要向下填充。
示例原始读取结果:
| 省份 | 城市 |
|---|---|
| 浙江 | 杭州 |
| 宁波 | |
| 江苏 | 南京 |
| 苏州 |
清洗代码:
import pandas as pd
# df 是 read_html 拿到的原始表格
df = pd.read_html(html)[0]
# 向下填充空值,处理 rowspan
df["省份"] = df["省份"].ffill()
# 如果有 colspan(横向合并)用 bfill / ffill(axis=1)
print(df)
局限性:
read_html无法区分是“原生空单元格”还是“合并单元格留白”,业务上如果有真实空数据,需要额外标记区分。
4. 多表格批量抓取 + 自动命名导出
一个页面存在多个业务表格,自动识别、批量保存为不同 sheet:
import pandas as pd
import requests
url = "多表格页面"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers)
tables = pd.read_html(resp.text)
with pd.ExcelWriter("多表格批量导出.xlsx", engine="openpyxl") as writer:
for idx, df in enumerate(tables):
sheet_name = f"table_{idx+1}"
# sheet名称不能超过31字符,过滤非法字符
sheet_name = sheet_name[:30]
df.to_excel(writer, sheet_name=sheet_name, index=False)
print("全部表格导出完成")
配合 match 参数批量筛选:
# 只提取标题包含 订单、营收 的表格
tables = pd.read_html(resp.text, match="订单|营收")
5. 编码、乱码、特殊字符处理
常见报错:中文乱码、UnicodeDecodeError
# requests 指定编码
resp = requests.get(url, headers=headers)
resp.encoding = "utf-8"
# 备选:resp.encoding = "gbk"
tables = pd.read_html(resp.text)
导出时固定编码:
df.to_csv("result.csv", index=False, encoding="utf-8-sig")
6. 高级过滤:配合 BeautifulSoup 预处理 HTML
当页面表格结构极度混乱、attrs/match 不够用时,可以先用 BeautifulSoup 提取目标 table 片段,再丢给 pandas:
import pandas as pd
import requests
from bs4 import BeautifulSoup
url = ""
resp = requests.get(url, headers={"User-Agent": "xxx"})
soup = BeautifulSoup(resp.text, "lxml")
# 按 class 提取目标table标签
table_tag = soup.find("table", class_="table-data-list")
html_fragment = str(table_tag)
df_list = pd.read_html(html_fragment)
df = df_list[0]
print(df)
优点:精准裁剪页面,剔除无关表格,减少 pandas 解析压力。
7. 异常捕获与健壮性封装(生产可用模板)
封装函数,增加超时、异常、空表格判断:
import pandas as pd
import requests
def read_html_table_safe(url, match_text=None, attrs=None):
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"}
try:
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
tables = pd.read_html(resp.text, match=match_text, attrs=attrs, flavor="lxml")
if not tables:
print("页面未匹配到表格")
return None
return tables
except requests.exceptions.RequestException as e:
print(f"网络请求异常:{e}")
return None
except ValueError as e:
print(f"解析表格失败:{e}")
return None
# 调用
res = read_html_table_safe("url", match_text="销售额")
if res:
df = res[0]
print(df)
8. 性能对比 & 选型总结表
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| pd.read_html(url) | 静态<table>,无JS渲染 | 代码最少、速度快 | 不执行JS,反爬弱 |
| requests + read_html | 静态页面,需要UA/Header | 可控请求头、Cookie简单 | 动态表格无效 |
| Playwright + read_html | JS渲染、SPA页面、登录页 | 所见即所得,支持JS | 速度慢、资源占用高 |
| BeautifulSoup 单独解析 | 结构异常、合并单元格复杂 | 自由度最高 | 代码量大,需要自己组装DataFrame |
9. 进阶避坑清单
read_html不支持 ajax 异步加载表格,不要反复调试这个函数;- Playwright 尽量加等待选择器,不要只用固定 sleep,不稳定;
- 网页表格里有图片、图标、a标签链接:pandas默认只提取文本,链接会丢失;需要拿到链接则要 bs4 单独解析;
- 超大表格:分批读取,不要一次性加载整个页面;
- 反爬限制:短时间高频抓取会封IP,需要增加随机延时、代理IP;
- 嵌套表格(table 里面套 table):pandas会拆分成多个df,需要人工合并处理。
10. 综合实战:动态分页 + 合并单元格清洗 + 导出
import pandas as pd
from playwright.sync_api import sync_playwright
all_data = pd.DataFrame()
url = "动态分页表格地址"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, timeout=30000)
while True:
page.wait_for_selector("table")
html = page.content()
df = pd.read_html(html)[0]
# 清洗合并单元格
df["大类"] = df["大类"].ffill()
all_data = pd.concat([all_data, df], ignore_index=True)
next_btn = page.locator("text=下一页")
if next_btn.is_disabled():
break
next_btn.click()
page.wait_for_timeout(1200)
browser.close()
all_data.to_excel("动态分页汇总.xlsx", index=False)
print("完成,总条数:", len(all_data))
0 条笔记