|
~ To be, or not to be, or to let it be. ~ null-i.net |
| Linux/Mozc辞書データで漢字変換〜DB用コード | |
|
Mozcのdictionary01.txtを圧縮して、DBへ入れる例(2019-08-09) 前置き†基本的な漢字変換が行える情報ををMozc辞書から抜き出すことを目的としたコードの例になります。 shell†#!/bin/sh
# 事前に用意するものは以下
# - dictionary.txt の一式 (dictionary00.txt から 09.txt)
# - suffix.txt
# - このshell。パーミッションを忘れずに。(chmod 744とか)
# - wash_dic.pl (これも実行権限を付けて、perlのPATHは合わせる)
# - ツールが出力先とするディレクトリは事前に mkdir しておく
# db/ tsv/ の2つ
#
# 事前にインストールが必要なものは、環境により異なりますが、
# sqlite3コマンド、perlのUnicode::Japaneserライブラリ、など
#
# その状態でこのshellを実行すると、 db/ に変換後のSQLite DB 一式と、
# そのDBの対応表となる case.txt が作成されます
#
#--- 以下、このshellの仕様の話 ----
# 漢字変換に使えるDBを作成する
#
# Androidアプリとして使用するために、1DBのサイズは1MB以下にする必要がある
# - 分割したDBはAndroid java のコードで参照するので、
# 対応表を case.txt として出力する
# - Androidアプリで使うのでなければ、この分割処理は不要
#
# dictionary.txt から必要な情報のみを拾い、検索の優先順位を並び替える
# (dictionary.txt: "かな,左文脈ID,右文脈ID,コスト,漢字")
# - 今回は文脈解釈は一切しない、コストのみを手がかりにする
# - もっとも低いコストを採用してソート&マージする
# - ただし人名、組織、地域、引用文字列(?)は優先度を下げる
# - 漢字変換には要らなさそうな単語は削る
#
# ./wash_dic.pl で文字を洗う。
# - 変換前後で同じ文字列の場合は、変換後の文字は省略してサイズを減らす
# - コストを振り直す(127以下へ)
#
# dictionary99.txt を自作・追加した
# これを作成した 2019.06時の辞典では「令和」が無かったので
# れいわ 1916 1916 1598 令和
#
echo "sort text files" #------------------------------------
cat dictionary0*.txt dictionary99.txt | sort -k1,1 -k5,5 -k4,4n > dic.dic
echo "unique by src/dst"
awk '
BEGIN {
L_SRC="src"
L_IDL=0
L_IDR=0
L_COST="0"
L_DST="dst"
}
{
if($5 ~ /.*[!\?].*/ && $5 !~ /^[!\?]+$/){
# !?を含む単語(!?のみは除く)は、ほぼ固有名詞なので除く
# 予測変換が有効だと、生きてくるのだと思うが...
next;
}
if($5 ~ /.*[・=].*/ && $5 !~ /^[・=]+$/ && $5 !~ /.*[^ぁ-んァ-ンヴ0-9ー・=].*$/){
# おなじく、・=を含む単語は除く。漢字は除外(空港施設とか、地名との組み合わせとか)
next;
}
if($1 == L_SRC && $5 == L_DST){
n_cost=$4;
if(1917 <= $2 && $2 <= 1926 && 1917 <= $3 && $3 <= 1926){
# 人名(1917)〜引用文字列(1926)は優先順位を下げる
n_cost=c_cost+1000000
}
# (人名以外で)より低いコストで上書きするが
# 実動作と目視確認の結果、数字や接尾のコストが強すぎた
# 名詞数字: 兆、澗、垓 はコストが0
# 名詞接尾: 展(0, 4298)、産(1, 836, 5768) など
# なので、複数かつコスト0/1の場合は、他方の数字を優先させる
if(L_COST > n_cost && n_cost > 1){
L_COST=n_cost;
}
else if(L_COST < n_cost && L_COST <= 1){
L_COST=n_cost;
}
}else{
if(L_SRC == "src" && L_DST == "dst"){
# 初期値だけは無視
}else{
if(L_SRC == L_DST){
# 無変換の候補は最後に持ってくる
L_COST=L_COST+2000000
}
printf("%s %s %d\n", L_SRC, L_DST, L_COST);
}
L_SRC=$1;
L_COST=$4
L_DST=$5
if(1917 <= $2 && $2 <= 1926 && 1917 <= $3 && $3 <= 1926){
# 人名(1917)〜引用文字列(1926)は下げる
L_COST=L_COST+1000000
}
}
}' dic.dic > dic.dic.tmp
echo "sort again by cost" #--------------------------------
cat dic.dic.tmp | sort -k1,1 -k3,3n -k2,2 > dic.dic.sort
echo "make tsv file. (delete cost, add index)" #----------
awk '{
printf("%d %s %s\n", NR, $1, $2);
}' dic.dic.sort > dictionary.tsv
./wash_dic.pl dictionary.tsv # dictionary.washed を生成
echo "wash suffix.txt" #----------------------------------------
cat suffix.txt |\
sort -k1,1 -k5,5 -k4,4n |\
awk '
BEGIN {
L_SRC="src"
L_IDL=0
L_IDR=0
L_COST="0"
L_DST="dst"
}
{
if(length($1) == 1 && $1 != $5){
# suffix による一文字変換は無視する
# 19.06 現在の対象となる語は
# ッ デ 無 ノ ヘ
# 無(71:形容詞・イ段,ガル接続,無い) はsuffixを重ねる場合の想定だろうか?
# "ッ、デ" は口語表現?今回はコスト計算しないので、対象から除外する
# "ノ、ヘ" は片仮名か平仮名か漢字か、見分けがつかず混乱しやすいので除外
next;
}else if($1 == L_SRC && $5 == L_DST){
n_cost=$4;
if(L_COST > n_cost){
# 文脈を問わず、より低いコストで上書きする
L_COST=n_cost;
}
}else{
if(L_SRC == "src" && L_DST == "dst"){
# 初期値だけは無視
}else{
if(L_DST ~ /[ァ-ン]/){
# カタカナの候補は最後に持ってくる
# "です" よりも "デス" が優先される事象を避ける
L_COST=L_COST+1000000
}
printf("%s %s %d\n", L_SRC, L_DST, L_COST);
}
L_SRC=$1;
L_COST=$4
L_DST=$5
}
}' > suffix.tmp
cat suffix.tmp | sort -k1,1 -k3,3n -k2,2 |\
awk '{
printf("%d %s %s\n", NR, $1, $2);
}' > suffix.tsv
./wash_dic.pl suffix.tsv # suffix.washed を生成
echo "split tsv" #----------------------------------------
# 以下のファイル分割は、Androidアプリに入れるために1MB以下にする為の処理
rm tsv/*
awk '
BEGIN {
INDEX="-";
STATE="-";
DB_NAME="none";
DB_INDEX=0;
OUT_FILE="none";
}
function openOk() {
STATE="";
DB_INDEX++;
DB_NAME=sprintf("my_db%d", DB_INDEX);
OUT_FILE=sprintf("tsv/%s", DB_NAME);
}
{
if(substr($2, 1, 1) != INDEX){
INDEX=substr($2, 1, 1);
if(INDEX ~ /[あ-ゔ]/){ # 本当は"ヶ"の仮名が終端だけど、無視する
if(INDEX ~ /ぁ/ || INDEX ~ /ぃ/ || INDEX ~ /ぅ/ || INDEX ~ /ぇ/ || INDEX ~ /ぉ/){
# 2019.06時点では、小文字でめぼしいものはないので、無視する
STATE="-";
}else{
openOk();
}
}
else if(INDEX ~ /[ア-ヶ]/){ # "ヵ", "ヶ" を拾うのが目的
# おそらく変換に使用しないけど、DB上は乗せておく
openOk();
}
else if(INDEX ~ /[0-9]/){
openOk();
}
else{
STATE="-";
}
if(STATE == ""){
printf("case \"%s\": return \"%s\";\n", INDEX, DB_NAME);
}else{
#printf("//%s%s: %s\n", STATE, INDEX, $0);
}
}
if(STATE == ""){
SUFF="";
if(INDEX ~ /[ゑゐ0-9ゔがぎぐげござずぜぞっだづでぢどぬねのへばびぶべぼぱぴぷぺぽむめゆょらりるれろゎわをんヵヶ]/){
# サイズが64KB以下になる文字は分けない
SUFF="";
}
else if(length($2) <= 4){
SUFF="_1234";
}
else if(INDEX ~ /[あいおかし]/){
# 件数ではなく、単純にサイズの問題として
# 分割しても 500KB を超える特定文字は, 5-9 文字をより細かく刻む
if(length($2) <= 5){
SUFF="_5";
}else if(length($2) <= 6){
SUFF="_6";
}else if(length($2) <= 7){
SUFF="_7";
}else if(length($2) <= 8){
SUFF="_8";
}else if(length($2) <= 9){
SUFF="_9";
}
}
else if(length($2) <= 6){
SUFF="_56";
}
else if(length($2) <= 8){
SUFF="_78";
}
OFILE=sprintf("%s%s", OUT_FILE, SUFF);
OFILE=sprintf("%s%s", OUT_FILE, SUFF);
print $0 >> OFILE;
}
}' dictionary.washed | tee case.txt
cp suffix.washed tsv/suffix
echo "create db" #--------------------------------------------------
rm db/*
for ff in `ls -1 tsv/*`; do
SQDB=db/`basename $ff`
echo "read $ff (-> $SQDB)"
sqlite3 $SQDB " create table mozc (cost int, src text, dst text); "
sqlite3 -separator " " $SQDB ".import $ff mozc"
# indexは 単純にサイズが1.5倍近くなるので、処理速度を検証後にかんがえる
#sqlite3 $SQDB "create index my_index on my_mozc(src);"
done
shell内で使う、wash_dic.pl†#!/usr/bin/perl -w
# 事前作業として、
# 一行目のperlのPATHの変更と、Unicode::Japanese のインストール
# あと、このスクリプトの実行権限付与(chmod)をお忘れなく
#
# 実行すると "cost over:" の警告が出るはずです。
# メジャーな姓名の場合は(同音で別字の)総数が99個を超えるので
# これらは優先順位づけを諦める(99固定)という処理をやっています。
# それがどの程度の頻度か、動かしてみてご確認下さい。
#
#---------------------------------------
# ソート済みの変換表一覧 通番,かな,漢字 データを洗う
# - 通番を振り直す. int-1byte(<128) に収める
# - かな=漢字 のデータは SI/SO に置き換える(chr14, 15)
# SI/SOは実際にDBを読み込む処理に合わせるので
# 任意の一文字なら何でもOKです(+/- でも #/$ でも何でも)
#
# 特にカタカナ判定は、shellだと荷が重いのでperlスクリプトにしました
#--- 以下はデバッグログ関数 ----------------
sub dbg_printf {
printf STDOUT @_ ;
$|=1;
}
sub err {
printf STDOUT @_ ;
$|=1;
}
#-------------------------------------------
# カタカナ変換は tr を使うと、どうも安定しない(指定が難しい)ので
# 別途 Unicode::Japanese; をインストールした
# それでも..."ヴ" だけは不安なので(環境依存?)、別で再変換をかける。
# use utf8;
# binmode STDOUT, ':utf8';
# $hira2kana =~ tr/ーぁ-んゔ/ーァ-ンヴ/;
use Unicode::Japanese;
my $in_file = "$ARGV[0]";
my $out_file = "dictionary_washed.tsv";
if($in_file =~ /([^\.]+)\.(tsv|txt)/){
$out_file = sprintf("%s.washed", $1);
dbg_printf("output:$out_file\n");
}else{
die "non txt or tsv[$in_file]";
}
open($IN, "< $in_file") or die "$! [$in_file]\n";
open($OUT, "> $out_file") or die "$! [$out_file]\n";
my $log_line = 0;
my $log_si_count = 0;
my $log_so_count = 0;
my $cost;
my $last_kana = "__NO_DATA__";
while(<$IN>){
$line = $_;
$log_line++;
my ($kana, $number, $kanji);
if($line =~ /^([\d]+)\t([^\t]+)\t([^\t\n]+)$/){
# 33 ゔぁーじにあ ヴァージニア
# 34 ゔぁーじにあしゅう ヴァージニア州
$number=$1;
$kana=$2;
$kanji=$3;
}else{
err("unexpected line:%s\n", $_);
next;
}
if($kana eq $last_kana){
$cost++;
}else{
$cost=1;
$last_kana = $kana;
}
if($cost > 99){
# 1byte(127) 以内に収めるのを目標にする
err("cost over: %s\n", $_);
$cost=99;
}
if($kana eq $kanji){
$kanji = chr(15); # SI shift-in
$log_si_count++;
}elsif(length($kana) == length($kanji)){
my $hira2kana = Unicode::Japanese->new($kana)->hira2kata->get;
$hira2kana =~ s/ゔ/ヴ/g;
if($kanji eq $hira2kana){
#dbg_printf("wash: %s\t%s\t%s\n", $cost, $kana, $kanji);
$kanji = chr(14); # SO shift-out
$log_so_count++;
}
}
printf($OUT "%s\t%s\t%s\n", $cost, $kana, $kanji);
}
close($OUT);
dbg_printf("total:%d, wash1:%d, wash2:%s\n", $log_line, $log_si_count,
$log_so_count);
あとがき†似たようなことに挑戦する人の参考になればと思います。 |
|