Python小爬虫从能抓到能用:解析、清洗与可复用设计 如果你正在用 Python 爬虫做数据处理大概率会有一种感受真正让人头疼的并不是把网页请求回来而是请求回来之后那一堆乱糟糟的文本和表格。我自己写第三版小爬虫的时候最深的体会是爬虫脚本跑通其实不稀奇稀奇的是跑完之后的数据能直接进入下一步分析。这篇文章不准备讲那些重量级爬虫框架重点是一个人在“数据处理027”这个系列里反复迭代出来的经验如何让一个 Python 小爬虫从“能抓”变成“能用”。如果你刚好也在写自己的第一个、第二个或第三个爬虫可以把它当作一条从“跑通脚本”走向“稳定处理数据”的参考路径。1. 先搞清楚这轮迭代真正要解决什么问题1.1 从“能爬到数据”到“能拿到干净数据”很多教程会让你以为爬虫的终点是拿数据。今天抓一个标题明天抓一张图片后天抓一个表格看起来都成功了。但你把抓到的内容塞进 Excel 或数据库时问题才真正浮出水面标题里混着空格和换行日期一会儿是“2023-01-01”一会儿是“2023年1月1日”数字后面还带着“万”“元”之类的单位。这个阶段你会意识到爬虫的难点不是请求网络而是把网页里面向人展示的内容改造成面向程序处理的结构化数据。“数据处理027”这次迭代我给自己定的目标很明确不追求抓取量不搞多线程也不引入消息队列。要做的只是把一个很小的爬虫脚本整理成一套能重复使用的数据处理流程。换句话说从“这次能抓到”变成“下次还能稳定地用”。这个转变非常重要。因为单次跑通只需要运气稳定地跑通则需要考虑输入、解析、清洗、输出、异常和边界条件。如果你只想抓一次数据手动复制粘贴都可能比写爬虫更快。但如果你以后每个星期都要更新同一份报表那爬虫真正的价值就不在“抓”而在“可复用”。1.2 为什么前两个版本总是卡在解析和清洗上我见过很多人写爬虫第一版用正则硬抠能抠到一部分内容但网页一改版就失效。第二版换成了 HTML 解析库抓取稳定了一些但写出来的清洗代码东一块西一块今天处理这个字符明天处理那个格式。到第三版才发现真正的问题不是用什么解析工具而是有没有一套明确的处理顺序。网页数据是给人看的不是给程序用的。这意味着你拿到的原始数据里必然包含大量不适合直接使用的部分。比如HTML 标签里的 class、id、style 会被带进来。文本里可能有全角空格、不间断空格、tab 或换行。同一个字段可能存在多种写法。空值会被写成空字符串、None、-、暂无或直接缺失。编码不一致导致中文变成乱码。这些问题如果等到数据已经写入 CSV 或数据库再去处理会非常被动。更合理的做法是在爬虫的解析阶段就把数据尽量切成最小字段然后在清洗阶段按统一规则处理。这个顺序看起来简单但很多人是边抓边洗洗到一半发现漏字段又得回去重新抓。所以第三版小爬虫的核心不是某个“高级函数”而是先把整条链路拆成请求 → 解析 → 字段拆分 → 清洗 → 校验 → 输出。每一步的输入和输出都很明确问题就能定位到具体环节而不是一团乱麻。2. 最小可运行的爬虫脚本应该长什么样2.1 环境准备与依赖选型在动手写代码之前先确认 Python 环境。常见做法是创建虚拟环境避免把依赖装到全局。如果你用的是 Python 3.10 或更高的版本直接用标准库的venv就够了。python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate爬虫和数据处理阶段我一般只安装最常用的几个库requests发送 HTTP 请求。beautifulsoup4解析 HTML。lxmlBeautifulSoup 的底层解析器比默认的html.parser更宽容也更快。pandas如果数据量稍大或需要做复杂变换直接用 DataFrame 会省很多事。pip install requests beautifulsoup4 lxml pandas注意具体版本没有固定说法安装时看 pip 给出的兼容信息即可。如果是在团队项目里使用最好把依赖版本锁到requirements.txt里避免过几个月环境装不上。2.2 一个基础的请求与解析骨架下面是一个很基础的结构适合先跑通一条样例数据。不要一上来就封装类、抽象函数先把一条数据从源头抓到清洗完成。import requests from bs4 import BeautifulSoup URL https://example.com/data # 示例地址请替换成真实公开数据页 def fetch_html(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 很多页面没有声明编码让 requests 根据响应内容猜测 resp.encoding resp.apparent_encoding return resp.text def parse_data(html): soup BeautifulSoup(html, lxml) rows [] for tr in soup.select(table tbody tr): # 假设每行有三列名称、日期、数量 cells tr.find_all(td) if len(cells) 3: continue rows.append({ name: cells[0].get_text(stripTrue), date: cells[1].get_text(stripTrue), amount: cells[2].get_text(stripTrue), }) return rows if __name__ __main__: html fetch_html(URL) data parse_data(html) for item in data[:5]: print(item)这个骨架有两个细节值得注意。第一请求头里的User-Agent只是最基本的礼貌性标识不要伪造得太离谱也不要模仿某些浏览器特定版本后又看到对方返回 403 就继续加更多头。更常见的问题是超时时间设置。timeout10是必须的否则某个页面卡住时整个脚本会一直挂在那里。第二resp.encoding resp.apparent_encoding这行能避免很多中文乱码。如果页面里明确声明了charsetutf-8requests 通常能正确识别但没有声明时靠猜测更可靠。不过这也只是常见处理思路极端情况下还需要手动指定编码。2.3 处理数据时的输出设计一开始不要急着写 CSV 或数据库。先把解析结果直接print到终端跑一条样例确认字段是完整的。这样做有两个好处能立即看到字段名和字段顺序是否合理。能发现解析器是不是把多个单元格拼成了一个单元格。当你确认数据形态基本正确后再决定输出格式。如果只是给分析用CSV 最简单如果后续要增量更新SQLite 比 CSV 更合适。输出设计的原则是字段名尽量用英文字段不要带空格不要依赖中文字段名做程序判断。我自己在“数据处理027”这个版本里最早把输出写成一个很长的 CSV 文件后来发现每次都要用 pandas 重新处理一遍干脆直接落到 SQLite用 SQL 做后续查询。但这并不意味着每个人都应该用数据库。数据量小、一次性使用CSV 完全够用。3. 数据清洗才是“小爬爬虫”的重头戏3.1 拿到文本后先别高兴先做字段拆分很多爬虫抓到的不是干净的字段而是一整块文本。比如一个div里写着“金额1,200元 日期2023-05-01”直接get_text()拿到的是一长串。你当然可以用正则切分但正则写多了就会变成天书。更推荐的做法是在解析阶段就定位到最小的 HTML 节点。如果你用的 BeautifulSoup尽量避免对整个div调用get_text()而是继续用select_one定位到具体类名或标签amount price_tag.select_one(.amount) date price_tag.select_one(.date)这样切出来的字段更接近“结构化的值”后续清洗压力会小很多。如果整个页面结构不支持这么细粒度地定位那就只能拿到文本后手动切分。这个时候建议先把切分规则写成一个独立的小函数方便调试。3.2 异常数据的几种典型形态与处理顺序清洗数据不是一次把所有问题都解决而是按顺序消除“我知道的脏数据”。结合常见经验我会按这个顺序处理统一去空白去掉字符串首尾空格、换行、不间断空格把内容里的连续空格压缩成一个。处理缺失值把、-、暂无、null统一替换成None或默认值。统一日期格式多种日期写法转成YYYY-MM-DD。这里要注意不同系统的日期语义不要想当然。处理数字去掉千分位逗号、货币符号、中文单位必要时乘以单位系数。比如“1.2万”可以转成 12000。去重根据主键字段去重。主键怎么定取决于你后续怎么使用这批数据。写一个示例清洗函数import re def clean_amount(value): if value is None: return None value value.strip() value value.replace(,, ) # 简单处理中文单位只做示例 if value.endswith(万): return float(value[:-1]) * 10000 return float(value) def clean_date(value): if value is None: return None value value.strip() # 把 2023年5月1日 转成 2023-05-01 match re.match(r(\d{4})年(\d{1,2})月(\d{1,2})日, value) if match: y, m, d match.groups() return f{y}-{int(m):02d}-{int(d):02d} return value这个例子只是展示处理思路具体字段和逻辑要根据你的数据调整。重要的是每个清洗函数只做一件事用单元测试或一些样例数据去验证不要把所有逻辑堆在一个长函数里。3.3 把清洗结果落成 CSV 或数据库当数据清洗完再写入文件就更安全。写入 CSV 时有一个常见坑用默认utf-8编码写中文再用 Excel 打开会乱码。解决办法是使用utf-8-sig编码。import csv with open(cleaned_data.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[name, date, amount]) writer.writeheader() writer.writerows(cleaned_rows)如果数据条目很多或者之后要增量抓取可以改用 SQLiteimport sqlite3 conn sqlite3.connect(data.db) conn.execute( CREATE TABLE IF NOT EXISTS items ( name TEXT, date TEXT, amount REAL, PRIMARY KEY (name, date) ) ) conn.executemany( INSERT OR REPLACE INTO items (name, date, amount) VALUES (?, ?, ?), [(r[name], r[date], r[amount]) for r in cleaned_rows] ) conn.commit() conn.close()数据库的好处是能自然去重也能用 SQL 做筛选和聚合。但如果只是一个小爬虫没必要从一开始就上数据库。先跑通清洗流程再决定存储层。4. 从单次脚本到可复用小工具的三步改造4.1 把 URL 列表从代码里抽出来第一版爬虫的 URL 通常是硬编码在代码里的。改起来麻烦还容易改错。第三版应该把 URL 列表放到单独的文件里比如urls.txt或配置文件。https://example.com/list?page1 https://example.com/list?page2 https://example.com/list?page3在爬虫中读入from pathlib import Path urls Path(urls.txt).read_text(encodingutf-8).splitlines() urls [u.strip() for u in urls if u.strip()]这样新增页面时不需要改代码只改配置文件。尤其当你抓取的是同一站点下的分页页面时这个设计会让脚本变得更好维护。4.2 加日志、加重试、加限速脚本如果只是自己跑一次可以不加日志。但如果你想让它定时跑或者扔到服务器上跑就必须能看到“跑到哪一步了”“哪条失败了”。Python 标准库的logging比print更合适因为可以同时输出到终端和文件。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.StreamHandler(), logging.FileHandler(crawler.log, encodingutf-8) ] )网络请求失败很常见不一定是你写错了。简单的重试机制可以显著提高成功率import time def fetch_with_retry(url, retries3, delay2): for attempt in range(retries): try: return fetch_html(url) except requests.RequestException as e: logging.warning(请求失败第 %s 次重试%s, attempt 1, e) time.sleep(delay) logging.error(重试多次后仍失败%s, url) return None同时不要忘记限速。连续快速请求会让服务器压力增大也可能让你的 IP 被临时限制。最简单的做法是两次请求之间睡1到2秒for url in urls: html fetch_with_retry(url) if html: rows parse_data(html) # 处理 rows time.sleep(1.5)这里的关键不是固定的sleep值而是你要意识到爬虫不是“越快越好”而是“在别人允许的范围内尽量不打扰对方”。4.3 用配置文件控制爬取行为到了第三版把爬取间隔、重试次数、超时时间、输出路径这些参数放到配置文件里会让你切换场景时更方便。常见格式是 JSON 或 YAML。{ request_interval: 1.5, retries: 3, timeout: 10, encoding_override: null, output_path: ./output, urls_file: ./urls.txt, user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) }读取后直接作为全局配置import json with open(config.json, encodingutf-8) as f: config json.load(f)如果只是个人小工具JSON 比 YAML 更省心不用额外安装依赖。把配置和代码分离的价值在于你不需要碰代码就可以调整爬虫的“性格”。更重要的是它让你的脚本从“写给我自己的一次性脚本”变成“可以被别人使用的小工具”。5. 新手最容易踩的坑和排查顺序5.1 请求失败不一定是被封先看状态码遇到请求失败第一反应不要是“对方是不是封我 IP 了”而是先看状态码。404URL 或路径错误。403服务器拒绝访问可能是缺少权限或请求头不符合要求。500/502/503服务器本身出问题歇一会儿再试。200请求成功但页面里可能没有你想要的数据。如果是 403检查User-Agent是否太简单或者对方是否需要特定 Cookie。很多公开数据页面只需要有一个正常的浏览器标识即可访问不需要过度伪装。如果是 404仔细对比 URL尤其是分页参数里是不是有不可见字符或中文编码问题。5.2 解析结果为空时先看响应内容而不是正则爬虫解析不出数据最常见的错误是直接在解析器里反复调试而忽略了响应内容本身。正确做法是先打印或保存响应内容肉眼看一下html fetch_html(url) print(html[:1000])这一步能快速判断几个问题页面是否返回了403或反爬页面。中文是否乱码。目标数据是不是在 HTML 里还是通过 JavaScript 异步加载的。是否使用了 iframe 或动态渲染需要专门的渲染工具。如果数据是异步加载的你用 requests 拿到的是空壳 HTML再怎么调 BeautifulSoup 也没用。这时候要么找接口要么换用无头浏览器工具但后者的复杂度和资源消耗会高很多不适合小爬虫。5.3 依赖版本和运行环境的坑有时候同一个爬虫脚本在 A 电脑上能跑在 B 电脑上报错。常见的差异有Python 版本不同比如dict合并写法在某些旧版本里不兼容。依赖库版本不同比如 BeautifulSoup 的lxml解析器安装不完整。中文路径问题Windows 和 Linux 对路径的处理不一样。虚拟环境没有激活安装到了全局环境中。更稳妥的做法是把依赖固定到文件里pip freeze requirements.txt然后在另一台机器上pip install -r requirements.txt如果你的脚本要长期运行建议把你现在的测试环境标记清楚至少确认 Python 主版本、依赖库版本。5.4 一个通用的排查链路当脚本表现不正常时不要乱猜。按照下面这个链路逐层检查看现象报错、卡住、无输出、输出乱码、速度变慢、结果不完整。看输入URL 是否正确、URL 列表是否为空、文件路径是否存在、字符编码是否一致。看环境依赖版本、虚拟环境是否激活、权限、磁盘空间、网络连接是否正常。看参数超时时间、重试次数、请求间隔、headers、编码覆盖。看工具边界目标网站是否有反爬、是不是动态渲染、接口是否改动、HTML 结构是否变化。这套排查顺序可以解决绝大多数小爬虫问题。关键是不要跳到最后一步“猜是反爬”很多问题其实就是写错了一个参数。6. 这类小爬虫的适用边界以及下一步怎么走6.1 适合做什么不适合做什么“数据处理027”这个版本的思路适合解决小规模、低频、公开数据的抓取和清洗。它的能力上限大概在几百到几千个页面数据量在几万到几十万行。适合的场景包括定时从公开目录页抓取新闻标题和发布时间。抓取公开表格数据然后做简单统计。抓取个人自己的数据页面作为自动化任务的输入。学习用理解 HTTP 请求、HTML 解析和文本清洗。不太适合的场景也要提前说清楚大规模分布式抓取需要任务队列和分布式调度。需要登录、验证码、复杂反爬对抗的站点这一类已经超出“小爬爬虫”的范畴。目标网站本身禁止爬取或者数据涉及个人隐私和版权不应触碰。需要实时响应或者高可用这需要更完整的监控和部署方案。另外合规问题不能忽视。即使是公开数据也要查看网站的robots.txt和使用条款。合理的请求频率、合理的 User-Agent、不抓取非授权数据是对线路和资源的基本尊重。6.2 想从“脚本”走向“工程”还缺哪几块拼图如果你发现自己已经离不开这个小爬虫而且数据量在增长那么下一步不是继续往脚本里堆功能而是把它拆成一个更清晰的小工程。通常缺几块拼图日志系统能追溯每一次请求和清洗结果。错误隔离单条数据失败不能导致整个脚本崩溃。增量抓取记录上次抓取的位置只抓新增数据。数据校验抓完之后有校验规则防止“看似成功实际全是空值”。任务调度用 cron 或系统计划任务定时运行。监控报警当连续失败超过阈值时能主动提醒。这些并不复杂但需要一点点补。不要等到出了问题再回头补那时代价高得多。6.3 长期使用的几条建议每个版本都用一个小样例数据验证以后再跑全量不要让脚本直接面对全部 URL。清洗函数尽量独立并且保存好样例输入和预期输出下次改结构时可以回归测试。抓取频率宁慢勿快保持对目标站点的友好。数据文件加日期版本比如data_20250216.csv避免覆盖历史数据。所以如果你正准备写自己的第三版爬虫我的建议是先别把目标定在“抓到更多数据”而是把目标改成“抓到的数据能直接被下游使用”。等你发现连数据处理都变得顺滑后才算真正把爬虫从玩具变成了工具。