From 1e42521fec7d43f0ba50db2db7eee011ec49ebb9 Mon Sep 17 00:00:00 2001 From: rui40000 Date: Wed, 8 Apr 2026 16:14:18 +0800 Subject: [PATCH] refactor: Align UTF8ConverterNode logic with fix_encoding.py for robust cleaning --- utf8_converter_node.py | 255 +++++++---------------------------------- 1 file changed, 43 insertions(+), 212 deletions(-) diff --git a/utf8_converter_node.py b/utf8_converter_node.py index 133f0be..3f9b5f6 100644 --- a/utf8_converter_node.py +++ b/utf8_converter_node.py @@ -1,5 +1,4 @@ import os -import torch def is_binary_file(path): try: @@ -13,240 +12,65 @@ def is_binary_file(path): def try_decode(data): """ - 尝试多种编码格式解码二进制数据 - Try to decode binary data with multiple encodings + 尝试多种编码格式解码二进制数据 (对齐 fix_encoding.py) """ - # 1. Try UTF-8 + # 1. UTF-8 try: return data.decode("utf-8"), "utf-8" except: pass - # 2. Try UTF-16 + # 2. UTF-16 with BOM + if data.startswith(b'\xff\xfe') or data.startswith(b'\xfe\xff'): + try: + return data.decode("utf-16"), "utf-16" + except: + pass + + # 3. GB18030 (covers GBK and more) try: - return data.decode("utf-16"), "utf-16" + return data.decode("gb18030"), "gb18030" except: pass - - # 3. Try GBK - try: - return data.decode("gbk"), "gbk" - except: - pass - - # 4. Fallback: UTF-8 with ignore + + # 4. Fallback return data.decode("utf-8", errors="ignore"), "fallback-ignore" -def clean_text(text): +def clean_text(text, remove_emoji=True): """ - 严格清洗文本: - - 删除不可见控制字符 (0-31, 保留 \n \t) - - 删除代理对 (Surrogates) - - 删除 4 字节字符 (Emoji, > 0xFFFF) + 严格清洗文本 (对齐 fix_encoding.py): + - 删除控制字符 (保留换行和制表) + - 删除 surrogate 区 + - 删除 emoji (默认强制删除 > 0xFFFF 的所有 4 字节字符) + - 强制统一换行符为 Unix 风格 (\n) """ cleaned = [] for c in text: code = ord(c) - # 删除不可见控制字符 (U+0000 - U+001F),保留 \n (10) 和 \t (9) + # 删除控制字符(保留换行和制表) if 0 <= code < 32 and c not in ("\n", "\t"): continue - # 过滤非法 surrogate 字符 (U+D800 - U+DFFF) + # 删除 surrogate 区 if 0xD800 <= code <= 0xDFFF: continue - # 删除 Emoji 和其他 4 字节字符 (> 0xFFFF) - # 注意:某些 Emoji(如 ⚠️ U+26A0)位于 BMP (Basic Multilingual Plane) 内,码点 < 0xFFFF - # 因此仅判断 code > 0xFFFF 是不够的,需要引入 regex 或 Unicode 范围判断 - # 这里我们扩展过滤范围,根据 Unicode 标准过滤 Emoji - # 简单起见,如果需要严格禁止 Emoji,可以使用 Unicode 范围 - # ⚠️ (Warning Sign) is U+26A0 - # ⚡ (High Voltage) is U+26A1 - # ⚽ (Soccer Ball) is U+26BD - # 常见 Emoji 范围包括: - # U+1F300-U+1F5FF (Miscellaneous Symbols and Pictographs) - # U+1F600-U+1F64F (Emoticons) - # U+1F680-U+1F6FF (Transport and Map Symbols) - # U+2600-U+26FF (Miscellaneous Symbols) -> 包含 ⚠️, ⚽, ☁️ 等 - # U+2700-U+27BF (Dingbats) -> 包含 ✈️, ✉️, ✏️ 等 - # - # 用户要求:允许特殊符号如 ★(U+2605), →(U+2192), 《(U+300A), ·(U+00B7) - # ⚠️ (U+26A0) 在 Miscellaneous Symbols 区域,与 ★ (U+2605) 同区! - # 这是一个难点。 - # 策略更新: - # 1. 删除所有 > 0xFFFF 的字符 (4字节字符) -> 已实现 - # 2. 针对 BMP 内的 Emoji,我们需要更细致的黑名单或白名单。 - # 考虑到用户说 "严禁使用任何 Emoji",但 "允许星号、箭头等"。 - # 星号 ★ U+2605 - # 警告 ⚠️ U+26A0 - # 它们非常接近。 - # 我们可以使用 unicode property 或 regex。但在不引入外部库(如 emoji)的情况下, - # 我们可以过滤特定的 Emoji 块,但保留特定的白名单字符。 - - # 简单处理:保留 > 0xFFFF 过滤。 - # 针对 BMP 内的 Emoji,用户提到的 ⚠️ 是 U+26A0。 - # 许多 Emoji 位于 U+2000-U+2FFF 之间,但也包含数学符号和箭头。 - # 让我们尝试过滤 variation selectors (U+FE00-U+FE0F) - # ⚠️ (U+26A0) 通常后面跟着 U+FE0F (VS16) 变成 Emoji 样式。 - # 但单字符 ⚠️ 也是存在的。 - - # 鉴于用户明确指出 ⚠️ 未删除,我们需要加强过滤。 - # 常见 BMP Emoji 范围: - # U+2600-U+26FF (杂项符号) -> 混合了 Emoji 和 符号(如 ★) - # U+2700-U+27BF (Dingbats) -> 混合了 Emoji 和 符号(如 ✂️) - - # 强制过滤列表 (手动列出常见 BMP Emoji 区域或字符) - # 或者,更激进地,如果字符属于 "Symbol, Other" (So) 类别且不是白名单? - # Python 的 unicodedata 库可以帮忙。 - - import unicodedata - try: - category = unicodedata.category(c) - except: - category = "Cn" # Not assigned - - # 4字节字符统统删除 - if code > 0xFFFF: + # 删除 emoji(默认删除所有 > 0xFFFF 字符) + if remove_emoji and code > 0xFFFF: continue - - # 针对 BMP 字符的特殊过滤 - # 过滤 Variation Selectors (U+FE00 - U+FE0F) - if 0xFE00 <= code <= 0xFE0F: - continue - - # 过滤特定 Emoji 字符 (黑名单补丁) - # ⚠️ U+26A0, ⚡ U+26A1, ✋ U+270B 等 - # 这是一个无底洞,但我们可以尝试过滤掉呈现为 Emoji 的符号 - # 许多现代 Emoji 实际上是基本字符 + VS16。 - # 如果我们删除了 VS16,它们会变成黑白文本符号。 - # 但用户希望 "删除",即完全消失。 - - # 使用 unicode category 过滤? - # ★ (BLACK STAR) -> 'So' (Symbol, Other) - # ⚠️ (WARNING SIGN) -> 'So' - # 两者类别相同。 - - # 既然无法通过类别区分,我们只能依赖码点范围,并设置白名单。 - # 允许:星号(★ U+2605)、方块(■ U+25A0)、箭头(→ U+2192) - # - # 让我们检查 ⚠️ (U+26A0) - # - # 临时方案:增加对常见 BMP Emoji 的过滤,如果不小心误杀,后续再调整。 - # - # 范围 U+2600 - U+26FF (Miscellaneous Symbols) - # 包含: - # 2600-2604 (太阳云雨等) -> Emoji? Yes (☀️, ☁️) - # 2605-2606 (星星) -> 允许 (★, ☆) - # 260E (电话 ☎️) -> Emoji - # 26A0 (警告 ⚠️) -> Emoji - # - # 我们可以只过滤那些明显的 Emoji 范围,或者根据用户反馈的 Bad Case (⚠️) 进行定点清除。 - # 但为了通用性,最好能区分。 - # - # 既然用户要求 "严禁使用任何 Emoji",且 "允许使用的特殊符号仅限 UTF-8收录范围,例如..." - # 这是一个比较模糊的边界。 - # - # 让我们引入 `unicodedata` 并结合范围判断。 - # - # 更新逻辑: - # 1. 保留 code > 0xFFFF 过滤 (处理了绝大多数 Emoji) - # 2. 增加对 BMP Emoji 的过滤。 - # 由于 Python 标准库没有 is_emoji,我们只能根据 Block 进行粗略过滤,并豁免常用符号。 - - # 定义需要检查的 Block (潜在 Emoji 区域) - # Dingbats: U+2700–U+27BF - # Miscellaneous Symbols: U+2600–U+26FF - # Transport and Map Symbols: U+1F680-U+1F6FF (已被 >0xFFFF 覆盖) - - # 针对 BMP 的补充过滤: - if 0x2600 <= code <= 0x27BF: - # 白名单 (用户明确允许或常见的非Emoji符号) - # U+2605 ★, U+2606 ☆ - # U+25A0-U+25FF (Geometric Shapes) -> 不在 2600-27BF 范围内,安全 - # U+2190-U+21FF (Arrows) -> 不在范围内,安全 - # U+300A 《, U+00B7 · -> 不在范围内,安全 - - # 允许的例外列表 (Decimal) - # 9733 (★), 9734 (☆) - allowed_in_range = [9733, 9734] - - if code not in allowed_in_range: - # 进一步检查:如果是 ⚠️ (9888) 或其他 Emoji,则过滤 - # 简单粗暴:在这个范围内,除了白名单,全部视为潜在 Emoji/不常用符号进行过滤? - # 这样可能误杀太严重。 - # - # 让我们仅过滤特定的 Emoji 子集。 - # ⚠️ U+26A0 (9888) - # ⚡ U+26A1 (9889) - # ⚰️ U+26B0 - # ⚽ U+26BD - # ⛄ U+26C4 - # ⛳ U+26F3 - # ... - # - # 更好的方法:只过滤 > 0xFFFF 的字符 + 代理对 + 控制字符。 - # 对于 BMP 内的字符,除非用户指定要过滤,否则保留。 - # 但用户明确反馈 ⚠️ 未删除。 - # - # 让我们针对性地过滤 "Emoji Presentation" 的字符。 - # 但没有库很难做到。 - # - # 妥协方案:硬编码过滤常见的 BMP Emoji 范围,或仅过滤用户提到的 ⚠️。 - # 考虑到 "严禁使用任何 Emoji",我们应该扩大过滤范围。 - - # U+2600-U+26FF 包含很多天气、星座、棋子等,大多被视为 Emoji。 - # U+2700-U+27BF (Dingbats) 包含剪刀、飞机、信封等,大多被视为 Emoji。 - - # 如果我们过滤掉这两个区段,除了白名单? - # 用户提到的 "允许特殊符号": - # 星号 ★ (U+2605) -> 在 U+2600-U+26FF - # 方块 ■ (U+25A0) -> 不在 - # 箭头 → (U+2192) -> 不在 - # 书名号 《 (U+300A) -> 不在 - # 间隔号 · (U+00B7) -> 不在 - - # 结论:只要保护好 ★ (及其他可能的符号),我们可以激进地过滤 U+2600-U+26FF 和 U+2700-U+27BF。 - pass # 逻辑将在下方实现 - - # 实现: - # 1. 过滤 Dingbats (U+2700 - U+27BF) - if 0x2700 <= code <= 0x27BF: - continue - - # 2. 过滤 Miscellaneous Symbols (U+2600 - U+26FF),但保留白名单 - if 0x2600 <= code <= 0x26FF: - # 白名单: - # U+2605 ★ (9733) - # U+2606 ☆ (9734) - whitelist_26xx = {0x2605, 0x2606} - if code not in whitelist_26xx: - continue - # 3. 过滤 Miscellaneous Technical 中常见的 Emoji (U+23xx) - # ⌚ (231A), ⌛ (231B), ⌨ (2328) - # ⏩ (23E9) - ⏳ (23F3) - # ⏸ (23F8) - ⏺ (23FA) - if code in (0x231A, 0x231B, 0x2328, 0x23CF): - continue - if 0x23E9 <= code <= 0x23F3: - continue - if 0x23F8 <= code <= 0x23FA: - continue - - # 4. 过滤其他零散的常见 Emoji 符号 - # ⭐ (2B50), ⭕ (2B55) - if code in (0x2B50, 0x2B55): - continue - cleaned.append(c) - return "".join(cleaned) + result_text = "".join(cleaned) + # 强制统一换行符为 Unix 风格 (\n),防止 \r 导致打标工具报错 + result_text = result_text.replace("\r\n", "\n").replace("\r", "\n") + return result_text class UTF8ConverterNode: """ - UTF-8 编码转换与文本清洗节点 (重构版): - 使用鲁棒的解码策略读取文件或处理文本,并执行严格的字符清洗。 + UTF-8 编码转换与文本清洗节点 (工业级强力重构版): + 完全对齐 fix_encoding.py 的逻辑,使用鲁棒的解码策略并执行极其严格的字符清洗。 """ @classmethod @@ -258,6 +82,12 @@ class UTF8ConverterNode: "default": "", "placeholder": "输入文本 / Input Text" }), + "remove_emoji": ("BOOLEAN", { + "default": True, + "label_on": "是 / Yes", + "label_off": "否 / No", + "tooltip": "是否强制删除所有 4 字节字符(包含绝大多数 Emoji)" + }), }, "optional": { "input_file": ("STRING", { @@ -273,7 +103,7 @@ class UTF8ConverterNode: FUNCTION = "run" CATEGORY = "Rui-Node🐶/文本处理📝" - def run(self, input_text, input_file=""): + def run(self, input_text, remove_emoji, input_file=""): log = [] text_content = "" encoding_used = "none" @@ -285,9 +115,8 @@ class UTF8ConverterNode: # 检查是否为二进制文件 if is_binary_file(input_file): - # 虽然是二进制,但我们仍尝试解码,或者直接报错 - # 根据需求"转化为utf-8",我们尝试强制解码 - log.append("警告: 检测到可能的二进制文件") + log.append("警告: 检测到可能的二进制文件 (跳过)") + return ("", "\n".join(log)) try: with open(input_file, "rb") as f: @@ -310,10 +139,10 @@ class UTF8ConverterNode: # 2. 清洗文本 log.append("-" * 20) - log.append("开始清洗文本...") + log.append("开始工业级强力清洗文本...") original_len = len(text_content) - cleaned_content = clean_text(text_content) + cleaned_content = clean_text(text_content, remove_emoji=remove_emoji) final_len = len(cleaned_content) removed_count = original_len - final_len @@ -324,7 +153,9 @@ class UTF8ConverterNode: log.append(f"移除字符数: {removed_count}") if removed_count > 0: - log.append("清洗详情: 已移除所有 Emoji、4字节字符、代理对及非法控制字符。") + log.append(f"清洗详情: 已移除非法控制字符、代理对,并统一换行符为 \\n。") + if remove_emoji: + log.append("已开启 remove_emoji: 删除了所有 4 字节字符。") else: log.append("结果: 文本已符合规范,无需修改。")