爬取数据得到的是行,而非答案。从爬虫获取的数据几乎总是杂乱的:价格以带货币符号的字符串形式存储,日期格式有三种不同写法,字段填写不完整,同一商品因两个页面都链接到它而被列出两次。在这些数据变得有用之前,你需要先清洗它,而 Python 中用于这项工作的标准工具是 pandas。
本指南以一个真实的爬取数据集(商品列表的 CSV 文件)为例,借助 pandas 将其从原始行处理为有价值的洞察。你将把数据加载到 DataFrame 中,检查数据,处理缺失值和重复项,修正列类型,将价格和日期解析为真正的数字和时间戳,然后进行筛选、分组、聚合、排序并导出结果。每段代码都是真实可用的,可直接复制粘贴。唯一的前提假设是:输入数据是以负责任的方式从公开来源采集的。
你将构建什么
一个简短、可复用的清洗与分析管道,将原始爬取数据转化为整洁的数据集和若干汇总表。最终,你将处理典型商品行的每一个字段:
- product_name:自由文本,通常有多余的大小写和空白。
- category:用于聚合的分组列。
-
price:爬取为类似
"$1,299.00"的字符串,需转换为浮点数。 - rating:含缺失值需处理的数值列。
- in_stock:以文本形式不一致存储的布尔标志。
- scraped_at:需解析为真实时间戳的日期字符串。
同样的工作流适用于任何表格化爬取数据,包括列表、评论、职位发布或房产数据行。如果你的爬虫已经输出干净的 JSON 或 CSV,可以直接跳到分析部分。关于这两种格式及各自适用场景的深入介绍,请参阅 JSON vs CSV:主要区别。
为什么爬取数据需要先清洗
原始爬取数据很少直接适合分析。提取层拉取的是选择器内的任何文本,因此价格以 "$1,299.00" 而非数字 1299.0 的形式返回,当页面未能渲染时评分单元格可能为空,当分页存在重叠时同一条目可能出现两次。将这些数据直接用于计算会产生错误,或者更糟,产生悄然错误的答案。
清洗在任何分析运行之前修复三类问题:来自不完整提取的缺失值、来自重叠爬取的重复行,以及跨价格、日期和文本列的格式不一致。处理好这些问题后,pandas 的其他操作, 排序、分组、聚合, 就只需几行代码。如果你正在将这种规范融入更大的数据管道,结构化和清洗网络爬取数据用于 AI 和 ML 中的模式可以很好地推广到单个 CSV 之外的场景。
前置条件
跟随本指南操作所需的内容不多。
基础 Python。 你应能够运行脚本并读懂几行代码。了解列表和字典是什么就足够了,繁重的工作由 pandas 完成。
Python 3.8 或更高版本。 使用 python --version 检查版本。如果尚未安装,请从 python.org 安装。
一个爬取得到的 CSV 或 JSON 文件。 任何表格导出文件均可。下面的示例假设文件名为 products.csv,且包含上述列,但无论你的列叫什么名字,方法都是一样的。
搭建项目
创建虚拟环境以保持项目隔离,然后安装 pandas。这是唯一必需的依赖;matplotlib 是可选的,仅在末尾的快速绘图中需要。
python --version python -m venv pandas_env source pandas_env/bin/activate pip install pandas matplotlib
在 Windows 上,使用 pandas_env\Scripts\activate 代替 source 行来激活环境。pandas 提供 DataFrame 及下面所有清洗方法;matplotlib 是调用 .plot() 时 pandas 用于绘图的库。
第一步:将数据加载到 DataFrame
pandas 的一切都从 DataFrame 开始,它是一种类似表格的行列结构,就像电子表格。其中单列称为 Series。将爬取的 CSV 读入 DataFrame 只需一次调用,JSON 同样简单,使用 read_json 即可。
import pandas as pd # Load the scraped product listings df = pd.read_csv("products.csv") # JSON works the same way: # df = pd.read_json("products.json") # Show the first five rows print(df.head())
head() 方法打印前五行,是文件成功加载、列位置正确的第一个确认。如果你的爬取数据每行一个 JSON 对象,可向 read_json 传入 lines=True。从此以后,所有操作都在内存中的 df 对象上进行;在末尾导出之前,不会触及原始文件。
在到达 pandas 之前,products.csv 越干净,你实际需要用到本指南的内容就越少。Scraper API 会自动将受支持的页面解析为结构化 JSON 字段,价格、标题、评分和库存状态已分别解析为类型化值,因此你加载的行接近可分析状态,而非一堆原始字符串。少花时间修复数据类型,多花时间发现洞察。
第二步:检查数据集
在做任何修改之前,先看看你有什么。三个方法能告诉你几乎所有信息:head() 显示示例行,info() 报告列类型及每列的非空值数量,describe() 给出数值列的汇总统计。
# Shape: (rows, columns) print(df.shape) # Column names, dtypes, and non-null counts df.info() # Summary stats for numeric columns print(df.describe())
仔细阅读 info():这是你早期发现问题的地方。如果 price 显示为 object 而非数字,说明它仍然是字符串,对其进行算术运算将会失败。如果 rating 的非空值数量少于行数,说明存在需要处理的缺失数据。这一个调用就能告诉你接下来哪些步骤是真正需要的。
第三步:处理缺失值
缺失单元格来自不完整的提取,即某个字段未能渲染、或某页上的选择器没有匹配到任何内容。首先按列统计缺失数量,然后决定是删除受影响的行,还是用合理的默认值填充。
# Count missing values per column print(df.isnull().sum()) # Drop rows missing a product name or price (can't analyse those) df = df.dropna(subset=["product_name", "price"]) # Fill missing ratings with the column average instead of dropping df["rating"] = df["rating"].fillna(df["rating"].mean())
删除还是填充取决于判断,而非规则。当缺失字段至关重要且无法推断时,删除该行(没有价格的列表对价格分析毫无用处)。当列是次要的,且合理替代值优于丢失整行时,选择填充;这里将缺失的 rating 替换为列均值,这样这些商品仍能参与类别聚合。对 dropna 使用 subset 可以使删除操作精准,而非丢弃任何含单个空白单元格的行。
第四步:删除重复行
重叠爬取和重复运行会产生重复行,并悄然虚增计数和平均值。先找到它们,然后删除,保留第一次出现的记录。
# How many fully duplicated rows are there? print(df.duplicated().sum()) # Drop exact duplicate rows, keep the first df = df.drop_duplicates(keep="first") # Or de-duplicate on a unique key, like a product URL or ID df = df.drop_duplicates(subset=["product_name", "category"])
裸调用的 drop_duplicates() 只删除所有列完全相同的行,会遗漏某列不同的近似重复。当你的爬取数据有自然键(商品 ID 或规范 URL)时,用 subset 在该键上去重,这样同一条目的两次捕获即使爬取时间戳不同也会合并为一条。在任何计数之前运行此步骤,以确保你的总数反映的是不同商品,而非爬取产生的副本。
第五步:修正数据类型并解析价格
这一步将爬取的字符串转化为可计算的值。爬取的 price 以 "$1,299.00" 这样的文本形式到达;你去除货币符号和千位分隔符,然后转换为浮点数。同样的方法可以统一库存标志并整理文本列。
# "$1,299.00" -> 1299.0 df["price"] = ( df["price"] .str.replace(r"[$,]", "", regex=True) .astype("float") ) # Tidy text: strip whitespace and normalise case df["product_name"] = df["product_name"].str.strip() df["category"] = df["category"].str.lower() # "Yes"/"No" text flag -> real booleans df["in_stock"] = df["in_stock"].str.lower().map({"yes": True, "no": False})
.str 访问器允许你对整列执行字符串操作,因此正则表达式 [$,] 在 astype("float") 将列转为数值之前,会从每个价格中去除美元符号和逗号。对 category 进行小写化处理比看起来更重要:如果半数行写的是 "Phones",另半数写的是 "phones",在未标准化之前,分组操作会将它们视为两个类别。将库存标志映射为真正的布尔值,可以在下一步中干净地进行筛选。
任何类型转换之后,再次运行 df.info(),确认该列显示为 float64、bool 或 datetime64,而非 object。你认为是数值型的列上出现 object 数据类型,是后续计算抛出异常或悄然返回错误结果的最常见原因。
第六步:解析日期
日期字符串在 pandas 理解它们为时间戳之前,对基于时间的分析毫无用处。to_datetime 转换该列,一旦它成为真正的日期时间,你就可以按它排序、筛选范围,以及提取月份等部分。
# Parse the scraped timestamp into a real datetime df["scraped_at"] = pd.to_datetime(df["scraped_at"], errors="coerce") # Now date parts are available df["scrape_month"] = df["scraped_at"].dt.to_period("M")
传入 errors="coerce" 是处理爬取数据的安全默认选项:任何 pandas 无法解析的值会变为 NaT(日期时间的空值等价物),而非抛出异常并停止整个脚本。如果每行共享一种已知格式,可以传入 format="%Y-%m-%d" 使解析更快且更严格。有了真正的日期时间列,.dt 访问器就能提供 year、month、day,以及 to_period 等按月分桶的辅助方法。
第七步:筛选与选择
数据清洗和类型转换完成后,你就可以对数据提问了。布尔索引按条件筛选行,你还可以组合多个条件进一步缩小范围。
# Only in-stock products over $100 premium = df[(df["in_stock"]) & (df["price"] > 100)] # Just the columns you care about premium = premium[["product_name", "category", "price", "rating"]] print(premium.head())
括号内的每个条件生成一个布尔 Series,& 逐行合并它们,因此需要将每个条件用括号括起来以保证运算符优先级正确。用双括号选择列列表会返回一个更窄的 DataFrame,当爬取数据有三十列而你只需要四列来回答特定问题时,这非常方便。
第八步:分组、聚合与排序
真正的价值在于汇总。groupby 按键拆分行,然后你对每组进行聚合, 每个类别的平均价格、每个类别的商品数量、每个类别的最高评分, 并对结果排序,以便一目了然地读取。
# Average price per category, highest first avg_price = ( df.groupby("category")["price"] .mean() .sort_values(ascending=False) ) # Several aggregates at once summary = df.groupby("category").agg( product_count=("product_name", "count"), avg_price=("price", "mean"), max_price=("price", "max"), avg_rating=("rating", "mean"), ) print(avg_price) print(summary)
单指标形式 groupby("category")["price"].mean() 每个类别返回一个数字,读起来几乎像英语一样直观。当你需要在一张表中呈现多个指标时,带命名聚合的 .agg() 更为简洁:每个关键字成为一个输出列,(column, function) 元组说明要汇总哪个字段以及如何汇总。在末尾链式调用 sort_values 对结果排序,让最贵和最便宜的类别分别排在顶部和底部。
第九步:应用统计
要快速读取单列的数值情况,内置的统计方法几乎涵盖了你所需的一切,无需离开 pandas。
# Full statistical summary of the price column print(df["price"].describe()) # Individual measures median_price = df["price"].median() std_price = df["price"].std() print(f"Median price: {median_price:.2f}") print(f"Std deviation: {std_price:.2f}")
对单列调用 describe() 可一次性给出计数、均值、标准差、最小值、最大值和四分位数,是快速发现异常值的方式(比如爬取错误导致价格为零或高达数百万)。对于价格数据,median() 通常比均值更能反映真实情况,因为少数极贵商品会使均值上偏,对比两者可以告诉你分布的偏斜程度。
第十步:可视化与导出
图表让分布一目了然,pandas 通过 matplotlib 直接绘图。分析完成后,将清洗好的数据写出,供下一个环节(笔记本、仪表板或团队成员)使用。
import matplotlib.pyplot as plt # Average price per category as a bar chart avg_price.plot(kind="bar", title="Average price per category") plt.ylabel("Price") plt.tight_layout() plt.show() # Export the cleaned dataset df.to_csv("products_clean.csv", index=False) summary.to_csv("category_summary.csv") # JSON export works the same way df.to_json("products_clean.json", orient="records", indent=2)
向 to_csv 传入 index=False 可防止 pandas 将行号作为多余的第一列写入,这在几乎所有情况下都是你想要的干净导出格式。对于 JSON,orient="records" 生成每行一个对象的列表,这是大多数其他工具期望的格式。至此,原始爬取数据已成为整洁的文件和汇总表,可以交付或绘制图表。
输出结果示例
运行管道后,按类别的汇总呈现为一张简洁的小表。以 orient="records" 导出为 JSON 时,每行如下所示:
[ { "category": "laptops", "product_count": 128, "avg_price": 942.55, "max_price": 2499.00, "avg_rating": 4.31 }, { "category": "phones", "product_count": 204, "avg_price": 611.20, "max_price": 1399.00, "avg_rating": 4.12 } ]
这些数字是示例性的,但结构与代码实际产生的完全一致:每个类别一条记录,携带你在 .agg() 中定义的计数和聚合值。这种结构化输出即可送入下一个阶段,无论是价格追踪仪表板、笔记本还是模型。
扩展到单文件之外
单个 CSV 是起点,而非终点。随着爬取规模增长,以下几个习惯可以让同一管道在更大体量下继续运行。
-
合并多个文件。 当一次爬取写入多个 CSV 文件时,用
pd.concat([...])将它们全部读取并合并为一个 DataFrame,再进行清洗,然后去重,防止重叠运行导致重复计数。 -
分块处理大文件。 如果文件太大无法全部加载到内存,可向
read_csv传入chunksize并进行迭代,逐块清洗和聚合,从而避免一次性持有整个数据集。 -
将清洗步骤封装为函数。 将第三步到第六步包装成一个
clean(df)函数,使每批新数据都得到相同处理,让管道保持可复现性,而非散落各处的一次性代码。 - 选择合适的输入格式。 嵌套数据(带回复的评论、带变体列表的商品)通常比 CSV 更适合 JSON。正确的选择取决于你的数据形态,这在 JSON vs CSV 中有详细介绍。
如果 pandas 在你频繁爬取的某个列上开始显得力不从心,Python 爬取和数据库 的更广泛生态系统介绍了单个 DataFrame 力所不及时可以使用的工具。
核心要点
- 分析前先清洗。 爬取数据到达时带有缺失单元格、重复行和字符串类型的数字;先修复这些问题,否则后续所有计算都值得怀疑。
- 用 head、info 和 describe 检查数据。 这三个方法告诉你数据的形状、类型和缺失值数量,让你清楚地知道需要哪些清洗步骤。
-
将价格和日期解析为真实类型。 去除货币符号并转换为浮点数,用
to_datetime转换日期字符串,并通过info()确认没有数值列仍为object类型。 - groupby 加 agg 是洞察所在。 按键分组,用命名聚合一次计算多个指标,并对结果排序以便一目了然地读取。
- 输入越干净,清洗越少。 数据到达 pandas 时越结构化,整个管道就越短。
常见问题
如何在 Python 中安装 pandas?
在终端中运行 pip install pandas,最好在虚拟环境中操作,以避免与其他项目冲突。然后在脚本中使用 import pandas as pd 导入,这是几乎所有 pandas 代码使用的惯用别名。如果你还想使用本文展示的快速图表,请同时安装 matplotlib,命令为 pip install pandas matplotlib。
pandas 中 DataFrame 和 Series 有什么区别?
DataFrame 是整张表,行列并存,就像电子表格或数据库表。Series 是表中的单列,一个一维的带标签数组。当你用 df["price"] 选择一列时,得到的是 Series;大多数清洗方法(如 fillna 和 str.replace)作用于 Series,然后你将结果赋值回该列。
如何将爬取的价格字符串转换为数字?
使用 .str 访问器去除非数值字符,然后转换类型。对于 "$1,299.00" 这样的值,调用方式为 df["price"].str.replace(r"[$,]", "", regex=True).astype("float"),这会去除美元符号和逗号,并将列转为可计算的浮点数。之后务必用 df.info() 检查该列是否不再是 object 类型。
爬取数据中的缺失值应该删除还是填充?
取决于该列的重要程度。当缺失字段至关重要且无法合理推断时,删除该行(没有价格的列表对价格分析毫无用处)。当列是次要的,且替代值优于丢失整行时,填充该值,例如用列均值替换缺失的评分,使该商品仍能参与类别聚合。使用 dropna(subset=[...]) 精准定位特定列,而非删除任何含单个空白单元格的行。
如何按类别汇总爬取数据?
使用 groupby 后跟聚合。对于单个指标,df.groupby("category")["price"].mean() 给出每个类别的平均价格。对于多个指标,使用命名聚合,df.groupby("category").agg(count=("product_name", "count"), avg_price=("price", "mean")),其中每个关键字成为一个输出列。在末尾链式调用 sort_values 对结果排序。
可以自动清洗和分析爬取数据吗?
可以。将清洗步骤封装为函数,对每批新数据调用它,然后按计划运行整个脚本,使每次新爬取都以相同方式处理。将逻辑保存在一个可复现的函数中,而非散落各处的笔记本单元格,这是随数据增长保持管道可靠性的关键。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
