在政务公告、招标、政策类爬虫场景里,标题清洗是绕不开的一环。很多时候拿到的原始标题看着正常,入库之后导出、检索、分词就各种问题:看不见的零宽空格、"这类HTML引号实体、各式各样的Emoji、全角半角混杂、私有乱码字符,稍有不慎还会把正常标点一起删掉。
最开始我的清洗方式很朴素,链式调用一堆replace,遇到一个脏字符就追加一条替换逻辑。
初始版本长这样:
def clean_title(title_str):
return (str(title_str).replace('\xa0', ' ').replace('\r', '').replace('\n', '').replace('\u2002', ' ').replace(
'\u2003', ' ').replace('\u3000', ' ').replace('\u200b', ' ').replace('\u200d', ' ').replace(
'\u201c', '"').replace('\u201d', '"').replace('\\u201c', '"').replace('\\u201d', '"').replace(
'\u2014', '------').replace('<br />', '').replace('<br/>', '').replace('<b>', '').replace('</b>', '').replace(
'</br>', '').replace('<br>','').replace('<p>', '').replace('</p>', '').replace('<P>', '').replace(
'</P>', '').replace(' ', ' ').replace('<', '<').replace('>', '>').replace('', '')
.replace('', '').replace('🤗','').replace('🥇','').replace('🧐','').strip())
这种写法短期能用,但痛点非常明显:
- HTML实体无穷无尽,
" " ' '不可能一个个手动写替换; - Emoji数量上万,还在持续新增,碰到一个加一个,维护成本爆炸;
- 正则区间写不好极易翻车,我之前踩过一个大坑------错误的Unicode区间直接把《》这类中文书名号一并清除,整条标题只剩一对引号。
问题拆解:标题里常见脏字符分类
结合政策、公文类爬虫的样本,我把脏字符分成几大类:
- 空白类特殊字符 :不间断空格
\xa0、半角空格\u2002、全角空格\u3000、零宽空格\u200b、零宽连接符\u200d,肉眼看不见,但是会导致分词和去重失效; - HTML实体 :
" " < >,网页转义后的引号、空格,是政务网站高频出现的脏数据; - 残留HTML标签 :
<br> <b> <p>等换行、加粗段落标签; - Unicode引号与破折号 :
\u201c \u201d \u2014,部分网页直接输出Unicode转义字符串\u201c; - 私有乱码字符 :网页导出、PDF转文本带来的私有域字符,比如
; - Emoji表情 :爬虫偶尔抓到的表情符号,公文场景不需要,但要注意不能误伤
★ ☆ ✓这类业务标记符号; - 不可见控制字符:C0/C1控制码,一般是网页源码或者附件解析带入的垃圾字符。
这里有一个核心取舍:公文标题里★、☆是合法标记,不能和Emoji一起一刀切清除 。很多网上流传的Emoji正则直接带上\u2700-\u27BF,这个区间包含五角星、对勾,用于资讯、商品标题没问题,但政策清洗场景属于高危区间。
最终稳定版清洗函数
核心优化点:优先使用html.unescape一次性解析全部HTML实体,不再逐个写replace;替换规则用字典维护方便迭代;Emoji正则选用高位平面的安全区块,避开中文和公文常用符号。
import html
import re
def clean_title(title_str):
# 第一步:自动解析所有HTML实体,"、 一次性处理
s = html.unescape(str(title_str))
replace_map = {
# 各类特殊空白
'\xa0': ' ',
'\r': '',
'\n': '',
'\u2002': ' ',
'\u2003': ' ',
'\u3000': ' ',
'\u200b': ' ',
'\u200d': ' ',
# 引号、破折号unicode兼容
'\u201c': '"',
'\u201d': '"',
'\\u201c': '"',
'\\u201d': '"',
'\u2014': '------',
# 残留html标签
'<br />': '',
'<br/>': '',
'<br>': '',
'</br>': '',
'<b>': '',
'</b>': '',
'<p>': '',
'</p>': '',
'<P>': '',
'</P>': '',
# PDF/网页解析带来的私有乱码
'': '',
'': '',
}
for old, new in replace_map.items():
s = s.replace(old, new)
# 安全Emoji正则:只清理高位平面emoji,保留★、☆、✓等公文标记
emoji_pattern = re.compile(
"["
"\U0001F600-\U0001F64F"
"\U0001F300-\U0001F5FF"
"\U0001F680-\U0001F6FF"
"\U0001F1E0-\U0001F1FF"
"\U0001F900-\U0001F9FF"
"\U0001FA00-\U0001FA6F"
"\U0001FA70-\U0001FAFF"
"\ufe0f"
"]+",
flags=re.UNICODE
)
s = emoji_pattern.sub('', s)
# 可选:过滤C0/C1不可见控制字符
ctrl_pattern = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]')
s = ctrl_pattern.sub('', s)
return s.strip()
测试样例:
test1 = '广东省科学技术厅等四部门印发《关于核定"十五五"期间享受支持科普事业发展进口税收优惠政策享惠主体名单的实施办法》的通知'
test2 = '★关于开展2026年度项目申报的通知🤗'
print(clean_title(test1))
print(clean_title(test2))
输出结果:
广东省科学技术厅等四部门印发《关于核定"十五五"期间享受支持科普事业发展进口税收优惠政策享惠主体名单的实施办法》的通知
★关于开展2026年度项目申报的通知
正则区间说明(避坑重点)
\U0001F600-\U0001F64F:基础表情;\U0001F300-\U0001F5FF:图形、物品符号;\U0001F680-\U0001F6FF:交通、地图符号;\U0001F1E0-\U0001F1FF:各国国旗emoji;\U0001F900-\U0001F9FF:补充表情;\U0001FA00-\U0001FAFF:新版扩展emoji;\ufe0f:变体选择符,仅标记字符渲染为emoji,不会删除★本体;- 没有包含
\u2700-\u27BF:这个区块包含五角星、对勾,公文场景保留。如果是电商、短视频标题,确认不需要这类标记,再手动加入该区间。
后续维护思路
- 优先用
html.unescape处理实体,不要手写一堆&xxx;替换; - 稳定的特殊字符、私有乱码放到
replace_map字典,新增脏字符只追加字典,可读性远好于链式replace; - Emoji不推荐全量黑名单,按业务场景选择安全区间;
- 清洗顺序有讲究:先解析HTML实体,再做字符替换,最后正则过滤emoji和控制字符,顺序颠倒容易失效;
- 清洗不等于强过滤:政务类数据要保守,能保留的业务标记尽量保留,避免清洗丢失原始信息。
总结
数据清洗不是简单的删符号,很多时候的坑都来自对Unicode区块不熟悉,网上复制来的万能正则不一定适配你的业务。尤其是政策、公告这类对标题严谨性要求很高的场景,宁可少清一点,也不要误删《》、★这类具备业务含义的符号。这个清洗函数经过政务标题样本验证,兼顾通用性和安全性,可以直接放到爬虫管道中使用。