由於歷史因素,中文的寫法分為正體中文 (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 $_;