1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go で Aho-Corasick CLI を作る — 多数パターンを1パス同時マッチ、failure link は KMP の一般化、重複マッチも全部拾う

1
Posted at

古典文字列アルゴリズム Aho–Corasick を Go の CLI として書いた。辞書全体(多数パターン)を、パターン数に依らず O(text + patterns + matches)1 パスで同時マッチする構造。実装の肝は 3 層: (1) 全パターンの trie(goto)(2) failure link — 次の文字でマッチが伸びないとき、いま一致している文字列の「最長の真の接尾辞であって、かつどれかのパターンの接頭辞」へ飛ぶ。これは KMP の失敗関数を 1 パターンから集合へ一般化したもの(3) dictionary-suffix link — 1 箇所で同時に終わる複数パターン("hers" は "he" も終える)を全部報告する。確率的データ構造シリーズ(#274-278)から古典アルゴリズムへ路線変更。

📦 GitHub: https://github.com/sen-ltd/aho-corasick-cli

スクリーンショット

問題設定

探したい針(パターン)が 1 本ではなく多数ある。単一パターン検索(KMP のような良いものでも)をパターンごとに回すと O(P · text)(P はパターン数)。Aho–Corasick は 1 パスで済ませる: 辞書全体からオートマトンを組み、テキストを 1 回走査して、重複を含む全マッチを吐く。

CLI はこう動く(実出力):

$ ac search -p he -p she -p his -p hers ushers
       1  she
       2  he
       2  hers          # 重複マッチも 1 パスで全部見つかる
— 3 match(es) of 4 pattern(s) in 6 bytes, automaton has 10 states

$ ac count --patterns signatures.txt --from app.log
ERROR               6034
WARN                4107
timeout             2025
connection refused  1960
...
— 22323 total match(es) in one pass over 542889 bytes

「ログ 1 本を舐めて、複数のエラーシグネチャの出現数を一括カウント」——まさに Aho–Corasick の出番。

構築: trie + 2 種類のリンク

1. trie(goto)。全パターンを挿入。辿るとテキストの一致接頭辞を消費する。

2. failure link。次の文字でマッチが伸びないとき、テキストを読み直さずにどこへ行くか? いま一致している文字列の 最長の真の接尾辞であって、かつどれかのパターンの接頭辞 へ。これはまさに KMP の失敗関数を、1 パターンから集合へ一般化したもの。BFS で、各ノードのリンクを親から導いて構築する:

f := m.fail[u]
for {
    if c, ok := m.children[f][b]; ok { m.fail[v] = c; break }
    if f == 0 { m.fail[v] = 0; break }
    f = m.fail[f]
}

failure link は必ず より浅い ノードを指すので、走査がテキスト上を後戻りしない。これが線形性の肝。

3. dictionary-suffix link。1 つのテキスト位置で複数パターンが同時に終わることがある——hers を一致させると heher も終わる。各ノードを「同じ位置で終わる、より短いパターン」へリンクし、報告時にその鎖を辿って全部吐く:

if m.out[m.fail[u]] != -1 { m.dict[u] = m.fail[u] } else { m.dict[u] = m.dict[m.fail[u]] }

TestDictSuffixReportsNested"hers" のマッチで hersheher が報告されることを、TestClassicUshers は教科書の {he, she, his, hers} × "ushers" を pin する。

走査: 1 回の線形パス

node := 0
for i := 0; i < len(text); i++ {
    node = m.step(node, text[i])          // goto、失敗時は failure link を辿る
    for t := node; t != -1; t = m.dict[t] // dictionary-suffix の鎖を辿る
        if pid := m.out[t]; pid != -1 { emit(pid, endsAt: i) }
}

step は goto 辺が見つかる(か root に着く)まで failure link を辿る。ならして全体はテキスト長に線形。TestAgainstBruteForce が、重複ありの混合パターンで素朴な O(P·n) マッチャとカウント一致を確認する。

UTF-8

マッチはバイト単位なので、マルチバイトのパターンもそのまま動き、オフセットはバイトオフセットになる。TestUnicodeByteMatching東京都 を重複パターン 東京京都東京都 で検索し、3 つとも見つける。

aho/aho.go — オートマトン: trie, failure & dict link, FindAll / Count(テスト 10 件)
main.go    — CLI: search / count, パターンは flag or ファイル, テキストは arg/ファイル/stdin

まとめ

Aho–Corasick は「多数の針を 1 パスで」の定番。鍵は failure link が KMP の一般化であること、dictionary-suffix link で重複マッチを取りこぼさないこと、そして全体が テキスト長に線形であること。#274-278 の確率的データ構造から古典文字列アルゴリズムへ振ったが、「前処理でオートマトンを組み、本番は 1 パスで安く」という発想は同じ系譜だ。

📦 GitHub: https://github.com/sen-ltd/aho-corasick-cli

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?