LunaTranslator 文字处理全解析:16 种文本清洗/去重方法的原理、配置与实战指南
【免费下载链接】LunaTranslator视觉小说翻译器 / Visual Novel Translator项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator
导读:在 Visual Novel Translator(LunaTranslator)的 HOOK 模式下,从游戏内存中截取的文本经常带有乱码、重复字符、HTML 标签、读音注音等“脏数据”。本文以官方文档 docs/cht/textprocess.md 为骨架,逐条讲解 16 种内置文字处理方法的作用、触发场景、参数含义与源码级实现原理,并给出内嵌翻译限制、执行顺序配置、自定义 Python 处理等实战要点,帮助读者针对不同引擎的游戏快速组合出最合适的清洗方案。
一、文字处理机制概述
1.1 为什么需要文字处理
HOOK 模式通过注入游戏进程、挂钩文本绘制函数来截取屏幕上显示的文字。这种方式的优点是速度快、无 OCR 误差,但也带来了特有的“脏数据”问题:
- 重复字符:游戏为了绘制阴影、描边、发光等效果,同一字符会被反复绘制多次,HOOK 会多次截取;
- 乱码字符:日文游戏使用了 Shift_JIS 编码,某些特殊字符无法用该字符集表示,截取后呈现为乱码;
- 脚本残留:游戏脚本中的 HTML 标签、
{汉字/读音}注音标记、控制符等也会被一并截获; - 整段重复:部分引擎每帧刷新整行文字,导致同一句话被截取多次。
此时就需要对截取到的原始文本进行文字处理(text processing)。正如官方文档开篇所述:"一般在 HOOK 模式下,有時會讀取到錯誤的文字,例如有重複的文字,或者其他亂七八糟的文字,這時需要使用文字處理來解決。"
1.2 处理的执行顺序
从源码结构看,文字处理并不只有一个函数,而是由多条链组成:
- HOOK 截取后的立即处理:核心入口是 myutils/post.py 中的
POSTSOLVE()函数,它以postprocess_rank(处理顺序列表)为顺序,逐个执行启用的处理方法; - 翻译前后的二次处理:项目还提供了基于类的处理链(如 transoptimi/myprocess.py),在 LunaTranslator.py 中通过
solvebeforetrans()在翻译前调用各处理类的process_before()、翻译后调用process_after()对结果进行再加工。
本文聚焦于前者——即文档核心描述的POSTSOLVE处理链中的 16 种方法。
1.3 配置存储与默认状态
所有处理方法的开关与参数统一存储在 defaultconfig/postprocessconfig.json 中。每个方法是一个配置项,典型结构如下:
"_2": { "use": true, "name": "HOOK_去除重复字符_AAAABBBBCCCC->ABC", "args": { "重复次数(若为1则自动分析去重)": 1, "保持非重复字符": true }, "argstype": { "重复次数(若为1则自动分析去重)": { "type": "intspin", "min": 1, "max": 10000 }, "保持非重复字符": { "type": "switch" } }, "isHookOnly": true }其中:
use:是否启用(默认启用三个最常用方法:Unicode正规化、HOOK_去除重复字符、HOOK_去除重复行_ABCDABCDABCD->ABCD);args:方法的参数及默认值;argstype:参数的类型定义,intspin表示整数微调框,combo表示下拉选择,switch表示开关;isHookOnly: true:仅对 HOOK 模式生效,对 OCR 等其他文本源无效;isExUse: true:属于“内嵌翻译安全集”,在内嵌翻译模式下仍可生效。
在POSTSOLVE()的执行逻辑中(post.py),每个方法都会被检查:必须存在于配置、use为真、满足isHookOnly/isExUse约束后,才会根据其函数签名(1 个参数或 2 个参数)被调用,异常会被捕获而不影响整条链继续执行。
二、基础过滤类方法(适用于所有文本源)
2.1 过滤文字中的非日文字符集字符(_remove_non_shiftjis_char)
作用:过滤掉无法使用 Shift_JIS 字符集编码的字符,主要针对日文游戏的乱码问题。由于乱码多出现在日文游戏,该方法在配置中默认存在。
官方示例:
エマさんԟのイԠラストは全部大好き! → エマさんのイラストは全部大好き!源码实现(post.py):
def _remove_non_shiftjis_char(line: str) -> str: return line.encode("shift-jis", "ignore").decode("shift-jis")原理很直观:将文本用shift-jis编码,ignore参数会丢弃无法编码的字符,再解码还原,从而剔除非法字符。
注意:该方法主要面向日文游戏。若游戏使用其他编码(如中文游戏使用 GBK),此方法可能不适用,可考虑使用下面的 Unicode 正规化或字符串替换。
2.2 过滤控制字符(_remove_control)
作用:过滤掉 ASCII 控制符(即\x00–\x1F以及\x7F范围内的字符),例如文档中提到的 这类不可见字符。这些字符通常来自游戏内部格式标记。
源码实现(post.py):
def _remove_control(line: str) -> str: return "".join(r for r in line if not is_ascii_control(r))is_ascii_control定义在 myutils/utils.py,用于判断字符是否为 ASCII 控制字符。
2.3 过滤英文标点(_remove_symbo)
作用:过滤掉 ASCII 英文字符集中的标点符号:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~源码实现(post.py):
def _remove_symbol(line: str) -> str: return "".join(r for r in line if not is_ascii_symbol(r))2.4 过滤「」以外的字符(_remove_not_in_ja_bracket)
作用:仅保留日文角括号「」内的内容。适用于 HOOK 截取到大量行外文本、而有效对话都在「」内的情况。
官方示例:
こなみ「ひとめぼれってやつだよね……」 → 「ひとめぼれってやつだよね……」源码实现(post.py):
def _remove_not_in_ja_bracket(line: str) -> str: sections = re.findall(r"「[^」]*」", line) return "".join(sections) if sections else line注意:如果文本中没有任何「」括号,该函数会原样返回文本,避免误伤无括号对话。
2.5 去除花括号 {}(_1)
作用:许多日文游戏脚本使用花括号给汉字注音,常见格式为{漢字/讀音}与{漢字:讀音}。该方法先按这些模式去除读音标注,再去除所有剩余花括号及其内容。
官方示例:
「{恵麻/えま}さん、まだ{起き/おき}てる?」 或 「{恵麻:えま}さん、まだ{起き:おき}てる?」 → 「恵麻さん、まだ起きてる?」源码实现(post.py):
def remove_braces(line: str) -> str: line = re.sub(r"\{(\w+)(.*?)\}(.*?)\{\/\1\}", r"\3", line) line = re.sub(r"\{([^}]*?)[:/](https://link.gitcode.com/i/5792e983c556de7bc766a613952b035f)\}", r"\1", line) line = re.sub(r"\{.*?\}", r"", line) return line三步策略:
- 第一步处理成对的
{tag...}...{/tag}标记(类似富文本闭合标签),仅保留标签之间的内容; - 第二步处理
{漢字/讀音}或{漢字:讀音}模式,只保留汉字部分; - 第三步兜底删除所有剩余
{...}结构。
2.6 Unicode 正规化(fulltohalf)
作用:将文本中的全角/半角、兼容字符统一规范化,解决 HOOK 截取文本中全角英文字母、全角标点与标准文本不一致的问题。配置中默认启用,默认类型为NFKC。
官方示例:
???(I guess he doesn’t want to talk to strangers...) → ???(I guess he doesn’t want to talk to strangers...)源码实现(post.py):
def unicode_normalization(text: str, args: dict) -> str: return unicodedata.normalize(args.get("type", "NFKC"), text)即 Python 标准库unicodedata.normalize()。配置中type参数提供四种选择(postprocessconfig.json 中fulltohalf项):
| 模式 | 说明 |
|---|---|
NFD | 规范分解(把组合字符拆成基字符+附加符号) |
NFC | 规范分解后重组(默认的合成形式) |
NFKD | 兼容分解(全角→半角、字体变体→标准字符) |
NFKC | 兼容分解后重组(最常用,默认值) |
对于日文游戏,全角英文(I)、全角标点(???、...)会在翻译时造成干扰,使用NFKC后即可归一为半角形式,显著提升翻译引擎的识别率。
2.7 截取指定行数(lines_threshold_1)
作用:只保留文本中指定数量的行,用于处理 HOOK 一次性截取多行、其中只有部分行需要翻译的情况。
配置参数(见 postprocessconfig.json):
maxzishu(截取行数):整数,取值范围-99999999~99999999,默认1;cut_reverse(截取末尾):开关,默认true。
源码实现(post.py):
def slice_lines(line: str, args: dict) -> str: max_lines = args["maxzishu"] splits = line.splitlines() if len(splits) > abs(max_lines): reverse = args.get("cut_reverse", True) splits = splits[-max_lines:] if reverse else splits[:max_lines] return "\n".join(splits) return line逻辑:按换行符拆分行列表,仅当行数超过设定值时才截取;cut_reverse为真时取末尾 N 行(新剧情文本通常出现在末尾),为假时取开头 N 行;若行数未超限则原样返回。
2.8 过滤角括号 <>(_4)
作用:过滤 HTML 标签。文档特别说明:"這個實際上是過濾 HTML 標籤,怕小白不知道是什麼意思所以這麼寫的名字。"主要应用于TyranoScript 引擎制作的游戏——其 HOOK 截取的是 innerHTML,往往夹带大量<div>、</div>、<div id="dsds">等标签。
源码实现(post.py):
def remove_angle_brackets(line: str) -> str: line = re.sub(r"<(.*?)>", r"", line) return line使用非贪婪匹配删除所有<...>结构。
2.9 过滤换行符(_6EX)
作用:合并文本中的换行。关键细节:如果来源语言不是日文,换行会被替换为空格而非直接删除,避免多个英文单词被拼接到一起(如hello world不会被处理成helloworld)。
源码实现(post.py):
def remove_line_breaks(line: str) -> str: ws = getlangsrc().space line = ws.join(sect for sect in line.splitlines() if sect) return linegetlangsrc().space根据当前源语言返回对应的“连接符”——日文用空字符串(日文不需要空格分词),其他语言用空格。这也是文档所述行为的源码印证。
2.10 过滤数字(_91)
作用:过滤掉0~9全部数字。
def remove_digits(line: str) -> str: line = re.sub(r"([0-9]+)", r"", line) return line2.11 过滤英文字母(_92)
作用:过滤掉A~Z与a~z英文字母。
def remove_alphabets(line: str) -> str: line = re.sub(r"([a-zA-Z]+)", r"", line) return line三、HOOK 去重类方法(仅对 HOOK 模式生效)
这一类是文档重点,也是实际使用中最常用、最复杂的部分。它们都带有isHookOnly: true标记,仅在 HOOK 文本源下生效。核心难点在于如何判断真实文本与重复模式。
3.1 HOOK 去除重复字符:AAAABBBBCCCC→ABC(_2)
适用场景:游戏先绘制一遍文字、再绘制阴影、再绘制描边等,HOOK 会多次截取被重复绘制的字符。
官方示例:
恵恵恵麻麻麻ささささんんんははは再再再びびび液液液タタタブブブへへへ視視視線線線ををを落落落とととすすす。。。 → 恵麻さんは再び液タブへ視線を落とす。配置参数:
重复次数(若为1则自动分析去重):整数(1~10000),默认1表示自动分析重复字数;也可指定确定的重复次数(如 3)避免分析误差;保持非重复字符:开关,默认true。
源码实现(post.py):
if times >= 2: guesstimes = times else: dumptime = Counter() cntx = 1 lastc = None for c in list(line) + [0]: if c != lastc: dumptime[cntx] += 1 lastc = c cntx = 1 else: cntx += 1 _max = max(dumptime.values()) ... guesstimes = sorted(xx) if guesstimes[0] == 1 and len(guesstimes) > 1: guesstimes = guesstimes[1:] guesstimes = guesstimes[0]自动分析时,代码统计所有连续相同字符的“游程长度”分布,取出现频次最高、且非 1 的长度作为估计的重复次数guesstimes。之后:
- 若开启“保持非重复字符”,则逐字符扫描,连续
guesstimes个相同字符只保留 1 个; - 若关闭,则直接按
line[i * guesstimes]等距取样。
文档也提醒:自动分析偶有不准,建议手动指定确定的重数字数(如该游戏固定每字符绘制 3 次)。
3.2 HOOK 去除重复行:ABCDABCDABCD→ABCD(_3)
适用场景:游戏不逐字符重复,而是整行文本快速刷新多次(非反复整理,而是一次性刷新多次)。
官方示例:
恵麻さんは再び液タブへ視線を落とす。恵麻さんは再び液タブへ視線を落とす。恵麻さんは再び液タブへ視線を落とす。 → 恵麻さんは再び液タブへ視線を落とす。源码实现(post.py):
def _3_f(line, args): times = args["重复次数(若为1则自动分析去重)"] if times >= 2: guesstimes = times else: guesstimes = len(line) while guesstimes >= 1: if line[: len(line) // guesstimes] * guesstimes == line: break guesstimes -= 1 line = line[: len(line) // guesstimes] return line自动模式从最长可能重复次数向下尝试,找到满足“开头1/N重复 N 次等于全文”的 N,然后截取前1/N作为结果。同样建议对刷新次数固定的游戏直接指定次数。
3.3 HOOK 去除重复行:S1S1S1S2S2S2→S1S2(_3_2)
适用场景:不同句子的刷新次数不一致(如第 1 句刷 3 次、第 2 句刷 2 次、第 3 句不刷),只能完全交给程序分析去重。
官方示例:
(前句重复3次)(中句无重复)(后句重复2次) → 恵麻さん……ううん、恵麻ははにかむように私の名前を呼ぶ。なんてニヤしていると、恵麻さんが振り返った。私は恵麻さんの目元を優しくハンカチで拭う。源码实现(post.py):通过反复将文本对半长度起检测“前缀重复”模式,把识别出的整段重复单元逐个剥离到缓存中,最后拼接去重结果。由于场景复杂,文档提示这种分析可能存在少量误差,属正常现象。
3.4 HOOK 去除重复行:ABCDBCDCDD→ABCD(_10)
适用场景:显示文字的 HOOK 函数在每显示一个字符时都会被调用,且每次参数指针向后移动,导致第一次调用得到完整文本,后续输出剩余子串直到长度为 0。
官方示例:
恵麻さんは再び液タブへ視線を落とす。麻さんは再び液タブへ視線を落とす。さんは再び液タブへ視線を落とす。んは再び液タブへ視線を落とす。は再び液タブへ視線を落とす。...す。。 → 恵麻さんは再び液タブへ視線を落とす。源码实现(post.py):统计字符频次、从文本末尾向前追溯匹配,找出最长的“真实文本”候选。
3.5 HOOK 去除重复行:AABABCABCD→ABCD(_13EX)
适用场景:游戏每绘制一个新字符,就把前面所有已绘字符再绘制一遍(前缀递增式重绘)。
官方示例:
恵麻恵麻さ恵麻さん恵麻さんは恵麻さんは再...恵麻さんは再び液タブへ視線を落とす。 → 恵麻さんは再び液タブへ視線を落とす。源码实现(post.py):从后向前逐步剥离“最长后缀重复”结构,最后逆序拼接还原真实文本。
重要提醒(文档原文强调):当文本有多行时,该处理会每行单独按上述逻辑去重,复杂度大增,经常难以正确识别。如果遇到处理失败,建议改用自定义 Python 处理来定制算法。
四、高级自定义处理方法
4.1 自定义 Python 处理(_11)
当内置方法都不够用时,可以编写 Python 脚本实现任意复杂逻辑。使用方法:
- 在文字处理设置中启用“自定义 Python 处理”,点击设置按钮打开脚本文件;
- 若脚本不存在,程序会自动在
userconfig目录生成mypost.py及以下模板(源码见 myutils/template/mypost.py,生成逻辑见 myutils/utils.py):
def POSTSOLVE(string: str): # 请在这里编写自定义处理 return string- 在
POSTSOLVE函数中编写处理逻辑,入参为原始文本,返回值作为处理后的文本。
源码实现(post.py):
def _mypost_process(line: str, file: str, module: str) -> str: mod = checkmd5reloadmodule(file, module) return mod.POSTSOLVE(line) if mod else linecheckmd5reloadmodule(myutils/utils.py)会检测脚本文件的 MD5 是否变化,从而在不重启程序的情况下热重载修改后的脚本——编辑保存mypost.py后无需重启即可生效,非常方便调试。此外,POSTSOLVE支持按游戏单独配置自定义脚本(在游戏专属文本处理配置中指定posts/xxx.py),详见 post.py 对savehook_new_data游戏级配置的读取逻辑。
4.2 字符串替换(stringreplace)
作用:不止是替换,也常用来“过滤”。例如把固定的乱码字符、反复刷新产生的倒三角字符等替换成空白来剔除。
四种模式组合(对应源码 myutils/utils.py 的parsemayberegexreplace):
| 正则 | 转义(escape) | 行为 |
|---|---|---|
| 关闭 | 关闭 | 普通字符串替换,把 key 当作字面量(内部会re.escape) |
| 关闭 | 开启 | key/value 先经过safe_escape解码(如\n表示换行符),再按字面量替换 |
| 开启 | 关闭 | 按正则表达式替换 |
| 开启 | 开启 | 先用safe_escape处理输入,再按正则替换 |
转义(escape)选项的意义:文档强调,开启转义后输入会被视为转义字符串而非字面值。例如用\n表示真正的换行符,从而可以实现“仅过滤出现在换行符前后的字符”这类需求。safe_escape的实现在 myutils/utils.py,本质是codecs.escape_decode。
此外parsemayberegexreplace还支持几个字符串替换的高级开关(可通过配置项开启):whole-word(整词匹配,自动加\b边界)与case-sensitive(大小写敏感;默认不敏感,即默认re.IGNORECASE)。
合并模式:字符串替换配置中若开启merge,则会把全局默认替换列表与当前列表合并执行(见 post.py 的string_replace),便于全局规则+游戏规则叠加。
五、内嵌翻译与执行顺序的注意事项
5.1 内嵌翻译(Embed Translate)下的限制
文档明确提示:内嵌翻译时大部分处理方法不会生效,这是为了减少游戏崩溃的可能。允许使用的方法仅有 5 种(它们在配置中带有isExUse: true标记):
過濾換行符號(_6EX)字串取代(stringreplace)自訂 Python 處理(_11)過濾角括號<>(_4)去除花括號{}(_1)
这一约束在 post.py 中得到印证:
if not useAll and isEx and not config.get("isExUse", False): continue即在内嵌翻译模式(isEx=True)下,凡未标记isExUse的方法都会被跳过。
5.2 处理顺序(postprocess_rank)
文档提示:"如果有非常複雜的錯誤形式,可以透過啟用多種處理方式,並調整他們的執行順序來得到豐富的處理方法組合。"
处理顺序由globalconfig["postprocess_rank"]列表决定,POSTSOLVE按该列表顺序执行每个启用的方法。不同的顺序会产生不同的结果:
- 例如“截取行数”放在“字符串替换”之前或之后,其作用对象会不同;
- 例如先“去除花括号”再“Unicode 正规化”,与反之,对注音标记的处理路径不同。
源码中postprocess_rank会与当前支持的方法集合做对齐并自动补齐新增方法(post.py)。用户可在设置界面拖拽调整顺序,实现对复杂错误文本的“流水线式”组合清洗。
5.3 游戏专属文字处理配置
从 post.py 可以看到,程序支持按游戏独立配置文字处理:当某个游戏的textproc_follow_default为假时,会读取该游戏的save_text_process_info(包含rank顺序、postprocessconfig配置以及自定义mypost脚本),实现“每个游戏一套处理方案、互不影响”。这对不同引擎、不同绘制方式的游戏同时游玩时非常实用。
六、实战排错指南
针对文档中出现的各类典型问题,整理一份“症状 → 处理方案”对照表:
| 症状 | 推荐处理方法 | 备注 |
|---|---|---|
文本出现エマさんԟのイԠ...这类乱码 | 过滤非 Shift_JIS 字符 | 仅适用日文游戏 |
| 文本每字符重复多次(描边/阴影效果) | _2去除重复字符 | 建议指定确切重复次数 |
| 整句重复 N 次 | _3去除重复行 | 刷新次数固定时指定次数更稳 |
| 各句重复次数不一致 | _3_2去除重复行(S1S1S1S2S2S2) | 可能有少量分析误差 |
| 文本逐字递减(子串输出) | _10去除重复行(ABCDBCDCDD) | |
| 文本前缀递增式重绘 | _13EX去除重复行(AABABCABCD) | 复杂情况建议自定义脚本 |
出现<div>等 HTML 标签 | 过滤角括号<> | TyranoScript 引擎常见 |
出现{漢字/讀音}注音 | 去除花括号{} | |
| 全角英文/标点干扰翻译 | Unicode 正规化(NFKC) | 默认已启用 |
| 多余的空白行/换行 | 过滤换行符 | 非日文源语言时会替换为空格 |
| 固定出现的乱码片段 | 字符串替换(替换为空) | 可配合正则、转义、整词、大小写选项 |
| 极其复杂的定制清洗需求 | 自定义 Python 处理(_11) | 保存后热重载,无需重启 |
通用建议:
- 从最小集合开始:默认启用的“Unicode 正规化 +
_2+_3”已能解决大多数常见问题; - 遇到复杂错误时逐步叠加方法,并利用顺序调整观察输出变化;
- 对重复类方法,若能确定游戏的绘制次数,手动指定次数通常比自动分析更可靠;
- 多行文本的复杂重复(如
_13EX)处理失败时,优先考虑编写自定义 Python 脚本; - 每个游戏单独配置一套处理方案,避免不同引擎游戏的配置互相干扰。
七、相关资源索引
- 官方文字处理文档(正体中文):docs/cht/textprocess.md,另有 docs/zh/textprocess.md、docs/en/textprocess.md 等多语言版本;
- 处理链核心实现:src/LunaTranslator/myutils/post.py
- 默认配置与全部方法清单:src/LunaTranslator/defaultconfig/postprocessconfig.json
- 字符串替换底层解析与工具函数:src/LunaTranslator/myutils/utils.py
- 自定义 Python 处理模板:src/LunaTranslator/myutils/template/mypost.py
- 自定义 Python 处理入口封装:src/LunaTranslator/transoptimi/myprocess.py
- 文本处理在翻译流程中的调用位置:src/LunaTranslator/LunaTranslator.py
【免费下载链接】LunaTranslator视觉小说翻译器 / Visual Novel Translator项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考