こんにちは、九兵衛です。前回の記事 (https://qiita.com/kyubey1228/items/e1ade213a04daad6713e) では、Ruby gemの基本的な構造と設定について説明しました。
今回は、実際にgemの中身を実装していく過程を共有したいと思います。
正直なところ、この過程は予想以上に複雑で、多くの発見と驚きがありました。
1. データファイルの準備と驚きの発見
まず最初に、中国語の文字を判別するためのデータファイルを用意しました。主に2つのファイルを使用しています:
-
Unihan_Variants.txt
: Unicode consortiumが提供する中国語変体字のデータ -
traditional_chinese_list.txt
: 追加の繁体字リスト
これらのファイルを詳しく調べていく中で、私は中国語の文字の複雑さに圧倒されました。例えば、「門」という字は日本語でも使いますが、中国語では簡体字で「门」と書きます。しかし、「門」自体も繁体字として使われているのです。このような複雑な関係が無数にあることを知り、言語の奥深さを実感しました。
また、Unihan_Variants.txt
ファイルの巨大さに驚きました。このファイルは約20MBもあり、単純に全てをメモリに読み込むのは非効率だと気づきました。ここで、ファイルの効率的な読み込み方法を考える必要があることを学びました。
2. ChineseProcessorクラスの実装と性能の壁
中国語の文字を処理するためのChineseProcessor
クラスを実装する際、最初は単純に全てのデータをハッシュに格納しようと考えていました。しかし、実際に実装してみると、メモリ使用量が膨大になり、処理速度も遅くなってしまいました。
ここで、RubyのSet
クラスの存在を思い出しました。Set
は重複を許さないコレクションで、要素の存在確認が高速です。これを使うことで、メモリ使用量を抑えつつ、高速な処理を実現できました。この発見は大きな breakthrough でした!
module UnihanLang
class ChineseProcessor
def initialize
@zh_tw = Set.new
@zh_cn = Set.new
@common = Set.new
load_chinese_characters
end
# ... (その他のメソッド)
end
end
また、データの読み込みを初期化時に一度だけ行うことで、パフォーマンスを大幅に向上させることができました。これは、「遅延評価」と「事前計算」のトレードオフを考える良い機会となりました。
3. 言語判定ロジックの難しさ
Unihan
クラスを実装する際、最も頭を悩ませたのは言語判定のロジックでした。単純に繁体字と簡体字の数を数えるだけでは不十分で、共通字の扱いや、テキスト全体が中国語でない場合の処理なども考慮する必要がありました。
特に、以下のような場合の処理に悩みました:
- 繁体字と簡体字が混在している場合
- ほとんどが共通字で構成されているテキスト
- 中国語以外の文字(例:英語や日本語)が混ざっている場合
これらの問題に対処するため、最終的に以下のようなロジックを実装しました:
def language_ratio(text)
only_tw_chars = text.chars.count { |char| @chinese_processor.only_zh_tw?(char) }
only_cn_chars = text.chars.count { |char| @chinese_processor.only_zh_cn?(char) }
chinese_chars = text.chars.count { |char| @chinese_processor.chinese?(char) }
return :unknown unless chinese_chars == text.length
return :tw if only_tw_chars > only_cn_chars
return :cn if only_cn_chars >= only_tw_chars
:unknown
end
このロジックを考案する過程で、言語判定の難しさと面白さを実感しました。
また、エッジケースを考慮することの重要性も学びました。
4. テスト駆動開発の威力
gem開発を進める中で、テスト駆動開発(TDD)の重要性を強く感じました。最初はテストを書くのが面倒に感じていましたが、複雑な言語判定ロジックを実装する際、テストがあることで自信を持ってリファクタリングできました。
特に、以下のようなエッジケースのテストが役立ちました:
describe '#determine_language' do
it '繁体字と簡体字が混在するテキストを正しく判定する' do
expect(unihan.determine_language('這是簡体字和繁體字的混合')).to eq('ZH_TW')
end
it '全て共通字で構成されたテキストを正しく判定する' do
expect(unihan.determine_language('中文')).to eq('ZH_CN') # デフォルトで簡体字と判定
end
it '中国語と英語が混在するテキストを正しく判定する' do
expect(unihan.determine_language('This is 中文 mixed with English')).to eq('Unknown')
end
end
これらのテストを書くことで、自分のロジックの欠陥を早期に発見し、修正することができました。TDDの素晴らしさを実感した瞬間でした。
まとめと次回予告
今回のgem開発を通じて、当初は単純な作業だと思っていましたが、実際に取り組んでみると、予想以上に奥が深く、多くの学びがありました。
次回の完結編では、gemのパッケージング、公開の過程、そして開発中に直面した更なる課題とその解決方法について詳しく説明します。また、このgemを実際に使用する例も紹介し、将来の展望についても触れる予定です。
言語処理の世界はまだまだ探求の余地がありそうです。次回もお楽しみに!