Python爬虫数据处理全流程:清洗、去重、保存与断点续跑

发布时间:2026/8/30 3:48:39
Python爬虫数据处理全流程:清洗、去重、保存与断点续跑 先给结论这篇处理的是 Python 爬虫里的“后半程”。很多人学爬虫最兴奋的是把页面抓下来一看到 HTML 和 JSON 就不知道下一步该干嘛。这篇重点不是讲 requests 有多少参数也不是逐行分析 BeautifulSoup 源码而是把“抓下来之后的数据怎么清洗、去重、保存、断点续跑、排查报错”完整走一遍。适合已经写过简单爬虫、想把手里的数据变成可分析表格的人也适合准备把爬虫脚本接到数据处理流程里的开发者。我建议先建立一条主线抓取只是第一步解析、清洗、落盘、重试、日志才是真正决定脚本能不能长期使用的部分。下面按我实际落地时的顺序拆开讲。1. 先把爬虫在数据处理闭环里的位置说清楚1.1 抓取只是起点不是全部很多初学者容易把“能拿到网页源码”当成任务完成。实际上从原始网页到一份干净数据表中间至少隔了四道工序抓取、解析、清洗、存储。抓取解决的是“把内容下载下来”解析是把无结构的 HTML 变成有结构的记录清洗是处理空值、类型、重复和格式问题存储是决定数据用 CSV、Excel、数据库还是接回业务流程。这一篇的核心场景是目标网站是公开页面数据量不大几百到几千条最终产物是一张可以直接用 Excel、pandas 或 BI 工具打开的表格。如果你要处理百万级数据或者需要分布式采集那另当别论不在这次讨论范围里。1.2 这次用到的技术栈和选择原因整个示例我会用到这样一组工具requests 发送 HTTP 请求 BeautifulSoup4 解析 HTML lxml BeautifulSoup 的解析器速度比默认的 html.parser 更快 pandas 做字段规整和去重 csv / openpyxl 保存文件选择这些工具的原因很简单它们都是 Python 生态里最常用的组合资料多、报错容易搜、适合教学和中小型任务。如果你只是抓少量数据用html.parser也完全够不一定非要装 lxml。但如果在同一个页面里要反复解析几百条记录lxml 会明显舒服一些。1.3 运行环境和前置准备在开始之前先把环境准备好。建议使用 Python 3.9 以上版本。安装依赖用 pip 即可pip install requests beautifulsoup4 lxml pandas如果你的机器已经装了 Anacondapandas 通常自带只需要补 requests 和 beautifulsoup4。这里先说明一下原始标题和素材里没有给出明确版本所以上面这个组合是我在常见环境中按稳定兼容原则选的。实际落地时你可以用pip list看一下当前版本优先保证 requests 和 BeautifulSoup4 能正常导入。2. 搭一个能重复运行的抓取脚本2.1 先确定要抓的页面和数据字段不要一上来就写代码。先花两分钟回答三个问题目标页面是公开的、允许访问的吗我要从页面里提取哪些字段这些字段在页面结构里是稳定存在的吗这里选用一个公开的爬虫练习站点quotes.toscrape.com做演示。它的页面结构清晰适合教学也不会给任何真实业务网站造成压力。我们假设要抓取的内容是名言文本、作者、标签。这是最常见的列表页结构。2.2 请求页面并检查响应拿到页面之前先确认访问是否成功。import requests url https://quotes.toscrape.com/ resp requests.get(url, timeout10) print(resp.status_code) print(resp.encoding) print(len(resp.text))这里要养成一个习惯不要直接写resp.text就去解析。先看状态码再看页面长度。状态码不是 200 时后面所有解析都没有意义。timeout10也不能省。没有超时时间脚本在遇到网络阻塞时可能一直挂着看起来像死机。如果resp.status_code不是 200先排查网络、URL 是否拼错、目标站点是否限制访问。不要立刻怀疑代码库坏了。2.3 解析 HTML 并提取目标字段这一步用 BeautifulSoup 把页面里需要的元素挑出来。from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) items soup.select(div.quote) records [] for item in items: text item.select_one(span.text) author item.select_one(small.author) tags item.select_one(div.tags) records.append({ text: text.get_text(stripTrue) if text else , author: author.get_text(stripTrue) if author else , tags: tags.get_text(stripTrue) if tags else })注意这里我用了select_one而不是直接访问属性并且每次都判断是否为空。原因是真实页面经常会有个别条目标签缺失如果直接.text遇到空元素就会抛异常。先判断再加get_text(stripTrue)是解析阶段最基础的防御写法。2.4 先跑单条不要直接循环全站拿到解析代码后我的建议是先只打印前三条确认字段长度和内容符合预期。for r in records[:3]: print(r)这一步几乎不花时间但能避免一个很蠢的错误字段写错了循环 50 页之后才发现解析结果全是空的。打印结果时重点看三件事字段有没有取到正确内容有没有多余换行或空白标签文本是否像预期一样是以tags:开头的原始串。3. 数据清洗要处理的核心问题3.1 把页面里的文本转换成能分析的数据直接抓下来的内容通常还不能直接用。比如名言文本会带“”引号标签会带tags:前缀作者虽然看着正常但可能有前后空格。清洗时先做类型和内容标准化def clean_text(value: str) - str: if not value: return value value.replace(“, ).replace(”, ) value value.replace(tags:, ).strip() return value这里的重点是不要写一个自以为通用的清洗函数。清洗规则越简单越好只要针对当前页面处理干净即可。等页面多了再抽成规则表或配置。为什么先处理这些因为后续不管保存成 CSV 还是导入数据库这些杂字符都会成为统计和匹配的干扰项。比如你想统计哪个作者出现次数最多结果同一作者因为空格不同被拆成了两条那统计结果就是错的。3.2 处理空值和类型缺失爬虫最容易出现的问题之一就是空值。某个字段缺失不代表这条记录没用但如果你不处理后续排序、分组、入库都会出问题。一般我会按这个顺序处理import pandas as pd df pd.DataFrame(records) df[text] df[text].astype(str).str.strip() df[author] df[author].astype(str).str.strip() df[tags] df[tags].fillna()这里astype(str)会把空值统一转成字符串fillna()则保证标签字段不会出现nan。如果你希望保留缺失状态不要把空值一律替换成空字符串但如果是备导入 Excel填充成空字符串通常更直观。3.3 去重不能只看整行列表页去重时最怕的是同一篇文章以不同标签或不同样式出现。整行去重可能失效因为标题一样但采集时间或标签不同。针对示例数据合理的去重逻辑是用“名言文本 作者”作为唯一判断。df df.drop_duplicates(subset[text, author], keepfirst)keepfirst表示保留第一次出现的记录。如果你更信任后抓取到的数据可以改成keeplast。这里要理解一个原则去重字段需要根据业务含义决定不是无脑对整行去重。否则看似干净实际数据还是脏的。3.4 用 pandas 还是只用列表数据量小比如几百条只用 list 和 dict 也能处理。数据到了几千条需要分组、排序、去重时pandas 会更方便。我一般会这样区分100 条以内没必要引入 pandas直接处理列表。100 到 1 万条pandas 处理起来顺手代码也短。1 万条以上要考虑分批写入、内存占用和存储格式。这节课的示例按几千条规模设计用 pandas 是合适的。4. 保存成 CSV 或 Excel并设计可重复运行4.1 CSV 编码直接影响 Excel 是否乱码保存 CSV 是新手最容易踩坑的地方。最典型的问题是用utf-8编码保存Excel 打开后中文全部变成乱码。解决方式有两种。第一种保存时带 BOMdf.to_csv(quotes.csv, indexFalse, encodingutf-8-sig)utf-8-sig会在文件开头写入 BOMExcel 能正确识别。第二种保存成 Exceldf.to_excel(quotes.xlsx, indexFalse)Excel 文件没有乱码问题但要注意需要安装openpyxlpip install openpyxl对于最终要给非技术同事使用的场景我建议直接存 Excel。对于要接后续程序化处理的优先存 CSV。4.2 增量写入与文件去重如果你要每天跑一次每次抓取的不只是页面最新内容还包含历史内容最简单的方式是不要每次都覆盖文件。我的做法是先读取旧文件再合并新数据去重后覆盖写回。import os output_path quotes.csv if os.path.exists(output_path): old_df pd.read_csv(output_path) df pd.concat([old_df, df], ignore_indexTrue) df df.drop_duplicates(subset[text, author], keeplast) df.to_csv(output_path, indexFalse, encodingutf-8-sig) else: df.to_csv(output_path, indexFalse, encodingutf-8-sig)这个模式虽然简单但已经很接近增量同步的思路。真实业务里的“增量更新”本质上也是先确定唯一键再合并去重。4.3 分批抓取与断点续传的思考如果目标网站有 20 页不建议一次性把全部页码全部塞进循环然后就不管了。更稳的写法是“按页码分批抓取每页抓完就落盘一次”。这样即使跑到第 15 页请求超时你也只需要从第 15 页继续而不是重新抓全部。断点续传的核心不是把抓取功能写得多高级而是每页都有独立保存结果已完成的页码可以跳过失败页码记录到错误列表。在个人脚本阶段用一个start_page变量就能解决大部分问题。5. 批量抓取时的稳定性和失败处理5.1 不要一上来就开并发看到“批量”两个字很多人的第一反应是用多线程甚至直接上框架。我的经验是先用单线程把 1 页跑通再扩展到 5 页最后再考虑并发。原因很简单并发会同时放大请求错误、文件写入冲突、日志混乱三类问题。数据量只有几百条时单线程和并发在总耗时上的差异可能只有几秒但排错成本差很多。5.2 超时、重试和失败记录网络请求必须考虑三个参数超时时间重试次数失败记录。一个比较稳妥的写法是封装一个带重试的请求函数import time import requests def fetch_page(url, retries3, timeout10): for attempt in range(retries): try: resp requests.get(url, timeouttimeout) if resp.status_code 200: return resp except requests.RequestException as e: print(f请求异常: {url}, 第 {attempt 1} 次, 错误: {e}) time.sleep(2) return None这里time.sleep(2)不是随便写的。当请求失败时短暂等待可以给网络和服务端恢复时间。重试次数不要设置太大3 次通常足够。5.3 请求频率要控制即使是公开数据也应该以正常浏览的速度去访问。不要用极短间隔疯狂请求。通常可以在每次请求之间加一个time.sleep(1)到time.sleep(3)。如果页面数量不多甚至可以直接顺序请求不要并发。这里要理解一个边界抓取公开数据用于学习是正常的但高频请求会给对方服务器造成压力也可能触发反爬机制。控制频率既是对目标站点的尊重也是让脚本更稳的策略。5.4 日志和运行状态怎么看脚本跑起来后如果没有任何输出你会很难判断是卡住了、是失败了还是在正常等待。建议每个关键节点都留下简单日志第 1 页抓取成功获取 10 条记录 第 2 页抓取成功获取 10 条记录 第 3 页请求失败准备重试 第 4 页跳过因为已经是本地数据使用print足够。print 的内容要包含页码、成功/失败、记录数这样你一眼就能知道问题出现在哪个环节。6. 常见报错和排查顺序6.1 请求阶段的问题常见的请求阶段报错包括ConnectionError网络不通或目标站点拒绝连接。Timeout服务器响应太慢或请求时间过短。HTTP 403访问被拒绝需要检查请求头和访问频率。HTTP 404URL 写错或页面路径变更。排查顺序是先确认 URL 能不能在浏览器打开再检查请求头再看超时和重试参数。不要一开始就怀疑自己代码有问题。6.2 解析为空的问题select_one(div.quote)返回None是最常见的解析问题。原因通常是页面结构变化或者请求回来的内容不是完整 HTML。这时先打印前 500 个字符print(resp.text[:500])看到内容后再决定是修改选择器还是调整字段提取逻辑。如果打印出来是乱码先看编码如果打印出来是空字符串再看请求是否真的成功。6.3 数据错位和字段混在一起抓下来的数据看起来有内容但字段错位往往是因为某个子标签里嵌套了其他结构。比如名言文本里包含了作者信息标签里包含了多余文本。这种问题只能回到页面源码检查节点关系没有通用解法。建议用item.prettify()打印单条记录内部的 HTML再重新写选择器。6.4 保存文件乱码或行数不对乱码问题优先看编码utf-8-sig基本能解决 Excel 中文乱码。行数不对则要看去重逻辑。如果总行数比预期少很多先检查是不是drop_duplicates的字段选择太宽或太窄如果行数比预期多先检查是不是翻页解析重复了。6.5 通用排查链路如果脚本运行异常我一般按下面顺序排查看输出日志确认卡在哪一步打印原始响应前 500 字符确认请求是否成功打印单条解析结果确认字段是否取对检查文件是否存在、编码是否正确检查重试和去重逻辑是否把数据误删或重复写入。这个顺序在大多数小型爬虫任务里都适用。不要跳过第 2 步直接改参数。7. 合规边界和数据使用建议7.1 可以抓什么做技术练习时优先选择公开数据、开放接口、或者专门用于教学和测试的网站。比如本系列使用的quotes.toscrape.com、各家的公开 API 文档示例接口都是安全的练习对象。公开数据也要看使用目的。学习解析、做数据分析、做个人研究通常没问题。如果是商业用途需要确认目标网站的数据使用条款。7.2 robots 协议和数据使用规范在写爬虫之前可以先查看目标网站的robots.txt文件例如https://quotes.toscrape.com/robots.txt。它会在文件里说明哪些路径允许访问哪些不允许。虽然robots.txt不是法律文件但它是一个重要的访问边界信号。个人学习场景下如果某个路径明确禁止抓取建议避开而不是想办法绕过。7.3 不应做的事我不会在博客里教你如何处理需要登录才能看到的非公开数据也不会展开讲如何绕过验证码、模拟签权、突破访问限制。这些内容不只是合规风险还很容易把学习重点带偏。爬虫的核心能力是处理公开信息、清洗数据和构建自动化流程而不是对抗目标系统。7.4 适合接入爬虫的场景比较健康的爬虫应用场景包括抓取天气、汇率、公共统计数据等开放数据监控公开页面的内容变化把多页面的公开信息聚合到本地做分析配合数据处理框架做定期更新的数据管道。在这些场景里爬虫只是数据入口真正创造价值的是后续清洗、分析和可视化。8. 把脚本整理成一个可维护的小项目8.1 文件结构建议当脚本只有几十行时一个.py文件就够了。但当你要跑多页、要清洗、要存文件、要重试建议拆成模块。project/ ├── crawler.py # 请求和解析 ├── clean.py # 清洗和去重 ├── storage.py # 保存和读取文件 ├── config.py # URL、字段、参数配置 └── run.py # 主流程拆分的意义不是显得专业而是让每一步可以单独测试。你不需要把全部逻辑塞进一个文件然后靠注释区分。8.2 主流程要直观主流程应该像读文章一样一眼能看出顺序。def main(): start_page 1 end_page 3 all_records [] for page in range(start_page, end_page 1): url fhttps://quotes.toscrape.com/page/{page}/ resp fetch_page(url) if resp is None: continue records parse_page(resp.text) all_records.extend(records) time.sleep(2) df clean_records(all_records) save_to_excel(df, quotes_final.xlsx) print(f完成共 {len(df)} 条数据)这样写出来的代码即使一个月后再打开也能快速知道这个脚本在干什么。8.3 配置和代码分离不要把所有 URL 和字段选择器写死在业务逻辑里。把它们放到 config 中后续换页面时只需要改配置。# config.py BASE_URL https://quotes.toscrape.com/ PAGE_TEMPLATE https://quotes.toscrape.com/page/{}/ ITEM_SELECTOR div.quote FIELDS { text: span.text, author: small.author, tags: div.tags } OUTPUT_PATH quotes_final.xlsx这样做的原因很实际页面结构经常因为运营调整而变如果选择器散落在各个函数里改起来很容易漏。9. 从数据表到后续分析9.1 验证结果脚本跑完后不要只看着print就收工。打开文件随机抽 5 条确认内容没有错位、没有乱码、没有明显的重复。可以用 pandas 快速做一次统计验证df pd.read_excel(quotes_final.xlsx) print(df[author].value_counts().head(10)) print(df.shape)如果作者统计结果和页面展示明显不一致说明解析或清洗环节还有问题。9.2 接上数据处理流程清洗干净的数据下一步通常是分析或可视化。你可以直接在这个结果上统计高频标签、作者排行、名言长度分布等。这就是“数据处理”的价值爬虫负责把网页里的信息搬进来而真正让你得到结论的是后面的清洗和分析。9.3 后续可以优化的方向如果你想把脚本升级成更正式的组件可以按顺序考虑把输出改成数据库比如 SQLite便于增量更新用日志库替代 print方便查看历史记录加入配置文件校验避免 URL 或字段配置写错把抓取、清洗、存储拆成三个独立函数方便单测。但我不建议在小项目里一上来就引入队列、分布式、消息中间件。先把 1000 条数据跑稳比架构炫技有用得多。回到最开始的问题Python 小爬虫到数据处理这一步最值得投入时间的地方不是“多抓几个网站”而是把抓取之后的清洗、去重、保存和失败重试做扎实。我自己的经验是很多脚本跑一次能用跑第二次就出问题原因通常不是请求写错了而是没有处理重复、没有控制频率、没有记录失败页面。如果你现在手里正有一个爬虫任务可以先把单页跑通再扩展多页每一步都用数据表验证。这样处理数据量涨到几千条时也不会手忙脚乱。