技术杂谈:用 Perl 制作正简 (繁简) 中文自动转换的小工具

PUBLISHED ON DEC 10, 2018

    由于历史因素,中文的写法分为正体中文 (traditional Chinese) 和简体中文 (simplified Chinese) 两种。这两种文字算是同一种语言的两种变体,稍加学习后阅读上应该不会太困难。不过,如果能根据不同网站访客的习惯给予相对应的文字,对于访客来说更加方便。我们先前在这里介绍在网页客户端转换文字的方式,本文则是介绍转换文字的小工具,两者的使用时机不同,可以相互参考。

    正简转换

    基本原理

    比起文字翻译,正简 (或繁简) 转换会来得简单一些,因为正体字和简体字算是同一种语言的变体,文法是通用的。正简转换注重的是字词间的转换,依照转换的粒度 (granularity),可分为 (1) 字和字对转和 (2) 词和词对转两种。

    许多工具会实现字和字转换,这是因为字对字转换在实现上比较简单。在繁转简时,由于一字多义的情形较少,直接用字符编码转换通常可顺利转换。但在简转繁时,由于一字多义的情形比较多,需要考虑上下文来转换,混合词对词转换反而比较能够抓到字词转换的语境。

    其实词和词对转在实现上不会太困难,也是要准备一份词语对照表;但有时无法一对一对转,需考虑上下文语境。一般常见的方式是从最长的词语优先转换,接着才转换短词语,因为长词语专一性较高,转错的机率比较低。

    演算思维

    本文的转换程序没用到机器学习 (machine learning) 这类复杂的算法,而用相对简单的想法来转换文字。我们使用 (1) 长词语优先和 (2) 专业词语优先的方式来转换,因为这两类词语的专一性较高,比较不会转错。参考以下的流程:

    • 将 6 字符的电脑用语由繁转简
    • 同上,依序转换 5 字符到 2 字符的电脑用语
    • (未实现) 将 6 字符的生活用语由繁转简
    • (未实现) 同上,依序转换 5 字符到 2 字符的生活用语
    • 将其余的字符由繁转简

    为什么特地要挑出电脑用语呢?因为笔者的网站大部分都是这方面的内容,故会优先转换这类内容。如果读者的网站内容是不同的领域,也可以改用该领域的内容来转换。接着,会将生活用语的部分进行转换,这是笔者之后想在这个工具中加入的部分。会不会在转换生活用语时因过度转换而出错呢?虽然有可能但机率不高,这部分还需要更多的实测来确认。最后则以字对字转换转换做为退路 (fallback)。

    程序实现

    在本例中,我们将其转成 JSON 格式,因为 JSON 的键值对刚好适合用来储存这类型的数据,之后要重新用别的程序语言实现这类工具时,转换上不会太困难。

    一开始要先引入相关的模块:

    use utf8;
    use open qw(:utf8);
    use Encode qw(encode_utf8 decode_utf8);
    use JSON;

    Perl 和 UTF8 相关的情境有 (1) 脚本本身、(2) 终端机的输出入、(3) 文件的输出入等,不同的模块适用不同的情境。

    设定以 UTF8 来输出入文字:

    binmode(STDIN, ":utf8");
    binmode(STDOUT, ":utf8");
    binmode(STDERR, ":utf8");

    BEGIN 区块中载入词语表:

    BEGIN {
        # Load the 6-character term table.
        open $FH_6, "<", "ITDict_6_ts.json";
        binmode($FH_6, ":utf8");
        $IT_term_6_ts_ref = decode_json encode_utf8(<$FH_6>);
        %IT_term_6_ts = %$IT_term_6_ts_ref;
        $check_6 = join "|", keys %IT_term_6_ts;
        close $FH_6;
    
        # Load more term tables.
    
        # Load the character table.
        open $FH, "<", "tongwei_ts.json";
        binmode($FH, ":utf8");
        $tongwei_ts_ref = decode_json encode_utf8(join "", <$FH>);
        %tongwei_ts = %$tongwei_ts_ref;
        $check = join "|", keys %tongwei_ts;
        close $FH;
    }

    为什么要将这些程序代码写在 BEGIN 区块呢?因为我们的程序会以行 (line) 为单位读取文字文件。写在 BEGIN 区块内的程序只会执行一次,之后就可以重覆使用读入的表格。

    在读入文件时,要用 binmode 以 UTF8 编码读取词语表,这和标准输出入相同。

    我们将词语表内的词用 join函数串在一起,因为我们要把 $check_6 做为常规表示式使用。其他的表格也是同样的道理。

    进行词语转换的任务:

    # Decode the input string.
    $_ = decode_utf8 $_;
    
    # Perform term-to-term conversion.
    s/($check_6)/$IT_term_6_ts{$1}/g;
    s/($check_5)/$IT_term_5_ts{$1}/g;
    s/($check_4)/$IT_term_4_ts{$1}/g;
    s/($check_3)/$IT_term_3_ts{$1}/g;
    s/($check_2)/$IT_term_2_ts{$1}/g;
    
    # Perform character-to-character conversion.
    s/($check)/$tongwei_ts{$1}/g;
    
    # Encode the output string.
    $_ = encode_utf8 $_;

    读者可能会觉得这个程序没头没尾的,这是因为我们的程序会以行为单位来执行,每次程序会读入一行后执行上述程序代码。

    一开始要先用 decode_utf8 将输入译码,之后 Perl 才能解析。我们这里把常规表示式做为查表的工具,查到词语符合时就进行代换,这样写会比直接用循环扫字串来得更简洁。最后要输出文字前记得将文字用 encode_utf8 再将文字编码一次,要不然输出的文字会变成乱码。

    使用本程序的指令如下:

    $ perl -00 -p -i.bak zhConvert.pl path/to/file.txt

    藉由 -p,我们可以将文字文件以行为单位读入。在默认情形下,修改后的文字会输出到终端机,搭配 -i 可将输出直接写入文字文件,这时就不会输出到终端机。我们在这里搭配 -00 参数,可将文字文件以段落 (paragraph) 为单位输入,避免因文字换行造成转换错误。

    最后附上这个程序的完整程序代码:

    use utf8;
    use open qw(:utf8);
    use Encode qw(encode_utf8 decode_utf8);
    use JSON;
    use File::Spec;
    
    BEGIN {
        binmode(STDIN, ":utf8");
        binmode(STDOUT, ":utf8");
        binmode(STDERR, ":utf8");
    
        # Load the 6-character term table.
        open $FH_6, "<", File::Spec->rel2abs("ITDict_6_ts.json");
        binmode($FH_6, ":utf8");
        $IT_term_6_ts_ref = decode_json encode_utf8(<$FH_6>);
        %IT_term_6_ts = %$IT_term_6_ts_ref;
        $check_6 = join "|", keys %IT_term_6_ts;
        close $FH_6;
    
        # Load the 5-character term table.
        open $FH_5, "<", File::Spec->rel2abs("ITDict_5_ts.json");
        binmode($FH_5, ":utf8");
        $IT_term_5_ts_ref = decode_json encode_utf8(<$FH_5>);
        %IT_term_5_ts = %$IT_term_5_ts_ref;
        $check_5 = join "|", keys %IT_term_5_ts;
        close $FH_5;
    
        # Load the 4-character term table.
        open $FH_4, "<", File::Spec->rel2abs("ITDict_4_ts.json");
        binmode($FH_4, ":utf8");
        $IT_term_4_ts_ref = decode_json encode_utf8(<$FH_4>);
        %IT_term_4_ts = %$IT_term_4_ts_ref;
        $check_4 = join "|", keys %IT_term_4_ts;
        close $FH_4;
    
        # Load the 3-character term table.
        open $FH_3, "<", File::Spec->rel2abs("ITDict_3_ts.json");
        binmode($FH_3, ":utf8");
        $IT_term_3_ts_ref = decode_json encode_utf8(<$FH_3>);
        %IT_term_3_ts = %$IT_term_3_ts_ref;
        $check_3 = join "|", keys %IT_term_3_ts;
        close $FH_3;
    
        # Load the 2-character term table.
        open $FH_2, "<", File::Spec->rel2abs("ITDict_2_ts.json");
        binmode($FH_2, ":utf8");
        $IT_term_2_ts_ref = decode_json encode_utf8(<$FH_2>);
        %IT_term_2_ts = %$IT_term_2_ts_ref;
        $check_2 = join "|", keys %IT_term_2_ts;
        close $FH_2;
    
        # Load the character table.
        open $FH, "<", File::Spec->rel2abs("tongwei_ts.json");
        binmode($FH, ":utf8");
        $tongwei_ts_ref = decode_json encode_utf8(join "", <$FH>);
        %tongwei_ts = %$tongwei_ts_ref;
        $check = join "|", keys %tongwei_ts;
        close $FH;
    }
    
    # Decode the input string.
    $_ = decode_utf8 $_;
    
    # Perform term-to-term conversion.
    s/($check_6)/$IT_term_6_ts{$1}/g;
    s/($check_5)/$IT_term_5_ts{$1}/g;
    s/($check_4)/$IT_term_4_ts{$1}/g;
    s/($check_3)/$IT_term_3_ts{$1}/g;
    s/($check_2)/$IT_term_2_ts{$1}/g;
    
    # Perform character-to-character conversion.
    s/($check)/$tongwei_ts{$1}/g;
    
    # Encode the output string.
    $_ = encode_utf8 $_;
    你或许对以下产品有兴趣