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

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

由於歷史因素,中文的寫法分為正體中文 (traditional Chinese) 和簡體中文 (simplified Chinese) 兩種。這兩種文字算是同一種語言的兩種變體,稍加學習後閱讀上應該不會太困難。不過,如果能根據不同網站訪客的習慣給予相對應的文字,對於訪客來說更加方便。我們先前在這裡介紹在網頁客戶端轉換文字的方式,本文則是介紹轉換文字的小工具,兩者的使用時機不同,可以相互參考。

{{< figure src="img/blog/zh-convert.png" alt="正簡轉換" width="70%" >}}

基本原理

比起文字翻譯,正簡 (或繁簡) 轉換會來得簡單一些,因為正體字和簡體字算是同一種語言的變體,文法是通用的。正簡轉換注重的是字詞間的轉換,依照轉換的粒度 (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 $_;
關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。