AI编程(四十二):提取表格数据 — 把真正的”一张大表”完整搬回家

作者:

在

上一篇你抓下了三行小表,算是爬虫入门了;今天上强度:面对一张带表头、几十行、好几列的”真表格”,让程序把表头和数据一格不落地搬下来,还能顺手存成 CSV。手艺还是那套(find + get_text),变的只是”按图索骥”的图更复杂一点。

一、从”三行小表”到”一张大表”

先说今天要解决的真实麻烦。

上一篇的值班表只有三行两列,说难听点,复制粘贴也就十秒钟。但你在工作里真正会碰到的表长这样:学校网站上的全校课程表(几十行、星期一到星期五五六列)、政务网站上的补贴发放公示表(上百行、姓名/金额/银行卡号七八列)、购物网站上的参数对比表(十几行、三个产品列)。

这种表,复制粘贴就不是十秒钟的事了——粘错了行、漏了列、表头和数据对不上,是常有的事。更要命的是它每周都更新,你每周都得重粘一遍。

今天的目标很明确:写一个程序,运行一下,一整张大表原封不动地变成一个 CSV 文件——表头一行不少,数据一行不落,列和列对得齐齐整整。

先给你吃颗定心丸:今天不引入新的库。requests、BeautifulSoup、csv 都是前41篇的老朋友。今天唯一的”新知识”只有一句话:一张真正的表格里,表头和数据是分开住的,抓的时候要分开抓,再拼起来。就这一句。剩下的全是手艺的重新组合——你甚至可以说,今天是”旧手艺的实战演练日”。

二、先看懂”真表格”的解剖图

上一篇的小表长这样(简化版):

周一张三

而真实的表格长这样:

日期值班人班次联系电话
10-19张三早班138****0001
10-20李四晚班138****0002
10-21王五早班138****0003

对比一下,多了三样东西,今天全靠它们干活:

  1. 和

    :表格分成了”表头区”和”数据区”,就像一张 Excel 表的第一行是标题、下面才是数据。thead 里装标题行,tbody 里装数据行。
  2. :表头单元格(table header),专门装”这一列叫什么”。数据单元格还是老朋友

    。th 是标签,td 是内容——一个是列名,一个是值。
  3. 行变多了、列变多了:每行从 2 个格子变成 4 个格子,
    从 3 行变成 N 行。

用人话说:上一篇的表是”一张便签纸”,今天的表是”一张真正的 Excel”。Excel 有标题行、有数据区、有列名——HTML 表格也一样,就是用 thead/th 和 tbody/td 表达的。

类比记忆:把表格想成一个花名册。thead 是封面上印的栏目名(”姓名、部门、工号”),tbody 是一页页的具体名单。你整理花名册的时候,肯定是”先把栏目名抄下来当表头,再一条条抄名单”——程序抓表也是这个顺序。

这里必须提前打一针疫苗(细节等会儿第五节展开):如果你用 find_all("td") 抓,表头会整个漏掉——因为表头是 th 不是 td。这是今天最高频的坑,先记住它。

三、动手:把一张大表完整搬成 CSV

照旧,先用”自己捏的网页”练手,手熟了再上真网站。

准备:一张像样的大表

新建文件 值班表2.html(直接照抄):


  
    

2026年10月值班总表

发布日期:2026-10-18

日期值班人班次联系电话
10-19张三早班138****0001
10-20李四晚班138****0002
10-21王五早班138****0003
10-22赵六晚班138****0004
10-23孙七早班138****0005

注意跟上一篇的区别:4 列、表头用 th、数据在 tbody 里、5 行数据。这就是”真表格”的标准长相。

程序:四步搬表

新建 spider2.py:

from bs4 import BeautifulSoup
import csv

# 第1步:拿到源码(先拿本地文件练,一会儿换成真网页)
with open("值班表2.html", encoding="utf-8") as f:
    html = f.read()

# 第2步:拆成套娃结构
soup = BeautifulSoup(html, "html.parser")

# 第3步:分开抓表头和数据
# 3.1 抓表头:thead 里的所有 th
header_cells = soup.find("thead").find_all("th")
header = [cell.get_text(strip=True) for cell in header_cells]
print("表头是:", header)

# 3.2 抓数据:tbody 里的每一行,每行里的所有 td
rows = []
for tr in soup.find("tbody").find_all("tr"):
    cells = tr.find_all("td")
    row = [cell.get_text(strip=True) for cell in cells]
    rows.append(row)

print(f"共抓到 {len(rows)} 行数据")
for row in rows:
    print(row)

# 第4步:存成 CSV
with open("值班总表.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(header)   # 先写表头
    writer.writerows(rows)    # 再写数据

print("已保存为 值班总表.csv")

运行:

python spider2.py

预期输出:

表头是: ['日期', '值班人', '班次', '联系电话']
共抓到 5 行数据
['10-19', '张三', '早班', '138****0001']
['10-20', '李四', '晚班', '138****0002']
...
已保存为 值班总表.csv

打开生成的 值班总表.csv——用 Excel 或 WPS 打开,表头一行、数据五行、列对得齐齐整整。一张大表,完完整整搬回家了。

逐行拆给你看

代码不长,但每个零件都值得看一眼:

  • soup.find("thead").find_all("th"):先找”表头区”,再在里面找所有 th。注意这个链式写法——find 返回的盒子上还能再 find_all,意思是在这个盒子里继续找。这样写的好处是:万一页面里有好几个表格(真网站常有的事),你只在”第一个表格的表头区”里找,不会抓串。
  • get_text(strip=True):比上一篇多了一个 strip=True。它的意思是”取文字,顺便把前后的空格、换行都擦掉”。真网页的格子里常有换行和缩进,不加 strip 抓出来的值是 " 张三 " 这种带空格的,看着一样,比对和筛选的时候全是毛病。
  • for tr in soup.find("tbody").find_all("tr"):一行一行来,每次把一行里的 td 全取出来变成一个列表,攒进 rows。rows 就是”列表的列表”——第8篇的字典列表你都玩过了,这个不陌生。
  • csv.writer + writerow / writerows:第25篇学的 CSV 手艺原样复用。writerow 写一行(表头),writerows 写多行(数据)。注意 newline="" 不能省,不加它 Windows 上生成的 CSV 会多出空行;utf-8-sig 是为了让 Excel 打开中文不乱码——这两个小参数是第25篇讲过的老坑,今天顺手就防了。

换成真网页:还是只改一行

和上一篇一样,把”读文件”换成”发请求”:

import requests

resp = requests.get("https://example.com/duty", timeout=10)
resp.encoding = resp.apparent_encoding   # 防中文乱码,老疫苗
html = resp.text

其余一行不动。这就是分层测试的好处:提取逻辑在本地文件上验证过是通的,换真网页只动”取源码”这一步,出错也只可能出在这一步,排查范围缩到最小。

四、跟AI说需求 → 读懂 → 改进

第一步:跟AI说需求

今天的参考需求长这样(注意它比上一篇多了”表头/数据分离”和”输出文件”两个要素):

“我有一个网页,里面有一张表格,结构是:

里一行

是表头(4列:日期、值班人、班次、联系电话),

里每行是 4 个

的数据。请写 Python 代码:用 BeautifulSoup 分别抓出表头和所有数据行,保存成 CSV 文件(表头在第一行),打印抓到的行数。先用本地文件 值班表2.html 测试,代码里留好注释,标出哪一段是’以后换成真网页要改的地方’。”

要素清单:目标(存CSV)、结构(thead/th + tbody/td、几列几行)、输出(文件名、格式)、容错(留注释、打印行数)。四要素齐了,AI 一次就能写对。上一篇练习3你写的”抓全校课表”需求,今天正好用上——如果当时写得够具体,直接粘给 AI 就行。

第二步:读懂AI给的代码

读懂的标准还是”能回答三个问题”:

  1. 哪一行抓表头?为什么用 th 不用 td?(表头住在 th 里,td 只装数据)
  2. 哪一行把”一行”变成”一个列表”?(row = [cell.get_text(strip=True) for cell in cells])
  3. CSV 文件里,表头是怎么保证在第一行的?(先 writerow(header) 再 writerows(rows)——顺序决定一切)

能答上这三个,你就读懂了。答不上?把对应那行单独贴给AI:”这行在干什么?用花名册的比喻解释。”

第三步:改进

读懂之后提新要求。今天推荐这两个:

“①现在如果某行数据缺一个格子(

只有3个),列就错位了。帮我加上检查:每行格子数必须和表头列数一样多,不一样就在屏幕上打印警告,标出是第几行、少了哪个。②帮我加一句统计:打印每个’班次’各有多少人(早班X人、晚班Y人)。”

第①个改进用的是第5篇的条件判断 + 第33篇的计数思路;第②个用的是字典计数(第8篇)或者 collections.Counter。看出来了吧:今天没有新知识,改进用的全是老手艺。这就是系列设计的小心思——每隔几篇来一次”纯组合练习”,把散装手艺拧成一股绳。

五、四个必踩的坑(先打疫苗)

坑1:find_all("td") 抓不到表头,或者表头行的格子数对不上。表头是 th 不是 td。如果你不分 thead/tbody、直接 find_all("tr") 再 find_all("td"),表头那一行会抓出一个空列表(因为它的格子全是 th),然后写进 CSV 变成一行空白,列名全丢。疫苗:抓表一定分两路——表头走 th,数据走 td。顺带记住:万一遇到”偷懒网页”(表头也用 td 加粗显示),就用 find_all(["th", "td"]) 两样都要,先看源码确认是哪种,别猜。

坑2:没加 strip=True,抓出来的值带一堆空格换行。真网页为了排版好看,源码里格子内部常有换行和缩进,比如

n 张三n

。不 strip,抓出来是 "n 张三n",存进 CSV 后肉眼看不出问题,但你一做筛选、比对、去重就全崩了。疫苗:表格提取的 get_text 一律带 strip=True,写成肌肉记忆。

坑3:页面上有多个表格,抓错了对象。真网页经常一张页子里塞好几个表(导航表、布局表、数据表……早期网页甚至拿表格排版)。soup.find("table") 抓到的第一个可能根本不是你要的。疫苗:先 F12 在查看器里确认目标表格的 id 或 class(比如 class="duty-table"),然后 soup.find("table", class_="duty-table") 精确定位。定位你来(看源码),写码它来(AI)——上一篇的规矩继续有效。找不到特征?退而求其次:把几个表格都抓下来各自打印前两行,肉眼挑出目标,再回头加定位条件。

坑4:合并单元格(rowspan/colspan)让行列错位。公示表里”一等奖 3 人”这种合并格很常见,HTML 里写作 rowspan="3"——一个格子占三行的位置。后果是:接下来两行每行只有 3 个 td(而不是 4 个),你按”每行4列”硬拼,数据就错位了。疫苗:今天先学会识别它——如果发现”某些行的格子数和表头列数对不上”,十有八九是合并单元格,别硬写代码硬凑。正确姿势:跟AI说”这个表格有 rowspan/colspan 合并单元格,请帮我写一个能把表格补全成规整行列的函数”,让它帮你展开合并格。这是硬骨头,留给你当练习3的选做题,能啃下来说明你表格提取出师了。

六、跟AI协作的正确姿势(抓表格特供版)

  1. 给AI”表格的源码片段”,而不是整页源码。F12 找到
    ,右键复制这个元素(Copy element),只把这一段给AI:”表格源码是这样,请写提取代码。”几十行解决战斗,AI 写出来的定位代码也准得多。
  2. 让AI先写”检查”,再写”提取”。新需求可以这么提:”先写一段代码,打印出这个表格一共有几行、每行几个格子,确认行列整齐之后,再写提取保存的部分。”这叫先勘测后施工,是爬虫的第一美德——行列都不齐就闷头抓,抓回来的是垃圾。
  3. 让AI解释生成的 CSV 结构:”这个 CSV 是怎么保证表头在第一行的?writerow 和 writerows 有什么区别?”——第25篇的复习点,值得再问一遍。
  4. 改完先跑本地文件,再跑真网页。老规矩:本地文件跑通 = 逻辑没问题;换真网页崩了 = 是取源码/定位的问题。分层测试,永远省你最多的时间。
  5. 七、今日小练习

    练习1:跑通大表搬运。照抄 值班表2.html 和 spider2.py,运行并确认生成的 值班总表.csv 在 Excel 里打开是”1行表头 + 5行数据、4列对齐”。然后改 HTML:加一行周六的值班数据,重新运行,确认 CSV 变成 6 行数据(不用改一行代码)。

    提示:这题练的是”程序抓的是文件的当前内容”。如果行数没变,检查你保存的是不是同一个文件、跑的是不是同一个脚本。

    练习2:给表格”数个数”。修改 spider2.py,在保存 CSV 之前打印两条统计:一共几列(用表头的长度算)、一共几行数据。再加一条”体检”:遍历每一行,如果某行的格子数不等于列数,打印”第N行格子数不对,有M个格子”。

    提示:列数 = len(header),行数 = len(rows);体检就是循环里一句 if len(row) != len(header):(第5篇的手艺)。写完手动把 HTML 里某行删掉一个

    ,get_text 出来是空字符串),问它:”空格子怎么区分’没课’和’漏抓’?”——这是个好问题,能带你触到爬虫的深水区。合并单元格的表格(坑4)如果也想啃,就当本课毕业考。

    下篇预告

    下一篇(第43篇):爬虫进阶 — 一个人干完一个部门的活儿。今天你搬的是一张静态大表,下一篇上真正的强度:一个网站的数据分散在好几页(分页),抓回来的东西有重复(去重),中途网络抽风、网页改版报错(异常处理)。三样手艺一上,你的爬虫就从”能跑”进化到”能干活”。今天先把你”行列整齐、一格不落”的手艺练扎实——下一篇的地基就打在这上面。

    评论

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注

    测试,看警告能不能报出来——故意弄坏来测试,是程序员的基本功。

    练习3(动脑+选做):把上一篇的”全校课表”需求落地。拿出你练习3写的需求,粘给AI,让它写完整代码。课表比值班表多一个难点:表头是”星期一~星期五”,左边还有一列”第1~6节课”(就是个每行都有的普通格子,注意它也算一列)。目标:生成的 CSV 第一行是”节次/星期一/…/星期五”。

    提示:把”每行第一个

    是节次”这个信息明确写进需求里。选做挑战:如果 AI 给的代码没处理”某天没课的空格子”(