目的
かな氏名の間に空白を入れたい。かな氏名を分割したい。
例:「さくらもりかおり」 → 「さくらもり かおり」
方針
名字の辞書と突合して探す。
Wikipediaによると、日本には10~20万の名字があるらしい。
レアな名字もあるだろうから、ある程度の数の辞書を用意して、以降、判別できなかった文字を追加していけばよいのでは。
例えば、上位20位までで約2000万人ぐらいいるらしい。
仕様案①(後ろから)
後ろから1文字ずつ短くして、マッチすればそこで検索を終える。
名前の長さ分、検索を繰り返す。
例
- さくらもりかおり←NG
- さくらもりかお←NG
- さくらもりか←NG
- さくらもり←MATCH
仕様案②(前から)
前から1文字ずつ長くして、0件に絞れればそこで検索を終え、ひとつ手前を名字とする。
名字の長さ分+1回、検索を繰り返す。
- さ*←n件
- さく*←n件
- さくら*←n件
- さくらも*←n件
- さくらもり*←1件...MATCH
- さくらもりか*←0件...行き過ぎ
課題①
どちらの案にせよ、一番長い名字がマッチする。
例えば、「さくら もりこ」の時、「さくらもり こ」と分割する。
これを適正に処理したい時は、名字だけでなく名前の辞書との突合も必要になる。
この課題は保留。
課題②
辞書の整備が大変。徐々に育てていく感じかな。
実装方法
「案①(後ろから)」が簡単そうなのと、空振りをしなくてよいので少しでも速度が速いのではないかと考え、とりあえずこちらを採用。
加えて辞書を、先頭1文字で分冊にすることで、検索速度を向上させられるのではないかと愚考する。
50音といいつつ、「ん」から始まることはないと思う一方で、濁音、半濁音の可能性がある(例「がなは」)ので、70冊ぐらいになるんじゃないかと思ったり。
仮に名字の読みが7万種類あった時、70冊に分ければ1冊あたり(期待値)1,000件になるので、速度向上が期待できそう。
辞書
名字だけを記載する。重複してもよいが登録時にはねるので、重複しない方が早い。例えば以下のサンプルを参照。カタカナを利用したければカタカナで、半角カナを利用するなら半角カナで、辞書を用意すること。
あまみ
きさらぎ
はぎわら
たかつき
あきづき
みうら
みなせ
きくち
ふたみ
ほしい
しじょう
読み込み速度の検証をしたかったのだけど、名字を大量に入手できなかったので(タイピングするのが面倒だったので)Excel VBA でダミーデータを作成してみた。
Sub main()
Dim keys As String
keys = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもらりるれろわをやゆよだぢづでどがぎぐげござじずぜぞばびぶべぼぱぴぷぺぽ"
Dim pos, i, j As Integer
Dim buf As String
pos = 1
For i = 1 To 70
For j = 1 To 1000
buf = Mid(keys, i, 1) & Format(j, "0000")
ActiveSheet.Cells(pos, 1).Value = buf
pos = pos + 1
Next
Next
End Sub
コーディング
(1)目次と辞書を宣言する
目次としてIDXを用意する。
辞書DICSは配列を用意する。とりあえず1,024冊分。上限に達したら拡張するなどの仕掛けが、本来は必要。
Dictionary型を使っているのは、List型より早いと聞いたから。
private Dictionary<string, int> IDX = new Dictionary<string, int>();
private Dictionary<string, string>[] DICS = new Dictionary<string, string>[1024];
(2)人名辞書をメモリ上に展開する
コンストラクタで辞書を用意する。
引数として受け取ったファイルを読む。
先頭1文字をIDXに保存する。あれば該当するDICSに追加し、なければIDXの追加と共に、空のDICSを追加する。
次いで、DICSから取り出したdicに名前を追加する。重複すれば読み飛ばす。
かなの時は関係ないけれど、TrimとToLowerを実行している。
Try~Catch が速度的に妥当かどうかは気にしていない。
環境にもよると思うけれど、7万件の名字も数秒で読み込めたので、速度的には問題ないでしょう。
public Jisho(string infile)
{
Dictionary<string, string> dic;
StreamReader sr = new StreamReader(infile, Encoding.GetEncoding("Shift_JIS"));
int idx_cnt = 0; //目次カウンタ
int pos = 0; //利用する辞書番号
while ( sr.Peek() != -1)
{
string buf = sr.ReadLine().Trim().ToLower();
if (buf.Length > 0)
{
//目次作成
try
{
IDX.Add(buf.Substring(0, 1), idx_cnt); //先頭1文字を目次に登録
pos = idx_cnt;
idx_cnt++;
}
catch(Exception e) //キー重複時は辞書番号を取得する
{
pos = IDX[buf.Substring(0, 1)];
}
//辞書作成
try
{
if(DICS[pos]==null) //その目次に辞書が無ければ作成
{
dic = new Dictionary<string, string>();
}
else //その目次に辞書があれば取り出し
{
dic = DICS[pos];
}
dic.Add(buf, buf); //辞書に単語を追加
DICS[pos] = dic; //辞書を目次に戻す
}
catch(Exception e) //キー重複時は何もしない
{
}
}
}
sr.Close();
}
(3)照合する
対象の文字列を受け取り、空文字列でなければ処理を行うメソッド。
空文字列の時、0を返す。
先頭1文字を取り出し、目次と照合する。目次がマッチしなければ、-2を返す。
辞書を取得後、文字列を後ろから1文字づつ削りながら照合する。
マッチすれば、先頭からの文字数を返す。
マッチしなければ、-1を返す。
public int IsMatch(string value)
{
string buf = value.ToLower().Trim();
string wk;
Dictionary<string, string> dic;
int pos = 0; //目次
if(buf.Length == 0)
{
return 0;
}
//目次検索
try
{
pos = IDX[buf.Substring(0,1)]; //目次取得
}
catch(Exception e)
{
return -2; //目次自体がない
}
dic = DICS[pos]; //辞書取得
for (int cnt = value.Length; cnt > 0; cnt--)
{
try
{
wk = dic[value.Substring(0, cnt)];
return cnt;
}
catch(Exception e) //取得失敗時はデクリメントして再挑戦
{
}
}
return -1; //辞書にない
}
呼び出し方法。マッチすれば、先頭からの文字数を返すので、元の文字列をSubstringで編集すればよい。
int pos = Js.IsMatch(氏名);
if(pos > 0)
out_buf = 氏名.Substring(0, pos) + " " + 氏名.Substring(pos);
else
out_buf = buf + " " + pos.ToString();
応用1
一応、雑な実行ファイルの形にして、動作確認してみた。ご要望があれば公開予定。
(辞書ファイルと対象ファイルを読み込むと、スペース区切りにしてくれるツール)
応用2
辞書と合致するかどうかをチェックする仕組みだから、表記ゆれやタイプミスを調べるツールに使えるかも。
「桜守歌織」を「桜守香織」とタイプミスした時に、チェッカーを通せばアンマッチで見つける事ができるとか。修正は人力になるけど。