LunaTranslator 文本处理方法全解析:从 HOOK 乱码清洗到自定义 Python 后处理
发布时间:2026/9/15 16:13:31来源:尧图网络
LunaTranslator 文本处理方法全解析从 HOOK 乱码清洗到自定义 Python 后处理【免费下载链接】LunaTranslator视觉小说翻译器 / Visual Novel Translator项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator本文围绕 LunaTranslator视觉小说翻译器内置的文本处理Text Process体系展开完整讲解每一种处理方法的用途、适用场景与配置参数并结合源码揭示其底层实现原理。读完本文你将掌握如何诊断 Hook 模式下各种乱码/重复文本的成因如何通过去重过滤器、字符串替换、Unicode 正规化等手段组合出高效的清洗管线以及如何在处理失败时编写自定义 Python 脚本兜底最终获得干净、可读、适合翻译的原文。为什么要做文本处理Hook 模式的脏文本问题LunaTranslator 在 HOOK 模式下直接注入游戏进程读取文本。这种方式速度最快、也最适合即时翻译但代价是读到的文本往往不是干净的最终显示文本常见的问题包括同一句话被重复提取多次重复字符、重复行混入绘制阴影、描边等环节产生的冗余调用带 HTML 标签TyranoScript 游戏的 innerHTML花括号注音标记、控制字符、乱码字符、全角标点等噪声。文档 textprocess.md中文对照见 docs/zh/textprocess.md开篇就点明一般在 Hook 模式下有时会读取到错误的文本例如有重复的文本或者其他乱七八糟的文本这时需要使用文本处理来解决。文本处理就是针对这类问题设计的一道可编排的后处理管线。从源码看这条管线的主入口是 post.py 中的POSTSOLVE(line, isEx, isFromHook, useAll, skippreprocess)函数它按照全局配置postprocess_rank中记录的处理顺序逐一执行所有已启用use: true的处理项每个处理项输出作为下一个处理项的输入从而形成链式流水线带isHookOnly标记的处理项仅对 HOOK 模式读取到的文本生效带isExUse标记的处理项才会在**内嵌翻译嵌入模式**中生效每个处理项的启用状态与参数定义在 postprocessconfig.json 中用户可在设置界面勾选并调整执行顺序。文档特别强调如果有非常复杂的错误形式可以通过激活多种处理方式并调整他们的执行顺序来得到丰富的处理方法组合。这意味着文本处理不是选一个开关的二元操作而是一套可以自由组合、排序的过滤矩阵。基础过滤类清理无法编码与无意义的字符这一类处理目标单一、副作用小适合作为管线的基础层主要用于消除看起来像乱码的内容。过滤文本中的非日语字符集字符Shift-JIS 过滤用途过滤掉无法使用 shift-jis 字符集编码的字符。由于乱码问题通常出现在日语游戏中该方法是专门为日语游戏预设的兜底手段。示例摘自文档エマさんԟのイԠラストは全部大好き会被处理成エマさんのイラストは全部大好きԟ、Ԡ这类无法用 Shift-JIS 编码的字符被丢弃。源码实现post.py非常简洁def _remove_non_shiftjis_char(line: str) - str: return line.encode(shift-jis, ignore).decode(shift-jis)即对整行文本做一次按 Shift-JIS 编码、忽略无法编码的字符、再解码回来的往返操作从根上保证输出文本必然是合法的 Shift-JIS 字符集内容。注意该方法可能把合法但超出 Shift-JIS 范围的字符如部分简体汉字一并删除因此只建议在日文游戏出现明显乱码时启用。过滤控制字符用途过滤掉文本中的 ASCII 码控制符例如文档中列出的 等。源码实现post.py 与 utils.pydef is_ascii_control(c: str): # 不要管\r\n return cinranges(c, (0, 0x9), (0xB, 0xC), (0xE, 0x1F), (0x7F, 0xA0))可以看出其过滤区间为0x00–0x09、0x0B–0x0C、0x0E–0x1F、0x7F–0xA0并且特意保留了\r\n0x0D、0x0A换行符——因为换行符对于后续的截取行数等处理仍有意义不应在此被误删。过滤英文标点用途过滤掉文本中的 ASCII 标点符号!#$%()*,-./:;?[\]^_{|}~。源码实现utils.py将其定义为四个连续区间def is_ascii_symbol(c: str): return cinranges(c, (0x21, 0x2F), (0x3A, 0x40), (0x5B, 0x60), (0x7B, 0x7E))即!到/、:到、[到、{到~四段。注意该过滤不区分中文全角标点适合在纯日文/英文文本中剔除意外混入的半角符号时使用。过滤「」以外的字符用途当钩取到的文本在日文引号「」之外还有大量冗余内容时只保留所有「…」片段。示例摘自文档こなみ「ひとめぼれってやつだよね……」将被处理为「ひとめぼれってやつだよね……」。源码实现post.pydef _remove_not_in_ja_bracket(line: str) - str: sections re.findall(r「[^」]*」, line) return .join(sections) if sections else line一个值得注意的细节当正则匹配不到任何「…」片段时函数返回原始文本而不是空串从而避免误伤正常文本。过滤数字 / 过滤英文字母这两个方法在文档中仅以略标注但源码实现明确post.pydef remove_digits(line: str) - str: line re.sub(r([0-9]), r, line) return line def remove_alphabets(line: str) - str: line re.sub(r([a-zA-Z]), r, line) return line分别通过正则[0-9]与[a-zA-Z]整段删除数字或英文字母适合在不需要数字如血量、日期显示或不需要罗马字注音的文本场景中使用。规范化与结构化让文本形态符合翻译预期这一类处理侧重于把文本整形成更符合翻译与排版预期的形态其中部分方法已默认启用。去除花括号 {}用途许多游戏脚本使用{}等符号给汉字加注音振假名/ふりがな典型格式为{汉字/注音}与{汉字:注音}。示例摘自文档「{恵麻/えま}さん、まだ{起き/おき}てる」或「{恵麻:えま}さん、まだ{起き:おき}てる」将被处理成「恵麻さん、まだ起きてる」。源码实现post.py采用三步正则递进处理def remove_braces(line: str) - str: line re.sub(r\{(\w)(.*?)\}(.*?)\{\/\1\}, r\3, line) # ① 成对标签如 {ruby}内容{/ruby} 取内容 line re.sub(r\{([^}]*?)[:/](https://link.gitcode.com/i/babb4b1ee593bbbccb5bd7569792dd65)\}, r\1, line) # ② {汉字/注音}、{汉字:注音} 保留汉字 line re.sub(r\{.*?\}, r, line) # ③ 兜底删除其余所有花括号及其内容 return line第一步处理{标记}内容{/标记}形式的成对标签保留内容第二步按/或:切分注音并保留汉字部分第三步兜底删除所有剩余花括号块。整个过程与文档描述完全一致会先按照这些模式尝试去除注音然后去除所有花括号及其内的内容。Unicode 正规化全角转半角用途对文本执行 Unicode 等价性正规化最常见的效果是全角字符转半角。示例摘自文档 ’ 会转化成???(I guess he doesn’t want to talk to strangers...)。源码实现post.pydef unicode_normalization(text: str, args: dict) - str: return unicodedata.normalize(args.get(type, NFKC), text)它直接调用 Python 标准库unicodedata.normalize。注意这是默认启用的处理项postprocessconfig.json 中fulltohalf: {use: true}并且可以通过参数type在NFD / NFC / NFKD / NFKC四种形式间切换配置中默认NFKC即兼容性分解再组合这正是全角转半角的来源。处理英文与日文混排文本时这一项对翻译质量的提升非常明显。截取指定行数用途当 HOOK 一次读到多行文本、而你只需要其中一部分时按行截取指定数量的行。参数见 postprocessconfig.json截取行数maxzishuintspin 类型范围-99999999 ~ 99999999默认1截取末尾cut_reverseswitch 类型默认true即默认从末尾取。源码实现post.pydef 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即当总行数大于设定的行数时才触发截取截取末尾开启时取最后 N 行否则取最前 N 行。与文档描述一致该方法会截取截取行数所指定的行数如果激活了截取末尾则会截取末尾的指定行数文本。过滤尖括号 用途本质是过滤 HTML 标签。div、/div、div iddsds等都会被删除。文档指出这主要用于 TyranoScript 游戏——这类游戏引擎的 HOOK 提取到的文本是 innerHTML天然带有大量标签。源码实现post.pydef remove_angle_brackets(line: str) - str: line re.sub(r(.*?), r, line) return line过滤换行符用途把多行文本合并为一行。关键行为差异文档强调如果源语言不是日语那么当过滤换行符时将会把换行符替换成空格而非过滤掉来避免多个单词连到一起——即日文场景删除换行非日文场景如英文用空格连接避免单词粘连。源码实现post.pydef remove_line_breaks(line: str) - str: ws getlangsrc().space line ws.join(sect for sect in line.splitlines() if sect) return line分隔符取自源语言对象getlangsrc().space日语源的space为空串直接拼接其余语言源的space为空格。同时if sect会跳过空行避免产生多余分隔。HOOK 专用去重五个去重算法的适用场景与原理这是文本处理中最核心、也是文档着墨最多的一组方法。它们的共同背景是游戏绘制文字时往往画一遍正文、画一遍阴影、再画一遍描边HOOK 就会多次提取到被重复绘制的内容。五个过滤器全部带有isHookOnly: true标记见 postprocessconfig.json即只对 HOOK 模式生效对 OCR、剪贴板等文本源无效。① HOOK 去除重复字符 AAAABBBBCCCC-ABC现象与原因文本因多次绘制同一批字符而变成恵恵恵麻麻麻さささんんん…这种逐字符重复的形态。效果文档示例恵恵恵麻麻麻さささんんんははは再再再びびび液液液タタタブブブへへへ視視視線線線ををを落落落とととすすす。。。→恵麻さんは再び液タブへ視線を落とす。参数默认已启用见 postprocessconfig.json重复次数(若为1则自动分析去重)intspin范围1~10000默认1保持非重复字符switch默认true。实现细节post.py当重复次数指定为2的确定值时直接按该值抽样当为1时自动分析——用Counter统计连续相同字符的游程长度的出现频次取出现最多的游程长度作为去重步长若步长为 1 且存在更大的次高频游程则向后顺延一位。保持非重复字符开启时只跳过连续相同且长度为步长的块其余字符原样保留关闭时则按步长整段取样。文档特别提醒默认的重复次数是 1 会自动分析重复的字数但也有分析得不准确的情况建议指定一个确定的重复字数——这是最常用的过滤器遇到误判时优先手动指定重复次数。② HOOK 去除重复行 ABCDABCDABCD-ABCD现象与原因与①类似但通常不是反复刷新而是快速一次性刷新多次形成整行整行的重复。效果文档示例恵麻さんは再び液タブへ視線を落とす。恵麻さんは再び液タブへ視線を落とす。恵麻さんは再び液タブへ視線を落とす。→恵麻さんは再び液タブへ視線を落とす。参数同样提供重复次数(若为1则自动分析去重)默认1自动分析建议手动指定确定值。实现细节post.py指定次数时验证line[:len//n] * n line后截取自动分析时从len(line)向下试探找到最大的 n 满足前len//n个字符重复 n 次恰好等于整行然后返回line[:len//n]。③ HOOK 去除重复行 S1S1S1S2S2S2-S1S2现象与原因每个句子的刷新次数不完全相同无法用统一倍数去重只能完全交给程序分析。效果文档示例文本中恵麻さん……ううん、恵麻ははにかむように私の名前を呼ぶ。重复 3 次、なんてニヤニヤしていると、恵麻さんが振り返った。无重复、私は恵麻さんの目元を優しくハンカチで拭う。重复 2 次最终分析得到恵麻さん……ううん、恵麻ははにかむように私の名前を呼ぶ。なんてニヤニヤしていると、恵麻さんが振り返った。私は恵麻さんの目元を優しくハンカチで拭う。实现细节post.py采用贪心匹配从行长度的一半开始递减试探检查前dumplength个字符与紧跟其后的dumplength个字符是否完全一致一致则把这段加入结果缓存、跳过该段继续若某段无法找到任何重复则把该位置单字符保留后继续。文档如实说明因为过于复杂会存在少许的分析错误这也是无法避免的但一般都能基本正确地得到结果。④ HOOK 去除重复行 ABCDBCDCDD-ABCD逐字符前缀收缩现象与原因HOOK 到的显示文本函数其参数是指向显示文本的指针每显示一个字符就调用一次且每次把参数指针后移一个字符。于是第一次调用已拿到完整文本后续每次输出剩余子串直到长度递减为 0。效果文档示例恵麻さんは再び液タブへ視線を落とす。麻さんは再び液タブへ視線を落とす。さんは再び液タブへ視線を落とす。んは再び液タブへ視線を落とす。…す。。→ 分析后恢复为恵麻さんは再び液タブへ視線を落とす。实现细节post.py统计各行内字符出现频次按频次从高到低处理对每个高频字符从其最后一次出现位置向前比对扩展收集候选片段最终取其中最长的候选作为真实文本。⑤ HOOK 去除重复行 AABABCABCD-ABCD逐字符累加绘制现象与原因每绘制一个字符都会把前面所有字符再绘制一遍形成恵麻恵麻さ恵麻さん恵麻さんは…这样前缀不断变长的形态。效果文档示例恵麻恵麻さ恵麻さん恵麻さんは恵麻さんは再恵麻さんは再び…恵麻さんは再び液タブへ視線を落とす。→恵麻さんは再び液タブへ視線を落とす。实现细节post.py从每一位置向后取最长后缀验证当前剩余文本是否以此后缀逐步删尾后反复拼接的递减模式收集后逆向拼接还原。文档明确提示该处理最容易出错当有多行文本时会每行单独按照上面的逻辑来重复从而带来更多的复杂性。由于过于复杂这个处理经常难以正确处理如果遇到了建议写自定义 Python 处理来解决。进阶方法字符串替换与自定义 Python 处理当内置过滤器无法覆盖特定游戏/引擎的怪癖时LunaTranslator 提供了两个高自由度出口。字符串替换替换与过滤二合一用途文档指出它*不止是替换主要也可以用来过滤*——例如把固定的若干乱码字符、反复刷新的倒三角字符等替换成空字符串即可实现过滤。三个开关的组合逻辑文档逐条说明均可在设置界面独立开关均不激活普通字符串替换把替换内容中匹配的原文替换为对应值激活转义输入内容按转义字符串而非字符串字面量解析。例如用\n表示换行符从而可以过滤仅在换行符前后出现的字符激活正则按正则表达式替换。源码实现post.py 与 utils.py替换规则列表internal中每条规则是一个字典支持key查找、value替换、escape转义、regex正则、whole-word整词匹配、case-sensitive大小写敏感六个字段。其执行逻辑为if fil.get(escape, False): key safe_escape(key) # 把 \n 等转义序列解析为真实字符 value safe_escape(value) if not fil.get(regex, False): key re.escape(key) # 普通替换时先转义正则元字符 value functools.partial((lambda value, _: value), value) if fil.get(whole-word, False): key r\b key r\b flags 0 if fil.get(case-sensitive, False) else re.IGNORECASE line re.sub(key, value, line, flagsflags)可以看到即使不勾选正则内部仍走re.sub只是先对key做了re.escape保证字面匹配不勾选大小写敏感时默认忽略大小写\b整词边界只对英文词有意义。safe_escapeutils.py本质是codecs.escape_decode负责把\n、\t等转义序列解码为真实字符。自定义 Python 处理终极兜底方案用途撰写 Python 脚本进行任意复杂度的处理是文档在多个复杂去重场景下反复推荐的兜底方案。模板自动生成当处理脚本不存在时LunaTranslator 会自动在userconfig目录下生成mypost.py内容为 myutils/template/mypost.py 中的模板def POSTSOLVE(string: str): # 请在这里编写自定义处理 return string调用机制post.py处理项_11会调用mod.POSTSOLVE(line)其中模块通过checkmd5reloadmodule(file, module)按 MD5 校验加载——每次文本处理时都会检测脚本文件的 MD5一旦你修改了脚本就会自动热重载无需重启程序。此外在 myprocess.py 中还有一种类式自定义处理Process类 process_before/process_after钩子适合需要同时干预翻译前后两个阶段的场景其脚本路径同样可通过gobject.getconfig(myprocess.py)配置。注意POSTSOLVE接收的是经过前面所有已启用过滤器处理之后的文本因此应放在管线末尾用于做最后一公里的修正。执行顺序、组合策略与内嵌翻译的约束执行顺序postprocess_rank所有处理项按postprocess_rank列表的顺序依次执行。从 post.py 可以看到模块加载时会自动把源码中已实现的所有处理项键补充进该顺序列表globalconfig[postprocess_rank] [ key for key in globalconfig[postprocess_rank] if key in processfunctions.keys() ] globalconfig[postprocess_rank].extend( processfunctions.keys() - set(globalconfig[postprocess_rank]) )建议的编排思路是先用基础清洗控制字符、英文标点、Shift-JIS 过滤打底 → 再按需做 Unicode 正规化与换行合并 → 然后处理 HOOK 重复字符→行→ 最后用字符串替换或 Python 脚本收尾。遇到特定游戏时可先观察原始文本形态倒推出应该启用哪些过滤器及其先后顺序。内嵌翻译的可用范围isExUse内嵌翻译嵌入模式为了降低游戏崩溃概率只允许少数处理项生效。文档明确列举可用方法为过滤换行符、字符串替换、自定义python处理、过滤尖括号、去除花括号{}。在配置文件中这对应各处理项的isExUse: true标记见 postprocessconfig.json 中_6EX、stringreplace、_11、_4、_1等条目。在 post.py 中这一约束由如下逻辑强制实现if not useAll and isEx and not config.get(isExUse, False): continue if not useAll and not isFromHook and config.get(isHookOnly, False): continue即内嵌翻译调用POSTSOLVE时isExTrue会跳过所有未标记isExUse的处理项而所有isHookOnly去重过滤器也都不会作用于非 HOOK 文本源。这意味着如果你依赖去重过滤器必须使用 HOOK 模式而不是内嵌翻译。多配置与存档级文本处理LunaTranslator 还支持按游戏分别保存文本处理配置post.py当当前游戏关闭了跟随默认textproc_follow_default时会读取该游戏存档中的rank与postprocessconfig甚至可指定独立的posts/{name}.py自定义脚本。相关多配置机制的进一步说明可参考 docs/zh/gooduse/multiconfigs.md。常见问题与排查建议去重后仍有残留重复优先为_2/_3指定确定的重复次数而不是依赖自动分析1因为自动分析在文本不规则时可能取错步长。AABABCABCD-ABCD类场景处理失败文档明确建议改用自定义 Python 处理mypost.py的POSTSOLVE利用热重载机制边改边试。英文文本被错误删掉换行/连词检查过滤换行符是否开启——非日语源语言下它会以空格连接同时确认过滤英文字母过滤英文标点是否误开。内嵌翻译与 HOOK 行为不一致这是设计约束而非 Bug内嵌翻译只执行isExUse标记的少量处理项去重类过滤器均不生效请改回 HOOK 模式使用完整管线。过滤「」以外的字符后文本变空该实现只在存在匹配时替换若游戏不使用「」引号则不会误删若启用了其他过滤器导致引号先被删除可调整执行顺序。结语LunaTranslator 的文本处理体系是一个基础清洗 → 规范化 → HOOK 去重 → 高级定制的多层管线底层由 post.py 中 20 余个纯函数实现配置与默认值集中在 postprocessconfig.json执行顺序由postprocess_rank编排并以内嵌翻译白名单isExUse与 HOOK 专属isHookOnly双标记约束作用域。掌握了每种方法的成因、算法与参数再配合自定义 Python 兜底绝大多数 Hook 乱码问题都能在几分钟内得到干净的解决。【免费下载链接】LunaTranslator视觉小说翻译器 / Visual Novel Translator项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网