MeCrab

日本語の文を形態素に分けて、構造にする。解析も辞書も、ブラウザの中で完結する。

入力した文章は、どこにも送信されません。解析も辞書もこのページの中にあります。

例文

0 形態素 / —

解析結果

辞書を読み込んでいます(0.0 MB)

辞書はこのサイトと同じ場所に置かれた静的ファイルです。外部のサーバーには接続しません。読み込みが終わると、下の外部リクエストの計測が 0 件から始まります。

読み込みは初回のみです。次回からはブラウザのキャッシュから開きます。

mecrabv— (Pure Rust, Apache-2.0)
targetwasm32-unknown-unknown
wasm size197 KB (gzip 87 KB)
dictionary
dictionary size
dictionary load
parse time
morphemes
external requests
server round-trips0
C / C++ / Fortran0 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)
}

これがいま上で動いているコードです。