MeCrab
日本語の文を形態素に分けて、構造にする。解析も辞書も、ブラウザの中で完結する。
入力した文章は、どこにも送信されません。解析も辞書もこのページの中にあります。
0 形態素 / —
解析結果
辞書を読み込んでいます(0.0 MB)
辞書はこのサイトと同じ場所に置かれた静的ファイルです。外部のサーバーには接続しません。読み込みが終わると、下の外部リクエストの計測が 0 件から始まります。
読み込みは初回のみです。次回からはブラウザのキャッシュから開きます。
| mecrab | v— (Pure Rust, Apache-2.0) |
|---|---|
| target | wasm32-unknown-unknown |
| wasm size | 197 KB (gzip 87 KB) |
| dictionary | — |
| dictionary size | — |
| dictionary load | — |
| parse time | — |
| morphemes | — |
| external requests | — |
| server round-trips | 0 |
| C / C++ / Fortran | 0 bytes |
ページ読込後の外部リクエスト: — 件
外部リクエストの計測は、wasm と辞書の読み込みが完了した時点から開始しています。文章の入力・解析・コピーを行っても、この数は動きません。開発者ツールの Network タブでも確認できます。
辞書は IPADIC(奈良先端科学技術大学院大学)を MeCrab 形式に変換したものです。ライセンス全文: /dict/IPADIC-COPYING.txt
辞書の展開(gzip の解凍)はブラウザの DecompressionStream API を使用しています。形態素解析そのものは Pure Rust (wasm) で行っています。
正直な制約
| 制約 | 実際に何が起きるか | なぜ |
|---|---|---|
| 辞書にもとづく解析です | 辞書にない語は、文字の種類(漢字・カタカナ・数字など)から推定して分割します。推定した語には点線の枠が付きます。例として「サチュレーション」と入力すると「サチ」と「ュレーション」に割れ、辞書にない後半に点線の枠が付きます | IPADIC の語彙は 2007 年ごろに整備されたもので、新しいカタカナ語を多く含みません |
| 意味は理解していません | 分割・品詞・読みまでを出します。要約や言い換え、固有表現の抽出はしません | これは辞書引きと最短経路探索(Viterbi)であり、クラウドの大規模言語モデルではありません。だから 1 文が 1 ミリ秒以下で終わり、どこにも送信されません |
| 読みが出ない語があります | 数字・未知語の「読み」と「原形」は「—」になり、ふりがなも付きません。読みを持たない一部の記号も同様です | 未知語の定義(unk.def)の項目が読み・原形のフィールド自体を持たないためです。IPADIC の句読点のように読みを持つ記号は、そのまま表示されます |
| 一度に解析するのは 3,000 字までです | 超えた分は解析されず、注記が出ます | 上限は結果表示(チップと表)の都合です。解析自体は、エンジン側の上限である 4,000 字でも数十ミリ秒(実測 18〜22 ミリ秒)で終わることを確認しています |
| 口語やくだけた表記は分割が揺れます | 「えっと」が「えっ」+「と」になるなど、話し言葉では不自然な区切りが出ることがあります | 辞書とコスト値が書き言葉のコーパスで学習されているためです |
実装コード
// crates/mecrab-wasm/src/analyzer.rs:200-278 — verbatim, the code running above
pub fn analyze_tokens(&self, text: &str) -> Result<Vec<Token>, MecrabWasmError> {
let dict = self.dictionary()?;
check_length(text)?;
// char.def's own grouping flag decides what gets cut — the flag whose
// handling is the cubic path. The default type identifies the category so
// that two adjacent categories are two runs, exactly as mecrab groups
// them. Characters outside the BMP fall to a default char info whose
// group bit is 0, so an emoji wall is never cut here; mecrab's own
// lattice still groups the Default category, bounded at
// MAX_GROUPING_SIZE + 1 = 25 characters (measured: 4,000 emoji is 3,975
// single-character tokens plus one 25-character node), which is exactly
// why this guard does not need to cut it too.
let bounds = segment_bounds(text, |character| {
let info = dict.char_def.get_char_info(character);
if info.group() {
Some(info.default_type())
} else {
None
}
});
let solver = ViterbiSolver::new(dict);
let mut tokens = Vec::new();
let mut unmatched = 0u32;
for (segment_start, segment_end) in bounds {
let segment = text.get(segment_start..segment_end).ok_or_else(|| {
MecrabWasmError::internal(format!(
"segment [{segment_start}..{segment_end}] is not a slice of the input"
))
})?;
let lattice = Lattice::build(segment, dict).map_err(|error| {
MecrabWasmError::internal(format!("mecrab could not build a lattice: {error}"))
})?;
let path = solver.solve(&lattice).map_err(|error| {
MecrabWasmError::internal(format!("mecrab could not solve the lattice: {error}"))
})?;
for node in &path {
// BOS/EOS are zero-width and are already dropped by mecrab's own
// backward pass; dropping them again by WIDTH rather than by the
// surface string EOS itself means a user who types EOS gets their
// token back.
if node.end_byte <= node.start_byte {
continue;
}
// Whitespace is not a morpheme, and upstream mecab is the
// authority: piping 'foo bar baz' through mecab emits three
// nodes and no whitespace node, and -Owakati answers with
// single spaces. mecrab's lattice emits a 記号,空白 node per run
// instead, which reached the page as a focusable but visually
// empty chip and made the wakati separator indistinguishable
// from the token beside it. Dropped here rather than at the
// display layer so BOTH ops inherit it: wakati_line maps over
// exactly this Vec, which is what keeps "the wakati line is the
// chips, joined" true by construction. Offsets stay honest —
// they are still the shim's own measurements of the input, and
// the only gap they can now leave is whitespace.
//
// No backticks in this range: it is copied verbatim into a
// JavaScript template literal on the page (SNIPPET-SYNC).
if node.surface.chars().all(char::is_whitespace) {
continue;
}
let is_unknown = if let Some(flag) = unknown_flag(&lattice, node) {
flag
} else {
unmatched = unmatched.saturating_add(1);
field_count(&node.feature) != IPADIC_FEATURE_FIELDS
};
tokens.push(token_from(node, segment_start, is_unknown)?);
}
}
self.unmatched.set(unmatched);
Ok(tokens)
} これがいま上で動いているコードです。