数据质量保证:让Scrapy爬出来的「料」干净又靠谱

📂 所属阶段:第四阶段 — 实战演练(项目开发篇)


很多新手刚开始用 Scrapy 写爬虫,总会碰到同一个场景:辛辛苦苦跑完 3000 条数据,打开 CSV 或 Excel 一看,心凉半截——标题空了一半、价格字段里塞的是乱码字符串、爬了十几分钟突然卡住再也动不了。

脏数据和不稳定会直接拖垮后面的分析、报表或者前端展示。所以,从项目实战的第一天起,就要给爬虫放进两个「标准零件」:数据验证exception-handling。一个负责当好「质检车间」,一个负责当好「安全气囊」。下面我们直接在 Scrapy 里把它们落地。


1. 数据验证:用 Pipeline 做第一道「车间质检」

Scrapy 的 Pipeline 天生就是处理数据的流水线,非常适合用来做「筛选合格数据、剔除垃圾、初步清洗」的质检工作。

这里提供一份电商项目里可直接复用的 ValidationPipeline,注释已经写得很清楚了,你可以立刻复制到项目里微调:

from scrapy.exceptions import DropItem

class ValidationPipeline:
    """
    电商类数据通用验证 Pipeline
    功能:
    1. 必填字段缺失检查
    2. 价格数据类型转换 + 合理性验证
    3. URL 格式初步校验
    """
    def process_item(self, item, spider):
        # ---------- 第一步:必填字段缺失检查 ----------
        # 电商通常必填项:产品唯一URL、标题、价格
        required_fields = ["product_url", "product_title", "product_price"]
        for field in required_fields:
            # 既检查字段是否存在,也检查值是否真的“有意义”
            if not item.get(field) or str(item.get(field)).strip() == "":
                raise DropItem(
                    f"⚠️  丢弃无效 item:{field} 缺失或为空 "
                    f"| 原 item(截取前100字符):{str(item)[:100]}..."
                )

        # ---------- 第二步:价格双重检查 ----------
        try:
            # 去掉人民币符号、千位分隔符等,再转成浮点数
            clean_price = (
                str(item["product_price"])
                .replace("¥", "")
                .replace("¥", "")
                .replace(",", "")
                .strip()
            )
            item["product_price"] = float(clean_price)
            # 做一步合理性检查:价格不能是负数
            if item["product_price"] < 0:
                raise ValueError
        except ValueError:
            raise DropItem(
                f"⚠️  丢弃无效 item:product_price 格式不合理 "
                f"| 原价格:{item['product_price']} "
                f"| 关联 URL:{item['product_url']}"
            )

        # ---------- 第三步:URL 格式初步校验 ----------
        # 只做最基础的:必须以 http:// 或 https:// 开头
        if not item["product_url"].startswith(("http://", "https://")):
            raise DropItem(
                f"⚠️  丢弃无效 item:product_url 不是有效 HTTP/HTTPS 链接 "
                f"| 原链接:{item['product_url']}"
            )

        # 全部通过,交给下一个 Pipeline
        return item

📝 Pipeline 使用提示:写完记得去 settings.py 里启用,优先级建议放在去重 Pipeline 之后、入库 Pipeline 之前。

# settings.py 中的配置片段
ITEM_PIPELINES = {
    # 假设你已有去重 Pipeline,优先级为 100(数字越小优先级越高)
    # "myproject.pipelines.DuplicatesPipeline": 100,
    "myproject.pipelines.ValidationPipeline": 200,  # 打开质检,优先级 200
    # "myproject.pipelines.SaveToMySQLPipeline": 300,
}

2. exception-handling:用 Middleware 当「安全气囊」,崩了也能救

Pipeline 管的是「数据合格」,Middleware 管的是「爬虫能稳稳地跑多久」——专门对付网络请求、响应过程中的各种意外:超时、404、500 服务器崩溃等。

下面是一个分情况重试/放弃ErrorHandlingMiddleware,能让爬虫像装了气囊一样,遇到颠簸先自救,实在不行再停下来记录原因:

import logging
from scrapy import Request
from scrapy.exceptions import IgnoreRequest

class ErrorHandlingMiddleware:
    """
    Scrapy 网络请求exception-handling Middleware
    功能:
    1. 对超时、5xx 临时错误自动重试(除 500 外,因为 500 常代表服务端内部逻辑错误)
    2. 对 4xx 客户端错误直接放弃(404/403 等重试也没用)
    3. 所有错误都用详细日志记录,方便后续排查
    """
    # 允许自动重试的状态码
    ALLOWED_RETRY_STATUS_CODES = [502, 503, 504]
    # 允许自动重试的异常类型
    ALLOWED_RETRY_EXCEPTIONS = [
        TimeoutError,              # 请求超时
        ConnectionRefusedError,    # 连接被拒绝
    ]
    # 独立设置最大重试次数(可与 settings.py 的 RETRY_TIMES 配合)
    MAX_RETRY_TIMES = 3

    def process_response(self, request, response, spider):
        """
        处理正常返回但状态码不对的响应
        """
        if response.status in self.ALLOWED_RETRY_STATUS_CODES:
            retry_times = request.meta.get("retry_times", 0)
            if retry_times < self.MAX_RETRY_TIMES:
                retry_times += 1
                spider.logger.warning(
                    f"🔄 状态码 {response.status} 触发重试:第 {retry_times} 次 "
                    f"| URL:{request.url}"
                )
                new_request = request.copy()
                new_request.meta["retry_times"] = retry_times
                # dont_filter=True 防止被去重中间件拦截
                new_request.dont_filter = True
                return new_request
            else:
                spider.logger.error(
                    f"❌ 放弃重试(超过 {self.MAX_RETRY_TIMES} 次):"
                    f"状态码 {response.status} | URL:{request.url}"
                )
                raise IgnoreRequest

        # 4xx 客户端错误直接放弃
        elif response.status >= 400:
            spider.logger.warning(
                f"🚫 放弃请求(客户端/不可恢复服务端错误):"
                f"状态码 {response.status} | URL:{request.url}"
            )
            raise IgnoreRequest

        # 状态码正常,直接返回给 Spider
        return response

    def process_exception(self, request, exception, spider):
        """
        处理请求过程中直接抛出的异常
        """
        if isinstance(exception, tuple(self.ALLOWED_RETRY_EXCEPTIONS)):
            retry_times = request.meta.get("retry_times", 0)
            if retry_times < self.MAX_RETRY_TIMES:
                retry_times += 1
                spider.logger.warning(
                    f"🔄 异常触发重试:{type(exception).__name__} "
                    f"| 第 {retry_times} 次 | URL:{request.url}"
                )
                new_request = request.copy()
                new_request.meta["retry_times"] = retry_times
                new_request.dont_filter = True
                return new_request
            else:
                spider.logger.error(
                    f"❌ 放弃重试(超过 {self.MAX_RETRY_TIMES} 次):"
                    f"{type(exception).__name__} | URL:{request.url}"
                )
                return None

        # 其他未知异常直接记录并放弃
        spider.logger.error(
            f"🚫 放弃请求(未知异常):{type(exception).__name__} "
            f"| 详情:{str(exception)} | URL:{request.url}"
        )
        return None

把它挂到 settings.pyDOWNLOADER_MIDDLEWARES 里,就能自动生效。


3. 小结:质量保证的「黄金 3 步走」

无论爬虫项目大小,质量保证的核心逻辑其实就这三步,只是用 Scrapy 落地时选择了不同的模块:

步骤核心目标Scrapy 落地模块
1. 验证剔除脏数据,保留有价值的合格数据Pipeline
2. exception-handling应对临时网络波动/服务器错误,尽量爬全数据Middleware
3. 日志记录把筛选、丢弃、重试等动作全部记下来,方便后续补爬或优化代码spider.logger / Scrapy 日志系统

💡 记住一句话:数据质量决定分析质量。用 10% 的时间提前做好验证和exception-handling,后续的数据分析、监控、维护成本会成倍降低。别等数据脏了再回头洗,直接在源头拦下来。


🔗 扩展阅读