自学教程 学编程从这里开始

Pandas 读取 HTML 表格进阶

前置说明:本篇为上篇《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_htmlJS渲染、SPA页面、登录页所见即所得,支持JS速度慢、资源占用高
BeautifulSoup 单独解析结构异常、合并单元格复杂自由度最高代码量大,需要自己组装DataFrame

9. 进阶避坑清单

  1. read_html 不支持 ajax 异步加载表格,不要反复调试这个函数;
  2. Playwright 尽量加等待选择器,不要只用固定 sleep,不稳定;
  3. 网页表格里有图片、图标、a标签链接:pandas默认只提取文本,链接会丢失;需要拿到链接则要 bs4 单独解析;
  4. 超大表格:分批读取,不要一次性加载整个页面;
  5. 反爬限制:短时间高频抓取会封IP,需要增加随机延时、代理IP;
  6. 嵌套表格(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 条笔记