Skip to content

Instantly share code, notes, and snippets.

@na2co3-ftw
Last active October 11, 2021 01:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save na2co3-ftw/407ede910f520c52ddcf0f4ece9e1210 to your computer and use it in GitHub Desktop.
Save na2co3-ftw/407ede910f520c52ddcf0f4ece9e1210 to your computer and use it in GitHub Desktop.

PDIC/Unicodeの辞書型式

PDIC/Unicodeの辞書(拡張子が.dicのやつ)の仕様を簡単に解説します。
ある程度プログラムの知識は要るかもしれませんが、バイナリファイルとかを普段扱わない人でも読めるように配慮しようと思います。

本家資料はこちらです
一部分かりやすさのために、本家資料の用語と違う言いかたをしているところがあります。

ここで対象にするのは辞書のバージョンが6.10のものです。PDIC/Unicode Ver.5以降で使われているようです。

バイナリファイル

最近のコンピュータでは、ファイルは2進数8桁のバイトという単位で、数値がひたすら並んだものです。(圧縮ファイルとかだと違ったりしますが) そのバイトの羅列を、適当なバイト数ごとに切り分けたりして読み込んでいきます。

二進数で16桁とか32桁とかの数値を記録する時は、複数のバイトに切り分けて並べます。並べかたには2種類の方法があって、PDICの辞書型式ではリトルエンディアンという方法に揃えています。
32桁の0x12345678というデータがあると、0x78 0x56 0x34 0x12 という感じで、バイト単位で逆順に並べます。

逆に、オンかオフだけのデータ(フラグ)の場合は、1バイトの中に複数のフラグを含ませたりします。それぞれの桁に対応させればいいので。
まとめられたバイトから、フラグを個別に読みとる場合はビット積を使うことが多いです。(数値) & 0x02 とかやると、数値のうち、0x02のフラグがオフなら0に、オンなら0以外になるので、そのままif文の条件式に入れたりします。

全体の構成

辞書ファイルは、大きく分けて4つの部分でできています。ヘッダー部拡張ヘッダー部インデックス部データ部です。ファイルの中でこの順番に並んでいます。

データ部が本体です。その中には、単語がUnicodeの文字コード順に並んで記録されています。と言いたいところですが、違います
データ部はブロックの集まりになっていて、順番に物理ブロックIDが振られています。それぞれのデータブロックには単語が文字コード順に並んで入っています。ひとつの単語が複数のブロックにまたがって入ることはありません。
データブロックには、いっぱいに単語が詰まってるわけではなくて、最後の方に余白があります。

そしてこのデータブロックの順番を、インデックス部で記録しています。インデックス部に記録された順番にデータブロックを読み込んでいくと、全体として文字コード順に単語が得られます。
あとインデックス部には、それぞれのデータブロックについて、その中で最初に入っている単語の見出し語も記録しています。

なんでこんなことをしているかというと、PDICは巨大な辞書を扱う前提で作られているので、一度に辞書ファイル全部を読み書きしたくないのです。
見出し語から検索するときは、インデックス部にある「それぞれのデータブロックの最初の見出し語」と比較して、目的の単語が入っていそうなデータブロックを絞り込みます。そしてその分のデータブロックだけを実際に読み込んで、目的の単語を見つけます。

一方、単語を増やした時や、内容を書き換えて長くなった時は、それ以降の単語のデータを全部後ろにずらして、すきまを空けないといけません。また、単語を消した時や、内容が短くなった時は、それ以降の単語を全部前に詰めないといけません。
データとして記録されているファイルは巨大な配列みたいなものなので、途中に挿入したり途中を削除したりするためには、それより後ろを全部ずらす、つまりコピーして上書きしないといけないんですね。
しかし、データブロックの最後の方に余白があるので、その単語が入っているデータブロックだけを書き換えれば済みます。それでもし余白に収まらなくなってはみ出た場合は、ブロックを半分ぐらいに切って2つにします。増えた方のブロックは、空いてるところ(データ部の末尾とか)に置きます。
逆にブロックの中の余白が多くなりすぎた場合は、いくつかのブロックを合体させて、使わなくなったブロックを空きブロックにしたりします。空きブロックはインデックスから除外されます(多分)。空きブロックは次にまた新しくブロックを作らないといけない時に、優先的に使われていきます。

こうした工夫により、ファイル全体ではなく、1ブロック〜数ブロック分の読み書きだけで検索・編集ができます。編集していくうちに、データブロックの物理的な順番は、単語の実際の順番と合わなくなっていくので、インデックスでその順番を記録しています。

見出し語について

PDICの単語には見出し語という項目がありますが、少し扱いがやっかいです。内部では、キーワードと本来の見出し語をタブ("\t")で連結したものを、見出し語として扱っています。 キーワードは、入力した見出し語の大文字を小文字に変換したり、ハイフンを半角スペースにしたりして自動生成されます。Tools > 設定 > 動作環境 > 登録語編集 > キーワード欄を表示する にチェックを入れると手動でも編集できます。

例: 入力した見出し語 "Foo-bar"
→ キーワード "foo bar", 内部データとしての見出し語 "foo bar\tFoo-bar"

さっき、単語を文字コード順で並べると書きましたが、正確にはこの、内部データとしての見出し語の文字コード順で並べます。

ファイル内の全ての単語で、キーワードと見出し語が同じである場合は、そのまま内部データとして使うことがあります。ファイルを読む側の立場から言えば、"abc"のようにタブが含まれていない場合は、キーワードも見出し語も"abc"であるということです。
この省略をする場合は、全ての単語で省略しないといけません。混在していると文字コード順のソートに影響が出てしまうからです。

BOCU-1

PDIC/Unicodeでは、テキストを全てUnicodeで扱いますが、そのエンコーディング方法としてはBOCU-1を使います。

近い文字コードの文字が連続しているところを省略して、前の文字からの差分を記録するような方法で、Unicode文字全てを扱いながら容量を減らすものです。詳しい説明は他に譲ります。

ヘッダー部

ファイルの先頭から始まります。長さは1024バイト固定です。
辞書全体に関する情報や、全体のファイル構造に関わる情報が主に入っています。
ここではデータの読み書きに必要な分だけを紹介します。
位置と長さはバイト単位。位置はそれぞれの値の開始位置のことです。

位置 長さ 名前 説明
0x00 100 headername 固定
0x8C 2 version 辞書のバージョン。6.10では0x060Aです。
メジャーバージョン(0x06の部分)が同じなら、読み込みはできるそうです。
0x94 2 index_block インデックス部のブロック数
0xA0 4 nword 登録単語数
0xA5 1 dictype 属性
0xA8 4 olenumber 最新のOLEオブジェクト番号 (リンクデータとかするのに多分使います)
0xB6 1 index_blkbit インデックス部の物理ブロック番号のビット数
0xB8 4 extheader 拡張ヘッダーの長さ(バイト単位)
0xBC 4 empty_block2 先頭空きブロック番号
0xC0 4 nindex2 インデックスの数
0xC4 4 nblock2 データ部のブロック数
0xC8 8 cypt 暗号コード
0xD8 8 dicident 辞書識別子
辞書を新規作成する時に生成された8バイトの乱数

headernameの内容は

"             =============== Dictionary for PDIC ===============               "

です。(なんか1つだけShift_JISの全角スペース)

ここで書いた「名前」は、変数のような感じで後の説明で使います。

最初の256バイトにだけ情報が入っています。ヘッダー部全体は1024バイトなので、残り768バイトは余白です。

dictype

属性には以下のフラグを含みます。

  • 0x01 バイナリの圧縮をしている (圧縮されたデータがなくてもtrueになります。圧縮されたデータを含む場合は必ずtrueです)
  • 0x08 BOCU-1でエンコードしている (多分必ずこのフラグはtrueです)
  • 0x40 暗号化している (暗号化の方法はよく分かりません)

拡張ヘッダー部

ヘッダー部の次、つまりファイルの先頭から1024バイトの地点から始まります。
長さはextheaderバイトです。大抵は0バイトです。
中身は多分読まなくていいです。

インデックス部

拡張ヘッダー部の次、つまりファイルの先頭から1024 + extheaderバイトの地点から始まります。
長さは1024*index_blockバイトです。
データ部の物理ブロックについての情報(インデックス要素といいます)が、順番にnindex2個記録されています。

そしてインデックス要素を並べた後、最後に

長さ
4 0x00000000

が置かれ、これ以上インデックス要素がないことを表します。

インデックス部の長さは1024の倍数バイトなので、余ったりします。(ちなみにPDIC/Unicodeでは、インデックス部の長さは初期状態で1024*16です。後でインデックスが増えて伸びると、後に続くデータ部がずれて面倒だから多めにしてあります。1単語しか入ってない辞書とか作ると、16000バイト以上の余白ができます。)

インデックス要素

長さ
2 or 4 物理ブロック番号。index_blkbit0だと2バイト、1だと4バイト
可変 そのブロックの中で最初の単語の見出し語(BOCU-1)
1 0x00。見出し語の長さは可変なので、0x00が終了の目印になります。

データ部

インデックス部の次、つまりファイルの先頭から1024 + extheader + 1024*index_blockバイトの地点から始まります。
データブロックが並んで入っています。
それぞれのデータブロックには物理ブロックIDがついています。
それぞれのデータブロックの開始位置は、ファイルの先頭から1024 + extheader + 1024*index_block + 1024*(物理ブロック番号)バイトの地点になります。

データブロック

長さ
2 ブロックの長さ
可変 単語データ(複数)
2 or 4 0x0000 or 0x00000000

ブロックの長さは、フィールド長サイズのフラグ0x8000を含んでいます。
実際のブロックの長さは、最上位以外のビットを使います。((ブロックの長さ) & 0x7FFF)
データブロック内で、長さが「2 or 4」となっているものは、フィールド長サイズのフラグがfalseなら2バイト、trueなら4バイトになります。またtrueの場合はひとつのデータブロックにひとつしか単語を含むことができません。

ひとつのデータブロックの長さは、1024*(ブロックの長さ)バイトです。つまり、長さが2以上だと物理ブロックIDに欠番がでます。

データブロックも、長さ1024の倍数バイトなので、余白があります。検索・編集時のパフォーマンスのため、余白はデータブロックのうちある程度の割合を占めるように調整されます。

単語データ

長さ
2 or 4 長さ
1 見出し語省略バイト数
1 属性。
可変 見出し語(BOCU-1)
1 0x00
可変 訳語(BOCU-1)

長さは見出し語から単語データの最後までのバイト数です。最初の2+1+1バイトは含みません。

属性の下位4ビット((属性) & 0x0F)で、単語レベルを表します。

  • 0x00 なし
  • 0x010x0F 1 〜 15

上位4ビットは以下のフラグを含みます。

  • 0x10 拡張構成
  • 0x20 暗記必須マーク (≠暗記マーク)
  • 0x40 修正マーク

属性が0xFFの場合は「リファレンス登録語」という、既に廃止された機能の項目らしいです。飛ばしましょう。

暗記必須マークは Tools > 設定 > 動作環境 > 「暗記必須単語」機能を使う をチェックしたときだけ編集できます。これは過去バージョンのように使いたい人向けで、非推奨みたいです。
チェックしていない時(デフォルト)で編集できる暗記マークは、辞書ではなく単語帳ファイル(Bookmark.txt)に保存されます。辞書提供者が作るマークではなくて、辞書利用者が利用するためのマークということですね。

見出し語の省略

ひとつ前の単語の見出し語と、最初のnバイトが一致している時、そのnを「見出し語省略バイト数」に入れて、見出し語には「最初のnバイト分を削ったもの」を入れます。
ブロック内で最初の単語の場合は、見出し語省略バイト数は0です。そして見出し語にはそのままの文字列が入ります。

例:

見出し語 見出し語(BOCU-1) 省略バイト数 記録される見出し語
"aaa\taaa" b1 b1 b1 09 b1 b1 b1 0 b1 b1 b1 09 b1 b1 b1
"aabb\taabb" b1 b1 b2 b2 09 b1 b1 b2 b2 2 b2 b2 09 b1 b1 b2 b2
"abc\tabc" b1 b2 b3 09 b1 b2 b3 1 b2 b3 09 b1 b2 b3
"abcd\tABCD" b1 b2 b3 b4 09 91 92 93 94 3 b4 09 91 92 93 94

単語データ (拡張構成)

見出し語と訳語以外の記述を含む場合は、拡張構成になります。

長さ
2 or 4 長さ
1 見出し語省略バイト数
1 属性。
可変 見出し語(BOCU-1)
1 0x00
可変 訳語(BOCU-1)
1 0x00
可変 拡張データ(複数)
1 0x80

拡張データ

最初の1バイトが拡張データの属性を表します。
下位4ビット((属性) & 0x0F)で、データの内容の種類を表します。

  • 0x01 用例
  • 0x02 発音記号
  • 0x04 リンクデータ (外部ファイルへのリンクやファイル埋め込みなど)

上位4ビットは以下のフラグを含みます。

  • 0x10 バイナリデータ
  • 0x40 圧縮データ 圧縮データは必ずバイナリデータなので、実際には0x40単体では使いません。必ず0x50になります。

拡張データの構造は、バイナリデータかどうかで変わります。

拡張データ(テキスト)

長さ
1 属性
可変 内容(BOCU-1)
1 0x00

拡張データ (バイナリ)

長さ
1 属性
2 or 4 内容の長さ
内容の長さバイト 内容

データの圧縮方法は独自実装のRangeCoderというものらしいです。
圧縮とリンクデータの型式は省略します。

データブロック (空きブロック)

長さ
2 0x0000
4 次の空きブロックの物理ブロックID

最初の空きブロックの物理ブロックIDはempty_block2で示されます。
それ以降順番に、次の空きブロックの物理ブロックIDでつながっていきます。
最後の空きブロックでは、次の空きブロックの物理ブロックID0xFFFFFFFFになります。
空きブロックが一つもない場合は、empty_block20xFFFFFFFFになります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment