自然语言处理应用篇 · 第2章

文本预处理:脏数据的清洗流水线

真实语料充满 HTML、表情、错别字和冗余符号。"Garbage in, garbage out"——本章构建一套可复用的中英文本清洗流水线。

🎯 学习目标

  • 建立文本预处理的完整流程意识
  • 会去除 HTML 标签、URL、@提及、表情符号、特殊符号
  • 掌握大小写、全角/半角、Unicode NFKC、繁简等归一化操作
  • 会做中英文句子分割
  • 掌握英文 NLTK 分词、停用词、词干提取、词形还原
  • 掌握中文停用词过滤、去重与长度过滤,写出完整 pipeline 函数

1为什么预处理决定 NLP 上限

NLP 项目中,数据清洗往往占用 60%~80% 的时间。模型再先进,喂进去的是噪声也学不到规律。一条典型的原始评论可能长这样:

"<p>这个手机<a href=...>真的</a>很Nice!!!👍👍  购买链接 https://x.co/abc
@客服小二  价格¥2999(全角数字),发货超快~~~          "

其中对机器无用甚至有害的成分包括:HTML 标签、URL、@提及、emoji、重复标点、全角字符、多余空白……预处理就是把它变成干净文本:

"这个手机真的很nice 价格2999,发货超快"
(再经分词)→ ["这个", "手机", "真的", "很", "nice", "价格", "2999", "发货", "超快"]

2标准预处理流水线

原始文本
 ├─ 1. 基础清洗:去 HTML/URL/@/#话题#/emoji/特殊符号/多余空白
 ├─ 2. 归一化:大小写、全角→半角、繁→简、Unicode 规范化、数字统一
 ├─ 3. 句子分割(sentence segmentation)
 ├─ 4. 分词(tokenization)………… 中文第3章详讲
 ├─ 5. 停用词过滤、(英文)词干化/词形还原
 ├─ 6. 过滤:去重、去空文本、长度过滤
 └─ 干净 token 列表 → 进入向量化/建模

清洗没有万能模板:做情感分析时"!!""太差"这类情绪信号要保留;做实体识别时标点和大小写可能是特征;训练大模型时反而倾向少清洗、保留真实分布。先明确任务,再决定删什么。

3基础清洗:正则实战

import re
from bs4 import BeautifulSoup

raw = '''<p class="x">华为Mate60真的太香了!!!👍</p>
<a href="https://xx.com">点击购买</a> @华为主页 #数码#  联系扣扣123456
客服电话400-800-1234,价格¥2999。'''

# ① 去 HTML 标签:优先用 BeautifulSoup(能顺带取纯文本)
soup = BeautifulSoup(raw, "lxml")
text = soup.get_text(separator=" ")

# 正则兜底去标签(简单场景)
text = re.sub(r"<[^>]+>", "", text)

# ② 去 URL
text = re.sub(r"https?://\S+|www\.\S+", "", text)

# ③ 去 @提及 和 #话题#(话题内容可保留,只去 # 号)
text = re.sub(r"@[\w\-_]+", "", text)
text = re.sub(r"#([^#]+)#", r"\1", text)

# ④ 去 emoji(常见 Unicode emoji 范围)
emoji_pattern = re.compile(
    "[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF"
    "\U0001F680-\U0001F6FF" "\U0001F1E0-\U0001F1FF" "]+",
    flags=re.UNICODE)
text = emoji_pattern.sub("", text)

# ⑤ 合并重复标点、去除多余空白
text = re.sub(r"([,。!?,.!?])\1+", r"\1", text)
text = re.sub(r"\s+", " ", text).strip()

print(text)
# 华为Mate60真的太香了! 点击购买  数码 联系扣扣123456
# 客服电话400-800-1234,价格¥2999。

只保留 / 删除指定字符

# 中文文本:只保留中英文、数字、常见中文标点
text_zh = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()《》\s]", "", text)

# 去掉所有数字(分类任务中数字往往无意义);也可归一化为占位符 <NUM>
text_nonum = re.sub(r"\d+", "<NUM>", text)
# "联系扣扣<NUM> 客服电话<NUM>,价格¥<NUM>"

把数字、邮箱、手机号、URL 替换成 <NUM>、<URL>、<TEL> 等占位符而不是直接删除,既能降噪声又保留"这里曾有某类信息"的结构,在分类、NER 中往往效果更好。

4文本归一化

大小写(英文必须)

"NLP is Great".lower()     # 'nlp is great'
# 让 "Apple"、"apple"、"APPLE" 成为同一个特征
# 例外:专名识别、真实文本生成任务可保留大小写

全角转半角

def full_to_half(text):
    """全角字符(ABC123)转半角(ABC123)。"""
    result = []
    for ch in text:
        code = ord(ch)
        if code == 0x3000:                 # 全角空格
            code = 0x20
        elif 0xFF01 <= code <= 0xFF5E:     # 其他全角字符
            code -= 0xFEE0
        result.append(chr(code))
    return "".join(result)

print(full_to_half("价格¥2999"))   # 价格¥2999

Unicode 规范化(NFKC)

import unicodedata
# 视觉相同但码点不同的字符统一:① 罗马数字、连体字、上下标等
unicodedata.normalize("NFKC", "①②③")      # '123'
unicodedata.normalize("NFKC", "Hello")  # 'Hello'

# 组合字符(é = e + 重音符)归一,避免同一词出现多种码点
unicodedata.normalize("NFC", "café")

繁体转简体

pip install opencc-python-reimplemented
from opencc import OpenCC
cc = OpenCC("t2s")          # traditional → simplified
print(cc.convert("臺灣繁體字很好學"))   # 台湾繁体字很好学
# 反向用 s2t;其他配置:s2tw、tw2sp 等

5句子分割

# 中文:按句末标点切分(保留分句可用 finditer)
def split_sentences_zh(text):
    parts = re.split(r"(?<=[。!?;\n])", text)   # 在标点【之后】切,保留标点
    return [s.strip() for s in parts if s.strip()]

print(split_sentences_zh("你好!我学习Python。你呢?"))
# ['你好!', '我学习Python。', '你呢?']

# 英文:NLTK 的 sent_tokenize 能处理 Mr. Smith 这种缩写陷阱
import nltk
# nltk.download("punkt_tab")
para = "Hello! I am Mr. Smith. What's up?"
print(nltk.sent_tokenize(para))
# ['Hello!', 'I am Mr. Smith.', "What's up?"]

6英文处理四件套(NLTK)

① 分词

from nltk.tokenize import word_tokenize
# nltk.download("punkt_tab")
print(word_tokenize("Don't stop. Can't wait!"))
# ['Do', "n't", 'stop', '.', 'Ca', "n't", 'wait', '!']
# 能正确处理缩写 n't、标点分离

# 简单场景也可以用正则
from nltk.tokenize import regexp_tokenize
regexp_tokenize("I love NLP 3000", pattern=r"\w+")
# ['I','love','NLP','3000']

② 停用词过滤

from nltk.corpus import stopwords
# nltk.download("stopwords")

en_stop = set(stopwords.words("english"))   # 务必转 set,查询 O(1)
text = "this is a very good movie and i love it"
tokens = [w for w in word_tokenize(text.lower())
          if w.isalpha() and w not in en_stop]
print(tokens)
# ['good', 'movie', 'love']  the/is/a/and/i/it 全被过滤

③ 词干提取 Stemming(粗粒度、快)

from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
for w in ["running", "runs", "ran", "studies", "studying", "cars"]:
    print(w, "→", stemmer.stem(w))
# running → run ; runs → run ; studies → studi ; cars → car
# 规则砍后缀,结果有时不是真词(studi),但特征归并目的达到了

④ 词形还原 Lemmatization(细粒度、需词性)

from nltk.stem import WordNetLemmatizer
# nltk.download("wordnet")
lemm = WordNetLemmatizer()
print(lemm.lemmatize("studies", pos="n"))   # study
print(lemm.lemmatize("was", pos="v"))       # be
print(lemm.lemmatize("better", pos="a"))    # good(能识别不规则变化)
# 比 stemming 更准确(输出真实词条),但稍慢,最好传词性
Stemming 词干提取Lemmatization 词形还原
原理规则砍后缀词典 + 词性还原到原形
速度快稍慢
结果可能不是词(studi)一定是真实词条
适用搜索/分类粗粒度归并要求准确的语义处理

7中文停用词

中文停用词:的、了、是、在、也、就、很、一个、我们……它们高频但信息量低。常用停用词表:

  • 哈工大停用词表(约 1000+ 词,最全最常用)
  • 百度停用词表、四川大学机器智能实验室停用词库
  • GitHub 搜索 "stopwords_hit.txt" 即可获得
def load_stopwords(path="stopwords_hit.txt"):
    try:
        with open(path, encoding="utf-8") as f:
            return set(line.strip() for line in f if line.strip())
    except FileNotFoundError:
        # 文件不存在时用内置最小集合兜底,保证程序可跑
        return set("的 了 是 在 和 也 就 都 很 我 你 他 她 它 们 这 那 一个 一些 "
                   "吧 呢 啊 吗 哦 嗯 被 把 让 给 对 从 向 于 以".split())

stopwords_zh = load_stopwords()
print(len(stopwords_zh))

# 结合 jieba 分词后的过滤(jieba 第3章细讲)
import jieba
raw = "这个手机真的是太好用了,我非常喜欢它的外观"
tokens = [w for w in jieba.lcut(raw)
          if w.strip() and w not in stopwords_zh]
print(tokens)
# ['手机', '真的', '太', '好用', '非常', '喜欢', '外观'](停用表不同结果略有差异)

停用词表不是越长越好:情感分析中"不、没、别、无"是否定词,绝不能删("不好"删掉"不"就成"好"了);NER、问答、机器翻译通常不删停用词。按任务定制词表。

8去重与过滤

docs = ["重复文本", "重复文本", "", "  ", "短", "正常长度的评论内容"]

# 去空、去空白、长度过滤(分词后 token 数或原始长度)
min_len, max_len = 5, 500
clean = []
seen = set()
for d in docs:
    d = d.strip()
    if not d or not (min_len <= len(d) <= max_len):
        continue
    if d in seen:          # 精确去重
        continue
    seen.add(d)
    clean.append(d)

# 近似重复(水军刷屏、模板文案)可用 SimHash / MinHash,NLP 进阶内容
print(clean)

9完整可复用清洗器

import re, unicodedata
from bs4 import BeautifulSoup

class TextCleaner:
    def __init__(self, keep_punct=True, normalize_number=False):
        self.keep_punct = keep_punct
        self.normalize_number = normalize_number
        self.emoji_re = re.compile(
            "[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF"
            "\U0001F680-\U0001F6FF" "\U0001F1E0-\U0001F1FF" "]")

    def clean(self, text):
        text = BeautifulSoup(text, "lxml").get_text(" ")  # HTML → 纯文本
        text = unicodedata.normalize("NFKC", text)        # Unicode 归一
        text = re.sub(r"https?://\S+|www\.\S+", " <URL> ", text)
        text = re.sub(r"1[3-9]\d{9}", " <TEL> ", text)
        text = re.sub(r"[\w.+-]+@[\w-]+\.[\w.]+", " <EMAIL> ", text)
        text = re.sub(r"@[\w\-_]+", " ", text)
        text = self.emoji_re.sub(" ", text)
        if self.normalize_number:
            text = re.sub(r"\d+", "<NUM>", text)
        if not self.keep_punct:
            text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s<>]", " ", text)
        text = re.sub(r"\s+", " ", text).strip()
        return text


cleaner = TextCleaner(normalize_number=True)
demo = "<b>加我微信</b>> https://t.cn/x 电话13800000000 😊哈哈!!!"
print(cleaner.clean(demo))
# 加我微信 <URL> 电话<TEL> 哈哈!!!

10本章小结与练习

📌 关键结论

  • 清洗流水线:去噪(HTML/URL/emoji)→ 归一化 → 分句 → 分词 → 停用词 → 过滤。
  • 敏感信息用占位符 <URL>/<TEL>/<NUM> 替换,比硬删更保留结构。
  • 英文:lower + word_tokenize + stopwords + stem/lemmatize;中文:归一化 + jieba + 中文停用词表。
  • 清洗策略服从任务:情感分析保留否定词,NER/翻译保留停用词与大小写。
练习1:为什么做情感分析时不能把"不、没、别"放进停用词表?
这些否定词直接决定情感极性:"不好吃""没兴趣""别买"都是负面,删掉否定词后变成"好吃/兴趣/买",语义甚至极性反转,会严重误导模型。情感分析要保留否定词,必要时还需专门处理否定转移(如把"不+形容词"合并为一个特征)。
练习2:将全角字符串"QQ邮箱:123@qq.com"清洗为半角小写文本。
unicodedata.normalize("NFKC", s) 或自定义全角转半角函数得到 'QQ邮箱:123@qq.com',再对需要的部分 .lower()(中文冒号可按需替换为英文)。NFKC 能一次性处理全角字母数字与常见符号。
练习3:用 NLTK 对英文句子 "The cats were running quickly!" 完成:分词、小写、去停用词、词形还原。
word_tokenize 得 ['The','cats','were','running','quickly','!'];lower 并过滤停用词 the/were 与标点后 ['cats','running','quickly'];WordNetLemmatizer 对 running 传 pos='v' 得 'run',cats 得 'cat',最终 ['cat','run','quickly']。