数据预处理(一):文本清洗、去除噪声、统一编码
好,咱们正式开始进入数据预处理阶段。这一步,说白了就是把“脏数据”洗成模型能读懂的样子。
我记得刚做 NLP 项目那会,接过一批用户评论数据。一看,满屏的 HTML 标签、特殊符号、乱码……当时我就意识到,数据清洗不是可选的,是必须的。
这一章,我挑三个最核心的预处理动作来讲:文本清洗、去除噪声、统一编码。这三个动作做好了,后面模型训练的很多坑,你根本不用踩。
文本清洗:从全角半角到简繁转换
先说文本清洗。很多人一上来就是正则表达式哗哗一顿删,结果删错了。
我习惯按顺序来:全角转半角 → 大小写统一 → 繁体转简体 → 按需去除数字和标点。
全角与半角 :英文、数字、基本标点统一用半角。全角字母(例如 'A')要转成半角 'A'。这一步不做,模型会认为 'hello' 和 'hello' 是两个词。
举个例子,Python 里可以用 str.maketrans 做映射,也可以直接写个函数遍历字符。
import unicodedata
def fullwidth_to_halfwidth(s: str) -> str:
result = []
for ch in s:
if unicodedata.east_asian_width(ch) == 'F':
# 全角字符偏移量为 65248
result.append(chr(ord(ch) - 65248))
else:
result.append(ch)
return ''.join(result)
text = "hello,world!"
print(fullwidth_to_halfwidth(text))
# 输出:hello,world!
大小写统一没什么好说的,转成小写就行。为什么?因为敏感词列表里通常是小写。你想想看,'LOAN' 和 'loan' 在语义上没有区别,统一后模型就不必学两套模式了。
繁体转简体,我个人常用 zhconv 或 OpenCC。这里注意一个坑:一简对多繁。例如「干」在繁体中可能是「幹」或「乾」,反向转换时得用词级别判断。但如果是正向(繁→简),基本是安全的。
from zhconv import convert
def trad_to_simp(s: str) -> str:
return convert(s, 'zh-cn')
text = "數據預處理很重要"
print(trad_to_simp(text))
# 输出:数据预处理很重要
去除噪声:该删的不该删的,心里要有数
噪声包括:HTML 标签、URL、邮箱、@提及、特殊符号、控制字符、冗余空格……
| 噪声类型 | 示例 | 处理方案 |
|---|---|---|
| HTML 标签 | <div>...</div> |
BeautifulSoup(text).get_text() |
| URL | https://example.com/path |
正则 r'https?://\S+' 移除 |
| 邮箱 | user@domain.com |
正则 r'\S+@\S+\.\S+' 移除 |
| 特殊符号 | ★ ♥ ➜ ❤ |
保留标点,其余 Unicode 符号过滤 |
| 不可见字符 | \x00 \t \r |
str.strip() + unicodedata.category 过滤 |
有一次,我从爬虫拿到的数据里混了 \xa0(不换行空格),肉眼完全看不出来。模型训练后精确率低了 5%。查了半天才发现是这些隐式字符在作怪。嗯,这里要注意——推荐用 unicodedata.category 扫一遍,直接扔掉 'Cc' 和 'Cf' 类别的字符。
一个小技巧:正则匹配前,先把 \n \t 替换成空格,再统一 re.sub(r'\s+', ' ', text)。分段拼接后,很多正则表达式会简单很多。
统一编码:从乱码到 UTF-8
说到编码,我真是又爱又恨。我遇到过一份来自银行的数据,源文件是 GB2312,接口又说是 UTF-8,结果读到 Python 里全是 锟斤拷……
重要:获取任何数据源时,必须明确原始编码。常见的数据源编码包括:UTF-8、GBK、GB2312、ISO-8859-1、Shift-JIS 等。
我的处理流程很简单:
- 检测编码:用
chardet或cchardet先检测。虽然不 100% 准确,但足够当作第一猜测。 - 解码:用检测到的编码解码,
bytes→str。 - 统一转成 UTF-8:内部处理一律用 UTF-8,写入文件时也固定为 UTF-8(不带 BOM)。
- 显式处理异常:用
errors='replace'或'ignore'兜底,但我会记录日志,方便回查。
import chardet
def detect_and_decode(raw_bytes: bytes) -> str:
result = chardet.detect(raw_bytes)
encoding = result['encoding'] or 'utf-8'
confidence = result['confidence']
print(f"检测到编码: {encoding}, 置信度: {confidence:.2%}")
try:
text = raw_bytes.decode(encoding, errors='replace')
except LookupError:
# 遇到不认识的编码,回退到 utf-8
text = raw_bytes.decode('utf-8', errors='replace')
return text
# 使用场景
with open('user_comment.dat', 'rb') as f:
content = f.read()
clean_text = detect_and_decode(content)
print(clean_text[:100])
个人习惯:所有数据清洗步骤写成一个 pipeline 函数,传入原始字符串,返回清洗后的字符串。每步做原子操作,方便调试和复用。举个例子:
def clean_pipeline(text: str) -> str:
# 1. 全角转半角
text = fullwidth_to_halfwidth(text)
# 2. 统一小写
text = text.lower()
# 3. 繁体转简体
text = trad_to_simp(text)
# 4. 去除 HTML 标签
text = BeautifulSoup(text, 'html.parser').get_text()
# 5. 去除 URL
text = re.sub(r'https?://\S+', '', text)
# 6. 去除邮箱
text = re.sub(r'\S+@\S+\.\S+', '', text)
# 7. 去除杂噪符号(保留汉字、字母、数字、基本标点)
text = re.sub(r'[^\w\s\u4e00-\u9fff,。!?、]', '', text)
# 8. 合并连续空白
text = re.sub(r'\s+', ' ', text).strip()
return text
我曾经维护过一个旧项目,洗数据的逻辑散落在 5 个不同的脚本里,互相覆盖。后来花了两个下午全部重构成 pipeline。嗯,重构后的代码不仅运行快了一倍,排查问题也轻松多了。
一点补充:不要过度清洗
有两点我想提一下。
第一,保留必要的标点。像句号、问号、叹号、逗号,对句子边界判别很有用。如果你的模型是序列标注模型(比如 BiLSTM-CRF),这些标点可以作为天然的分段特征。
第二,数字不一定都要删。敏感电话号码、身份证号里的数字是模式,但是普通数字(如“100 元”)中的 100 可能保留更好。我的做法是:先把疑似敏感数据(连续 8 位以上数字)替换成占位符 [NUM],再清掉其他杂散数字。
一个原则:清洗标准要跟你的敏感数据定义对齐。如果你要识别银行卡号,那保留数字连续片段才有意义;如果你要识别人名,数字全删了也无妨。
好,数据预处理的第一个阶段就先聊到这儿。文本清洗、去除噪声、统一编码这三板斧,配合成一个 pipeline,就能解决 80% 的脏数据问题。下一阶段我们还会深入分词、去停用词等动作,但那些都是建立在这套干净数据之上的事了。