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

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


很多新手做Scrapy爬虫会踩这个坑:爬完3000条数据,打开Excel一看——一半标题是空的、价格是个乱码字符串/甚至缺着、爬了20分钟突然就卡壳超时页面再也没动。

脏数据和不稳定直接拖垮后续的分析/展示工作,这时候「数据验证+异常处理」就是给爬虫加的「质检车间+安全气囊」,接下来我们直接上手看Scrapy里的标准落地。


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

Pipeline 是Scrapy处理数据的专用模块,天生适合做「筛选合格、剔除垃圾、初步清洗」的验证工作。

这里给你一个电商类项目通用的「极简版+带注释可复用」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:
            # 不仅要检查字段是否存在,还要检查值是否真的有内容(比如不是空字符串/None/False)
            if not item.get(field) or str(item.get(field)).strip() == "":
                raise DropItem(f"⚠️  丢弃无效item:{field} 缺失或为空 | 原item:{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 格式不合理 | 原价格:{item['product_price']} | 关联URL:{item['product_url']}")

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

        # 所有检查通过,把合格的item传给下一个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. 异常处理:用Middleware当「安全气囊」,崩了也能救

Pipeline管「数据合格」,Middleware就管「爬虫能爬多久」——专门处理网络请求/响应里的各种错误,比如超时、404、500服务器挂了。

这里给你一个分情况重试/放弃的ErrorHandlingMiddleware:

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

class ErrorHandlingMiddleware:
    """
    Scrapy网络请求异常处理Middleware
    功能:
    1. 自动重试超时、5xx服务器错误(除了特别极端的500)
    2. 放弃4xx客户端错误(比如404/403,重试没用)
    3. 用详细日志记录所有错误,方便后续排查
    """
    # 定义允许重试的状态码和异常
    ALLOWED_RETRY_STATUS_CODES = [502, 503, 504]  # 500一般是服务端内部逻辑错误,可能不是临时的,暂不自动重试
    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}次 | URL:{request.url}"
                )
                # 把当前重试次数写进meta,下次middleware还能读
                new_request = request.copy()
                new_request.meta["retry_times"] = retry_times
                # 优先用Scrapy自带的重试调度(dont_filter=True防止重复URL被去重拦截)
                new_request.dont_filter = True
                return new_request
            else:
                spider.logger.error(
                    f"❌ 放弃重试(超过{self.MAX_RETRY_TIMES}次):状态码{response.status} | URL:{request.url}"
                )
                raise IgnoreRequest
        # 4xx直接放弃
        elif response.status >= 400:
            spider.logger.warning(
                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__} | 第{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}次):{type(exception).__name__} | URL:{request.url}"
                )
                return None
        # 其他异常直接记录并放弃
        spider.logger.error(
            f"🚫 放弃请求(未知异常):{type(exception).__name__} | 详情:{str(exception)} | URL:{request.url}"
        )
        return None

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

其实不管什么爬虫项目,质量保证的核心逻辑都是这3步,结合Scrapy只是落地方式不一样:

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

💡 记住:数据质量决定分析质量。如果前期投入10%的时间做好验证和异常处理,后期的数据分析、展示、维护工作会轻松10倍不止!


🔗 扩展阅读