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

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 上期回顾](#📌 上期回顾)
- [🎯 本讲目标](#🎯 本讲目标)
- 为什么文本清洗如此重要?
- 文本清洗的四大场景
-
- [场景1:基础文本清洗 - 去除噪音字符](#场景1:基础文本清洗 - 去除噪音字符)
- [场景2:金额标准化 - 统一货币格式](#场景2:金额标准化 - 统一货币格式)
- [场景3:日期解析 - 处理多种时间格式](#场景3:日期解析 - 处理多种时间格式)
- [场景4:单位转换 - 统一数值单位](#场景4:单位转换 - 统一数值单位)
- 整合:构建清洗管道
- 完整示例:清洗真实采集数据
- 清洗质量验证
- 进阶技巧:处理复杂清洗场景
- 实战案例:清洗电商数据
- 清洗规则设计的最佳实践
- 本讲总结与检查清单
- [📖 下期预告](#📖 下期预告)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: 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实体 ( → 空格, < → <)
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
代码解析:
- 防御性编程:首先检查输入是否为空或非字符串类型,避免后续处理报错
- HTML实体处理 :
html.unescape()自动处理常见实体( /</&等) - 不可见字符清理 :使用Unicode范围
\u200b等定位零宽字符,这些字符肉眼不可见但会干扰文本处理 - 空白字符规范化:区分横向空白(空格/制表符)和纵向空白(换行),分别处理避免误杀有效换行
- 参数可控:通过布尔参数让调用者选择清洗力度,适应不同场景(如代码块需要保留缩进)
测试用例
python
# test_cleaning.py
def test_clean_text():
"""测试基础文本清洗"""
# 用例1: 多余空白
assert clean_text(" 标题\n\t ") == "标题"
# 用例2: HTML实体
assert clean_text("价格 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
代码解析:
- 货币识别:通过字典映射常见符号到ISO货币代码,便于后续数据库存储
- 单位处理:中文"万千"和英文"k/M"统一转换为乘数,避免精度损失
- 使用Decimal而非float :金融数据必须精确,
Decimal('1.1') + Decimal('2.2') == Decimal('3.3')而1.1 + 2.2 != 3.3(浮点误差) - 正则清理:先移除千分位逗号,再提取纯数字,最后转换
- 双向转换:既能解析输入,也能格式化输出,保证数据流转的一致性
测试用例
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)
代码解析:
- 分层策略:先尝试相对时间(更常见),再尝试格式化字符串(更准确)
- 格式优先级 :从最具体到最宽松,避免
2024-01-15被错误解析为2024年01月15日格式 - 年份补全:当格式缺少年份时(如"1月15日"),自动使用基准日期的年份
- 正则提取 :相对时间中可能混合时分信息(
今天 14:30),需二次提取 - 异常安全 :
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
代码解析:
- 后缀清理:先移除语义词("次"/"播放"等),留下纯数字+单位
- 单位识别:中英文单位统一转换为乘数(万/w/k/m/b)
- 精度处理 :先转
float支持小数(1.5万),再乘乘数,最后转int - 逗号兼容:千分位逗号可能出现在单位转换前,需提前清理
整合:构建清洗管道
设计清洗流程
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]
代码解析:
- 规则注册模式 :通过
add_rule动态添加清洗规则,支持不同项目复用 - 异常容错:清洗失败时记录错误但不中断流程,保证数据流完整性
- 错误追踪 :
errors列表记录所有清洗异常,便于后续排查脏数据来源 - 工厂函数 :
create_news_cleaner封装特定场景配置,新项目只需调整规则
完整示例:清洗真实采集数据
python
# 模拟采集到的脏数据
raw_items = [
{
"title": " 重要新闻 标题 \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}")
代码解析:
- 变化追踪 :通过对比
before和after判断字段是否真正发生变化,避免记录无效清洗 - 可追溯性:每条变化记录包含时间戳和数据项标识,便于定位问题数据源
- 摘要统计:在清洗完成后输出每个字段的变化率,帮助评估清洗规则的影响范围
- 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()
代码解析:
- 验证器模式 :通过抽象基类
ValidationRule定义验证接口,不同验证逻辑实现为独立验证器,符合开闭原则 - 多重验证:一个字段可以添加多个验证器(如标题既要检查长度,又要检查是否包含敏感词)
- 验证与清洗分离:先清洗再验证,确保验证时数据已经格式化,避免验证逻辑处理多种原始格式
- 错误收集:不因验证失败而中断流程,而是收集所有错误便于批量审查和修正规则
技巧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": " 新闻标题 ",
"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()
配置化的优势:
- 零代码切换规则:新增字段或调整验证阈值时,只需修改YAML文件,无需改动Python代码
- 环境隔离:开发/测试/生产环境可使用不同配置文件(如测试环境放宽验证规则)
- 版本管理:配置文件可纳入Git版本控制,清洗规则的变更历史一目了然
- 团队协作:非技术人员也能通过修改配置文件调整清洗策略
实战案例:清洗电商数据
让我们用一个完整的电商爬虫案例,串联本讲所有知识点:
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 ",
"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 ... → 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 B<C>") == "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
本讲总结与检查清单
核心知识点回顾
✅ 文本清洗四大场景:
- 基础清洗:去空格、HTML实体、不可见字符
- 金额标准化:货币符号识别、单位转换、Decimal精度
- 日期解析:多格式支持、相对时间、时区处理
- 单位转换:浏览量、文件大小、时长统一
✅ 进阶技巧:
- 清洗日志:记录前后对比,便于审查规则效果
- 异常值检测:范围验证、日期验证、长度验证
- 配置化:用YAML驱动清洗流程,降低维护成本
✅ 最佳实践:
- 先保证正确再优化性能
- 保留原始数据,清洗结果另存
- 建立完善的测试用例库
- 提供明确的错误信息
验收任务
现在请你完成以下任务,检验本讲学习效果:
任务1:基础清洗工具包
- 实现
clean_text、parse_amount、parse_date、parse_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 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。