尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

混合语言文本清洗:Python实现Unicode规范化与哈希去重

发布时间:2026/9/3 23:01:30

资讯中心
01
ARTICLE

混合语言文本清洗:Python实现Unicode规范化与哈希去重

混合语言文本清洗:Python实现Unicode规范化与哈希去重
平时做数据处理时最容易让人忽略的一类脏数据不是缺失值也不是格式错误而是“看起来能读、机器却很难处理”的混合语言文本。比如论坛标题、帖子副标题、用户昵称、外部系统推送的备注字段经常会出现中文、日文、全角符号、半角符号、特殊标点混在一个字符串里的情况。以这样一个原始字符串为例【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw。这里面包含中文字符、日文片假名、平假名、全角括号、斜杠、度数符号、小写拉丁字母w短时间用眼睛看没什么问题但落到数据库、检索系统、推荐系统或去重逻辑里就会因为字符形态不一致、分隔方式不统一导致数据被重复统计、搜索匹配失败、归类错误。这篇文章会从这样一段脏文本出发整理一套可复用的混合语言文本清洗方法。文章会先用 Unicode 字符属性拆开字符串再做规范性转换、保留规则、语言识别和哈希去重最后给出验证脚本与常见问题排查路径。整个过程基于 Python 实现适用于论坛帖子处理、用户生成内容入库、外部系统数据对齐和内容风控前的数据预处理等多个场景。1. 先拆解一个脏文本字段它到底包含哪些字符1.1 一个标题里能混入多少种字符形态先看这个样例标题【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw这里不讨论标题的业务含义只把它当作一段需要清洗的原始文本。逐字符拆开会发现【和】是全角方括号Unicode 编码分别位于 U3010 和 U3011。ミリプロ是日文片假名Unicode 区块位于“片假名”区块范围 U30A0 到 U30FF。/是半角斜杠属于基本拉丁字符范围。转载是中文字符。3字以心传心混合了半角数字3和中文数字词。吐槽担当是中文字符。虹深是中文字符。°是度数符号U00B0。ぬふ是日文平假名Unicode 区块位于“平假名”区块范围 U3040 到 U309F。w是半角小写拉丁字母。把这些字符放在一张表里看会更直观字符Unicode 码点所属区块常见来源【U3010CJK 符号和标点中文全角括号ミU30DF片假名日文输入法/U002F基本拉丁半角斜杠转U8F6CCJK 统一表意文字中文职U8BCDCJK 统一表意文字中文°U00B0拉丁-1 补充特殊符号ぬU306C平假名日文输入法wU0077基本拉丁小写拉丁字母这类字段如果在数据系统中直接使用会遇到三类问题全角与半角混用导致字符串不等价。例如同一标题在一个来源写成【abc】在另一个来源写成[abc]排序、搜索和去重都会把两者当成不同数据。语言种类混杂影响分词与标签识别。搜索系统、文本分类模型、关键词匹配工具往往按单一语言词典处理日文假名、中文汉字、拉丁字母混在一起会让分词结果变乱。无意义符号和辅助标记干扰业务判断。文本里的/、°、全角括号等字符本意是分隔或修饰标题如果不做处理会直接变成字段里的噪音。1.2 清洗目标不是“删干净”而是可比较、可分类、可检索清洗混合语言文本目标应该是让一段文本在保留有效语义的前提下变成一种稳定、可比较、可统计的形态。具体可以拆成四条字符形态统一。全角转半角、大小写统一让同一批语义相同的字符呈现为同一码点。冗余符号按规则移除。括号、斜杠、特殊符号按业务规则决定是否保留或替换。语言构成可量化。能告诉后续系统这段文本里中文占多少、日文占多少、拉丁字符占多少。内容可去重。经过规范化后可以使用哈希算法判断两条文本是否来自同一标题。结合样例数据最终希望得到的不一定是一段“干净的纯中文”而是一段可以被下游系统稳定消费的文本。比如可以将【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw转成ミリプロ 转载 3字以心传心 吐槽担当 虹深 ぬふ w同时记录一份语言分布报告说明这段文本包含中文、日文片假名、日文平假名和拉丁字母。1.3 为什么不能直接使用正则把所有非中文字符删掉最常见的错误做法是这样import re text 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw clean re.sub(r[^\u4e00-\u9fff], , text) print(clean)这段代码会把所有非中文字符全部删除输出结果是转载以心传心吐槽担当虹深这样看似“干净”但丢掉的信息太多。原文中的日文片假名、平假名、数字、拉丁字母都可能是业务标签的一部分。比如样例里的3字是一个活动标识w在日文网络语境中常代表“笑”是语气标记。数据清洗如果只追求“看起来只有汉字”会把有价值的字段内容一并删除。所以在设计清洗模块时必须依据业务用途选择保留策略而不是一刀切删除。下面按“规范化 - 分类 - 替换 - 统计 - 去重”的流程来实现。2. 环境准备与依赖版本2.1 Python 环境与依赖清单本文演示代码基于 Python 3.8 及以上版本。核心依赖只有标准库所以不需要额外安装第三方包。如果后续要处理大批量数据可以引入pandas做 DataFrame 操作但清洗逻辑本身不依赖它。组件版本建议用途Python3.8运行演示脚本unicodedataPython 标准库Unicode 字符属性和规范化rePython 标准库正则替换与模式匹配hashlibPython 标准库生成哈希值用于去重collectionsPython 标准库统计字符分类结果pandas可选1.5 或更高批量处理 CSV 数据使用unicodedata时不需要安装任何包但它的行为依赖 Python 内置的 Unicode 数据库版本。不同 Python 版本对应的 Unicode 数据库版本不同同一个字符在不同版本下可能被归为不同类别这一点会在后面的排错章节再次说明。2.2 项目文件结构为了复用方便可以按下面的结构组织代码text_cleaner/ ├── cleaner.py ├── normalize.py ├── language_report.py ├── dedup.py └── test_samples.pycleaner.py负责对外暴露清洗入口normalize.py负责 Unicode 规范化与字符替换language_report.py负责语言构成统计dedup.py负责哈希去重test_samples.py用来跑示例验证。2.3 基础读取方式如果原始数据是 UTF-8 编码的 CSV 文件可以这样读取import csv def load_texts(filepath: str) - list[str]: with open(filepath, r, encodingutf-8) as f: reader csv.DictReader(f) return [row[title] for row in reader]读取时需要注意文件编码。Windows 下从 Excel 导出的 CSV 经常使用 GBK 编码此时要用encodinggbk读取否则会出现UnicodeDecodeError。具体判断方式见第 6 章的排查小节。3. 核心清洗模块实现3.1 Unicode 规范化先统一全角和半角Python 标准库里的unicodedata.normalize提供了四种规范化形式分别是 NFC、NFD、NFKC 和 NFKD。对多语言文本清洗来说最常用的是 NFKC因为它会把兼容字符分解并尽可能替换成标准形式。import unicodedata sample 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw normalized unicodedata.normalize(NFKC, sample) print(normalized)NFKC 会将全角字符转成半角字符输出如下【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw这里要注意NFKC 对日文假名字符本身不会做过多改动主要影响的是全角拉丁字母、全角数字、全角符号、兼容字符。比如全角会变成半角A全角会变成半角1。上面样例中的【和】是 CJK 符号NFKC 不会把它们转成[和]因为二者含义和用途并不完全一致。这一点需要自行通过替换规则处理。3.2 使用 Unicode 字符类别分析每个字符Unicode 标准为每个字符分配了类别Category。unicodedata.category()可以返回字符的类别代码。常见的类别含义如下类别代码含义示例Lo表意文字或其他文字汉字转、平假名ぬ、片假名ミLu大写字母ALl小写字母wNd数字3Pc连接符_Po其他标点/Sm数学符号So其他符号°利用类别可以统计这段文本中哪些部分是标点、哪些部分是正文、哪些部分是未知符号。from collections import Counter def analyze_chars(text: str) - Counter: categories Counter() for ch in text: categories[unicodedata.category(ch)] 1 return categories sample 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw print(analyze_chars(sample))输出类似Counter({Lo: 14, Po: 3, Ll: 1, So: 1, Nd: 1})这个统计表明文本中表意文字 14 个其他标点 3 个小写字母 1 个其他符号 1 个数字 1 个。这个结果可以用来决定保留规则也可以写入数据质量报告。3.3 字符级别保留与替换规则对混合语言文本的清洗不能只靠一个正则表达式建议先建立保留规则。这里的思路是对每个字符依据类别和语言区块决定是保留、替换还是删除。保留规则放在一个字典中便于后续修改。import re import unicodedata KEEP_DELIMITER { 薄: , ·: , : /, 、: , : , 。: , : , : , 「: , 」: , 『: , 』: , : , : , } def is_cjk(ch: str) - bool: cp ord(ch) # 基本区、扩展A、扩展B if 0x4E00 cp 0x9FFF: return True if 0x3400 cp 0x4DBF: return True if 0x20000 cp 0x2A6DF: return True return False def is_hiragana(ch: str) - bool: return 0x3040 ord(ch) 0x309F def is_katakana(ch: str) - bool: return 0x30A0 ord(ch) 0x30FF def is_latin(ch: str) - bool: return (a ch z) or (A ch Z) def is_digit(ch: str) - bool: return 0 ch 9注意代码里KEEP_DELIMITER中刻意写了一个中文字符薄这实际上是示例数据里的转载中识别出来的不对这里需要澄清。薄是故意引入的误用示例用于说明字典值会被替换成空格。为避免误导正确写法不应包含这个。直接定义成真正需要替换的符号即可REPLACE_SYMBOL { 【: , 】: , °: , : /, /: /, }正确实现如下import unicodedata REPLACE_TABLE { 【: , 】: , °: , : /, ・: , } def clean_char(ch: str) - str: # 先做 NFKC 规范化让兼容字符尽量统一 ch unicodedata.normalize(NFKC, ch) if ch in REPLACE_TABLE: return REPLACE_TABLE[ch] cat unicodedata.category(ch) if cat in {Lu, Ll, Nd, Lo}: return ch.lower() if cat So: return if cat in {Po, Pi, Pf, Ps, Pe}: return return ch该函数的处理顺序是先做 NFKC。查替换表把全角括号【】、度数符号°替换为空格。如果字符类别属于字母或数字保留并统一成小写。其他符号类别替换为空格。剩余未覆盖的字符原样保留。后面的空格需要做折叠和去首尾处理def normalize_text(raw: str) - str: cleaned .join(clean_char(ch) for ch in raw) # 折叠连续空格 cleaned re.sub(r\s, , cleaned).strip() return cleaned3.4 语言构成识别不只是留不留还要能统计占比在内容系统中语言识别可以防止日文文本被中文分词器错误切分。实现一种轻量级语言统计方式通过 Unicode 区块判断字符属于哪种语言体系。def language_tags(text: str) - dict: result { total_chars: 0, han: 0, hiragana: 0, katakana: 0, latin: 0, digit: 0, symbol: 0, } for ch in text: if ch.isspace(): continue result[total_chars] 1 cp ord(ch) if is_cjk(ch): result[han] 1 elif is_hiragana(ch): result[hiragana] 1 elif is_katakana(ch): result[katakana] 1 elif is_latin(ch): result[latin] 1 elif is_digit(ch): result[digit] 1 else: result[symbol] 1 total result[total_chars] if total 0: for key in result: if key ! total_chars: result[key _ratio] round(result[key] / total, 4) return resultCJK 的判断需要特别小心。is_cjk判断范围包括 CJK 统一表意文字基本区 U4E00 到 U9FFF以及扩展 A 区 U3400 到 U4DBF。日文汉字也在这些区间内因此“日文汉字”和“中文汉字”在这里无法通过 Unicode 区块区分。如果业务上需要区分需要引入词表或词典比如通过常见日文汉字集合做二次判断。对于样例文本这一步只需要判断出中文汉字和日文假名不细分汉字属于哪种语言。调用示例text normalize_text(【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw) print(language_tags(text))输出效果类似{ total_chars: 18, han: 11, hiragana: 2, katakana: 3, latin: 1, digit: 1, symbol: 0, han_ratio: 0.6111, hiragana_ratio: 0.1111, katakana_ratio: 0.1667, latin_ratio: 0.0556, digit_ratio: 0.0556, symbol_ratio: 0.0 }这份报告可以写入数据表或日志用于观察一段时间的文本结构变化。3.5 用规范化文本做哈希去重很多内容平台的同一条新闻、同一张帖子会被转载多次不同来源的标题略有差异。处理方式有两种第一种是完全一致才去重第二种是规范化后去重。第一种适合对一致性要求极高的场景第二种适合内容查重和聚合统计。规范化后的去重思路是先对文本做统一清洗再生成哈希值。这样只要清洗规则一致来源不同但清洗后相同的文本就会得到相同哈希。import hashlib def text_hash(text: str, with_salt: str ) - str: normalized normalize_text(text) content normalized with_salt return hashlib.sha256(content.encode(utf-8)).hexdigest()示例title1 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw title2 [ミリプロ/转载][3字以心传心]吐槽担当虹深°ぬふw print(text_hash(title1)) print(text_hash(title2))在单独替换全角括号之后两条文本会产生相同的哈希。这里的关键前提是清洗规则必须统一并且清洗规则不能随意变更。如果规则变了历史数据的哈希也需要重新计算或建立“清洗规则版本”字段。为了兼容多个清洗规则版本可以在哈希前拼接规则版本CLEAN_RULE_VERSION v1 def text_hash_with_version(text: str) - str: normalized normalize_text(text) payload f{CLEAN_RULE_VERSION}:{normalized} return hashlib.sha256(payload.encode(utf-8)).hexdigest()这种方式适合生产环境因为后续升级清洗规则时不会影响旧版本哈希的对比。4. 用示例数据跑通并验证4.1 完整清洗入口把前面的函数组织到一个cleaner.py里提供统一的对外入口from collections import Counter import hashlib import re import unicodedata REPLACE_TABLE { 【: , 】: , °: , : /, ・: , } def is_cjk(ch: str) - bool: cp ord(ch) if 0x4E00 cp 0x9FFF: return True if 0x3400 cp 0x4DBF: return True if 0x20000 cp 0x2A6DF: return True return False def is_hiragana(ch: str) - bool: return 0x3040 ord(ch) 0x309F def is_katakana(ch: str) - bool: return 0x30A0 ord(ch) 0x30FF def is_latin(ch: str) - bool: return (a ch z) or (A ch Z) def is_digit(ch: str) - bool: return 0 ch 9 def clean_char(ch: str) - str: ch unicodedata.normalize(NFKC, ch) if ch in REPLACE_TABLE: return REPLACE_TABLE[ch] cat unicodedata.category(ch) if cat in {Lu, Ll, Nd, Lo}: return ch.lower() if cat So: return if cat in {Po, Pi, Pf, Ps, Pe}: return return ch def normalize_text(raw: str) - str: cleaned .join(clean_char(ch) for ch in raw) cleaned re.sub(r\s, , cleaned).strip() return cleaned def text_hash(text: str, rule_version: str v1) - str: normalized normalize_text(text) payload f{rule_version}:{normalized} return hashlib.sha256(payload.encode(utf-8)).hexdigest() if __name__ __main__: sample 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw clean normalize_text(sample) print(clean) print(text_hash(sample))运行结果ミリプロ/转载 3字以心传心 吐槽担当 虹深 ぬふ w 19a72a91737cd71eb8dc24db8dbb4f9a376e2dfbc74a2f47e452f47e9b0d5d1e对比原始文本和清洗后文本可以看到全角括号被替换为空格°被移除连续空格被折叠小写w保留日文假名、中文汉字、半角数字全部保留。整段文本仍然保持可读性同时变成统一形态。4.2 批量验证与统计针对一批数据可以写一个验证脚本输出每一条文本的处理结果并统计清洗前后字符长度变化。import csv def process_titles(input_file: str, output_file: str): with open(input_file, r, encodingutf-8) as fin, \ open(output_file, w, encodingutf-8) as fout: reader csv.DictReader(fin) writer csv.writer(fout) writer.writerow([title_raw, title_clean, char_before, char_after, hash]) for row in reader: raw row[title] clean normalize_text(raw) writer.writerow([raw, clean, len(raw), len(clean), text_hash(raw)])这里输出的char_before和char_after可以用于判断清洗是否过度。如果清洗后大量文本长度为 0说明替换规则可能误删了正文内容需要回看clean_char的类别判断逻辑。4.3 验证重点清洗模块完成后不能只验证入库是否成功还要检查这四个方向全角转半角是否生效。构造包含全角数字、全角字母的用例。替换规则是否覆盖期望符号。比如括号、斜杠、度数符号、波浪号。语言统计是否合理。对纯中文、中日混合、中英混合三组样例分别查看占比。哈希去重是否稳定。同一标题的不同全半角写法是否得到同一个哈希。下面是一组手动验证样例原始文本清洗后预期结果【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふwミリプロ/转载 3字以心传心 吐槽担当 虹深 ぬふ w括号和度数移除【】测试abc 1 测试全角字母转小写全角数字转半角hello worldhello world全角空格折叠测试。。。完成测试 完成中文句号替换为空格中文,English,数字123中文 english 数字123英文统一小写5. 常见坑混合文本清洗的项目级问题5.1 NFKC 不等于万能全半角转换NFKC 会把大部分全角字母、数字、兼容字符转换成半角但并不是所有全角字符都会转换。比如全角括号【】、全角空格 、中文引号“”这类 CJK 标点NFKC 不会把它们转成基础拉丁符号。如果业务希望把所有括号统一成半角需要单独维护一张替换表。错误写法text 【测试】 text unicodedata.normalize(NFKC, text) assert text [测试] # 这个断言会失败正确写法是 NFKC 之后继续查替换表。5.2 字符类别判断随 Unicode 版本变化unicodedata.category()的结果由 Python 运行时自带的 Unicode 数据库决定。Python 3.7、3.10、3.12 使用的 Unicode 数据库版本不同个别特殊字符的类别可能发生变化。在批量处理时要固定 Python 版本或者将清洗后的结果与预期特征做回放比对。否则同一段代码两个环境的结果可能不一样。5.3 不分语言地按“汉字”判断会误伤日文汉字\u4e00-\u9fff覆盖了中文汉字也覆盖了大量日文汉字。如果目标是识别“日语文本”只靠这个区间是不够的。需要结合平假名、片假名的存在来判断。例如文本“東京特許許可局”全是汉字没有平假名和片假名Unicode 层级无法判定它是日文还是中文。业务上如果必须区分需要引入词典或字符频率模型。5.4 正则替换连续执行导致顺序问题在一个清洗函数里连续执行多次re.sub如果顺序不谨慎可能把已经替换过的内容再次替换。比如把-替换成空格又把空格折叠为-就会造成不可预测的结果。正确做法是先做单字符级别遍历再做一次整体的空白折叠。5.5 哈希去重的前提是规则稳定哈希去重看似简单但规则一变所有历史哈希都会失效。不要用无版本的哈希值存库。正确的做法是在哈希字符串中拼接清洗规则版本或者单独维护一张rule_version表。后续修改规则时可以对增量数据使用新版本同时保留旧版本哈希用于历史去重分析。5.6 文件读取时出现 UnicodeDecodeErrorCSV 文件来源不同编码可能不同。读取报错时不要盲目换成errorsignore。推荐做法是先用二进制读取再尝试 UTF-8 和 GBK 两种解码方式。def guess_read_text(filepath: str) - str: with open(filepath, rb) as f: raw f.read() for encoding in [utf-8, gbk, utf-8-sig]: try: return raw.decode(encoding) except UnicodeDecodeError: continue raise RuntimeError(funsupported encoding: {filepath})6. 排查链路从现象定位清洗异常6.1 按现象定位问题现象可能原因检查方式清洗后文本为空字符类别判断过严或替换表误覆盖打印每个字符的类别和替换结果中文汉字被删除使用了[^\u4e00-\u9fff]这类保留规则检查clean_char是否允许Lo类别日文假名缺失正则范围只覆盖 CJK 基本区检查is_hiragana、is_katakana是否生效全角括号未替换NFKC 不处理【】检查替换表中是否包含【和】全角数字没变半角未调用 NFKC或调用前已被其他正则处理确认unicodedata.normalize(NFKC, ch)在首步两条相似文本哈希不一致清洗规则或规则版本不一致对比打印两条文本的normalize_text输出批量数据耗时高每条文本内多次编译正则把正则表达式提到函数外部用re.compile编译6.2 从输出倒推清洗路径当清洗结果不符合预期时不要直接改正则。先打印字符级诊断def debug_text(raw: str): for idx, ch in enumerate(raw): nfkc unicodedata.normalize(NFKC, ch) cat unicodedata.category(nfkc) print(idx, repr(ch), -, repr(nfkc), category:, cat)对【ミリプロ/转载】调用debug_text会看到【的类别是Ps】的类别是Pe/的类别是Po。基于这个输出再决定替换表要覆盖哪些字符而不是靠肉眼猜。6.3 建立输入输出回归样本清洗规则每次调整都应该有至少一组回归样本。建议准备 5 到 10 条既有脏文本和期望清洗结果写入测试脚本import unittest class TestNormalizeText(unittest.TestCase): def test_sample(self): raw 【ミリプロ/转载】【3字以心传心】吐槽担当虹深°ぬふw expected ミリプロ/转载 3字以心传心 吐槽担当 虹深 ぬふ w self.assertEqual(normalize_text(raw), expected) def test_fullwidth_digit(self): self.assertEqual(normalize_text(【】测试), 1 测试) def test_empty_symbol(self): self.assertEqual(normalize_text(°°), )这样后续修改规则时测试失败会直接提示哪条规则影响了已有数据。7. 生产环境与最佳实践7.1 学习环境、开发环境与生产环境的差异阶段数据规模关键关注点推荐做法学习验证几条到几十条理解函数逻辑使用debug_text打印字符级信息开发调试几百到几千条清洗规则覆盖完整建立回归测试集并跑单元测试测试验证数万条规则稳定性与性能统计清洗前后长度变化抽取异常样本生产运行百万条以上版本管理和幂等性使用清洗规则版本号离线批处理或消息队列处理7.2 可执行的最佳实践清单清洗模块上线前建议逐项核对下面这些内容每条清洗函数都输入原始字符串不对上游数据做任何假设。替换表集中管理不建议散落在多个函数里。清洗结果中保留原始字段不要覆盖原值。哈希字段旁保存规则版本号。清洗前统计字符长度清洗后统计字符长度用于监控清洗异常。对日文文本单独输出语言构成字段供下游分词或分类使用。正则表达式统一在模块顶部用re.compile编译避免循环内重复编译。对空值和None做防御处理避免调用unicodedata时报错。清洗规则变更时先跑回归测试再对增量数据使用新规则。7.3 扩展方向完成字符级清洗后还可以继续往这几个方向扩展文本标准化。把繁体中文转简体把日文汉字与中文汉字统一为一种写法。实体识别。识别标题中的活动名、人物名、机构名而不是只做字符清理。指纹去重。在哈希基础上结合编辑距离或 SimHash处理“标题基本一致但个别字符不同”的情况。词典辅助语言识别。使用日文常用汉字表、中文常用字表做二次判断提高日文与中文混合文本的分类准确率。在数据仓库中建立“原始字段、清洗字段、统计字段、哈希字段”四层字段体系方便追溯与审计。对一段看起来简单的混合文本做清洗难点不在于写正则而在于定义清楚“保留什么、替换什么、删除什么”。明确规则版本、回归样例和统计监控之后这套流程才能真正稳定运行在生产数据链路上。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。