0.前言
SExtractor,功能最强大的galgmae文本提取工具,我不敢说这是一篇教程,因为有很多东西我自己也没用明白,都是一些经验之谈,故曰“心得”。
1.基本提取原理(TXT引擎)
1-1.按每行匹配
SE的基本提取原理是通过正则表达式匹配正文及人物名来将原文本中的有效信息转化为name-massage格式的json文件。如果你还不会正则,请转至Galgame的AI翻译绪论先学习正则表达式。
SE的提取引擎分为TXT和BIN两大类(忽略JSON和Xlsx)。说是两大类,其实你如果你理解得快的话,你会发现其实TXT和BIN的底层原理都是一样的,都是按每行匹配。
我们先不着急学BIN,先把TXT引擎搞懂,BIN就解决了90%。

什么是按每行匹配呢?顾名思义,我们把一个txt类型的脚本文件(这里以ks文件举例)拖进sublime里,左边这一列数字就是行,一个数字代表一行,而SE处理这个文本文件就会对每一行文件执行一次顺序命令。
我们先来确定两个概念:全局命令和顺序命令
全局命令,意思就是不管你这行命令放在哪里都不影响效果,所有诸如structure=paragraph
、separate=reg
、checkJIS=reg
等前面不带数字序号的命令都是全局命令,至于这些命令都有什么作用我们一会儿再说。
顺序命令,也就是说这些命令的执行是按顺序的。没错,就是按那些命令前面带的数字的顺序,顺序命令只有两个:\d\d_skip=和\d\d_search=


例如,对SE所举的这个示例和它的命令而言。它首先会把第一行
Text0
喂进去,然后按照顺序,对这行文本先匹配一次00_skip=^error
,发现匹配不上,于是跳过,继续执行下一顺序的命令10_search=^(?P<name>Name.*)$
,发现还是匹配不上,于是继续顺序执行下一命令,一直匹配到26_search=^(?P<unfinish>.+?)$
,哎,终于匹配上了。而且这行的所有文字都被匹配上了。于是把这行文本设置成unfinish分组。
我们先不用着急管这几个分组具体的意义,我们先把这个示例分析完。
第二行,通过10_search=^(?P<name>Name.*)$
匹配到了,于是这一行被设置成name分组
第三行,Text1。,通过25_search=^(.+?)(?<=」|。)$
匹配为msg分组(未命名分组默认为msg)
第四行,MaybeName2,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组
第五行,「Text2」,通过20_search=^(?P<pre_name>「.+」)$
匹配为msg分组,并为匹配到的上一行(不是文本文件内的上一行),也就是MaybeName2——添加name分组
第六行,MaybeName3,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组
第七行,「,通过21_search=^(?P<pre_nameANDunfinish>「.*)$
匹配为unfinish分组,并为匹配到的上一行,也就是MaybeName3——添加name分组
第八行,Text3,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组
第九行,33text,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组,并在postSkip=^[0-9]
中被淘汰,不匹配为任何分组,舍去
注意,postskip作用且只作用于已经通过search匹配到的文本。也就是说,对于每一次匹配到的文本,postskip都会进行一次检查,如果合格则保留,不合格则当成这次匹配没匹配到过任何东西。
第十行,Text333,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组
第十一行,error,通过00_skip=^error
跳过这一行,不匹配为任何分组
第十二行,」,通过26_search=^(?P<unfinish>.+?)$
匹配为unfinish分组
最终,经过十二行的匹配,我们得到了以下结果

相信大家透过上面每行的分组和最后的结果,就能对每个分组的作用猜个差不多。name分组和msg分组自不多说,而且当一个被匹配行同时属于name、unfinish、msg三个分组时,它的输出顺序为name>unfinish>msg。例如上述示例中,MaybeName2和MaybeName3都同时既属于name分组又属于unfinish分组,故按照顺序作为name分组输出。
特别地,对于unfinish分组,如果匹配时在一行msg分组之前有几个连续的unfinish分组,那么这几个unfinish连同这行msg会导出成为一行msg,并以\r\n
(可以在SE设置里调)来隔开原来不同的行。而如果一行unfinish分组的下一行是name分组或被skip命令跳过(这一实现依赖于structure=paragraph
,等会儿说不着急),那么这行unfinish分组将被当成msg分组处理。例如第七八九十行,最终匹配到了连续三组unfinish,并紧接着下一行被skip命令跳过,故第十行的unfinish分组被当成msg分组处理,连上前面两个unfinish,共同输出成「\r\nText3\r\nText333
这一行msg
1-2.structure=paragraph及两个段落选项
在以上实例中,如果我们去掉structure=paragraph
这行全局命令,那么提取出的文本会变成这样:

官方文档对于这行命令的解释是:提取结构,当为paragraph
时才会处理非name或msg的分组名,比如unfinish
。(不是所有引擎的正则都支持,TXT
和BIN
引擎肯定支持)
我反正是没咋看懂这句话,在我的心得中,这行命令只有两个作用:
1.让pre(del)_系列分组对上一被匹配行生效(仅限上一被匹配行与当前被匹配行中间没有skip打断的情况)。
2.让skip命令能够打断结构(也就是如果一行unfinish分组的下一行被skip命令跳过,那么这行unfinish分组将被当成msg分组处理)。
先来解释第一个作用。例如我们看MaybeName2和MaybeName3这两行,这两行都被匹配为了unfinish分组,其中MaybeName2的下一行「Text2」的分组是<pre_name>(相当于<pre_nameANDmsg>),意思是把这一行匹配为msg分组,并为匹配到的上一行——也就是MaybeName2——添加name分组。但是由于没有structure=paragraph
这行命令,<pre_name>无法为上一被匹配行的MaybeName2添加name分组。MaybeName3同理,于是这两行unfinish就继续以unfinish分组的身份参与输出,最终获得了等同于“从name掉到了msg上”的效果。
第二个作用相对好理解,原本由于第十一行被skip跳过,导致第十行的unfinish分组被当成msg分组处理,从而使第十行和第十二行成为两行单独的msg,但是在没有structure=paragraph
的情况下,即使第十一行被skip命令跳过,第十行依然保持unfinish分组,故这次第十行和第十二行就连起来了。
如果你只想要第一个作用而不想要第二个作用,那么你可以在加入structure=paragraph
命令的同时在设置里开启段落:skip不影响上一个unfinish
选项
然后提取出的文本就会变成这样:

看到这里,可能大家会好奇旁边的那个段落:skip不影响上一个ctrl
是什么意思,为了方便演示,我们稍微修改一下原文本(另外还有一点应该不用我说,看到段落两个字大家都能明白,这几个选项只有在structure=paragraph
的时候才起效):

此时如果我们两个选项都不选,那么提取出的文本会是这样的

因为我们说了,structure=paragraph
的第一个作用“仅限上一被匹配行与当前被匹配行中间没有skip打断的情况”下才能生效,而现在MaybeName2和「Text2」中间被skip跳了一次,所以「Text2」的<pre_name>分组仍然无法为MaybeName2添加新分组
但在开启不影响ctrl后,就可以变成这样:

可见,原本「Text2」的<pre_name>本应因一次skip的阻挡而无法为MaybeName2添加或删除分组,结果这个选项使「Text2」的<pre_name>分组强行为MaybeName2添加了分组。也就是说,这个选项的意义是强迫pre(del)_系列分组对前一被匹配行生效,即使前一被匹配行与当前被匹配行中间有一行或多行被skip命令跳过。
PS:本文的“行”字有时意义非常不同,一定要注意区分txt的文本行和SE的被匹配行的区别。
1-3.predel_系列分组及段落:name保留当前ctrl
这里的del指的是delete,删除的意思。也就是说,它的意义是删除前一被匹配行所属的某个分组。大家肯定已经了解到了,一个被匹配行可以有多个所属分组,所以我在这里用“某个”。
还记得我们前面说的“如果一行unfinish分组的下一行是name分组,那么这行unfinish分组将被当成msg分组处理”吗?这一效果的实现,其实在官方文档里也有说明,即“name
默认会predel_unfinish
”。也就是说,如果当前被匹配行的分组是<name>,那么上一被匹配行将默认被除去unfinish分组的身份。在上文示例中,失去了unfinish分组的匹配行也不属于其它任何分组,那么它将变成未命名分组,而未命名分组默认又为msg,所以才达成name截断unfinish这一效果。
我们再举issue中的一个示例,以下为示例文本及正则:


我们来分析这五行文本:
第一行,のの,被10_search=^(?P<nameANDunfinish>のの)$
同时匹配为name分组和unfinish分组
第二行,「一二三四」,被15_search=^(「.+」)$
匹配为msg分组
第三行,のの,被10_search=^(?P<nameANDunfinish>のの)$
同时匹配为name分组和unfinish分组
第四行,ちゃん,被20_search=^(?P<unfinishANDpredel_name>.+)$
匹配为unfinish分组,并同时删除上一匹配行——のの——的name分组
第五行,一二三四,被20_search=^(?P<unfinishANDpredel_name>.+)$
匹配为unfinish分组,并并同时删除上一匹配行——ちゃん——的name分组(但因为ちゃん本来就不属于name分组所以无所谓)

于是提取出的文本就变成了这样。至此,我们也可以解释最后一个选项段落:name保留当前ctrl
了。如果现在关了它,那么导出的文本就会变成:

可见,此选项开启的意义为让name分组可以被删除。
2.BIN引擎
在讲BIN之前我还是想说一下,如果一部游戏你已经沦落到要去自己写BIN正则,那你可能大概率提出来也没有用,封回去也全是问题(即使做截断)。所以,用BIN提完文本后,一定要做好充足的测试,不要等翻完了封回去丢进游戏里发现什么都运行不起来,白浪费钱。
BIN引擎的底层逻辑其实和TXT引擎完全一样,只是BIN不像TXT那样有换行,所以你得自己给BIN设置怎么分行,没错,就是separate=reg
这一命令。
例如对这样的一个文件和其正则,我们来进行分析


由于separate=\x00
,也就是以\x00(\x代表其后跟着的两个数字是十六进制数值,也就是我们在winhex上看到的左边一大坨)为换行标志,所以这个BIN文件本质上被分割成了只有三行
第一行是81 79 81 79 81 80 81 80 81 91 81 91
第二行是81 79
第三行81 79 81 79 42 81 79
首先对第一行执行00_skip=^[\S\s]{0,3}$
,未匹配上,跳过
由10_search=^([\x81-\xFC][\S\s]+)$全行匹配为message分组,然后执行checkJIS=[\r\n]
,均为全角符号,通过
第二行在00_skip=^[\S\s]{0,3}$
中直接被skip掉
注:在BIN引擎下,[\S\s]
代表的是任意一个地址。也就是说,00_skip=^[\S\s]{0,3}$
的意思是skip掉一行只有0-3个地址的行。
第三行,由10_search=^([\x81-\xFC][\S\s]+)$
匹配为message分组,然后执行checkJIS=[\r\n]
,有半角字符42(对应B),不通过,舍去。如果换成checkJIS=[\r\nB]


就不会被check掉了。
另外,如果你的一行中有错误字符,如下图

我们添加的第四行81 79 81 20 81 80 81 91 81 92中,81 20就是一个错误字符,81相当于在告诉电脑“我是一个全角字符,记得把我后面的地址一起读上”,结果电脑读完之后发现81 20对不上cp932的任何一个字符,它就蒙了,如果你开启checkJIS,那么这行就会被check掉。如果你不开check,也不开ignoreDecodeError=1
,那SE就会报错。另外的,ignoreDecodeError
虽然会忽略错误字符,但它会直接把原本应该有报错的那行全部舍去,相当于一个不去半角的checkJIS。但有时你开ignoreDecodeError
不开checkJIS又会匹配一些虽然合法但有病的字符,具体你们可以自己找个文本试一试。
3.CSV引擎
我们以GT生成的这个csv替换表为例进行说明

一般来说,最上面的这一行JP_Name,CN_Name,Count
叫列名。即这张csv表有三列,列名分别为
。而JP_Name CN_Name 和 Count
extractKey=reg
则代表了你想匹配哪几列。比如我想匹配
这两列,那么我只需要将JP_Name 和 CN_Name
extractKey
改为extractKey=^(JP_Name|CN_Name)$
即可
当正则为

时,输出的json如下:

可见,csv的匹配顺序为从上至下、从左至右的。
但有时我们的表可能没有列名,比如下面这种情况

这时我们可以加上extraData=nohead
来将extractKey改为按列序号匹配。比如此时我想把第一列匹配为name而将第二列匹配为msg,当使用以下正则时(列索引从0开始)

所提取出的文本是这样的:

上述正则中,extractKey=name0,1
的意思是将第0列送入匹配,并为第零列所有匹配到的内容自动添加name分组,将第1列送入匹配(,不送入第二列进行匹配)。
不过extraData=nohead
有个bug,当extractKey
送入匹配的某一列全是纯数字时,SE会报错。
4.其它
1.如果碰到name在mssage后面的情况,可以打开导出时name向上移动一位
。
2.下面那个强制设定名字,被设置的名字间用英文,隔开。当且仅当你的整个被提取行与某个名字完全匹配时,此被提取行被设定为name分组。
3.如果separate=reg
中的reg使用了多个匹配式,如separate=(\x00|\xFF)
,那么一定不能省略括号,否则即使提取出来的时候是对的,回封的时候也会出错。separate多余提出来的东西可以用postSkip删掉。
4.SE的命令是分大小写的,postskip没用,得postSkip。
5.替换字典可以自己更改,Tool/Font下也可以根据自己的字典制作相应的日繁替换字体,font to pic可以制作标准的cp932位图字体(漩涡社后期的yuris引擎常用)

自然地SE的字典读取路径也可以改。
FontCreator改字体名字的时候照着原字体把这几页看着哪个跟名字有关楞抄了就行(别改per EM)

也可以把各种奇奇怪怪的字符都加进来(比如给黑体加音符)
替换后的字体除了可以安装和uif外,也可以使用士佬制作的FontChanger(Hook大部分函数)来改,基本上这玩意替换不了的字体要么是位图,要么是编译后字体了。
https://pan.baidu.com/s/1Pzjj-OqAaoV3s61U7077bQ?pwd=apei
其它想到再写。