Python爬虫零基础入门【第四章:解析与清洗·第3节】文本清洗:去空格、去噪、金额/日期/单位标准化!

🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》

订阅后更新会优先推送,按目录学习更高效~

📌 上期回顾

在上一讲《XPath/Parsel:更稳定的解析方式》中,我们学习了如何使用CSS选择器和XPath从HTML中精准提取数据,并实现了双策略fallback机制来提高解析的稳定性。但你可能已经发现,即使成功提取到了数据,这些"原始字段"往往充斥着各种问题:

  • 标题带着多余的空格:" 热门新闻标题\n "
  • 金额格式五花八门:"¥1,234.56" / "1234.56元" / "1.2万"
  • 日期格式不统一:"2024-01-15" / "2024/1/15" / "今天 14:30"
  • 数字单位混乱:"123万次" / "1.5k views" / "2,345"

这些"脏数据"如果直接入库,会给后续的统计分析、数据比对带来巨大麻烦。今天这一讲,我们就来系统解决文本清洗问题,把混乱的原始数据转化为标准化、可分析的结构化数据。

🎯 本讲目标

交付物:

  • cleaning/text.py - 文本清洗工具函数
  • cleaning/amount.py - 金额标准化工具
  • cleaning/date.py - 日期解析工具
  • cleaning/unit.py - 单位转换工具
  • test_cleaning.py - 完整的单元测试

验收标准 :

给定30条包含各种格式的脏数据样本,输出全部符合统一规范(字段类型正确、格式一致、无噪音字符)。

为什么文本清洗如此重要?

真实场景中的数据质量问题

让我先给你看一个真实采集案例:

python 复制代码
# 从3个不同新闻站采集到的"浏览量"字段
raw_data = [
    {"source": "站点A", "views": "123,456次浏览"},
    {"source": "站点B", "views": "12.3万"},
    {"source": "站点C", "views": "1.5k views"},
]

# 如果不清洗,直接入库会怎样?
# 1. 无法做数值比较和排序(都是字符串)
# 2. 统计总浏览量时会报错(类型不兼容)
# 3. 数据分析时需要反复处理格式问题

清洗的本质:将"人类可读但机器难处理"的数据,转换为"机器友好且含义明确"的标准格式。

文本清洗的四大场景

场景1:基础文本清洗 - 去除噪音字符

常见问题分类
python 复制代码
# 问题1:多余的空白字符
dirty_text = "  标题内容\n\t  "  # 前后空格、换行、制表符

# 问题2:HTML实体残留
dirty_text = "价格 100元<促销>"  #   < >等

# 问题3:不可见字符
dirty_text = "内容\u200b\ufeff"  # 零宽空格、BOM标记

# 问题4:重复空白
dirty_text = "标题    中间    很多    空格"
实现clean_text工具函数
python 复制代码
# cleaning/text.py
import re
import html
from typing import Optional

def clean_text(text: Optional[str], 
               strip: bool = True,
               deduplicate_spaces: bool = True,
               unescape_html: bool = True) -> str:
    """
    通用文本清洗函数
    
    Args:
        text: 待清洗文本
        strip: 是否去除首尾空白
        deduplicate_spaces: 是否合并重复空格
        unescape_html: 是否转义HTML实体
    
    Returns:
        清洗后的文本
    """
    if not text or not isinstance(text, str):
        return ""
    
    # 步骤1: 转义HTML实体 (&nbsp; → 空格, &lt; → <)
    if unescape_html:
        text = html.unescape(见字符(零宽空格、BOM等)
    # \u200b=零宽空格, \ufeff=BOM, \u200c=零宽非连接符
    text = re.sub(r'[\u200b\ufeff\u200c\u200d]', '', text)
    
    # 步骤3: 统一换行符(Windows \r\n → Unix \n)
    text = text.replace('\r\n', '\n').replace('\r', '\n')
    
    # 步骤4: 合并多个空白字符为单个空格
    if deduplicate_spaces:
        text = re.sub(r'[ \t]+', ' ', text)  # 横向空白合并
        text = re.sub(r'\n{3,}', '\n\n', text)  # 保留最多2个连续换尾空白
    if strip:
        text = text.strip()
    
    return text

代码解析:

  1. 防御性编程:首先检查输入是否为空或非字符串类型,避免后续处理报错
  2. HTML实体处理 :html.unescape()自动处理常见实体(&nbsp;/&lt;/&amp;等)
  3. 不可见字符清理 :使用Unicode范围\u200b等定位零宽字符,这些字符肉眼不可见但会干扰文本处理
  4. 空白字符规范化:区分横向空白(空格/制表符)和纵向空白(换行),分别处理避免误杀有效换行
  5. 参数可控:通过布尔参数让调用者选择清洗力度,适应不同场景(如代码块需要保留缩进)
测试用例
python 复制代码
# test_cleaning.py
def test_clean_text():
    """测试基础文本清洗"""
    
    # 用例1: 多余空白
    assert clean_text("  标题\n\t  ") == "标题"
    
    # 用例2: HTML实体
    assert clean_text("价格&nbsp;100元") == "价格 100元"
    
    # 用例3: 不可见字符
    assert clean_text("内容\u200b测试") == "内容测试"
    
    # 用例4: 重复空格
    assert clean_text("A    B    C") == "A B C"
    
    # 用例5: 空输入
    assert clean_text(None) == ""
    assert clean_text("") == ""

场景2:金额标准化 - 统一货币格式

常见金额格式
python 复制代码
# 格式1: 带货币符号
"¥1,234.56"  # 人民币
"$99.99"     # 美元
"€50"        # 欧元

# 格式2: 带中文单位
"1234.56元"
"1.2万元"
"3千"

# 格式3: 英文单位
"1.5k"
"2.3M"
"100K"

# 格式4: 千分位分隔符
"1,234,567.89"
"1.234.567,89"  # 欧洲格式(逗号做小数点)
实现parse_amount工具函数
python 复制代码
# cleaning/amount.py
import re
from typing import Optional, Tuple
from decimal import Decimal, InvalidOperation

def parse_amount(text: Optional[str], 
                default_currency: str = "CNY") -> Tuple[Optional[Decimal], str]:
    """
    解析金额字符串,返回标准化数值和货币代码
    
    Args:
        text: 金额字符串
        default_currency: 默认货币(无符号时使用)
    
    Returns:
        (金额数值, 货币代码) 或 (None, "") 如果解析失败
    
    Examples:
        >>> parse_amount("¥1,234.56")
        (Decimal('1234.56'), 'CNY')
        >>> parse_amount("1.2万")
        (Decimal('12000'), 'CNY')
    
    
    text = text.strip()
    currency = default_currency
    
    # 步骤1: 识别货币符号
    currency_map = {
        '¥': 'CNY', '¥': 'CNY', '元': 'CNY',
        '$': 'USD', 'USD': 'USD',
        '€': 'EUR', 'EUR': 'EUR',
        '£': 'GBP', 'GBP': 'GBP',
    }
    
    for symbol, code in currency_map.items():
        if symbol in text:
            currency = code
            text = text.replace(symbol, '')
            break
    
    # 步骤2: 处理中文单位(万、千)
    multiplier = 1
    if '万' in text:
        multiplier = 10000
        text = text.replace('万', '')
    elif '千' in text or 'k' in text.lower():
        multiplier = 1000
        text = text.replace('千', '').replace('k', '').replace('K', '')
    elif 'm' in text.lower():
        multiplier = 1000000
        text = text.replace('m', '').replace('M', '')
    
    # 步骤3: 移除千分位分隔符和其他干扰字符
    text = re.sub(r'[,\s]', '', text)  # 移除逗号和空格
    text = re.sub(r'[^\d.]', '', text)  # 只保留数字和小数点
    
    # 步骤4: 转换为Decimal(避免浮点精度问题)
    try:
        amount = Decimal(text) * multiplier
        return amount, currency
    except (InvalidOperation, ValueError):
        return None, ""


def format_amount(amount: Decimal, 
                 currency: str = "CNY",
                 with_symbol: bool = True) -> str:
    """
    格式化金额为标准字符串
    
    Args:
        amount: 金额数值
        currency: 货币代码
        with_symbol: 是否包含货币符号
    
    Returns:
        格式化后的金额字符串
    
    Examples:
        >>> format_amount(Decimal('1234.56'), 'CNY')
        '¥1,234.56'
    """
    if amount is None:
        return ""
    
    symbol_map = {
        'CNY': '¥', 'USD': '$', 'EUR': '€', 'GBP': '£'
    }
    
    # 格式化为千分位分隔
    formatted = f"{amount:,.2f}"
    
    if with_symbol and currency in symbol_map:
        return f"{symbol_map[currency]}{formatted}"
    return formatted

代码解析:

  1. 货币识别:通过字典映射常见符号到ISO货币代码,便于后续数据库存储
  2. 单位处理:中文"万千"和英文"k/M"统一转换为乘数,避免精度损失
  3. 使用Decimal而非float :金融数据必须精确,Decimal('1.1') + Decimal('2.2') == Decimal('3.3')1.1 + 2.2 != 3.3(浮点误差)
  4. 正则清理:先移除千分位逗号,再提取纯数字,最后转换
  5. 双向转换:既能解析输入,也能格式化输出,保证数据流转的一致性
测试用例
python 复制代码
def test_parse_amount():
    """测试金额解析"""
    from decimal import Decimal
    
    # 用例1: 带符号
    assert parse_amount("¥1,234.56") == (Decimal('1234.56'), 'CNY')
    
    # 用例2: 中文单位
    assert parse_amount("1.2万元") == (Decimal('12000'), 'CNY')
    
    # 用例3: 英文单位
    assert parse_amount("1.5k") == (Decimal('1500'), 'CNY')
    
    # 用例4: 纯数字
    assert parse_amount("999.99") == (Decimal('999.99'), 'CNY')
    
    # 用例5: 异常输入
    assert parse_amount("无效金额") == (None, "")

场景3:日期解析 - 处理多种时间格式

常见日期格式
python 复制代码
# 格式1: ISO标准
"2024-01-15"
"2024-01-15 14:30:00"

# 格式2: 斜杠分隔
"2024/1/15"
"01/15/2024"  # 美国格式

# 格式3: 中文格式
"2024年1月15日"
"1月15日 14:30"

# 格式4: 相对时间
"今天 14:30"
"昨天"
"3小时前"
实现parse_date工具函数
python 复制代码
# cleaning/date.py
import re
from datetime import datetime, timedelta
from typing import Optional

def parse_date(text: Optional[str], 
              base_date: Optional[datetime] = None) -> Optional[datetime]:
    """
    解析日期字符串为datetime对象
    
    Args:
        text: 日期字符串
        base_date: 基准日期(用于相对时间计算,默认为当前时间)
    
    Returns:
        datetime对象或None
    
    Examples:
        >>> parse_date("2024-01-15 14:30:00")
        datetime(2024, 1, 15, 14, 30)
        >>> parse_date("3小时前")  # 假设现在是15:00
        datetime(..., 12, 0)  # 12:00
    """
    if not text or not isinstance(text, str):
        return None
    
    text = text.strip()
    if not base_date:
        base_date = datetime.now()
    
    # 策略1: 相对时间(今天、昨天、N小时前)
    relative_result = _parse_relative_date(text, base_date)
    if relative_result:
        return relative_result
    
    # 策略2: 常见格式(按优先级尝试)
    formats = [
        "%Y-%m-%d %H:%M:%S",      # 2024-01-15 14:30:00
        "%Y-%m-%d %H:%M",         # 2024-01-15 14:30
        "%Y-%m-%d",               # 2024-01-15
        "%Y/%m/%d %H:%M:%S",      # 2024/1/15 14:30:00
        "%Y/%m/%d",               # 2024/1/15
        "%Y年%m月%d日 %H:%M",      # 2024年1月15日 14:30
        "%Y年%m月%d日",            # 2024年1月15日
        "%m月%d日 %H:%M",          # 1月15日 14:30(补充年份)
        "%m-%d %H:%M",            # 01-15 14:30
    ]
    
    for fmt in formats:
        try:
            dt = datetime.strptime(text, fmt)
            # 如果格式不含年份,使用base_date的年份
            if '%Y' not in fmt:
                dt = dt.replace(year=base_date.year)
            return dt
        except ValueError:
            continue
    
    return None


def _parse_relative_date(text: str, base: datetime) -> Optional[datetime]:
    """解析相对时间表达式"""
    
    # 今天/昨天/前天
    if '今天' in text or '今日' in text:
        result = base.replace(hour=0, minute=0, second=0, microsecond=0)
    elif '昨天' in text or '昨日' in text:
        result = base - timedelta(days=1)
        result = result.replace(hour=0, minute=0, second=0, microsecond=0)
    elif '前天' in text:
        result = base - timedelta(days=2)
        result = result.replace(hour=0, minute=0, second=0, microsecond=0)
    else:
        result = None
    
    # 提取时分(如果有)
    if result:
        time_match = re.search(r'(\d{1,2}):(\d{2})', text)
        if time_match:
            hour, minute = int(time_match.group(1)), int(time_match.group(2))
            result = result.replace(hour=hour, minute=minute)
        return result
    
    # N小时前/分钟前
    hours_match = re.search(r'(\d+)\s*小时前', text)
    if hours_match:
        hours = int(hours_match.group(1))
        return base - timedelta(hours=hours)
    
    minutes_match = re.search(r'(\d+)\s*分钟前', text)
    if minutes_match:
        minutes = int(minutes_match.group(1))
        return base - timedelta(minutes=minutes)
    
    return None


def format_date(dt: Optional[datetime], fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
    """格式化日期为字符串"""
    if not dt:
        return ""
    return dt.strftime(fmt)

代码解析:

  1. 分层策略:先尝试相对时间(更常见),再尝试格式化字符串(更准确)
  2. 格式优先级 :从最具体到最宽松,避免2024-01-15被错误解析为2024年01月15日格式
  3. 年份补全:当格式缺少年份时(如"1月15日"),自动使用基准日期的年份
  4. 正则提取 :相对时间中可能混合时分信息(今天 14:30),需二次提取
  5. 异常安全 :strptime失败时捕获ValueError,继续尝试下一格式
测试用例
python 复制代码
def test_parse_date():
    """测试日期解析"""
    
    base = datetime(2024, 1, 15, 15, 0, 0)  # 2024-01-15 15:00:00
    
    # 用例1: 标准格式
    result = parse_date("2024-01-15 14:30:00")
    assert result == datetime(2024, 1, 15, 14, 30, 0)
    
    # 用例2: 相对时间
    result = parse_date("3小时前", base_date=base)
    assert result == datetime(2024, 1, 15, 12, 0, 0)
    
    # 用例3: 中文格式
    result = parse_date("2024年1月15日 14:30")
    assert result == datetime(2024, 1, 15, 14, 30, 0)
    
    # 用例4: 今天+时间
    result = parse_date("今天 14:30", base_date=base)
    assert result == datetime(2024, 1, 15, 14, 30, 0)

场景4:单位转换 - 统一数值单位

常见单位场景
python 复制代码
# 浏览量/播放量
"123万次" → 1230000
"1.5k views" → 1500
"2,345" → 2345

# 文件大小
"1.5MB" → 1572864 bytes
"2GB" → 2147483648 bytes

# 时长
"1小时30分" → 5400秒
"02:15:30" → 8130秒
实现parse_count工具函数
python 复制代码
# cleaning/unit.py
import re
from typing import Optional

def parse_count(text: Optional[str]) -> Optional[int]:
    """
    解析带单位的计数(浏览量、播放量等)
    
    Args:
        text: 计数字符串
    
    Returns:
        整数或None
    
    Examples:
        >>> parse_count("123万次")
        1230000
        >>> parse_count("1.5k views")
        1500
    """
    if not text or not isinstance(text, str):
        return None
    
    text = text.lower().strip()
    
    # 移除常见后缀
    text = re.sub(r'(次|views?|播放|阅读|人)', '', text, flags=re.IGNORECASE)
    text = text.strip()
    
    # 单位映射
    multiplier = 1
    if '万' in text or 'w' in text:
        multiplier = 10000
        text = text.replace('万', '').replace('w', '')
    elif 'k' in text:
        multiplier = 1000
        text = text.replace('k', '')
    elif 'm' in text:
        multiplier = 1000000
        text = text.replace('m', '')
    elif 'b' in text:
        multiplier = 1000000000
        text = text.replace('b', '')
    
    # 移除逗号分隔符
    text = text.replace(',', '').strip()
    
    try:
        # 支持小数(如1.5万)
        number = float(text)
        return int(number * multiplier)
    except ValueError:
        return None

代码解析:

  1. 后缀清理:先移除语义词("次"/"播放"等),留下纯数字+单位
  2. 单位识别:中英文单位统一转换为乘数(万/w/k/m/b)
  3. 精度处理 :先转float支持小数(1.5万),再乘乘数,最后转int
  4. 逗号兼容:千分位逗号可能出现在单位转换前,需提前清理

整合:构建清洗管道

设计清洗流程

python 复制代码
# cleaning/pipeline.py
from typing import Dict, Any, List, Callable
from .text import clean_text
from .amount import parse_amount
from .date import parse_date
from .unit import parse_count

class CleaningPipeline:
    """数据清洗管道"""
    
    def __init__(self):
        self.rules: Dict[str, Callable] = {}
        self.errors: List[Dict] = []
    
    def add_rule(self, field: str, cleaner: Callable):
        """
        添加字段清洗规则
        
        Args:
            field: 字段名
            cleaner: 清洗函数
        """
        self.rules[field] = cleaner
        return self
    
    def clean(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """
        清洗单条数据
        
        Args:
            item: 原始数据字典
        
        Returns:
            清洗后的数据字典
        """
        cleaned = {}
        
        for field, value in item.items():
            if field in self.rules:
                try:
                    cleaned[field] = self.rules[field](value)
                except Exception as e:
                    # 记录清洗失败
                    self.errors.append({
                        "field": field,
                        "value": value,
                        "error": str(e)
                    })
                    cleaned[field] = None  # 失败时填充None
            else:
                cleaned[field] = value  # 无规则则保持原值
        
        return cleaned
    
    def get_errors(self) -> List[Dict]:
        """获取清洗错误日志"""
        return self.errors


# 使用示例
def create_news_cleaner():
    """创建新闻数据清洗器"""
    pipeline = CleaningPipeline()
    
    # 标题:去除多余空格
    pipeline.add_rule('title', clean_text)
    
    # 发布时间:解析为datetime
    pipeline.add_rule('pub_date', parse_date)
    
    # 浏览量:转换为整数
    pipeline.add_rule('views', parse_count)
    
    # 价格:解析金额(如果有)
    pipeline.add_rule('price', lambda x: parse_amount(x)[0] if x else None)
    
    return pipeline


# 批量清洗
def batch_clean(items: List[Dict], pipeline: CleaningPipeline) -> List[Dict]:
    """批量清洗数据"""
    return [pipeline.clean(item) for item in items]

代码解析:

  1. 规则注册模式 :通过add_rule动态添加清洗规则,支持不同项目复用
  2. 异常容错:清洗失败时记录错误但不中断流程,保证数据流完整性
  3. 错误追踪 :errors列表记录所有清洗异常,便于后续排查脏数据来源
  4. 工厂函数 :create_news_cleaner封装特定场景配置,新项目只需调整规则

完整示例:清洗真实采集数据

python 复制代码
# 模拟采集到的脏数据
raw_items = [
    {
        "title": "  重要新闻&nbsp;标题  \n",
        "pub_date": "今天 14:30",
        "views": "12.3万次",
        "price": "¥1,234.56",
    },
    {
        "title": "另一条新闻\u200b",
        "pub_date": "2024-01-15",
        "views": "1.5k views",
        "price": None,
    }
]

# 创建清洗器
cleaner = create_news_cleaner()

# 清洗数据
cleaned_items = batch_clean(raw_items, cleaner)

print(cleaned_items)
# 输出:
# [
#     {
#         'title': '重要新闻 标题',
#         'pub_date': datetime(2024, 1, 22, 14, 30),
#         'views': 123000,
#         'price': Decimal('1234.56')
#     },
#     {
#         'title': '另一条新闻',
#         'pub_date': datetime(2024, 1, 15, 0, 0),
#         'views': 1500,
#         'price': None
#     }
# ]

# 检查清洗错误
if cleaner.get_errors():
    print(f"清洗错误: {len(cleaner.get_errors())}条")
    for error in cleaner.get_errors():
        print(f"  字段:{error['field']}, 原值:{error['value']}, 错误:{error['error']}")

清洗质量验证

编写验证脚本

python 复制代码
# validate_cleaning.py
from typing import List, Dict
from decimal import Decimal
from datetime import datetime

def validate_cleaned_data(items: List[Dict]) -> Dict[str, Any]:
    """
    验证清洗后的数据质量
    
    Returns:
        质量报告字典
    """
    report = {
        "total": len(items),
        "field_stats": {},
        "issues": []
    }
    
    for field in ['title', 'pub_date', 'views', 'price']:
        null_count = sum(1 for item in items if item.get(field) is None)
        report["field_stats"][field] = {
            "null_count": null_count,
            "null_rate": null_count / len(items) if items else 0
        }
    
    # 类型检查
    for i, item in enumerate(items):
        if item.get('pub_date') and not isinstance(item['pub_date'], datetime):
            report["issues"].append(f"第{i}条: pub_date类型错误")
        
        if item.get('views') and not isinstance(item['views'], int):
            report["issues"].append(f"第{i}条: views类型错误")
        
        if item.get('price') and not isinstance(item['price'], Decimal):
            report["issues"].append(f"第{i}条: price类型错误")
    
    return report

# 运行验证
report = validate_cleaned_data(cleaned_items)
print(f"✅ 总计: {report['total']}条")
print(f"📊 字段缺失率: {report['field_stats']}")
if report['issues']:
    print(f"⚠️ 发现问题: {len(report['issues'])}个")
    for issue in report['issues'][:5]:  # 只显示前5个问题
        print(f"   - {issue}")
else:
    print("🎉 数据质量验证通过!")

进阶技巧:处理复杂清洗场景

技巧1:清洗前后对比日志

在实际项目中,我们需要能够追溯每条数据的清洗过程,特别是当清洗结果不符合预期时。下志:

python 复制代码
# cleaning/logger.py
import json
from typing import Dict, Any
from datetime import datetime

class CleaningLogger:
    """清洗过程日志记录器"""
    
    def __init__(self, log_file: str = "cleaning_log.jsonl"):
        self.log_file = log_file
        self.changes = []
    
    def log_change(self, field: str, before: Any, after: Any, item_id: str = None):
        """
        记录单个字段的清洗变化
        
        Args:
            field: 字段名
            before: 清洗前的值
            after: 清洗后的值
            item_id: 数据项标识(如URL、ID等)
        """
        change = {
            "timestamp": datetime.now().isoformat(),
            "item_id": item_id,
            "field": field,
            "before": str(before),  # 转为字符串便于序列化
            "after": str(after),
            "changed": before != after
        }
        self.changes.append(change)
    
    def save(self):
        """保存日志到文件"""
        with open(self.log_file, 'a', encoding='utf-8') as f:
            for change in self.changes:
                f.write(json.dumps(change, ensure_ascii=False) + '\n')
        self.changes = []  # 清空缓存
    
    def get_summary(self) -> Dict[str, int]:
        """获取清洗摘要统计"""
        summary = {}
        for change in self.changes:
            field = change['field']
            if field not in summary:
                summary[field] = {'total': 0, 'changed': 0}
            summary[field]['total'] += 1
            if change['changed']:
                summary[field]['changed'] += 1
        return summary


# 使用示例:带日志的清洗管道
class LoggingCleaningPipeline(CleaningPipeline):
    """带日志功能的清洗管道"""
    
    def __init__(self, logger: CleaningLogger = None):
        super().__init__()
        self.logger = logger or CleaningLogger()
    
    def clean(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """清洗数据并记录变化"""
        cleaned = {}
        item_id = item.get('url') or item.get('id') or 'unknown'
        
        for field, value in item.items():
            if field in self.rules:
                try:
                    cleaned_value = self.rules[field](value)
                    # 记录清洗前后对比
                    self.logger.log_change(field, value, cleaned_value, item_id)
                    cleaned[field] = cleaned_value
                except Exception as e:
                    self.errors.append({
                        "field": field,
                        "value": value,
                        "error": str(e)
                    })
                    cleaned[field] = None
            else:
                cleaned[field] = value
        
        return cleaned
    
    def finalize(self):
        """清洗完成后保存日志"""
        print("📋 清洗摘要:")
        summary = self.logger.get_summary()
        for field, stats in summary.items():
            change_rate = stats['changed'] / stats['total'] * 100
            print(f"   {field}: {stats['changed']}/{stats['total']} 条变化 ({change_rate:.1f}%)")
        
        self.logger.save()
        print(f"✅ 日志已保存到 {self.logger.log_file}")

代码解析

  1. 变化追踪 :通过对比beforeafter判断字段是否真正发生变化,避免记录无效清洗
  2. 可追溯性:每条变化记录包含时间戳和数据项标识,便于定位问题数据源
  3. 摘要统计:在清洗完成后输出每个字段的变化率,帮助评估清洗规则的影响范围
  4. JSONL格式:日志使用每行一个JSON的格式,既易于人工阅读,也方便后续程序化分析

技巧2:异常值检测与处理

在清洗过程中,有些数据虽然格式正确,但数值明显异常(如浏览量为负数、日期在未来等)。我们需要在清洗时主动识别这些异常:

python 复制代码
# cleaning/validators.py
from datetime import datetime
from typing import Optional, Callable, Any
from decimal import Decimal

class ValidationRule:
    """数据验证规则基类"""
    
    def validate(self, value: Any) -> tuple[bool, str]:
        """
        验证数据
        
        Returns:
            (是否有效, 错误信息)
        """
        raise NotImplementedError


class RangeValidator(ValidationRule):
    """数值范围验证器"""
    
    def __init__(self, min_value: float = None, max_value: float = None):
        self.min_value = min_value
        self.max_value = max_value
    
    def validate(self, value: Any) -> tuple[bool, str]:
        if value is None:
            return True, ""  # 空值跳过验证
        
        try:
            num = float(value)
            if self.min_value is not None and num < self.min_value:
                return False, f"值 {num} 小于最小值 {self.min_value}"
            if self.max_value is not None and num > self.max_value:
                return False, f"值 {num} 大于最大值 {self.max_value}"
            return True, ""
        except (ValueError, TypeError):
            return False, f"无法转换为数值: {value}"


class DateRangeValidator(ValidationRule):
    """日期范围验证器"""
    
    def __init__(self, min_date: datetime = None, max_date: datetime = None):
        self.min_date = min_date
        self.max_date = max_date or datetime.now()  # 默认最大日期为当前时间
    
    def validate(self, value: Any) -> tuple[bool, str]:
        if value is None:
            return True, ""
        
        if not isinstance(value, datetime):
            return False, f"不是有效的datetime对象: {type(value)}"
        
        if self.min_date and value < self.min_date:
            return False, f"日期 {value} 早于最小日期 {self.min_date}"
        if self.max_date and value > self.max_date:
            return False, f"日期 {value} 晚于最大日期 {self.max_date}"
        
        return True, ""


class LengthValidator(ValidationRule):
    """文本长度验证器"""
    
    def __init__(self, min_length: int = None, max_length: int = None):
        self.min_length = min_length
        self.max_length = max_length
    
    def validate(self, value: Any) -> tuple[bool, str]:
        if value is None:
            return True, ""
        
        text = str(value)
        length = len(text)
        
        if self.min_length and length < self.min_length:
            return False, f"文本长度 {length} 小于最小长度 {self.min_length}"
        if self.max_length and length > self.max_length:
            return False, f"文本长度 {length} 大于最大长度 {self.max_length}"
        
        return True, ""


# 带验证的清洗管道
class ValidatedCleaningPipeline(LoggingCleaningPipeline):
    """带数据验证的清洗管道"""
    
    def __init__(self, logger: CleaningLogger = None):
        super().__init__(logger)
        self.validators: Dict[str, list[ValidationRule]] = {}
        self.validation_errors: list[Dict] = []
    
    def add_validator(self, field: str, validator: ValidationRule):
        """
        为字段添加验证规则
        
        Args:
            field: 字段名
            validator: 验证器实例
        """
        if field not in self.validators:
            self.validators[field] = []
        self.validators[field].append(validator)
        return self
    
    def clean(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """清洗并验证数据"""
        # 先执行清洗
        cleaned = super().clean(item)
        
        # 再执行验证
        item_id = item.get('url') or item.get('id') or 'unknown'
        for field, value in cleaned.items():
            if field in self.validators:
                for validator in self.validators[field]:
                    is_valid, error_msg = validator.validate(value)
                    if not is_valid:
                        self.validation_errors.append({
                            "item_id": item_id,
                            "field": field,
                            "value": value,
                            "error": error_msg
                        })
        
        return cleaned
    
    def get_validation_report(self) -> Dict:
        """获取验证报告"""
        return {
            "total_errors": len(self.validation_errors),
            "errors_by_field": self._group_errors_by_field(),
            "sample_errors": self.validation_errors[:10]  # 前10个错误样本
        }
    
    def _group_errors_by_field(self) -> Dict[str, int]:
        """按字段统计错误数量"""
        grouped = {}
        for error in self.validation_errors:
            field = error['field']
            grouped[field] = grouped.get(field, 0) + 1
        return grouped


# 实战示例:新闻数据清洗+验证
def create_validated_news_cleaner():
    """创建带验证的新闻清洗器"""
    logger = CleaningLogger("news_cleaning.jsonl")
    pipeline = ValidatedCleaningPipeline(logger)
    
    # 添加清洗规则
    pipeline.add_rule('title', clean_text)
    pipeline.add_rule('pub_date', parse_date)
    pipeline.add_rule('views', parse_count)
    pipeline.add_rule('price', lambda x: parse_amount(x)[0] if x else None)
    
    # 添加验证规则
    pipeline.add_validator('title', LengthValidator(min_length=5, max_length=200))
    pipeline.add_validator('pub_date', DateRangeValidator(
        min_date=datetime(2020, 1, 1),  # 最早2020年
        max_date=datetime.now()  # 不能是未来日期
    ))
    pipeline.add_validator('views', RangeValidator(min_value=0, max_value=1e9))
    pipeline.add_validator('price', RangeValidator(min_value=0, max_value=1e8))
    
    return pipeline


# 使用示例
raw_data = [
    {
        "url": "http://example.com/news1",
        "title": "正常新闻标题",
        "pub_date": "2024-01-15",
        "views": "10万",
        "price": "¥99.99"
    },
    {
        "url": "http://example.com/news2",
        "title": "异常",  # 标题太短!
        "pub_date": "2025-12-31",  # 未来日期!
        "views": "-100",  # 负数浏览量!
        "price": "¥9999999999"  # 价格异常!
    }
]

cleaner = create_validated_news_cleaner()
cleaned_data = [cleaner.clean(item) for item in raw_data]

# 输出验证报告
report = cleaner.get_validation_report()
print(f"\n⚠️ 验证发现 {report['total_errors']} 个问题:")
for field, count in report['errors_by_field'].items():
    print(f"   {field}: {count} 个错误")

print(f"\n📋 错误样本:")
for error in report['sample_errors']:
    print(f"   [{error['item_id']}] {error['field']}: {error['error']}")

# 保存清洗日志
cleaner.finalize()

代码解析

  1. 验证器模式 :通过抽象基类ValidationRule定义验证接口,不同验证逻辑实现为独立验证器,符合开闭原则
  2. 多重验证:一个字段可以添加多个验证器(如标题既要检查长度,又要检查是否包含敏感词)
  3. 验证与清洗分离:先清洗再验证,确保验证时数据已经格式化,避免验证逻辑处理多种原始格式
  4. 错误收集:不因验证失败而中断流程,而是收集所有错误便于批量审查和修正规则

技巧3:清洗规则的可配置化

随着项目发展,清洗规则会频繁调整。硬编码规则会导致代码修改频繁且容易出错。下面展示如何用配置文件驱动清洗流程:

python 复制代码
# cleaning/config.py
import yaml
from typing import Dict, Any
from .text import clean_text
from .amount import parse_amount
from .date import parse_date
from .unit import parse_count
from .validators import RangeValidator, DateRangeValidator, LengthValidator
from datetime import datetime

# 清洗函数注册表
CLEANER_REGISTRY = {
    'clean_text': clean_text,
    'parse_date': parse_date,
    'parse_count': parse_count,
    'parse_amount': lambda x: parse_amount(x)[0] if x else None,
}

# 验证器注册表
VALIDATOR_REGISTRY = {
    'range': RangeValidator,
    'date_range': DateRangeValidator,
    'length': LengthValidator,
}


def load_cleaning_config(config_file: str) -> ValidatedCleaningPipeline:
    """
    从YAML配置文件加载清洗管道
    
    Args:
        config_file: 配置文件路径
    
    Returns:
        配置好的清洗管道实例
    """
    with open(config_file, 'r', encoding='utf-8') as f:
        config = yaml.safe_load(f)
    
    pipeline = ValidatedCleaningPipeline()
    
    # 加载清洗规则
    for field, cleaner_name in config.get('cleaners', {}).items():
        if cleaner_name in CLEANER_REGISTRY:
            pipeline.add_rule(field, CLEANER_REGISTRY[cleaner_name])
        else:
            print(f"⚠️ 未知的清洗器: {cleaner_name}")
    
    # 加载验证规则
    for field, validators_config in config.get('validators', {}).items():
        for validator_config in validators_config:
            validator_type = validator_config.pop('type')
            if validator_type in VALIDATOR_REGISTRY:
                # 处理特殊参数(如日期字符串转datetime对象)
                params = _process_validator_params(validator_config)
                validator = VALIDATOR_REGISTRY[validator_type](**params)
                pipeline.add_validator(field, validator)
            else:
                print(f"⚠️ 未知的验证器: {validator_type}")
    
    return pipeline


def _process_validator_params(params: Dict[str, Any]) -> Dict[str, Any]:
    """处理验证器参数(如日期字符串转datetime)"""
    processed = {}
    for key, value in params.items():
        # 如果参数名包含'date'且值是字符串,尝试解析为datetime
        if 'date' in key.lower() and isinstance(value, str):
            processed[key] = datetime.fromisoformat(value)
        else:
            processed[key] = value
    return processed

配置文件示例(cleaning_config.yaml)

yaml 复制代码
# 清洗规则配置
cleaners:
  title: clean_text
  content: clean_text
  pub_date: parse_date
  views: parse_count
  price: parse_amount

# 验证规则配置
validators:
  title:
    - type: length
      min_length: 5
      max_length: 200
  
  pub_date:
    - type: date_range
      min_date: "2020-01-01T00:00:00"
      max_date: "2026-12-31T23:59:59"
  
  views:
    - type: range
      min_value: 0
      max_value: 1000000000
  
  price:
    - type: range
      min_value: 0
      max_value: 100000000

使用配置化清洗管道

python 复制代码
# main.py
from cleaning.config import load_cleaning_config

# 从配置文件加载清洗管道
pipeline = load_cleaning_config('cleaning_config.yaml')

# 清洗数据
raw_items = [
    {
        "title": "  新闻标题&nbsp;  ",
        "pub_date": "2024-01-15",
        "views": "12.3万",
        "price": "¥999.99"
    }
]

cleaned_items = [pipeline.clean(item) for item in raw_items]

# 查看清洗结果和验证报告
print("✅ 清洗完成!")
report = pipeline.get_validation_report()
if report['total_errors'] > 0:
    print(f"⚠️ 发现 {report['total_errors']} 个验证问题")
else:
    print("🎉 所有数据验证通过!")

pipeline.finalize()

配置化的优势

  1. 零代码切换规则:新增字段或调整验证阈值时,只需修改YAML文件,无需改动Python代码
  2. 环境隔离:开发/测试/生产环境可使用不同配置文件(如测试环境放宽验证规则)
  3. 版本管理:配置文件可纳入Git版本控制,清洗规则的变更历史一目了然
  4. 团队协作:非技术人员也能通过修改配置文件调整清洗策略

实战案例:清洗电商数据

让我们用一个完整的电商爬虫案例,串联本讲所有知识点:

python 复制代码
# ecommerce_cleaning.py
"""
电商数据清洗完整案例
目标:从3个电商网站采集商品数据并统一格式
"""

from cleaning.pipeline import ValidatedCleaningPipeline
from cleaning.logger import CleaningLogger
from cleaning.validators import RangeValidator, LengthValidator
from cleaning.text import clean_text
from cleaning.amount import parse_amount, format_amount
from cleaning.date import parse_date, format_date
from datetime import datetime
import json

# 模拟从不同网站采集的原始数据
raw_products = [
    # 网站A:格式较规范
    {
        "source": "网站A",
        "url": "https://a.com/product1",
        "title": "苹果iPhone 15 Pro 256GB",
        "price": "¥7,999.00",
        "sales": "10000+",
        "rating": "4.8",
        "update_time": "2024-01-20 10:30:00"
    },
    # 网站B:格式混乱
    {
        "source": "网站B",
        "url": "https://b.com/item/abc123",
        "title": "  iPhone15Pro\n256G&nbsp;  ",
        "price": "7999元",
        "sales": "1万+人付款",
        "rating": "4.8分",
        "update_time": "今天 10:30"
    },
    # 网站C:单位不统一
    {
        "source": "网站C",
        "url": "https://c.com/p/xyz789",
        "title": "【现货】iPhone 15 Pro (256GB)",
        "price": "$1,099.99",  # 美元!
        "sales": "8.5k sold",
        "rating": "96%",  # 百分制!
        "update_time": "2024/01/20 10:30"
    }
]

# 自定义清洗函数
def clean_product_title(text):
    """清洗商品标题:去噪 + 去除营销词"""
    if not text:
        return ""
    
    # 先做基础清洗
    cleaned = clean_text(text)
    
    # 去除常见营销词
    marketing_words = ['【现货】', '【正品】', '限时抢购', '热销']
    for word in marketing_words:
        cleaned = cleaned.replace(word, '')
    
    return cleaned.strip()


def parse_sales(text):
    """解析销量:统一转为整数"""
    if not text:
        return None
    
    from cleaning.unit import parse_count
    
    # 先移除"人付款"/"sold"等后缀
    text = text.replace('人付款', '').replace('sold', '').replace('+', '')
    
    return parse_count(text)


def parse_rating(text):
    """解析评分:统一转为5分制"""
    if not text:
        return None
    
    text = str(text).replace('分', '').replace('%', '').strip()
    
    try:
        rating = float(text)
        
        # 如果是百分制,转换为5分制
        if rating > 5:
            rating = rating / 20  # 100% -> 5.0
        
        return round(rating, 1)
    except ValueError:
        return None


# 创建电商清洗管道
def create_ecommerce_cleaner():
    """创建电商数据专用清洗器"""
    
    logger = CleaningLogger("ecommerce_cleaning.jsonl")
    pipeline = ValidatedCleaningPipeline(logger)
    
    # 清洗规则
    pipeline.add_rule('title', clean_product_title)
    pipeline.add_rule('price', lambda x: parse_amount(x)[0] if x else None)
    pipeline.add_rule('sales', parse_sales)
    pipeline.add_rule('rating', parse_rating)
    pipeline.add_rule('update_time', parse_date)
    
    # 验证规则
    pipeline.add_validator('title', LengthValidator(min_length=5, max_length=100))
    pipeline.add_validator('price', RangeValidator(min_value=0, max_value=100000))
    pipeline.add_validator('sales', RangeValidator(min_value=0, max_value=10000000))
    pipeline.add_validator('rating', RangeValidator(min_value=0, max_value=5))
    
    return pipeline


# 执行清洗
cleaner = create_ecommerce_cleaner()
cleaned_products = []

print("🔄 开始清洗电商数据...\n")

for raw_product in raw_products:
    cleaned = cleaner.clean(raw_product)
    cleaned_products.append(cleaned)
    
    # 打印清洗前后对比
    print(f"📦 {raw_product['source']} - {raw_product['url']}")
    print(f"   标题: {raw_product['title'][:30]}... → {cleaned['title'][:30]}...")
    print(f"   价格: {raw_product['price']} → {cleaned['price']}")
    print(f"   销量: {raw_product['sales']} → {cleaned['sales']}")
    print(f"   评分: {raw_product['rating']} → {cleaned['rating']}")
    print()

# 输出清洗摘要
print("\n" + "="*50)
cleaner.finalize()

# 验证报告
report = cleaner.get_validation_report()
if report['total_errors'] > 0:
    print(f"\n⚠️ 验证警告: {report['total_errors']} 个字段异常")
    for field, count in report['errors_by_field'].items():
        print(f"   - {field}: {count} 个问题")
else:
    print("\n✅ 所有数据验证通过!")

# 保存清洗后的数据
output_file = "cleaned_products.jsonl"
with open(output_file, 'w', encoding='utf-8') as f:
    for product in cleaned_products:
        # 格式化输出(将Decimal和datetime转为字符串)
        output_item = {
            **product,
            'price': float(product['price']) if product['price'] else None,
            'update_time': format_date(product['update_time']) if product['update_time'] else None
        }
        f.write(json.dumps(output_item, ensure_ascii=False) + '\n')

print(f"\n💾 清洗结果已保存到 {output_file}")

运行结果示例

json 复制代码
🔄 开始清洗电商数据...

📦 网站A - https://a.com/product1
   标题: 苹果iPhone 15 Pro 256GB... → 苹果iPhone 15 Pro 256GB...
   价格: ¥7,999.00 → 7999.00
   销量: 10000+ → 10000
   评分: 4.8 → 4.8

📦 网站B - https://b.com/item/abc123
   标题:   iPhone15Pro
256G&nbsp;   ... → iPhone15Pro 256G...
   价格: 7999元 → 7999.00
   销量: 1万+人付款 → 10000
   评分: 4.8分 → 4.8

📦 网站C - https://c.com/p/xyz789
   标题: 【现货】iPhone 15 Pro (256GB)... → iPhone 15 Pro (256GB)...
   价格: $1,099.99 → 1099.99
   销量: 8.5k sold → 8500
   评分: 96% → 4.8

==================================================
📋 清洗摘要:
   title: 3/3 条变化 (100.0%)
   price: 3/3 条变化 (100.0%)
   sales: 3/3 条变化 (100.0%)
   rating: 2/3 条变化 (66.7%)
   update_time: 1/3 条变化 (33.3%)
✅ 日志已保存到 ecommerce_cleaning.jsonl

✅ 所有数据验证通过!

💾 清洗结果已保存到 cleaned_products.jsonl

清洗规则设计的最佳实践

原则1:先保证正确,再考虑性能

很多新手在清洗时追求"一行搞定",写出复杂的正则表达式或链式调用,结果难以调试和维护。推荐做法

python 复制代码
# ❌ 不推荐:过度优化导致难以理解
def bad_clean(text):
    return re.sub(r'\s+', ' ', html.unescape(text.strip())).replace('\u200b', '')

# ✅ 推荐:分步骤,每步有明确意图
def good_clean(text):
    # 步骤1:去除首尾空白
    text = text.strip()
    
    # 步骤2:转义HTML实体
    text = html.unescape(text)
    
    # 步骤3:移除不可见字符
    text = text.replace('\u200b', '')
    
    # 步骤4:合并多余空格
    text = re.sub(r'\s+', ' ', text)
    
    return text

原则2:保留原始数据,清洗结果另存

永远不要覆盖原始字段,而是创建新字段存储清洗结果:

python 复制代码
# ✅ 推荐:保留原始数据
def safe_clean(item):
    return {
        'title_raw': item['title'],  # 原始标题
        'title': clean_text(item['title']),  # 清洗后标题
        'price_raw': item['price'],
        'price': parse_amount(item['price'])[0],
        # ... 其他字段
    }

# 这样做的好处:
# 1. 清洗规则出错时可回溯原始数据
# 2. 可对比清洗前后差异,评估规则效果
# 3. 便于调试和问题排查

原则3:建立清洗规则的测试用例库

为每个清洗函数准备多样化的测试样本,覆盖正常、边界、异常情况:

python 复制代码
# test_cleaning_comprehensive.py
import pytest
from cleaning.text import clean_text
from cleaning.amount import parse_amount
from decimal import Decimal

class TestCleanText:
    """文本清洗测试套件"""
    
    # 正常情况
    def test_normal_text(self):
        assert clean_text("正常文本") == "正常文本"
    
    # 边界情况
    def test_empty_string(self):
        assert clean_text("") == ""
    
    def test_none_input(self):
        assert clean_text(None) == ""
    
    def test_only_whitespace(self):
        assert clean_text("   \n\t   ") == ""
    
    # 异常情况
    def test_mixed_invisible_chars(self):
        text = "文本\u200b\ufeff\u200c测试"
        assert clean_text(text) == "文本测试"
    
    def test_html_entities(self):
        assert clean_text("A&nbsp;B&lt;C&gt;") == "A B<C>"
    
    def test_multiple_spaces(self):
        assert clean_text("A    B    C") == "A B C"


class TestParseAmount:
    """金额解析测试套件"""
    
    # 正常情况
    def test_simple_number(self):
        assert parse_amount("100")[0] == Decimal('100')
    
    def test_with_currency_symbol(self):
        assert parse_amount("¥1,234.56")[0] == Decimal('1234.56')
    
    # 边界情况
    def test_zero(self):
        assert parse_amount("0")[0] == Decimal('0')
    
    def test_decimal_precision(self):
        result = parse_amount("99.99")[0]
        assert result == Decimal('99.99')
    
    # 异常情况
    def test_invalid_text(self):
        assert parse_amount("无效金额")[0] is None
    
    def test_chinese_unit(self):
        assert parse_amount("1.5万")[0] == Decimal('15000')
    
    def test_english_unit(self):
        assert parse_amount("2.5k")[0] == Decimal('2500')

# 运行测试
# pytest test_cleaning_comprehensive.py -v

原则4:清洗失败时提供明确的错误信息

当清洗失败时,不要简单地返回None,而是记录为什么失败

python 复制代码
# 改进的清洗函数:返回元组 (结果, 错误信息)
def parse_amount_with_error(text):
    """
    解析金额,返回 (金额, 错误信息)
    
    Returns:
        (Decimal, str): (金额值, 错误信息),成功时错误信息为空字符串
    """
    if not text:
        return None, "输入为空"
    
    if not isinstance(text, str):
        return None, f"输入类型错误: 期望str,实际{type(text)}"
    
    try:
        # ... 清洗逻辑
        return amount, ""
    except ValueError as e:
        return None, f"数值转换失败: {str(e)}"
    except Exception as e:
        return None, f"未知错误: {str(e)}"

# 在管道中使用
class ErrorTrackingPipeline(CleaningPipeline):
    def clean(self, item):
        cleaned = {}
        for field, value in item.items():
            if field in self.rules:
                result, error = self.rules[field](value)
                cleaned[field] = result
                if error:
                    self.errors.append({
                        "field": field,
                        "value": value,
                        "error": error,  # 详细错误信息!
                        "item_url": item.get('url', 'unknown')
                    })
            else:
                cleaned[field] = value
        return cleaned

本讲总结与检查清单

核心知识点回顾

文本清洗四大场景

  1. 基础清洗:去空格、HTML实体、不可见字符
  2. 金额标准化:货币符号识别、单位转换、Decimal精度
  3. 日期解析:多格式支持、相对时间、时区处理
  4. 单位转换:浏览量、文件大小、时长统一

进阶技巧

  1. 清洗日志:记录前后对比,便于审查规则效果
  2. 异常值检测:范围验证、日期验证、长度验证
  3. 配置化:用YAML驱动清洗流程,降低维护成本

最佳实践

  1. 先保证正确再优化性能
  2. 保留原始数据,清洗结果另存
  3. 建立完善的测试用例库
  4. 提供明确的错误信息

验收任务

现在请你完成以下任务,检验本讲学习效果:

任务1:基础清洗工具包

  • 实现clean_textparse_amountparse_dateparse_count四个函数
  • 为每个函数编写至少5个测试用例
  • 测试用例覆盖正常、边界、异常三种情况

任务2:清洗真实数据

  • 准备30条包含各种格式问题的样本数据
  • 使用清洗管道处理这些数据
  • 输出清洗前后对比报告
  • 缺失率、错误率统计

任务3:配置化清洗器

  • 编写一个cleaning_config.yaml配置文件
  • 实现配置加载逻辑
  • 通过修改配置文件切换不同清洗策略
  • 验证配置变更后结果符合预期

常见问题FAQ

Q1:清洗时遇到编码问题怎么办?
python 复制代码
# 统一使用UTF-8编码
def safe_read(file_path):
    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
        return f.read()

# 清洗时移除非法字符
text = text.encode('utf-8', errors='ignore').decode('utf-8')
Q2:如何处理时区问题?
python 复制代码
from datetime import datetime, timezone
import pytz

def parse_date_with_timezone(text, tz='Asia/Shanghai'):
    """解析日期并添加时区信息"""
    dt = parse_date(text)
    if dt and dt.tzinfo is None:
        # 为naive datetime添加时区
        local_tz = pytz.timezone(tz)
        dt = local_tz.localize(dt)
    return dt
Q3:清洗规则冲突怎么办?
python 复制代码
# 使用优先级机制
class PrioritizedCleaningPipeline(CleaningPipeline):
    def add_rule(self, field, cleaner, priority=0):
        """添加带优先级的规则(数值越大优先级越高)"""
        if field not in self.rules:
            self.rules[field] = []
        self.rules[field].append((priority, cleaner))
        # 按优先级排序
        self.rules[field].sort(key=lambda x: x[0], reverse=True)
    
    def clean(self, item):
        cleaned = {}
        for field, value in item.items():
            if field in self.rules:
                # 使用优先级最高的规则
                _, cleaner = self.rules[field][0]
                cleaned[field] = cleaner(value)
            else:
                cleaned[field] = value
        return cleaned

📖 下期预告

恭喜你完成文本清洗这一关键环节!现在你的数据已经干净、标准、可用了。但问题来了:这些宝贵的数据应该存到哪里?怎样存才能保证后续方便查询、不丢失、可追溯?

下一讲《15|数据质量:缺失率、重复率、异常值》,我们将学习:

质量检测

  • 如何自动计算字段缺失率
  • 识别重复数据的多种策略
  • 异常值检测规则设计

质量报告

  • 生成可视化质量报告
  • 设置质量阈值告警
  • 抽样人工复核机制

持续改进

  • 根据质量报告优化清洗规则
  • 建立质量监控体系
  • 数据质量评分模型

在第15讲结束后,你将能够为每次采集生成专业的质量报告,像真正的数据工程师一样保证数据可靠性!

包含:

  • 完整的cleaning工具包
  • 电商数据清洗案例
  • 测试用例套件
  • 配置文件示例

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥


📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
明月_清风7 小时前
FastAPI 从入门到实战:3 分钟构建高性能异步 API
后端·python·fastapi
bellus-7 小时前
ubuntu26测试win10的ollama大模型性能
python
水木流年追梦7 小时前
大模型入门-Reward 奖励模型训练
开发语言·python·算法·leetcode·正则表达式
JavaWeb学起来7 小时前
Python学习教程(六)数据结构List(列表)
数据结构·python·python基础·python教程
devnullcoffee8 小时前
亚马逊Browse Node类目树数据采集实战:从PA-API到分布式爬虫
分布式·爬虫·亚马逊数据采集 api·亚马逊类目树数据·亚马逊 browse node·amazon 数据 api
liuyunshengsir8 小时前
PyTorch 动态量化(Dynamic Quantization)
人工智能·pytorch·python
电子云与长程纠缠8 小时前
UE5制作六边形包裹球体效果
开发语言·python·ue5
DFT计算杂谈8 小时前
KPROJ编译教程
java·前端·python·算法·conda
念恒123068 小时前
Python(循环中断)
开发语言·python
tsfy20039 小时前
Python 处理中文文件名的3个坑(附 Flask 上传解决函数)
开发语言·python·flask·文件上传·中文编码