AI编程(四十三):爬虫进阶 — 一个人干完一个部门的活儿

作者:

在

前两篇你的爬虫还是”一页纸搬运工”:给它一个网址,它抓一页。但真实世界的数据从来不长在一页上——公示名单分十页、商品列表翻到手软、新闻 archive 一路排到去年。今天给爬虫装上三件装备:会翻页的腿(分页抓取)、不收重货的账本(去重)、摔了能爬起来的膝盖(异常处理)。装完这三件,你的爬虫就从”能跑”进化成”能干活”——一个人干完一个部门的活儿。

一、从”一页纸”到”一个部门”的真实麻烦

先说今天要解决的事,它真实得不能再真实。

市人社局网站挂着一份补贴发放公示名单,你领导让你”把名单整理成 Excel”。你打开一看:好家伙,名单分了 10 页,每页 20 条,每条有姓名、身份证后四位、补贴金额。领导还补了一句:”每周这名单都更新,你每周都整理一遍。”

上周你学的本事是”抓一页、提表格、存 CSV”。那按老办法干:抓第 1 页,改网址抓第 2 页,再改抓第 3 页……十页改十次,粘十次,合并十次。这不叫编程,这叫把复制粘贴升级成了复制粘贴 Pro。

而且还有两个阴招在等着你:

  • 重复:第 5 页末尾和第 6 页开头有两条一模一样的记录(网站分页的常见毛病),你肉眼没看出来,交上去的表里”张三”领了两次补贴,领导以为数据造假;
  • 报错:抓到第 8 页时网络抽了一下风,程序”啪”地崩了,前 7 页白抓,从头再来。再跑到第 8 页,又崩。你对着屏幕怀疑人生。

这三个问题——翻页、去重、报错——就是今天全部的内容。好消息是:没有新库,requests、BeautifulSoup、csv、try/except(第10篇)、循环(第6篇)全是老朋友。今天真正的新概念只有一句话:

分页抓取 = 先看懂”页与页之间是怎么连的”,再写一个循环,把”抓一页”重复 N 次。

就这一句。剩下的,是把老手艺在新场景里重新组合——外加提前给你打三针疫苗。

二、看懂”翻页”的解剖图:页与页是怎么连上的

程序要翻页,先得知道”下一页”长什么样。翻页的连接方式就两种,F12 一看便知。

连法一:网址里带页码(最常见,也最好办)。你观察浏览器地址栏:

第1页:https://example.gov.cn/list?page=1
第2页:https://example.gov.cn/list?page=2
第3页:https://example.gov.cn/list?page=3

规律一目了然:网址里只有一个数字在变。这种页最好爬——把那个数字换成变量,循环 1 到 10 就完事了。今天主攻这种。

连法二:点”下一页”按钮才跳转(网址不变的动态加载)。有些网站地址栏永远是 https://example.gov.cn/list,翻页靠 JavaScript 加载数据。这种是硬骨头,今天的疫苗里会告诉你识别方法和正确姿势(不硬啃,找AI或者换数据源)。

类比记忆:连法一像一本页码印在页脚的书——想看第几页直接翻到那一页;连法二像一本撕一页才露出下一页的便签本——不点”下一页”你根本不知道下一页在哪。今天先学会读”印着页码的书”。

另外,翻页前必须做一次侦察:数清楚一共几页。公示名单页面底部一般写着”共 10 页”或者一排页码链接。把总页数告诉程序(写死 10 也行,能从页面抓出来更好),循环才有边界。不知道总页数就开抓,是爬虫的第一莽撞——要么漏抓,要么翻到第 11 页抓回一堆空数据。

三、动手:把 10 页公示名单一次搬完

照旧先用”自己捏的网页”练手。这次捏 3 页(3 页跑通了,30 页就是改个数字),每页一份名单,并且故意在第 2 页和第 3 页交界处放了两条重复数据——就等你的去重代码来抓它。

准备:三页名单(自己捏)

建一个文件夹 pagers,里面放三个 HTML 文件。

page1.html:



补贴公示 第1页

补贴发放公示名单

姓名证件号补贴金额
张三1001800
李四10021200
王五1003600
赵六1004900
共3页 下一页

page2.html(注意:开头两条和上一页末尾故意重复):



补贴公示 第2页

补贴发放公示名单

姓名证件号补贴金额
赵六1004900
王五1003600
孙七10051500
周八1006700

page3.html:



补贴公示 第3页

补贴发放公示名单

姓名证件号补贴金额
周八1006700
吴九10071100
郑十1008500
上一页 共3页

三页加起来 11 条记录,但实际只有 8 个人(1004、1003、1006 各重复了一次)。今天程序的及格线就一条:输出 8 行,一条不多,一条不少。

写码:翻页循环 + 去重 + 异常,三合一

新建 spider3.py:

import csv
import requests
from bs4 import BeautifulSoup

# ---- 配置区 ----
# 真网站上把这里换成 https://example.gov.cn/list?page={page}
BASE_URL = "file:///你的路径/pagers/page{page}.html"
TOTAL_PAGES = 3            # 侦察来的"共3页"
OUTPUT = "补贴名单.csv"

def fetch_table(page):
    """抓一页,返回 [(姓名, 证件号, 金额), ...]"""
    url = BASE_URL.format(page=page)
    resp = requests.get(url)
    resp.encoding = "utf-8"
    soup = BeautifulSoup(resp.text, "html.parser")
    table = soup.find("table", class_="notice-table")

    rows = []
    for tr in table.find("tbody").find_all("tr"):
        tds = tr.find_all("td")
        name = tds[0].get_text(strip=True)
        idno = tds[1].get_text(strip=True)
        money = tds[2].get_text(strip=True)
        rows.append((name, idno, money))
    return rows

# ---- 主流程:翻页 + 去重 + 容错 ----
seen = set()          # 账本:记下已经收过的人
all_rows = []         # 最终数据
failed_pages = []     # 记录失败的页,最后汇报

for page in range(1, TOTAL_PAGES + 1):
    try:
        rows = fetch_table(page)
    except Exception as e:
        print(f"第{page}页抓取失败:{e},跳过这页继续")
        failed_pages.append(page)
        continue                      # 摔了一跤,爬起来接着跑

    new_count = 0
    for row in rows:
        key = (row[0], row[1])        # 姓名+证件号 = 一个人的唯一标识
        if key in seen:
            continue                  # 账本上有他了,不收
        seen.add(key)                 # 记进账本
        all_rows.append(row)
        new_count += 1
    print(f"第{page}页:抓到{len(rows)}条,新增{new_count}条")

# ---- 保存 ----
with open(OUTPUT, "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["姓名", "证件号", "补贴金额"])
    writer.writerows(all_rows)

print(f"完成!共{len(all_rows)}条不重复记录,已存入{OUTPUT}")
if failed_pages:
    print(f"注意:第{failed_pages}页失败了,数据不完整,记得补抓!")

运行结果:

第1页:抓到4条,新增4条
第2页:抓到4条,新增2条
第3页:抓到3条,新增2条
完成!共8条不重复记录,已存入补贴名单.csv

打开 CSV:8 行整,张三到郑十一人一行,一个不多一个不少。领导要的活儿,你一个循环干完了。

逐块读懂(30 秒自查)

  1. BASE_URL.format(page=page):网址模板里的 {page} 被换成 1、2、3——这就是”页码印在页脚的书”的读法。真网站上你只需要改 BASE_URL 一个字符串。
  2. for page in range(1, TOTAL_PAGES + 1):翻页循环。range(1, 4) 产出 1、2、3(左闭右开的老规矩,第6篇)。
  3. try / except / continue:第10篇的手艺。某页崩了,打印原因、记一笔账、continue 跳到下一页——前 7 页不再白抓。
  4. seen 集合 + key:去重的账本。set 是一种”只记有没有、不记顺序”的容器(比列表快得多),if key in seen: continue 就是”账本上有他了,跳过”。今天唯一的一点点新知识,就这三行。

跟AI说需求的参考说法

“我有 3 个网页 page1.html 到 page3.html,每页是一个补贴名单表格。请写一个 Python 程序:①循环抓取 3 页;②按’姓名+证件号’去重(不同页可能有重复记录);③某页抓取失败要跳过继续,最后报告哪几页失败了;④结果存成带表头的 CSV,utf-8-sig 编码。”

注意这个需求的写法:把”去重依据”和”失败了怎么办”明确写出来。你不说,AI 默认给你写”崩了就崩了”的版本——上线第 8 页崩一次你就懂了。

四、改进:把”能跑”变成”能干活”

读懂之后提三个改进要求,一个比一个贴近真实工作:

“①把失败重试加上:某页失败后等 2 秒重试,最多重试 2 次,都失败才记进 failed_pages。②每页之间停 1 秒(别把人家网站打挂了)。③抓完打印一行汇报:总页数、成功页数、失败页数、原始记录数、去重后记录数。”

第①个用第10篇 try/except 套一层循环,加 time.sleep(2);第②个是 time.sleep(1)(import time);第③个是第25篇的字符串格式化 + 函数返回值小改造(fetch_table 让它失败时 raise,外面统一处理)。三个改进都不超过 5 行代码,但它们决定你的脚本是”演示品”还是”工具”。

特别是第②个,是给你的人品上保险——下一篇(爬虫礼仪)会专门讲为什么,今天先养成习惯:每页之间歇一歇。

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

坑1:翻页边界写错,最后一页永远抓不到。range(1, TOTAL_PAGES) 会把第 3 页漏掉(右开区间),range(0, TOTAL_PAGES) 会先抓一个不存在的 page=0。疫苗:写完先打印”我准备抓第几页”(print(f"正在抓第{page}页:{url}")),肉眼核对页码从 1 到 10 一页不漏。程序干活之前,先让它报数。

坑2:去重的”唯一标识”选错了,把不同人去成一个人。今天用”姓名+证件号”做 key,因为同名同姓真实存在——只用姓名当 key,两个”张三”会被你删得只剩一个,这比重复更可怕(丢数据是静默的,没人会发现)。疫苗:key 优先选编号类字段(证件号、学号、订单号、文章ID),没有编号才用”多个字段拼起来”。跟AI提需求时把 key 说清楚:”以证件号为唯一标识去重”。

坑3:file:// 跑得好好的,一换真网站就 404/403/崩。真网站会遇到四件套:网址规律其实带别的参数(?page=2&sort=asc)、需要 headers 伪装浏览器(第41篇的 headers 别忘了带)、网站直接 403 拒绝你(下一篇讲规则)、页码是动态加载的(连法二)。疫苗:换真网站时一次只改一个变量——先只改 BASE_URL 跑一页(TOTAL_PAGES = 1),一页通了再放开翻页。分层测试,第38篇的规矩。

另外提前透个底:“共10页”的总页数别永远写死。等你手熟了,跟AI说:”帮我从页面里抓出总页数(底部有’共N页’的文字),把 TOTAL_PAGES 变成自动的。”——正则(第28篇)或者字符串查找(第9篇)就能干。这是练习3的选做题。

六、跟AI协作的正确姿势(翻页爬虫特供版)

  1. 把”网址的翻页规律”喂给AI:把第 1、2、3 页的完整网址都贴给它:”看这三条网址,找出规律,帮我写翻页循环。”AI 一眼看出哪个参数是页码,比你自己猜快十倍。
  2. 让AI写”侦察版”再写”正式版”:”先写一个只抓第 1 页的版本,打印出’一共几行、每行几个字段、表格的 class 是什么’,我确认没问题后再写翻页版。”——老规矩:先勘测后施工。
  3. 把今天的坑当需求写进去:”要求:每页失败自动重试2次;页与页之间停1秒;按证件号去重;最后打印成功/失败页数和去重前后条数。”你把疫苗打了,AI 给的代码就自带抗体。
  4. 让AI解释 continue 和 break 的区别:”刚才代码里失败用 continue,如果我想’一页失败就全部停下’用什么?两者什么时候用哪个?”——答案是:补数据的活儿用 continue(缺哪页补哪页),要完整快照的活儿用 break(缺一页宁可重来)。这个判断题比语法本身值钱。

七、今日小练习

练习1:跑通三页搬运。捏好三个 HTML,照抄 spider3.py,运行并确认打印”共8条不重复记录”,CSV 里正好 8 行。然后把 page2.html 开头那两条重复记录删掉,重新运行——数字应该变成”第2页:抓到2条,新增2条”,总数还是 8。

提示:总数不变、各页新增数变了,就说明去重真的在干活。如果删完变成 10 条,检查 key 是不是写成了整行 row 之外的什么奇怪东西。

练习2:故意弄坏,看它摔不摔死。把 BASE_URL 里的 page{page} 临时改成 page{page}x(制造一个不存在的文件),运行。正确表现是:三页全部报错、打印三条失败信息、最后输出”注意:第[1, 2, 3]页失败了”,而不是程序崩溃弹出红色 traceback。

提示:崩溃 vs 报错继续,就是 try/except 有没有罩住 fetch_table 调用的区别。故意弄坏来测试,这已经是本系列第三次强调了——它真的是程序员的基本功。测完记得把网址改回来。

练习3(动脑+选做):把重试和汇报装上去。按第四节的三个改进要求跟AI协作实现它。验收标准:把 page3.html 改名藏起来,运行时应该看到”第3页失败,重试…重试…”的字样,最后汇报里写着”成功2页、失败1页、数据不完整”。选做挑战:让总页数自动从页面里抓(”共3页”三个字里把 3 抠出来),跑通后你会得到一个不用改任何数字就能抓任意页数网站的爬虫——这就是”工具”和”脚本”的分界线。

下篇预告

下一篇(第44篇):网页表单提交 — 让爬虫从”看”升级到”做”。今天你的爬虫翻了十页、去了重、摔了跤还能爬起来,但它本质上还是个”只读”的观众。下一篇上 requests.post:给搜索框填词、给查询表单提交条件,让程序主动向网站提问——”把 2026 年 9 月的公示名单给我”。这是爬虫从搬运工升级成办事员的关键一步。今天先把翻页循环练成本能——下一篇的表单提交,网址里带的参数就是从它长出来的。

评论

发表回复

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