パーサコンビネータを使って簡単なNGワードフィルタリング機能を作る

昔、 RSpec の入門とその一歩先へ - t-wadaの日記 を読んで「自分だったらどうつくるかなー」と考えていた。

そして時が経ち、パーサコンビネータを知った今となっては、簡単なものであればこれでいいんじゃないかと思っている。

というわけで、以下は F# の ParsecClone というライブラリを使った例。

フィルタリング対象の文字列を発見する

利用者が指定したワードにマッチするようにすればよい。

// cutting : string -> string
// word: NGワード
let dirtyToTurn cutting word = matchStr word |>> cutting

マッチしたら伏せ字に入れ替える関数を適用すれば、それらしいものになる。

NGワードを複数登録できるようにする

NGワードリスト内のどれかにマッチするようにする。

// words: NGワードリスト
let dirtyToTurn cutting words = anyOf matchStr word |>> cutting

それ以外の文字列

任意の文字列にマッチすれば良い。ParsecClone の場合は any 関数が合致する。

文章を構成する

文章は、NGワードとそれ以外の文字列が0個以上組み合わさっている、と考えられる。

let parser cutting dirtyWords =
  many (dirtyToTurn cutting dirtyWords <|> aby) >>= foldStrings

foldStrings は ParsecClone に存在する関数で、parse した文字列のリストを連結してくれる Parser。

NGワードが含まれているか判定したい

巷のパーサコンビネータライブラリは状態を持つことができるような仕組みを提供していることが多い。ParsecClone にも存在する。

let dirtyToTurn cutting words =
  anyOf matchStr word |>> cutting .>> setUserState true

let parser cutting dirtyWords =
  many (dirtyToTurn cutting dirtyWords <|> aby)
  >>= foldStrings
  .>>. getUserState

これで、最終結果としてNGワードが存在するかどうかとフィルタリング結果が取得できるようになる。

他にも色々やりたい

全角半角を区別せずにフィルタリングしたいとか、でもフィルタリング対象以外の文字列はきちんと復元されてほしいとか色々あるなら、もう少し実装を考える必要がある。

ソースコード

今回のコードは以下においている。

https://github.com/pocketberserker/Harvester

名前の由来はそのうち書く。

他の言語でできるの?

経験から、少なくとも Boost.Spirit(Qi, Karma)はこの手法で実装できる。 あと、割りと新顔の ParsecClone でもできるので、他のライブラリでもできるのではないかなとは思っている。