Skip to content

Instantly share code, notes, and snippets.

@YSRKEN
Last active August 13, 2026 21:59
Show Gist options
  • Select an option

  • Save YSRKEN/442841080386978487dbe31d429e8181 to your computer and use it in GitHub Desktop.

Select an option

Save YSRKEN/442841080386978487dbe31d429e8181 to your computer and use it in GitHub Desktop.
ATOK のユーザー辞書に一括登録するとき、長さの上限は「文字数」ではない — ATOK 35 実測メモ

ATOK のユーザー辞書に一括登録するとき、長さの上限は「文字数」ではない

4万件超の単語を ATOK の辞書ユーティリティで一括登録したら、54件が理由も分からず失敗した。 原因を突き止めるまでにやったことと、実測で分かった ATOK の仕様をまとめる。

環境は ATOK for Windows 35.0(2026-08 時点)。バージョンによって値が違う可能性はある。

要約

  • 読みの上限は32、単語(表記)の上限は100。ただし単位が特殊で、濁点・半濁点が それぞれ1つ分を占める は2つと数える。だから見た目18文字の読みが 落ちて、32文字の読みが通る
  • 読みは長音符 で始まれない。 伸ばす母音が前に無いため。
  • 読みに使える文字は限られる。半角数字は受け付けられて全角に正規化されるが、 (U+2212 MINUS SIGN)のような字は弾かれる。長音符 (U+30FC)と見分けがつかない。
  • 一括登録は失敗件数しか教えてくれない。どの行が落ちたかは、登録後の辞書を 一覧出力して入力と突き合わせるしかない。
  • その一覧出力は う゛ に書き換えるので、素朴に diff を取ると嘘の差分が出る。

発端: 「44,118行中54件が失敗しました」

辞書ユーティリティの一括登録は、終わると成功件数と失敗件数だけを出す。 どの行がなぜ落ちたかは一切言わない。

推測で当てにいって、ことごとく外した。

  • ファイル内の重複 → ゼロだった
  • 32文字を超える読み → ゼロだった
  • 特定の記号()を含む読み → 件数は一致したが、同じ記号を含む行が別の辞書では 普通に登録できていた。偶然の一致

落ちた行を特定する

.dic はジャストシステム独自のバイナリで、外から読む手段がない。 唯一の観測手段が [ツール]-[単語・用例の一覧出力] で、辞書の中身をテキストに書き出せる。

出力されるファイルの形式:

  • UTF-16LE + BOM、改行は CRLF
  • !! で始まるヘッダ行が数行、空行を挟んで本体
  • 本体は 読み <TAB> 単語 <TAB> 品詞 のTSV
  • 品詞には *(登録単語)/$(自動登録単語)のサフィックスが付くので、 比較は (読み, 単語) の対で行う
!!ATOK_TANGO_TEXT_HEADER_1
!!一覧出力
!!対象辞書;...\ATOK\DIC\MY-DIC.dic
!!単語種類;登録単語(*) 自動登録単語($)
!!読み範囲;(読みの先頭) → (読みの最終)
!!出力日時;26/08/14 01:32

@すかーれっど	《スカーレッド・コクーン》	名詞*

これと登録に使ったテキストを突き合わせれば、落ちた行が出る。

罠: 一覧出力は う゛ にする

そのまま差分を取ったら、54件のはずが 2,874件出た。

原因は (U+3094)。一覧出力はこれを + (U+309B)の2文字に分解して書く。 CP932 に存在しないので、出力の字種が CP932 相当に落ちているのだと思われる (ファイル自体は UTF-16 なのに、載る文字の種類は CP932 の範囲)。

"ゔ".encode("cp932")
# UnicodeEncodeError: 'cp932' codec can't encode character 'ゔ'

なので突合の前に正規化が要る。

def normalize(yomi: str) -> str:
    return yomi.replace("う゛", "ゔ").replace("ウ゛", "ヴ")

これで差分がちょうど54件になり、ファイル別の内訳も報告された件数と一致した。

本題: 長さの上限の数え方

落ちた54件を眺めても、文字数では線が引けなかった。

28文字 @すーぱーろまんしんぐふぁいなるふろんてぃあふぁんたじー   → 登録できた
28文字 @きゅうばんりゅうじごくばんかーぱーふぉーふぁいぶないん   → 落ちた

同じ28文字で結果が違う。違いは濁点・半濁点の数だった。

Unicode の NFD(正準分解)で数えると、きれいに分かれる。

import unicodedata

def kana_units(text: str) -> int:
    return len(unicodedata.normalize("NFD", text))

kana_units("あ")   # 1
kana_units("が")   # 2  ← か + ゛
kana_units("ぱ")   # 2  ← は + ゜
kana_units("ゔ")   # 2  ← う + ゛
kana_units("ー")   # 1

この物差しで測ると、上の2つは 3134。上限は32なので後者だけが落ちる。 44,118行すべてに当てはめたところ、54件を過不足なく的中し、取りこぼしゼロだった。

ダミーで境界を確かめる

推測を確信に変えるため、長さだけを変えたダミーを登録して境界を撃った。 読みは全部 zzz 始まりにしておくと、後で検索して一括削除できて便利。

読み 見た目 かな単位 結果
zzzよみ + ×27 32文字 32 登録できた
zzzよみ + ×28 33文字 33 落ちた
zzz + ×14 17文字 31 登録できた
zzz + ×15 18文字 33 落ちた

18文字の読みが落ちて、32文字の読みが通る。 文字数では説明がつかない。

単語(表記)側も同様に、101〜127文字のダミーを1文字刻みで撃って挟み込んだ。

単語のかな単位 結果
100 登録できた
101 〜 127 全滅

つまり:

読み  ≤  32 かな単位
単語  ≤ 100 かな単位

単語が上限を超える場合、読みを短くしても救えない。 その単語は ATOK に入らない。

なぜこの数え方なのか

一覧出力が う゛ と書くことと辻褄が合う。 ATOK は読みを内部で「清音のかな + 濁点」に分解して保持していて、 上限もその分解後の要素数で数えている、と考えると両方の現象が同時に説明できる。

.dic の内部形式は未解析なので断定はできないが、外から見える挙動はこれで一貫している。

長さ以外にも落ちる条件がある

長さだけ守れば通る、というわけではなかった。別のデータセット(約97,000語)を 同じ手順で流したときに引っかかったものを挙げる。

読みは長音符で始まれない

12件が落ちた。全部これだった。

ー                    → Identity
ーこいするきもち        → 恋するきもち
ーらくすゔぁーさすみーあ → ラクスvs.ミーア

先頭の を取ったら通った。伸ばす母音が前に無いのだから、読みとして成立しない。 言われてみれば当然だが、機械的に読みを組み立てていると簡単に混入する。 上の例は X -恋するきもち- のようなタイトルを区切りで割ったときに、 区切りの - が長音符に化けて残ったもの。

念のため、登録に成功している手元の辞書(6本・267,000語超)を調べたが、 長音符で始まる読みは1件も無かった。規則として扱ってよさそうだ。

読みに使える文字は限られる

(U+2212 MINUS SIGN)を含む読みが1件落ちた。

ゆ−じ  →  ゆーじ      読みの真ん中が U+2212、単語側は正しい長音符 U+30FC

見た目では区別がつかない。元データの ユ-ジ(U+FF0D 全角ハイフンマイナス)が NKF の --hiragana を通って U+2212 に化けたものだった。

一方で半角数字は受け付けられる。ダミーを zzzたんご63 という読みで登録したら、 一覧出力には zzzたんご63 と全角で出てきた。ATOK が正規化している。

つまり「かな以外は全部だめ」ではない。どこまで許されるかは網羅していない (半角ラテン文字は未検証)。安全側に倒すなら、取り込み実績のある字だけを通す 許可制にして、そこから外れた文字は全角に畳むか落とすのがよい。

DIC ⇄ TXT の往復は成立するか

一覧出力を辞書のバックアップに使えるか、という話。答えは条件付きでイエス

44,546行を登録して一覧出力し、正規化なしで突き合わせた結果:

  • 差分は 2,821件、その全てが読みに を含むもの。それ以外はゼロ
  • つまりエントリは1件も失われない。バックアップとして使える
  • ただし う゛ になり、文字列としては元に戻らない

で登録しても う゛ で登録しても同じ場所に入るので、実用上の変換結果は変わらない。 一度出力したテキストは不動点になる(再登録しても同じものが出てくる)。

実務的な結論

一括登録用のテキストを機械生成するなら、生成側で上限を検査するのが正しい。 ATOK は失敗件数しか返さないので、後から原因を追うコストが釣り合わない。

import re
import unicodedata

MAX_YOMI_UNITS = 32
MAX_SURFACE_UNITS = 100

# 取り込み実績のある読み文字。ここから外れた字は畳むか落とす
ALLOWED_YOMI = re.compile(r"[ぁ-ゖー゛0-9a-z:=]")


def kana_units(text: str) -> int:
    """ATOK が長さを数える単位数。濁点・半濁点はそれぞれ1単位を占める。"""
    return len(unicodedata.normalize("NFD", text))


def is_registrable(yomi: str, surface: str) -> bool:
    return (
        bool(yomi)
        and not yomi.startswith("ー")           # 読みは長音符では始まれない
        and all(ALLOWED_YOMI.fullmatch(c) for c in yomi)
        and kana_units(yomi) <= MAX_YOMI_UNITS
        and kana_units(surface) <= MAX_SURFACE_UNITS
    )

半角英数字は ATOK 側で全角に正規化されるので、そのままでも通る。ただし 入力と一覧出力の文字列が変わってしまうので、突合するつもりなら自分で 畳んでおいた方が扱いやすい。

読みを切り詰めるときは、単位ではなく文字の境界で切ること。 NFD 文字列を単純にスライスすると濁点だけが取り残された壊れた読みができる。

def truncate_to_units(text: str, limit: int) -> str:
    kept, used = [], 0
    for char in text:
        cost = kana_units(char)
        if used + cost > limit:
            break
        kept.append(char)
        used += cost
    return "".join(kept)

まとめ

  • 上限は 読み32 / 単語100 かな単位(ATOK 35 実測)
  • 濁点・半濁点は1単位を占める。文字数で測ると必ずどこかで外す
  • 読みは長音符 で始まれない
  • 読みに使える文字は限られる。半角数字は全角に正規化されて通るが、 (U+2212)のような字は弾かれる
  • 一括登録の失敗原因を追うには一覧出力との突合が要る。そのとき / う゛ の正規化を忘れない
  • 単語長オーバーは救済不能。読みを工夫しても入らない

2026-08-14 追記: 別のデータセット(約97,000語)を流した結果を受けて、長さ以外の 条件(先頭の長音符・読みの字種)を追記した。当初は長さの話だけを書いていたが、 それだけでは通らない。

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