返回博客
指南
Andrei OgiolanLast updated on May 7, 20264 min read

如何使用 Python 抓取 HTML 表格

如何使用 Python 抓取 HTML 表格
简而言之:大多数 HTML 表格都可以通过一行代码进行抓取 pandas.read_html。当表格分页、由 JavaScript 渲染或包含合并的表头时,请切换至 Requests + BeautifulSoup 或 Playwright 等无头浏览器。本指南为您提供了决策矩阵、三种方法的实操代码,以及将抓取的行转换为可直接用于数据处理管道的数据的清理步骤。

表格数据在公共网络中无处不在,从维基百科的信息框和股票筛选器,到政府统计数据、体育数据以及产品对比页面。如果你懂得如何使用 Python 抓取 HTML 表格,就能在几分钟内将这些行数据转换为干净的 DataFrame、JSON 文档,或存储在自己的数据库中。

但问题在于,HTML表格是一个看似宽泛却暗藏玄机的类别。有些表格结构清晰,位于 <table> 标记中,pandas 只需一行代码即可解析。还有些则是手工编写的 <div>,分页跨越数十个页面,或者只有在浏览器中运行 JavaScript 后才会填充数据。一种在维基百科上完美运行的方法,在单页应用中可能会悄无声息地返回零行。

本指南将详细介绍三种 Python 实现方法,并围绕两个实际问题展开:您应该选择哪种方法?以及当网站在下个季度更改标记时,如何确保您的爬虫继续正常运行?

如何使用 Python 抓取 HTML 表格:快速决策矩阵

在编写任何代码之前,请先确定哪种工具适合眼前的表格。选错工具是教程无法应对真实网站时最常见的原因。请使用下表进行自我筛选。

标准

pandas.read_html

Requests + BeautifulSoup

Playwright(或 Selenium)

最适合以下情况

表格位于初始 HTML 中且结构规范

需要对单个单元格进行控制或筛选

表格由 JavaScript 渲染

代码行数

约 3

30 至 80

40 至 100

每页速度

慢(完整浏览器)

支持JS

分页

手动循环

手动循环或隐藏API

点击和滚动

对标记变更的容错性

中等

高(由您编写选择器)

内存占用

三条经验法则:

  • 如果 pd.read_html(url) 返回了你预期的行,那就到此为止。这一行代码是你能写出的最易于维护的代码。
  • 如果表格已在 HTML 中,但你需要在数据进入 DataFrame 之前对单元格进行筛选、合并或规范化处理,请使用 Requests + BeautifulSoup。
  • 如果“查看页面源代码”显示 <div id="grid"> ,且数据仅在页面加载后才出现,你需要使用 Playwright 或隐藏的 JSON 接口。

本文剩余部分将演示如何在上述每种场景下使用 Python 抓取 HTML 表格,并涵盖那些会导致原本正常运行的代码出错的边界情况。

HTML 表格结构解析(以及抓取为何棘手)

教科书式的 HTML 表格如下所示:

<table id="employees" class="stripe">
  <thead><tr><th>Name</th><th>Position</th><th>Salary</th></tr></thead>
  <tbody>
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>$120,000</td></tr>
    <tr><td>Alan Turing</td><td>Researcher</td><td>$135,000</td></tr>
  </tbody>
</table>

五个标签承担了主要工作: <table> 是容器, <thead> 以及 <tbody> 用于分组行, <tr> 是行, <th><td> 分别是标题单元格和数据单元格。有两个属性会让情况变得复杂: colspan 使单元格横跨多列,而 rowspan 则使其横跨多行。这两者在财务和体育表格中被广泛使用。

在实际应用中,这些约定有一半被忽略。许多页面省略了 <thead><tbody>,省略结束标签,或将表格渲染为嵌套的 <div> 网格,导致任何解析器都无法将其识别为表格。现实中的网页抓取工作主要就是应对这种偏差,这也是为什么仅靠 pandas 无法应对所有网站的原因。

方法 1:pandas.read_html,一行代码搞定

pandas.read_html 是 pandas 数据处理库中一个便捷函数,它接受一个 URL 或 HTML 字符串,并返回一个 DataFrame 列表,每个 <table> 。根据 pandas 文档,它需要 lxml, html5lib,或 bs4 ,其底层机制是通过查找标准表格元素来识别表格。

其最大魅力在于,只需三行代码即可获得一个类型明确且可查询的 DataFrame:

import pandas as pd

tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue")
df = tables[0]
print(df.head())

但问题在于 read_html 只能识别响应主体中已存在的表格。如果表格是在页面加载后由 JavaScript 动态填充的,该函数会抛出 ValueError: No tables found 尽管该表格在浏览器中清晰可见。提前了解这一限制能节省大量调试时间。

设置 Python 环境

你可以通过一个全新的虚拟环境和三个包来运行本指南中的所有示例:

python -m venv .venv
source .venv/bin/activate
pip install pandas requests beautifulsoup4 lxml html5lib playwright
playwright install chromium

lxml 是 Python 可用的最快 HTML 解析器,也是大多数专业人士的首选。 html5lib 虽然速度较慢,但遵循 WHATWG 解析算法,这使其在面对损坏的标记时最为宽容。建议同时安装这两个包,以便在其中一个解析失败时能够切换解析器。

pandas.read_html 完整操作指南

让我们抓取一个真实的、结构良好的表格:维基百科上的各国GDP列表。整个工作流仅需四行代码。

import pandas as pd

url = "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)"
tables = pd.read_html(url)
print(f"Found {len(tables)} tables on the page")

gdp = tables[2]            # pick the right one by index
gdp.columns = [c[1] if isinstance(c, tuple) else c for c in gdp.columns]
print(gdp.head())

有三点需要注意。首先, read_html 返回了一个列表,因此需要通过索引访问。其次,维基百科的表格通常具有多级标题,pandas 将其表示为 MultiIndex。列表推导式通过保留较低层级来将其展平。第三,无需手动遍历行:每个单元格都已存在于可调用 .sort_values, .groupby.to_csv

若仅需数据进行快速分析,这确实就是你应编写的全部代码。

pandas.read_html 故障排除:常见错误

pd.read_html 的失败模式通常可预测。记住以下四点,你就能在一分钟内解决大多数问题。

  1. ValueError: No tables found. 页面要么由 JavaScript 渲染,要么需要登录才能访问。请跳转至 Playwright 部分。
  2. pandas 内部抓取器返回 HTTP 403 或 429 错误。默认 urllib 用户代理被拦截。请使用 Requests 自行抓取 HTML,并将字符串传递给 read_html:
import requests, pandas as pd
headers = {"User-Agent": "Mozilla/5.0 (compatible; analytics-bot/1.0)"}
html = requests.get(url, headers=headers, timeout=15).text
tables = pd.read_html(html)
  1. 表索引错误。使用 match= 按目标表中出现的字符串进行过滤,例如 pd.read_html(html, match="Population")。这比依赖 tables[3].
  2. 非 ASCII 内容中的乱码。通过显式读取字节来强制编码: response = requests.get(url); response.encoding = "utf-8"; tables = pd.read_html(response.text).

如果修复这些问题后仍无法解决,那么该表格几乎肯定需要使用 Requests + BeautifulSoup 或无头浏览器,而不是更多的 read_html 变通方案。

方法 2:Requests + BeautifulSoup,当你需要控制权时

pandas.read_html 在您希望每个单元格完全呈现为HTML中的原始样式时非常有效。但一旦您需要在提取过程中过滤行、合并两列的值、实时去除货币符号,或从 href ,它就不再是合适的工具了。

这时就需要使用 Requests + BeautifulSoup 了。Requests 负责处理 HTTP 层(头部、Cookie、会话、重试),而 BeautifulSoup 则提供了一个解析树,你可以通过 CSS 选择器、属性匹配或兄弟节点导航来遍历它。如果你刚接触 BeautifulSoup,我们关于使用 Python 和 BeautifulSoup 提取和解析网络数据的深度解析文章会详细介绍其 API 接口。 这一组合也是大多数生产环境中的爬虫最终采用的方案,因为每个步骤(获取、解析、提取、转换)都由你完全掌控。

接下来的三个部分将演示如何使用 Python 结合这一技术栈来抓取 HTML 表格:发送礼貌的请求、使用强大的表格选择器,以及在添加新列时仍能正常运行的行循环。

发送礼貌且符合实际的 HTTP 请求

反机器人防御机制主要依赖于几个简单的信号:缺失或默认的 User-Agent、无 Accept-Language、无Cookie,以及在一秒内耗尽会话的流量。模拟真实浏览器并复用连接:

import requests
from bs4 import BeautifulSoup

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})

response = session.get("https://example.com/employees", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")

三个小习惯至关重要。 Session() 可在调用之间保留 Cookie 和连接池。 raise_for_status() 将静默的 4xx/5xx 响应转换为可重试的异常。并且传递 "lxml" 作为解析器,其速度大约是内置解析器 html.parser

在页面上定位正确的表格

一旦获得 BeautifulSoup 对象后,下一个问题就是获取正确的 <table>。页面通常包含八到十五个此类元素(例如:布局表格、侧边栏组件、隐藏的分页控件)。请按以下稳定性顺序尝试选择器:

# 1. By stable id (best)
table = soup.find("table", id="employees")

# 2. By a class that's specific to this table
table = soup.find("table", class_="data-grid")

# 3. By a CSS selector
table = soup.select_one("section#payroll table.stripe")

# 4. By the heading that precedes it (when classes are dynamic)
heading = soup.find(["h2", "h3"], string=lambda s: s and "Employees" in s)
table = heading.find_next("table") if heading else None

当类名是自动生成的且在每次部署时都会改变(这是 React 的常见模式)时,建议优先使用通过 lxml,因为它能用一个表达式描述“位于标题文本包含‘Employees’的section内的第三个表格”。我们另有一份关于XPath与CSS选择器的指南,对这种权衡进行了更深入的探讨。

安全地遍历行并提取单元格

大多数爬取教程展示的行循环都采用基于位置的单元格索引: cells[0] 是名称, cells[1] 是姓名, cells[2] 是姓名,是职位,是薪资。一旦有人添加了“部门”这一列,该代码就会失效。稳健的模式是先读取一次表头,然后将其与每一行进行关联。

# Read headers from <thead> if present, else from the first row
header_cells = table.select("thead th") or table.select("tr:first-of-type th, tr:first-of-type td")
headers = [th.get_text(strip=True) for th in header_cells]

rows = []
for tr in table.select("tbody tr") or table.select("tr")[1:]:
    cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
    if not cells:
        continue
    rows.append(dict(zip(headers, cells)))

print(f"Extracted {len(rows)} rows with {len(headers)} columns")

这种做法能带来三大优势:新列会自动处理,因为键值来自表头而非索引;空行(常用于视觉分隔)会被跳过;且每个单元格都会经过 get_text(strip=True),该函数会压缩空格并移除 \n 那些困扰初学者的 cell.text 调用中如影随形的字符。这是你应该复制到每个 BeautifulSoup 项目中的行循环。

将抓取的行保存为 JSON、CSV 或 Parquet

一旦获得字典列表,保存它们只需针对每种格式写一行代码:

import json
import pandas as pd

# JSON, human-readable, UTF-8 safe
with open("employees.json", "w", encoding="utf-8") as f:
    json.dump(rows, f, indent=2, ensure_ascii=False)

# CSV via pandas (handles quoting, encoding, and missing keys)
df = pd.DataFrame(rows)
df.to_csv("employees.csv", index=False, encoding="utf-8")

# Parquet for analytical pipelines (smaller files, typed columns)
df.to_parquet("employees.parquet", index=False)

当数据消费者是脚本或前端时,选择 JSON;当人类用户计划在 Excel 或 BigQuery 中打开时,选择 CSV;当数据集超过数十万行或需供养 Spark、Snowflake 或 DuckDB 时,选择 Parquet。Parquet 文件通常比等效的 CSV 文件小 5 到 10 倍,且能保留数据类型。对于任何最终将导入关系型数据库的数据,请直接跳转至 df.to_sql ,这样可以完全跳过中间文件。

使用 colspan 和 rowspan 处理复杂表头

两行标题在金融、政府统计和体育数据表中很常见。顶部一行对列进行分组(如“2024年第一季度”、“2024年第二季度”),底部一行对列进行标注(如“收入”、“利润”)。像 ["Name", "Position", "Contact"] 这种硬编码列名的方式,初试尚可,但终将失效。以下是一个通用算法,它既能保留 colspanrowspan.

def expand_header(table):
    # Return a flat list of column labels from a multi-row <thead>
    rows = table.select("thead tr")
    if not rows:
        return [th.get_text(strip=True) for th in table.select("tr:first-of-type th")]

    grid = []  # grid[row_index] = list of column labels at that row
    for r, tr in enumerate(rows):
        while len(grid) <= r:
            grid.append([])
        col = 0
        for th in tr.find_all(["th", "td"]):
            # skip already-filled slots from previous rowspans
            while col < len(grid[r]) and grid[r][col] is not None:
                col += 1
            text = th.get_text(strip=True)
            colspan = int(th.get("colspan", 1))
            rowspan = int(th.get("rowspan", 1))
            for dr in range(rowspan):
                while len(grid) <= r + dr:
                    grid.append([])
                row_buf = grid[r + dr]
                # pad
                while len(row_buf) < col + colspan:
                    row_buf.append(None)
                for dc in range(colspan):
                    row_buf[col + dc] = text
            col += colspan

    # Combine the columns of each row, top-down, into a single label per column
    n_cols = max(len(r) for r in grid)
    flat = []
    for c in range(n_cols):
        parts = [grid[r][c] for r in range(len(grid)) if c < len(grid[r]) and grid[r][c]]
        # de-dup adjacent identical strings: ['Q1 2024', 'Q1 2024', 'Revenue'] -> 'Q1 2024 Revenue'
        seen = []
        for p in parts:
            if not seen or seen[-1] != p:
                seen.append(p)
        flat.append(" ".join(seen))
    return flat

将其与前文的 zip(headers, cells) 行循环,即可构建出能应对任何合并单元格组合的表头解析方案。当行跨列导致列中值重复时,相同思路(通过逐列 colspan 填充的二维网格)同样适用于表体:追踪哪些单元格已被占用,并在后续 <tr> 迭代中跳过。

抓取分页 HTML 表格(三种策略)

分页是使用 Python 抓取 HTML 表格时最被低估的部分。大多数教程仅展示“在无头浏览器中点击下一页按钮”,这既是最慢也是最不可靠的方法。请按优先级顺序优先尝试以下三种方法。

1. 增加 page-size 查询参数。许多表格支持 ?per_page=500?length=1000。一次请求即可获取所有行,无需循环处理。点击页面大小下拉菜单时检查 URL,通常能免费获得此参数。

2. 调用底层 JSON API。打开开发者工具,切换到“网络”标签页,按 Fetch/XHR进行筛选,然后点击下一页。几乎所有现代数据表格背后都有一个返回 JSON 的端点。直接调用它可以完全跳过 HTML 解析:

import requests
url = "https://example.com/api/employees"
all_rows = []
for page in range(1, 20):
    payload = requests.get(url, params={"page": page, "size": 100}, timeout=15).json()
    if not payload["items"]:
        break
    all_rows.extend(payload["items"])

3. 遍历页面查询字符串。当 URL 包含页码(?page=2, &start=20),请显式遍历该参数,并在表格数据为空时停止。这比驱动浏览器更可靠,因为无需点击任何按钮,也无需等待动画加载。

无头浏览器应作为最后手段,而非首选方案。仅将其用于“下一页”链接绑定到 JavaScript 处理程序且 URL 不发生变化的表格。

方法 3:用于 JavaScript 渲染表格的 Playwright

当表格仅在页面加载完成后才显示时,你需要能运行 JavaScript 的工具。Playwright 是现代的首选:它提供官方 Python 绑定,支持 Chromium、Firefox 或 WebKit 引擎,并具备可靠的自动等待机制。以下是使用 Python 抓取依赖 JS 的 HTML 表格的完整模板:

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import pandas as pd

URL = "https://example.com/dashboard"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(user_agent="Mozilla/5.0 ... Chrome/124.0 Safari/537.36")
    page.goto(URL, wait_until="domcontentloaded")

    # Wait for the actual data, not just the page load
    page.wait_for_selector("table#grid tbody tr", timeout=15000)

    html = page.content()
    browser.close()

# Hand the rendered HTML off to your existing parser
soup = BeautifulSoup(html, "lxml")
table = soup.find("table", id="grid")
# ... use the same row loop from earlier ...

# Or, when the table is well-formed, skip BeautifulSoup entirely:
df = pd.read_html(html, match="Department")[0]
print(df.head())

模式始终如一:导航、等待数据(不仅是 load),获取 page.content(),然后将该字符串输入到与处理静态 HTML 相同的解析代码中。有关安装、异步 API 和跟踪的信息,请参阅 Python 版 Playwright 文档

Selenium 和 Pyppeteer 也是有效的替代方案。Selenium 拥有更庞大的生态系统,如果您的团队已将其用于端到端测试,它将是稳妥的选择,我们的 Selenium 分步教程涵盖了相应的配置。Pyppeteer 更精简,但维护力度较小。如需更全面的无头工具对比,请参阅我们的 Playwright 网页抓取指南。对于新项目,Playwright 通常是最符合人体工程学的。

选择 HTML 解析器与处理空单元格

BeautifulSoup 是一个封装器。实际的解析工作由三个后端之一负责,而后端的选择比大多数教程所承认的更为重要。

解析器

速度

对错误 HTML 的容忍度

安装

html.parser

中等(内置于 Python)

lxml

比较严格,但务实

pip install lxml

html5lib

最慢

最高,遵循 WHATWG

pip install html5lib

默认 lxml。仅在 html5lib 仅当 lxml 在标记损坏(缺少闭合 </td>、未闭合 <tr>、孤立 < 字符)时,才切换至。你可以快速验证:

import time
from bs4 import BeautifulSoup

for parser in ["lxml", "html.parser", "html5lib"]:
    t0 = time.perf_counter()
    soup = BeautifulSoup(html, parser)
    rows = soup.select("tbody tr")
    print(f"{parser:10} {len(rows):4} rows in {time.perf_counter()-t0:.3f}s")

对于空单元格,编写一个辅助函数,使其返回合理的默认值而非导致程序崩溃:

def cell_text(cell, default=""):
    if cell is None:
        return default
    text = cell.get_text(" ", strip=True)
    return text if text else default

在所有对行进行索引的地方都使用它。 None 在每个调用位置进行检查会使循环代码杂乱,且无法处理单元格存在但仅包含 &nbsp;的情况。此辅助函数可同时处理这两种情况。

避免阻塞:头部信息、会话和代理

200 状态码表示请求已被接受。其他任何状态码(尤其是 403、429 或 503)通常意味着网站已检测到您的爬虫。请按顺序尝试以下方法,在第一个有效的方法上停止。

  1. 设置合理的请求头。设置 User-Agent, Accept-LanguageReferer 的值设置为真实的 Chrome 会话会发送的值。仅此一项就能解决惊人数量的阻塞问题。
  2. 持久化会话。使用 requests.Session() ,以便主页设置的 Cookie 能在后续请求中被发送。许多网站会在首次访问时发放会话 Cookie,并拒绝缺少该 Cookie 的请求。
  3. 针对 429 和 503 状态码采用指数退避策略。等待 2 ** attempt 秒,最多重试五次。当服务器提供 Retry-After 服务器提供的头部信息。
  4. 数据中心代理。价格低廉、速度快,足以满足大多数静态网站的需求。在工作池中轮换 IP 地址。
  5. 住宅代理。来自195个国家的真实住宅IP,用于数据中心IP范围已被封锁的情况。速度较慢但更难被检测到。
  6. 托管式抓取API。当您希望专注于数据解析而非基础设施时,WebScrapingAPI的Scraper API等服务可在单一端点后处理代理轮换、头部生成和重试,因此原有的BeautifulSoup或pandas代码仍可正常运行。

大多数项目只需采用前三个方案。如需更详尽的检测信号清单,请参阅我们的指南《为何爬虫会被封禁或IP被封》,其中深入探讨了TLS指纹识别、头部顺序及速率限制算法。若您在抓取维基百科条目时被封,则说明存在其他问题。

生产环境中的数据清洗、类型转换与导出

抓取到的表格几乎从未处于可分析状态。货币符号、百分号、脚注标记和尾随空格都会作为字符串混入其中。在保存前通过一次处理修复它们:

import pandas as pd

df = pd.DataFrame(rows)

# 1. Strip whitespace on every text column
str_cols = df.select_dtypes(include="object").columns
df[str_cols] = df[str_cols].apply(lambda s: s.str.strip())

# 2. Coerce numeric columns (errors='coerce' turns junk into NaN)
df["salary"] = pd.to_numeric(df["salary"].str.replace(r"[^0-9.\-]", "", regex=True),
                             errors="coerce")
df["growth_pct"] = pd.to_numeric(df["growth_pct"].str.rstrip("%"), errors="coerce")

# 3. Coerce dates
df["hired_at"] = pd.to_datetime(df["hired_at"], errors="coerce")

# 4. Drop rows where the primary key failed to parse
df = df.dropna(subset=["employee_id"])

# 5. Persist
df.to_parquet("employees.parquet", index=False)
df.to_sql("employees", con=engine, if_exists="replace", index=False)

errors="coerce" 标志是这条处理流程中被低估的英雄:错误的单元格会触发 NaN 而非抛出异常,你可以在稍后使用 df[df["salary"].isna()]进行排查。对于生产环境管道,请采用 Parquet 格式进行存储,并使用 to_sql 将清理后的数据写入 Postgres 或您选择的数据仓库。

法律与道德规范

本文仅为风险规避指南,并非法律建议。在抓取任何敏感信息前,请咨询律师。

  • 请阅读 robots.txt。它表达的是网站所有者的意愿,而非法律规则,但无视它会很快导致被封禁。相关规范详见 RFC 9309
  • 请阅读服务条款。特别是登录状态下的抓取,即使 robots.txt 未作限制,也往往违反服务条款。
  • 自行设置速率限制。对于小型项目,每秒一次请求是一个合理的默认值。添加随机延迟,以免请求频率过于规律。
  • 除非有合法依据,否则请避免处理个人数据。即使数据在技术上属于公开信息,GDPR 及类似法律依然适用。
  • 转载时请注明出处。标注来源 URL 和抓取日期。

掌握如何使用 Python 抓取 HTML 表格,技术占一半,道德占一半。技术层面的失误可能导致抓取失败;道德层面的失误则可能毁掉你的公司。

关键要点

  • 选择最简单且有效的工具。 pandas.read_html 对于结构清晰的静态表格,使用 Requests + BeautifulSoup;对于需要控制的场景,使用 Playwright;对于 JavaScript 渲染或交互驱动的表格,使用 Playwright。
  • 使用表头而非索引。将表头文本与单元格文本打包,这样你的爬虫就能应对新增的列。硬编码 cells[0], cells[1] 是技术债务。
  • 分页有三个层次。先尝试 per_page=500,接着是隐藏的 JSON API,最后是页码循环。无头浏览器是最后的手段。
  • 保存前先清理数据。 pd.to_numeric, pd.to_datetime,以及 errors="coerce" 将杂乱的抓取数据行转换为可分析的类型化 DataFrame。
  • 尊重网站。遵守 robots.txt 规则,限制请求频率,除非有明确的合法依据,否则避免收集个人数据。

常见问题

在抓取表格时,pandas.read_html 和 BeautifulSoup 有什么区别?

pandas.read_html 是一个高级快捷方式:它直接返回 DataFrame,但仅处理 HTML 响应中已存在的表格。BeautifulSoup 是一个低级 HTML 解析器,可让您完全控制保留哪些单元格、如何转换它们,以及如何处理非标准标记。当您需要 read_html 用于获取可直接分析的数据;当所需规则无法用“给我第 N 个表格”这种方式表达时,请使用 BeautifulSoup。

如何抓取仅在 JavaScript 执行后才显示的 HTML 表格?

首先确认它确实是由 JavaScript 渲染的:查看页面源代码(Ctrl+U),搜索表格中的某个词,如果该词缺失,则说明该表格是在客户端动态生成的。最快的解决方法是在开发者工具的“网络”标签页中找到底层 JSON 接口,并直接调用它。如果这不可行,可以驱动一个无头浏览器(如 Playwright),等待行选择器触发,然后将 page.content() 数据传给你的常用解析器。

当表格包含合并单元格(rowspan 或 colspan)时该怎么办?

将表格视为一个二维网格,逐个单元格填充,同时遵守 colspanrowspan 属性,而非将其视为行列表。对于每个 <th><td>时,将其值复制到其跨行/跨列所覆盖的单元格中,并跳过已被先前行跨行/跨列填充的单元格。这将生成一个矩阵,您可以将其传递给 pd.DataFrame ,且不会出现列数不匹配的情况。

如何在抓取表格后保持数字和日期列的正确数据类型?

使用正则表达式(str.replace(r"[^0-9.\-]", "", regex=True)),然后调用 pd.to_numeric(series, errors="coerce") ,使无法解析的值变为 NaN ,而非抛出异常。对于日期, pd.to_datetime(series, errors="coerce", format="%Y-%m-%d") 则使用等效方法。添加 format 参数,在处理大型列时可使解析速度提升约10倍,并防止因模糊字符串导致的误判。

我可以在本地 HTML 文件或原始 HTML 字符串上运行 pandas.read_html 吗?

可以。 pd.read_html 支持 URL、本地文件路径或原始 HTML 字符串。传递 pd.read_html(open("page.html").read()) 来提供字符串,或 pd.read_html("page.html") 传递文件路径。这对于单元测试(提交已知有效的 HTML 固定数据)以及在生产环境抓取器中将获取与解析分离非常有用。

总结

掌握如何使用 Python 抓取 HTML 表格,关键在于根据表格特性选择合适的工具。首先使用 pandas.read_html ,当需要单元级控制时再升级到 Requests + BeautifulSoup,仅在数据需通过 JavaScript 渲染时才启用 Playwright。叠加支持头部信息的行循环、通用的 colspan/rowspan 解析、智能分页以及 pandas 数据清洗步骤,您便拥有了一个能适应标记变更、而非在下次部署时崩溃的爬虫。

当您已无法满足于自建代理轮换和 JavaScript 渲染时,WebScrapingAPI 提供的 Scraper API 可通过单一端点处理请求层,确保您的解析代码持续运行。接下来,请浏览我们关于 JavaScript 表格及规避封禁的深入指南。

关于作者
Andrei Ogiolan, 全栈开发工程师 @ WebScrapingAPI
Andrei Ogiolan全栈开发工程师

安德烈·奥吉奥兰(Andrei Ogiolan)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。