爬虫跑通了不等于数据就能用了。我见过太多Python爬虫新手,小心翼翼绕过反爬、解决限速问题,终于抓下来几千条数据,接着就把DataFrame直接丢进分析流程里,结果统计出来的平均数离谱得像科幻小说,画出来的图表走势完全没法解释。这份缺失率、重复率、异常值TopN三合一的自动质量报告,就是专门解决"查数据干不干净"这个问题的。建议每个刚把爬虫跑通的读者都花半小时把它落地到自己项目里。
1. 爬虫数据到手后,先别急着分析,先做数据体检
1.1 脏数据的三大来源:反爬、页面结构、自身逻辑
很多刚接触Python爬虫的朋友,跑通一个爬虫拿到几千条数据后,习惯性就直接丢进DataFrame开始做统计或者画图。我以前也这么干,结果出过不少洋相:某个字段统计平均值时发现数值大到离谱,查了半天发现是反爬机制返回了一批"99999"之类的占位值;还有一次爬商品列表,同一个店铺的商品因为翻页逻辑没处理好,被重复抓了三次,数量虚高。
在爬虫项目里,这种情况不是个例,而是常态。我归纳了一下,脏数据基本来自三个源头:
- 目标网站本身数据质量差:很多平台的前端页面会展示占位符,比如商品价格显示"暂无"、评分是"--",解析下来就是空字符串或无效文本。
- 页面结构变化导致解析失败:网站改版、字段结构调整、反爬介入,都会让XPath或CSS选择器抓不到内容,那一列自然就缺了一大片。
- 爬虫自身逻辑缺陷:翻页重复、去重逻辑失效、多线程并发时变量串了,都会产生重复记录或者字段错位。
你爬得越勤,脏数据的种类就越丰富。如果不加检查直接进入下一步,任何基于这些数据的分析、入库、报表都会失真,甚至误导决策。这一节要做的,就是在数据采集完成后、进入下游使用之前,给数据做一次"体检",并自动生成一份可读的质量报告。
1.2 一份质量报告要回答的三个问题
质量报告不是给你看一堆复杂统计图,它只要回答三个实实在在的问题:
- 哪些字段缺失严重?缺的是空字符串还是NaN?缺失比例有多高?
- 数据里有多少重复记录?是整行重复,还是某个关键字段重复?
- 哪些值明显异常?异常值里最离谱的前N条是什么?
这三个问题对应三类指标:缺失率、重复率、异常值TopN。你不用手工一条条看数据,写一个脚本,每次爬完自动扫一遍,把结果输出成报告,你的爬虫项目就有了一个"质检员"。
2. 三大指标的定义和计算逻辑:先想清楚再写代码
开始写代码之前,我建议大家先把三个指标的定义搞清楚。很多教程直接甩代码,结果读者连"缺失"和"异常"的边界都模糊,换一批数据就不知道怎么用了。
2.1 缺失率:NaN、None、空字符串和占位符要分别处理
缺失率的定义很简单:缺失值数量除以总记录数,乘以100%。关键在"缺失"的判定上。
Pandas里最常见的缺失标记是NaN,用isna()就能识别。但爬虫数据里还有一种更隐蔽的"伪缺失":目标网站字段为空时,你可能解析到的是一个空字符串"",也可能是None,还可能是"N/A"、"null"这类占位文本。直接用isna()只能识别NaN,其他几种都会被当成正常值统计进去。
我的建议是写一个统一的判定函数,把这些"伪缺失"统一映射成缺失。爬虫在解析阶段就做这个归一化最好——解析到空值、空字符串、None、以及常见占位符时,统一替换成NaN,这样后续所有质量检查只用isna()就够了。注意不要只替换一种,因为同一个页面里,不同字段的空值表示方式可能完全不一样。
2.2 重复率:精确重复与业务重复,要看判定字段
重复率就是重复记录数除以总记录数,Pandas里df.duplicated().sum() / len(df)一行就能算。但这里有个新手容易忽略的点:duplicated()默认是全字段完全一致才算重复,这在真实爬虫场景里往往不够用。
举个例子,爬取招聘信息时,同一个职位在不同渠道可能被重复发布,但发布时间不同、薪资略有变化,全字段对不上,可业务上它就是同一条招聘。这种"业务重复"需要在duplicated()里指定subset参数,用关键字段去判定。是全部字段判重,还是只按"链接+标题"判重,这决定了重复率的实际意义。
我自己的习惯是两步走:先算全字段精确重复率,再按关键字段算业务重复率,两个都写进报告。这样既能发现数据采集层面的硬重复,也能发现业务层面的冗余。
2.3 异常值TopN:用IQR法则找出离群点,再排序取前N
异常值的定义在统计学里有很多流派,爬虫质检阶段我推荐用最简单也最稳的IQR(四分位距)法则,因为它不依赖数据分布形态,对非正态数据也适用。
四分位距(IQR)的原理很直白:先把数据按大小排序,切成四个等分,Q1是下四分位数(排在第25%位置的数),Q3是上四分位数(排在第75%位置的数),IQR就是Q3减Q1。正常的区间一般是:
[Q1 - 1.5 * IQR, Q3 + 1.5 * IQR]
落在区间外的数值就是离群点。然后把离群点按偏离程度排序,取前N个,就是我们要的异常值TopN。这个"1.5"是经验系数,实际使用中如果数据本身比较稳定,可以调大到2或3,减少误报。
TopN中的N应该由自己定,因为很多时候你关心的不是所有异常值,而是最离谱的前10条或前50条。这也是"异常值TopN"比单纯"异常值数量"更有用的原因——它能直接定位到具体样本,让你知道是解析问题还是真实业务现象。
3. 通用质量报告函数:一键扫描全字段
准备工作做完,下面进入实战。这一节我会给一个完整的、可以直接拿去用的函数。
3.1 环境准备
需要pandas和numpy。如果你已经跑过爬虫,这两个库大概率装了。没装的话:
pip install pandas numpy建议用Python 3.8以上版本,某些Pandas新特性(比如字符串类型的StringDtype)在旧版本上表现不太一致,功能本身不受影响,但新版本类型推断更省心。
3.2 核心代码实现
下面这个函数scan_data_quality是核心。它接收一个DataFrame,逐列计算字段类型、非空计数、缺失率、唯一值数量、样本示例以及数值字段的异常值TopN,最后汇总成一个报告:
import pandas as pd import numpy as np # 统一把爬虫中常见的"伪缺失"归一化为 NaN def normalize_missing(df, placeholder_list=None): if placeholder_list is None: placeholder_list = ['', 'N/A', 'NA', 'null', 'NULL', 'None'] df = df.replace(placeholder_list, np.nan) return df def scan_data_quality(df, top_n=10): if df.empty: print("警告:传入的DataFrame为空,无法生成质量报告。") return None df = normalize_missing(df) total_rows = len(df) report_lines = [] for col in df.columns: col_data = df[col] non_null = col_data.count() miss_count = total_rows - non_null miss_rate = miss_count / total_rows * 100 # 字段类型与唯一值 dtype_name = str(col_data.dtype) unique_count = col_data.nunique(dropna=True) # 默认取前两条非空样本 sample_values = col_data.dropna().astype(str).head(2).tolist() sample_str = ', '.join(sample_values) if sample_values else '无有效样本' # 数值字段计算异常值 TopN outlier_top = [] if pd.api.types.is_numeric_dtype(col_data): q1 = col_data.quantile(0.25) q3 = col_data.quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr outliers = col_data[(col_data < lower) | (col_data > upper)] outlier_top = outliers.dropna().sort_values( key=lambda x: abs(x - col_data.median()), ascending=False ).head(top_n).tolist() report_lines.append({ '字段': col, '类型': dtype_name, '有效样本数': non_null, '缺失率(%)': round(miss_rate, 2), '唯一值数': unique_count, '样例': sample_str, f'异常值Top{top_n}': outlier_top if outlier_top else '无' }) report_df = pd.DataFrame(report_lines) return report_df # 用法示例 # df = pd.read_csv('spider_result.csv') # quality_report = scan_data_quality(df, top_n=10) # print(quality_report.to_string(index=False))这段代码里有几个细节值得说。
一是normalize_missing函数,它把空字符串和常见占位符统一转成NaN。你在自己项目里可以根据目标网站的特点扩展这个列表,比如某网站用"暂无"表示空值,就加进去。这个列表维护得越好,报告里的缺失率就越接近真实情况。
二是用了col_data.count()而不是isna().sum(),因为count()本身就会跳过NaN,一行搞定,不容易出错。
三是异常值排序用的key=lambda x: abs(x - median),也就是离中位数越远越"异常"。这样排出来的TopN才是真正的"最离谱"记录,而不是单纯的最大最小——最大最小只反映极值,不反映分布集中程度。
3.3 输出报告解读
跑完上面函数,你会得到一个结构清晰的报告表。拿一个电商商品数据示例:
| 字段 | 类型 | 有效样本数 | 缺失率(%) | 唯一值数 | 样例 | 异常值Top10 |
|---|---|---|---|---|---|---|
| title | object | 980 | 2.00 | 930 | iPhone 15 国行, iPhone 15 美版 | 无 |
| price | float64 | 920 | 8.00 | 350 | 5999.0, 6199.0 | [999999.0, 0.01, ...] |
| shop_name | object | 950 | 5.00 | 210 | 某某数码旗舰店, ... | 无 |
看到这个表,第一反应应该是:price字段有8%缺失,而且Top10异常值里有999999、0.01这种明显不可能的价格。这个字段得重点排查,很可能解析阶段出了问题,或者目标网站上确实有标价异常的垃圾数据。这比你在Excel里肉眼猜要高效得多。
在实际项目里,我会把这张报告表导出成CSV或Excel,放到每次爬虫运行的输出目录里,用日期命名,方便回溯。
4. TopN的进阶玩法:不同类型字段分别怎么查
上一节的函数只处理了数值字段的异常值TopN。但爬虫数据的字段类型五花八门:文本型、时间型、URL型……每种类型都有适合自己的TopN检查方式。
4.1 数值型:IQR排序取TopN的更多思路
数值型字段除了按离中位数距离排序,还可以按业务含义定向排查。比如价格字段,除了IQR,还可以检查等于0、等于极大值、甚至是负数的情况,因为价格小于等于0在大多数电商场景里就是异常。在代码里加一个条件:
price_abnormal = df[(df['price'] <= 0) | (df['price'] > 100000)]这就是"业务规则TopN"——不是等统计跑完再看,而是主动定义什么值不合理。这两种思路可以结合:IQR负责抓意外离群点,业务规则负责抓已知错误值。实际项目中我经常两套规则同时跑,结果放在报告里对照着看。
4.2 文本字段:频次TopN与长度TopN
文本字段(比如商品标题、文章标题)没法用IQR,但有两个值得关注的维度:频次和长度。
频次TopN查的是某个值出现的次数,用value_counts().head(top_n)就能拿到。爬虫场景里频次异常偏高往往意味着重复采集,比如同一个店铺名出现几百次,可能翻页逻辑就把同一批商品重复抓了。这个指标比全字段重复率更灵敏,因为它能看到具体是哪个值在重复。
长度TopN也很有用。很多网站对标题有长度限制,如果某些记录的长度明显异常(例如一个商品标题有5000个字符),基本可以断定抓取时把不该抓的DOM内容也带进来了。实现就一行:
df[col].astype(str).str.len().nlargest(top_n)4.3 时间字段:范围检测与最新值检测
时间字段的异常更隐蔽,因为它不会像价格那样出现离谱的极大极小值。我常用的做法是检查三个点:
- 最早时间和最晚时间是否在合理范围内(比如商品发布时间不应该早于网站上线时间)。
- 有没有比当前时间还晚的未来时间记录,这通常是解析时区或格式错误导致的。
- 日期格式是否统一,有的字段混着"2024-01-01"和"2024/01/01"两种格式,排序时会乱。
对时间字段做TopN,本质上是把"异常"重新定义为"超出业务允许边界"。掌握了这种思路,你自己就能举一反三:地址字段可以按省市区分布做TopN,图片链接可以按URL域名做TopN,邮箱字段可以检查域名合法性。这些都属于质量报告的扩展,代码套路都是类似的。
5. 把质量报告接进爬虫流程,让检查自动化
手动跑一次质量报告不难,难的是每次爬完都记得跑。所以更好的做法是把质量检查写进爬虫主流程,让它在数据落地后自动执行。
5.1 在爬虫主循环里接管质量检查
如果你用的是requests加自写循环的爬虫,在数据解析完拼装成DataFrame之后,直接调用scan_data_quality,并把报告保存到输出目录:
def main(): all_data = [] for page in range(1, 20): items = fetch_page(page) all_data.extend(items) df = pd.DataFrame(all_data) if not df.empty: report = scan_data_quality(df) report.to_csv(f"quality_report_{datetime.now():%Y%m%d}.csv", index=False) df.to_csv("latest_data.csv", index=False)如果你用的是Scrapy这种框架,思路也一样:在Item Pipeline的最后一道处理里完成质量检查,不达标的数据可以选择丢弃或标记。重点是把质量检查当成流程的一部分,而不是事后诸葛。
5.2 报告呈现:从DataFrame到Markdown和Excel
DataFrame打印出来控制台查看够用,但要给别人看或留档,建议输出两种格式:Markdown和Excel。
Markdown便于贴到文档或群里,Excel方便进一步处理。代码很简单:
# 导出 Markdown 报告 report.to_markdown("质量报告.md", index=False) # 导出 Excel 报告,设置下格式,避免打开乱码 with pd.ExcelWriter("质量报告.xlsx", engine="openpyxl") as writer: report.to_excel(writer, index=False, sheet_name="概览") df.sample(min(100, len(df))).to_excel(writer, index=False, sheet_name="数据样例")注意to_markdown需要tabulate库,没有就装一下:pip install tabulate。
5.3 定时巡检与结果归档
质量报告生成之后,建议按日期归档,命名规范像quality_report_20250618.csv这样。如果你想定期巡检历史数据,可以用cron(Linux/macOS)或任务计划程序(Windows)定时运行脚本,把报告输出到固定目录。
再进一步,如果你追求自动化程度更高,可以在报告生成后自动发送邮件通知,甚至把缺失率或重复率超过阈值时触发告警。这部分的优先级可以放在爬虫项目第二阶段做,第一步先把报告跑通,把问题暴露出来。
6. 实践中的避坑清单和个人经验
这一节是我在多个爬虫项目里实际踩过的坑,建议大家收藏。
6.1 空DataFrame导致的除零错误
如果某天爬虫被反爬拦截,返回的data是空列表,DataFrame创建出来是空的,扫描函数里len(df)等于0,直接除零报错。所以函数里要加if df.empty的判断,同时在主流程里对空DataFrame单独处理。别小看这个,我在上线监控脚本时就被这事坑过一次,凌晨3点日志里一堆ZeroDivisionError。
6.2 编码问题:CSV读写和数据一致性
爬虫数据里混着中文、HTML转义字符甚至BOM、emoji是常事。写CSV时建议用encoding="utf-8-sig",这样Excel打开不会乱码。读进来的时候也一样,先统一编码再做质量检查,否则一堆乱码会被误判成"高缺失"或者"异常值"。
6.3 异常值不一定都要"处理"
最后一点是观念上的建议。质量报告的价值在于"发现问题",而不是"自动修掉问题"。有些异常值其实是正常业务现象,比如二手商品平台上某些标价1元引流的商品,它就是1元,不是数据错误。如果你直接把所有"异常值"都删掉或改掉,反而破坏了真实数据。正确的做法是:质量报告生成后先人工复核一遍,确认是解析问题再回到代码里修,确认是业务现象就记录在案。
6.4 组合字段的重复率判断要灵活
判定重复时,如果只按单一字段,可能误伤正常数据;如果按全字段,又可能漏掉业务重复。实战里我的经验是先做一次全字段精确判重,再做一次关键字段业务判重,两个数字都记录在报告里。比如电商场景里,按"商品ID"判重是硬规则,按"标题+价格"判重是软规则,两个结果都看看,信息更全,决策依据也更充分。
这个项目做完之后,你手上会多一个非常实用的质检模块,后续爬任何网站、采任何数据,都能直接复用。我个人现在的习惯是:每个爬虫项目落地的第一周,先连续跑三到五天的数据并用报告验证数据稳定性,等质量报告稳定了,再考虑数据分析或模型训练的事。你也可以试试,拿自己手头的爬虫数据跑一遍,看输出的报告里有没有让你意外的地方。