---
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 标签 + 转义字符(如 &lt; → <)"""
    # 先统一处理转义字符
    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 词形还原

中文没有复杂的词形变化,但英文单词会变来变去(runningrunsran),如果当成三个独立词,特征维度就会膨胀。所以需要把它们统一成原形或词干。

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. 小结速查

操作常用工具/代码片段
去除HTML标签lxml.html.fromstring(text).text_content()
清理URL/邮箱re.sub(r'https?://\S+|www\.\S+|\S+@\S+\.\S+', '', text)
全角转半角自定义 full_to_half 函数
Unicode规范化unicodedata.normalize("NFC", text)
压缩冗余re.sub(r'(.)\1{2,}', r'\1'*2, text) + re.sub(r'\s+', ' ', text)
中文停用词过滤jieba.lcut(text) + 自定义停用词表
英文词形还原WordNetLemmatizer + POS词性标注

🔗 扩展阅读


✅ 至此,你已掌握了文本清洗的整套流程。下一步就是将干净的数据输入到特征工程或模型中,开启真正的 NLP 之旅!