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 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
喵手2 小时前
Python爬虫零基础入门【第四章:解析与清洗·第1节】BeautifulSoup 入门:从 HTML 提取结构化字段!
爬虫·python·beautifulsoup·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·beautifulsoup入门
应用市场2 小时前
CNN池化层深度解析:从原理到PyTorch实现
人工智能·pytorch·python
小北方城市网2 小时前
微服务接口熔断降级与限流实战:保障系统高可用
java·spring boot·python·rabbitmq·java-rabbitmq·数据库架构
2401_841495642 小时前
【强化学习】DQN 改进算法
人工智能·python·深度学习·强化学习·dqn·double dqn·dueling dqn
幸福清风2 小时前
【Python】实战记录:从零搭建 Django + Vue 全栈应用 —— 用户认证篇
vue.js·python·django
qunaa01012 小时前
基于YOLO11-CSP-EDLAN的软夹持器夹持状态检测方法研究
python
SunnyDays10112 小时前
Python 文本转 PDF 完整指南:从字符串与 TXT 文件到专业 PDF 文档
python·txt转pdf·文本转pdf·文本文件转pdf
C系语言2 小时前
安装Python版本opencv命令
开发语言·python·opencv
FJW0208142 小时前
Python排序算法
python·算法·排序算法