import os import torch def is_binary_file(path): try: with open(path, "rb") as f: chunk = f.read(1024) if b"\x00" in chunk: return True except: return True return False def try_decode(data): """ 尝试多种编码格式解码二进制数据 Try to decode binary data with multiple encodings """ # 1. Try UTF-8 try: return data.decode("utf-8"), "utf-8" except: pass # 2. Try UTF-16 try: return data.decode("utf-16"), "utf-16" except: pass # 3. Try GBK try: return data.decode("gbk"), "gbk" except: pass # 4. Fallback: UTF-8 with ignore return data.decode("utf-8", errors="ignore"), "fallback-ignore" def clean_text(text): """ 严格清洗文本: - 删除不可见控制字符 (0-31, 保留 \n \t) - 删除代理对 (Surrogates) - 删除 4 字节字符 (Emoji, > 0xFFFF) """ 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) 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: 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) class UTF8ConverterNode: """ UTF-8 编码转换与文本清洗节点 (重构版): 使用鲁棒的解码策略读取文件或处理文本,并执行严格的字符清洗。 """ @classmethod def INPUT_TYPES(cls): return { "required": { "input_text": ("STRING", { "multiline": True, "default": "", "placeholder": "输入文本 / Input Text" }), }, "optional": { "input_file": ("STRING", { "multiline": False, "default": "", "placeholder": "文件路径 / File Path (优先读取 / Overrides input_text)" }), } } RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("filtered_text", "log") FUNCTION = "run" CATEGORY = "Rui-Node🐶/文本处理📝" def run(self, input_text, input_file=""): log = [] text_content = "" encoding_used = "none" # 1. 获取原始内容 (文件优先) if input_file and input_file.strip(): if os.path.exists(input_file): log.append(f"正在读取文件: {input_file}") # 检查是否为二进制文件 if is_binary_file(input_file): # 虽然是二进制,但我们仍尝试解码,或者直接报错 # 根据需求"转化为utf-8",我们尝试强制解码 log.append("警告: 检测到可能的二进制文件") try: with open(input_file, "rb") as f: raw_data = f.read() text_content, encoding_used = try_decode(raw_data) log.append(f"解码成功: 使用编码 {encoding_used}") except Exception as e: return ("", f"读取或解码文件时发生错误: {str(e)}") else: log.append(f"错误: 文件不存在 -> {input_file}") log.append("回退到使用 input_text") text_content = input_text encoding_used = "input_text" else: log.append("使用直接输入的文本") text_content = input_text encoding_used = "input_text" # 2. 清洗文本 log.append("-" * 20) log.append("开始清洗文本...") original_len = len(text_content) cleaned_content = clean_text(text_content) final_len = len(cleaned_content) removed_count = original_len - final_len # 3. 生成日志 log.append(f"原始长度: {original_len}") log.append(f"输出长度: {final_len}") log.append(f"移除字符数: {removed_count}") if removed_count > 0: log.append("清洗详情: 已移除所有 Emoji、4字节字符、代理对及非法控制字符。") else: log.append("结果: 文本已符合规范,无需修改。") return (cleaned_content, "\n".join(log)) # 节点映射字典 NODE_CLASS_MAPPINGS = { "UTF8Converter": UTF8ConverterNode } # 节点显示名称映射 NODE_DISPLAY_NAME_MAPPINGS = { "UTF8Converter": "转化为utf-8编码 / Convert to UTF-8" }