refactor: Align UTF8ConverterNode logic with fix_encoding.py for robust cleaning

This commit is contained in:
rui40000
2026-04-08 16:14:18 +08:00
parent ac8c6eb58d
commit 1e42521fec
+43 -212
View File
@@ -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("结果: 文本已符合规范,无需修改。")