技術雜談:用 Perl 製作正簡 (繁簡) 中文自動轉換的小工具

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    由於歷史因素,中文的寫法分為正體中文 (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 $_;
    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk