爬取数据得到的是行,而非答案。从爬虫获取的数据几乎总是杂乱的:价格以带货币符号的字符串形式存储,日期格式有三种不同写法,字段填写不完整,同一商品因两个页面都链接到它而被列出两次。在这些数据变得有用之前,你需要先清洗它,而 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 是可选的,仅在末尾的快速绘图中需要。

bash
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 即可。

python
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 对象上进行;在末尾导出之前,不会触及原始文件。

Crawlbase Crawling API

在到达 pandas 之前,products.csv 越干净,你实际需要用到本指南的内容就越少。Scraper API 会自动将受支持的页面解析为结构化 JSON 字段,价格、标题、评分和库存状态已分别解析为类型化值,因此你加载的行接近可分析状态,而非一堆原始字符串。少花时间修复数据类型,多花时间发现洞察。

第二步:检查数据集

在做任何修改之前,先看看你有什么。三个方法能告诉你几乎所有信息:head() 显示示例行,info() 报告列类型及每列的非空值数量,describe() 给出数值列的汇总统计。

python
# 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 的非空值数量少于行数,说明存在需要处理的缺失数据。这一个调用就能告诉你接下来哪些步骤是真正需要的。

第三步:处理缺失值

缺失单元格来自不完整的提取,即某个字段未能渲染、或某页上的选择器没有匹配到任何内容。首先按列统计缺失数量,然后决定是删除受影响的行,还是用合理的默认值填充。

python
# 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 可以使删除操作精准,而非丢弃任何含单个空白单元格的行。

第四步:删除重复行

重叠爬取和重复运行会产生重复行,并悄然虚增计数和平均值。先找到它们,然后删除,保留第一次出现的记录。

python
# 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" 这样的文本形式到达;你去除货币符号和千位分隔符,然后转换为浮点数。同样的方法可以统一库存标志并整理文本列。

python
# "$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(),确认该列显示为 float64booldatetime64,而非 object。你认为是数值型的列上出现 object 数据类型,是后续计算抛出异常或悄然返回错误结果的最常见原因。

第六步:解析日期

日期字符串在 pandas 理解它们为时间戳之前,对基于时间的分析毫无用处。to_datetime 转换该列,一旦它成为真正的日期时间,你就可以按它排序、筛选范围,以及提取月份等部分。

python
# 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 访问器就能提供 yearmonthday,以及 to_period 等按月分桶的辅助方法。

第七步:筛选与选择

数据清洗和类型转换完成后,你就可以对数据提问了。布尔索引按条件筛选行,你还可以组合多个条件进一步缩小范围。

python
# 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 按键拆分行,然后你对每组进行聚合, 每个类别的平均价格、每个类别的商品数量、每个类别的最高评分, 并对结果排序,以便一目了然地读取。

python
# 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。

python
# 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 直接绘图。分析完成后,将清洗好的数据写出,供下一个环节(笔记本、仪表板或团队成员)使用。

python
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 时,每行如下所示:

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;大多数清洗方法(如 fillnastr.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 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量