---
title: 文本清洗与规范化
description: 掌握文本预处理的完整流程:停用词过滤、正则表达式高级应用、词干提取、Unicode 规范化与大小写转换。
---
文本清洗与规范化:停用词过滤、正则表达式与词干提取
📂 所属阶段:第一阶段 — 文本预处理(基石篇)
🔗 相关章节:分词技术 · 文本特征工程
1. 为什么需要文本清洗?
我们接触到的非结构化文本,很少是“拿来就能用”的。无论是爬取的网页、社交媒体的帖子,还是客服聊天记录,里面都充斥着各种干扰信息:
原始网页推广文本:
"<p>【年度大促】🔥 手慢无!!!👉 跳转 https://example-discount.com 🎁 仅限新粉哦~</p>"
这些“脏东西”会 分散模型注意力、降低计算效率、干扰最终结果:
- 链接和表情完全不包含业务语义,却占据字符位置;
- HTML 标签只是排版标记,对语义理解毫无帮助;
- 重复的标点和语气词(“!!!”、“哦~哦~”)会引入冗余特征。
理想情况下,我们只保留最核心的词语序列:
["年度", "大促", "手慢", "无", "仅限", "新粉"]
💡 可以用一句话概括文本清洗的目标:把文本“洗干净”,只留下对任务有用的信息,扔掉噪音。
2. 基础清洗“工具箱”
日常大部分清洗操作靠 Python 正则 + 标准库 就能完成,遇到复杂场景再考虑第三方库。
2.1 去除 HTML 标签
简单的标签用正则就能搞定,但如果遇到嵌套结构或残缺标签,更推荐使用 lxml 来解析。
import re
from html import unescape
from lxml import html
def remove_html(text, use_lxml=False):
"""去除 HTML 标签 + 转义字符(如 < → <)"""
# 先统一处理转义字符
text = unescape(text)
if use_lxml:
# lxml 处理嵌套/残缺标签更稳定
try:
return html.fromstring(text).text_content()
except Exception:
pass # 如果解析失败,就回退到正则
# 回退方案:正则直接去掉所有标签
return re.sub(r'<[^>]+>', '', text)
text = "<p>这是一段<strong>嵌套HTML</strong>文本,还有<未闭合标签"
print(remove_html(text, use_lxml=True))
# 输出:这是一段嵌套HTML文本,还有
2.2 清理 URL、邮箱与冗余占位符
在分类、摘要等通用 NLP 任务中,这些信息通常可以直接删除,因为它们携带的语义有限。
def clean_noise(text):
"""组合清理 URL、邮箱、占位数字(可选)"""
# 清理 http/https/www 开头的 URL
text = re.sub(r'https?://\S+|www\.\S+', '', text)
# 清理标准格式邮箱
text = re.sub(r'\S+@\S+\.\S+', '', text)
# 可选:清理占位符类数字(比如工单ID、身份证片段提示)
# text = re.sub(r'\d{4,}', '', text)
return text
text = "工单处理进度可戳 https://xxx.com/ticket ,或发邮件至 service@xxx.com"
print(clean_noise(text))
# 工单处理进度可戳 ,或发邮件至
⚠️ 注意:如果你做的是专门提取链接或邮箱的任务,就不能这样清洗,反而要把它们保留下来。
2.3 全角转半角 + Unicode 规范化
跨平台或输入法带来的字符歧义,比如全角的“HELLO”和半角的“HELLO”应该被视为同一个词,因此需要统一。
import unicodedata
def full_to_half(text):
"""全角英数/空格/符号转半角"""
result = []
for char in text:
code = ord(char)
# 全角空格 → 普通空格
if code == 12288:
code = 32
# 全角字符区间(从!到~)
elif 65281 <= code <= 65374:
code -= 65248
result.append(chr(code))
return "".join(result)
def normalize_text(text):
"""统一字符编码(NFC最常用),还可以组合消除装饰符"""
# NFC:组合字符标准化(比如 "a" + "~" → "ã")
text = unicodedata.normalize("NFC", text)
# 可选:删除非间距装饰符(比如 "ñ" → "n",需谨慎)
# text = "".join([c for c in text if not unicodedata.combining(c)])
return text
text = "HELLO~123,我今年25岁啦"
text = full_to_half(normalize_text(text))
print(text)
# HELLO~123,我今年25岁啦
2.4 去除重复冗余 + 多余空格
社交媒体中常见的“啊啊啊啊啊啊开心”、连续换行和制表符都会造成特征膨胀,需要进行压缩。
def clean_duplicate_and_space(text, max_char_repeat=2):
"""压缩连续重复字符 + 合并多余空白"""
# 压缩重复字符(比如“啊啊啊啊”→“啊啊”)
text = re.sub(r'(.)\1{%d,}' % max_char_repeat, r'\1' * max_char_repeat, text)
# 合并换行/制表符/空格,并去掉首尾空白
text = re.sub(r'\s+', ' ', text).strip()
return text
text = " 啊啊啊啊啊 今天真的!真的!真的!开心到飞起\n\n"
print(clean_duplicate_and_space(text))
# 啊啊 今天真的!真的!开心到飞起
3. 停用词过滤:去掉“语义噪音”
停用词是指高频出现但几乎不携带业务语义的词,比如中文的“的、了、和”,英文的“the、a、is”。
把它们过滤掉,不仅能降低特征维度,还能让模型更关注真正有意义的内容。
3.1 中文停用词基础使用
我们可以先用 jieba 分词,然后根据停用词表筛掉无用的词语。
import jieba
# 基础中文停用词表(可根据业务补充,比如“点击、了解”这种通用推广词)
STOPWORDS_ZH = {
'的', '了', '和', '是', '就', '都', '而', '及', '与', '着',
'或', '一个', '没有', '我们', '你们', '他们', '这个', '那个',
'啊', '呀', '呢', '吧', '吗', '哦', '哈', '嗯', '哎',
'了', '着', '过', '的', '地', '得',
}
def filter_stopwords(tokens, stopwords, min_len=1):
"""过滤停用词 + 过滤过短词(比如单字语气词/标点残留)"""
return [
t for t in tokens
if t not in stopwords and len(t) >= min_len
]
# 测试
text = "我今天在省图书馆认真学习自然语言处理技术"
tokens = jieba.lcut(text)
filtered_tokens = filter_stopwords(tokens, STOPWORDS_ZH, min_len=2)
print(filtered_tokens)
# ['今天', '图书馆', '认真', '学习', '自然语言处理', '技术']
3.2 自定义停用词加载
在真实业务中,我们需要根据场景补充特定停用词。例如,医疗领域可以去掉“患者、您好”,电商领域可以去掉“亲、包邮”。这些词通常被保存在本地文件里。
def load_custom_stopwords(file_path):
"""从本地UTF-8文件加载停用词(一行一个)"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return set(line.strip() for line in f if line.strip())
except FileNotFoundError:
print(f"⚠️ 停用词文件 {file_path} 不存在,使用默认空表")
return set()
# 假设 stopwords_zh_ecommerce.txt 存了“亲、包邮、限时”
custom_stopwords = load_custom_stopwords("stopwords_zh_ecommerce.txt")
🧠 小技巧:停用词表不是一成不变的,建议根据具体任务的 badcase 持续迭代。例如,在情感分析任务中,“非常”、“极其”这类副词实际上很重要,不该轻易删掉。
4. 英文专属:词干提取 vs 词形还原
中文没有复杂的词形变化,但英文单词会变来变去(running、runs、ran),如果当成三个独立词,特征维度就会膨胀。所以需要把它们统一成原形或词干。
4.1 词干提取(Stemming)
词干提取速度很快,但结果不保证是有效的英文单词。适合对词语形态要求不高的任务,比如关键词提取、粗分类。
import nltk
from nltk.stem import PorterStemmer, LancasterStemmer
# 首次使用需下载punkt分词器
nltk.download('punkt', quiet=True)
porter = PorterStemmer() # 温和型,保留更多语义
lancaster = LancasterStemmer() # 激进型,压缩更狠
words = ["running", "runs", "ran", "runner", "easily", "fairly"]
print("Porter词干:", [porter.stem(w) for w in words])
print("Lancaster词干:", [lancaster.stem(w) for w in words])
# 输出
# Porter词干: ['run', 'run', 'ran', 'runner', 'easili', 'fairli']
# Lancaster词干: ['run', 'run', 'run', 'run', 'eas', 'fair']
4.2 词形还原(Lemmatization)
词形还原需要词性标注(POS Tag)才能准确还原,得到的是真正的字典单词。适合翻译、问答等对精度要求高的任务。
from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet
nltk.download('wordnet', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
lemmatizer = WordNetLemmatizer()
def get_wordnet_pos(tag):
"""把nltk的词性标签转为wordnet可用的"""
if tag.startswith('J'):
return wordnet.ADJ
elif tag.startswith('V'):
return wordnet.VERB
elif tag.startswith('N'):
return wordnet.NOUN
elif tag.startswith('R'):
return wordnet.ADV
else:
return wordnet.NOUN # 默认名词
text = "He is running faster than me because he runs every day and ran yesterday"
tokens = nltk.word_tokenize(text)
pos_tags = nltk.pos_tag(tokens)
lemmas = [
lemmatizer.lemmatize(token, get_wordnet_pos(tag))
for token, tag in pos_tags
]
print(lemmas)
# ['He', 'be', 'run', 'faster', 'than', 'me', 'because', 'he', 'run', 'every', 'day', 'and', 'run', 'yesterday']
📌 选择哪个方法?
– 追求速度、特征维度:选词干提取
– 追求准确性、下游任务要求严格:选词形还原
5. 快速搭建预处理管道
把前面的步骤封装成类,既能复用,又方便批量处理不同格式的文本。下面的示例支持中文与英文两种模式。
from functools import reduce
class SimpleTextPreprocessor:
def __init__(self, stopwords=None, lang='zh'):
self.stopwords = stopwords or set()
self.lang = lang
# 中文/英文通用清洗
def _clean_general(self, text):
steps = [
lambda x: remove_html(x, use_lxml=True),
clean_noise,
normalize_text,
lambda x: full_to_half(x) if self.lang == 'zh' else x,
clean_duplicate_and_space,
]
return reduce(lambda t, fn: fn(t), steps, text)
# 中文分词 + 过滤
def _process_zh(self, text):
tokens = jieba.lcut(text)
return filter_stopwords(tokens, self.stopwords, min_len=2)
# 英文分词 + 词形还原(也可选词干)
def _process_en(self, text, use_lemmatizer=True):
tokens = nltk.word_tokenize(text.lower()) # 英文通常统一小写
if use_lemmatizer:
pos_tags = nltk.pos_tag(tokens)
tokens = [
lemmatizer.lemmatize(t, get_wordnet_pos(tag))
for t, tag in pos_tags
]
else:
tokens = [porter.stem(t) for t in tokens]
# 同样可以过滤掉长度过短的词
return [t for t in tokens if len(t) >= 2]
# 主入口
def process(self, text, **kwargs):
cleaned = self._clean_general(text)
if self.lang == 'zh':
return self._process_zh(cleaned)
else:
return self._process_en(cleaned, **kwargs)
# 测试中文
zh_processor = SimpleTextPreprocessor(stopwords=STOPWORDS_ZH, lang='zh')
zh_raw = "<p>🔥🔥🔥 年度大促!!!快来 https://xxx.com 抢!抢!抢!亲,仅限新粉哦~</p>"
print(zh_processor.process(zh_raw))
# 输出:['年度', '大促', '快来', '仅限', '新粉']
✅ 经过管道处理后,脏文本被转换成可以直接喂给模型的干净词列表。
6. 避坑指南:不要“过度清洗”
文本清洗不是越干净越好,很多时候需要根据具体任务“手下留情”:
- 情感分析:感叹号、问号、表情符号(“😠”、“😊”)往往包含强烈的情感倾向,不应删除。
- 命名实体识别(NER):数字、全角字符可能出现在人名、公司名中(如“张三(总经理)”、“ABC株式会社”),不能强行转半角或删除。
- 机器翻译:重复字符和特定标点(如书名号、引号)可能是风格表达的一部分,需要保留。
🧼 清洗的终极原则:以任务需求为准,既能去噪,又不丢掉重要信息。
7. 小结速查
🔗 扩展阅读
✅ 至此,你已掌握了文本清洗的整套流程。下一步就是将干净的数据输入到特征工程或模型中,开启真正的 NLP 之旅!