📝 数据清洗 · 文本 NLP

文本 NLP 数据 · 数据预处理与清洗

真实文本数据充满乱码、广告、重复和符号噪声。本页用一份 12 条脏文本样本,带你走通「剔乱码 → 剔短句 → 去重 → 剔广告 → 清符号 → 标准化分词」六步清洗流水线。

⓪ 脏文本样本演练场

下面这份样本集覆盖了文本清洗中最典型的 12 种问题:红底行 = 需要清洗的脏数据,绿勾 = 正常样本。点击下载按钮可获得同款 CSV,跟着后面章节的代码亲手清洗一遍。

ID 文本内容 标签 检测结果
UTF-8 带 BOM · 文本字段已加引号,pandas 直接 read_csv 即可

① 剔除乱码 / 编码异常

🤔 为什么

「锟斤拷」是最经典的中文乱码——UTF-8 字节流被错误地按 GBK 解码的产物;「�」是 Unicode 替换符 U+FFFD,解码失败的兜底字符。乱码对模型是纯噪声,必须处理。

🛠️ 怎么做

正则快速定位 锟斤拷|� 直接剔除;想抢救数据可用 ftfy 自动修复、chardet 猜测原始编码后重新解码。

⚠️ 常见坑

chardet 对短文本猜不准;ftfy 不是万能的;爬虫数据常「一半 UTF-8 一半 GBK」,要按来源分批处理,别指望一把梭。

# 🔍 乱码检测与修复
import re
import pandas as pd

df = pd.read_csv("text_nlp_dirty.csv")

def has_garbled(text):
    # 锟斤拷:UTF-8 被当 GBK 解码的典型产物
    # �:Unicode 替换符 U+FFFD,解码失败的兜底字符
    return bool(re.search(r'锟斤拷|�', str(text)))

# 方案一:直接剔除(乱码样本通常无法还原,最常用)
df = df[~df['text'].fillna('').apply(has_garbled)]

# 方案二:尝试修复(知道数据来源、想抢救时)
# pip install ftfy chardet
from ftfy import fix_text            # 自动修复常见编码错乱
import chardet
raw = open('page.html', 'rb').read() # 爬虫拿到的原始字节
print(chardet.detect(raw))           # 猜测真实编码 {'encoding': 'GB2312', ...}
print(fix_text('锟斤拷锟斤拷'))        # 尝试还原为正常文本

② 剔除无意义短句

🤔 为什么

「好」「666」「,,,,,,,,,」几乎不带语义,还会拉低语料平均信息密度、干扰词频统计和长度分析。

🛠️ 怎么做

双重判定:总长度阈值 + 去掉标点后的有效长度阈值;纯标点、纯数字、纯空白直接过滤。

⚠️ 常见坑

阈值一刀切会误杀有价值的短评(「差评!」),建议按标签分布调阈值;先 fillna('') 再转字符串,否则 NaN 会变成字符串 'nan' 混进来。

# 📏 空文本 / 无意义短句过滤
import re
import pandas as pd

MIN_LEN = 5   # 去标点后的最小有效长度
df = pd.read_csv("text_nlp_dirty.csv")

# 先显式处理 NaN(别让 astype(str) 把 NaN 变成字符串 'nan')
df['text_clean'] = df['text'].fillna('').astype(str).str.strip()

def is_meaningless(text):
    if text == '' or text.isspace():
        return True                        # 空文本 / 纯空白
    core = re.sub(r'[\W_]+', '', text)     # 去掉标点、空白、下划线
    if 0 < len(core) < MIN_LEN:
        return True                        # 有效字符太少,如 "好"、"不错"
    if core.isdigit():
        return True                        # 纯数字,无文本信息
    if core == '':
        return True                        # 纯标点,如 ",,,,,,,,,,"
    return False

df = df[~df['text_clean'].apply(is_meaningless)]

③ 文本去重(完全重复 + 近重复)

🤔 为什么

重复样本让模型过拟合高频句式;更危险的是同一条文本同时进入训练集和测试集,造成数据泄漏、评估虚高。

🛠️ 怎么做

完全重复一行 drop_duplicates 搞定;近重复(同义改写、洗稿)用 SimHash 指纹 + 汉明距离 ≤ 5 判定。

⚠️ 常见坑

Python 内置 hash() 每次进程结果不同(PYTHONHASHSEED 随机),要可复现请换 md5;阈值 5 适配 64 位指纹,短文本要收得更严。

# 🔁 文本去重:完全重复 + 近重复
import pandas as pd

df = pd.read_csv("text_nlp_dirty.csv")

# ① 完全重复:一行搞定
df = df.drop_duplicates(subset=['text']).reset_index(drop=True)

# ② 近重复:SimHash 纯 Python 实现(不需要 datasketch,轻量可复现)
def simhash(text, n_feat=64):
    # 2-gram shingle(中文用 2-gram 比 3-gram 更稳定)
    shingles = [text[i:i+2] for i in range(max(len(text)-1, 0))]
    if not shingles:
        return 0
    vec = [0] * n_feat
    for s in shingles:
        h = hash(s)                      # 注意:要可复现请换成 int(md5(s.encode()).hexdigest(), 16)
        for i in range(n_feat):
            vec[i] += 1 if h & 1 else -1
            h >>= 1
    fp = 0                               # 把正负向量压成 64 位指纹
    for i, v in enumerate(vec):
        if v > 0:
            fp |= (1 << i)
    return fp

def hamming(a, b):
    return bin(a ^ b).count('1')

hashes = df['text'].fillna('').astype(str).apply(simhash)
drop_idx = set()
for i in range(len(hashes)):
    for j in range(i+1, len(hashes)):
        if hamming(hashes.iloc[i], hashes.iloc[j]) <= 5:   # 汉明距离 ≤ 5 → 判近重复
            drop_idx.add(j)              # 保留先出现的那条
df = df.drop(index=list(drop_idx)).reset_index(drop=True)

④ 剔除广告 / 水印内容

🤔 为什么

「下载APP送红包」「关注公众号」这类内容与标注任务无关,是典型脏数据;手机号、微信号等 PII 信息还涉及隐私合规问题。

🛠️ 怎么做

URL 正则 + 联系方式正则 + 关键词黑名单三管齐下;PII 建议打码为 [PHONE] 而不是整句删除——句子的其余部分往往仍有价值。

⚠️ 常见坑

黑名单会误伤正常评论(「这双鞋用优惠券后很值」);正则 \S+ 会把 URL 后面紧跟的中文也吞掉,必要时加边界。

# 🚫 广告 / 水印 / PII 过滤
import re
import pandas as pd

df = pd.read_csv("text_nlp_dirty.csv")

URL_RE   = re.compile(r'https?://\S+|www\.\S+')
PHONE_RE = re.compile(r'1[3-9]\d{9}')                 # 手机号
BLACKLIST = ['下载APP', '加微信', '关注公众号', '点击链接', '兼职']

def is_spam(text):
    text = str(text)
    if URL_RE.search(text):
        return True                        # 含链接 → 广告
    if any(w in text for w in BLACKLIST):
        return True                        # 命中广告关键词
    return False

df = df[~df['text'].fillna('').apply(is_spam)]

# 隐私合规:手机号打码而非整句删除(保住句子的其余信息)
df['text'] = df['text'].fillna('').str.replace(PHONE_RE, '[PHONE]', regex=True)

⑤ 清理特殊符号冗余

🤔 为什么

「!!!!」和「!」语义相同却多占 3 个 token;连续空格、制表符、成串 emoji 都会制造稀疏特征,白白消耗词表和算力。

🛠️ 怎么做

反向引用 ([!!??])\1+ 把连续标点归并为一个;emoji 二选一——直接删除,或用 emoji 库 demojize 成 :thumbs_up: 保留语义;\s+ 压缩多余空白。

⚠️ 常见坑

别把省略号「……」也归并掉(它是合法标点,归并规则要排除);demojize 的语言选 en,中文描述词库不全。

# ✂️ 特殊符号冗余清理
import re
import pandas as pd

df = pd.read_csv("text_nlp_dirty.csv")

def clean_symbols(text):
    text = str(text)
    # ① 连续标点归并:!!!! → !,??? → ?(注意排除省略号 ……)
    text = re.sub(r'([!!??。,,])\1+', r'\1', text)
    # ② emoji 处理(二选一)
    text = re.sub(r'[\U0001F300-\U0001FAFF☀-➿]', '', text)      # 直接删除
    # import emoji
    # text = emoji.demojize(text, language='en')                  # 或转成 :thumbs_up:
    # ③ 多余空白 / 制表符压缩为一个空格
    text = re.sub(r'\s+', ' ', text).strip()
    return text

df['text'] = df['text'].fillna('').apply(clean_symbols)

⑥ 文本标准化与分词

🤔 为什么

全角「ABC」和半角「ABC」在模型眼里是两个词;大小写、繁简不统一会把同一个词撕成多个词表项,浪费 embedding。

🛠️ 怎么做

全角→半角(码点偏移 0xFEE0)、英文统一小写、繁体转简体(提一句 OpenCC,pip install opencc),最后 jieba 分词 + 去停用词。

⚠️ 常见坑

全角空格   不在 0xFF01–0xFF5E 区间,要单独替换;大小写统一对情感任务可能丢信息(「OMG」与「omg」语气不同),按任务取舍。

# 🔧 文本标准化与 jieba 分词
import pandas as pd
import jieba

df = pd.read_csv("text_nlp_dirty.csv")

def normalize(text):
    text = str(text)
    # ① 全角 → 半角(字母、数字、常用符号:码点偏移 0xFEE0)
    text = ''.join(chr(ord(c) - 0xFEE0) if 0xFF01 <= ord(c) <= 0xFF5E else c for c in text)
    text = text.replace(' ', ' ')     # 全角空格单独处理
    # ② 英文统一小写(中文不受影响)
    text = text.lower()
    # ③ 繁体 → 简体(可选):pip install opencc
    # from opencc import OpenCC
    # text = OpenCC('t2s').convert(text)
    return text

STOPWORDS = {'的', '了', '是', '我', '也', '就', '都', '很', '还'}

def tokenize(text):
    words = jieba.lcut(normalize(text))                 # jieba 分词
    return [w for w in words if w.strip() and w not in STOPWORDS]  # 去停用词

df['tokens'] = df['text'].fillna('').apply(tokenize)
print(df['tokens'].head())

⑦ 亮点 demo:六步清洗流水线可视化

下面预置了一条「集大成」的脏文本:乱码 + 广告 URL + emoji + 连续感叹号 + 多余空格。点击执行后,流水线会逐步展示每一步剔除了什么(红色删除线 = 剔除、绿色高亮 = 替换结果),右侧柱状图同步显示剩余字符数的递减过程。

🧹 逐步清洗过程
点击「▶ 执行清洗」开始演示……
📉 每步剩余字符数

⑧ 完整 pipeline:端到端清洗

把前面六步串成一条可复用的流水线:顺序有讲究——先剔广告再清符号(广告正则依赖原始文本),先完全重复再 SimHash 近重复(省掉大量两两比较)。每步输出剩余样本数,就是一份现成的清洗报告。

# 🧹 端到端文本清洗 Pipeline(读 CSV → 六步 → 输出干净数据集 + 清洗报告)
import pandas as pd

# ---------- 各步骤函数前文已定义,此处直接复用 ----------
# has_garbled / is_meaningless / simhash / hamming / is_spam / clean_symbols / normalize / tokenize

def run_pipeline(input_csv, output_csv):
    report = {}
    df = pd.read_csv(input_csv)
    report['原始样本数'] = len(df)

    # ① 剔除乱码 / 编码异常
    df = df[~df['text'].fillna('').apply(has_garbled)]
    report['① 剔乱码后'] = len(df)

    # ② 剔除无意义短句 / 纯符号 / 纯数字
    df = df[~df['text'].fillna('').astype(str).str.strip().apply(is_meaningless)]
    report['② 剔短句后'] = len(df)

    # ③ 剔除广告 / 水印,手机号统一打码(PII 合规)
    df = df[~df['text'].fillna('').apply(is_spam)]
    df['text'] = df['text'].fillna('').str.replace(r'1[3-9]\d{9}', '[PHONE]', regex=True)
    report['③ 剔广告后'] = len(df)

    # ④ 清理特殊符号冗余(连续标点归并 + emoji + 空白压缩)
    df['text'] = df['text'].apply(clean_symbols)
    report['④ 清符号后'] = len(df)

    # ⑤ 去重:先完全重复(O(n)),再 SimHash 近重复(O(n²),放后面省算力)
    df = df.drop_duplicates(subset=['text'])
    hashes = df['text'].apply(simhash)
    keep, seen = [], []
    for idx, h in hashes.items():
        if all(hamming(h, s) > 5 for s in seen):   # 与所有已保留样本都不近似才保留
            keep.append(idx)
            seen.append(h)
    df = df.loc[keep].reset_index(drop=True)
    report['⑤ 去重后'] = len(df)

    # ⑥ 标准化 + jieba 分词 + 去停用词
    df['tokens'] = df['text'].apply(tokenize)
    report['⑥ 最终样本数'] = len(df)

    # 输出干净数据集(utf-8-sig 让 Excel 打开不乱码)
    df.to_csv(output_csv, index=False, encoding='utf-8-sig')

    # 打印清洗报告
    print('====== 清洗报告 ======')
    for k, v in report.items():
        print(f'{k}: {v}')
    return df

clean_df = run_pipeline('text_nlp_dirty.csv', 'text_nlp_clean.csv')