HaskellでPerl互換の正規表現ライブラリの使い方と比較
HaskellのPerl互換の正規表現ライブラリregexpr, regex-pcre, pcre-light, pcre-heavy, text-icuの使い方と差異についてまとめてみました. ただし, あくまで現時点での情報に基づいていることにご留意ください. また, 使い方といっても網羅することは到底叶いません. それでも, 使い勝手, ライブラリの特徴, 注意点などは感じ取っていただけるかと思います.
Haskell初心者の方へ
HaskellにはParsecという正規表現より強力なパーサライブラリがあります. 正規表現では記述に無理があるような場合でも, Parsecを使えばすんなり書けてしまうかも知れません. ぜひ利用を検討してみてください. 使い方については, 例えばReal World Haskellの第16章が参考になります.
簡単なまとめ
ライブラリの機能を簡単に整理すると次のようになります.
日本語 | 置換 | 分割 | Cラッパー | compileM | |
---|---|---|---|---|---|
regexpr | ○ | ○ | ○ | N | × |
regex-pcre | × | × | × | Y | ○ |
pcre-light | △ | × | × | Y | ○ |
pcre-heavy | ○ | ○ | ○ | Y | ○ |
text-icu | ○ | × | × | Y | ○ |
"Cラッパー"はバックエンドとしてpcreを使っているかを表しています. このようなライブラリではpcre向けのオプションを用意していることが多いようです. "compileM"は不正な正規表現文字列に対してfail
するコンパイル関数を持っているかを表しています. このような関数を持たない場合, 不正な正規表現を検知するためには例外を受ける必要があるでしょう.
注意点
pcre系のライブラリで日本語文字を扱う場合に上手く行かない原因として
utf8
オプションのつけ忘れ-XOverloadedStrings
とByteString
との併用
が挙げられます.
2.は一般にData.ByteString.UTF8.fromString
やConvertibleStrings
のメソッドconvertString
を用いて適切にデータ変換することで解決できます. 以下のコード例では, この2つをその場その場で適当に使ってしまっていますが, 基本的には互いに置き換え可能です.
次から各ライブラリについて使い方を中心に詳しく見ていきます.
regexpr
- 型がシンプルでわかりやすい
- 対象は
String
型 - Perl風の置換
- 関数による置換
型はString
とタプルおよびリストから構成されていて, それだけ見れば何ができるか分かります. 関数の個数も10個程度におさまっているので, 容易に使いこなせるでしょう.
使い方
> matchRegexPR "(\\d+)-(\\d+)-(\\d+)" "\t111-2222-3333\t" Just (("111-2222-3333",("\t","\t")),[(3,"3333"),(2,"2222"),(1,"111")])
マッチの全体, 前後, 番号付きのキャプチャが返ってきています.
マッチするものをすべて取り出すには, gmatchRegexPR
かmultiMatchRegexPR
を使います. gmatchRegexPR
は前回のマッチの後続部分のみを探索しますが, multiMatchRegexPR
は前回マッチした部分も探索します.
> gmatchRegexPR "ab*" "abbbbabb" [(("abbbb",("","abb")),[]),(("abb",("","")),[])] > multiMatchRegexPR "ab*" "abbbbabb" [(("abbbb",("","abb")),[]),(("abbb",("","babb")),[]),(("abb",("","bbabb")),[]),(("ab",("","bbbabb")),[]),(("a",("","bbbbabb")),[]),(("abb",("abbbb","")),[]),(("ab",("abbbb","b")),[]),(("a",("abbbb","bb")),[])]
\数
記法を使った置換. {}
で数を囲うことで曖昧さを排除できます.
> gsubRegexPR "(?:,(\\d+))|(?:(\\d+),)" "\\1\\{2}0" "123,456,789" "12304560789"
関数を使った置換も可能です.
> putStrLn $ gsubRegexPRBy "(\\d+)" (\n -> "{" ++ show (length n) ++ "桁の数}") "123abcd45678efghi" {3桁の数}abcd{5桁の数}efghi
regex-pcre
- regex-baseのバックエンド
- compileM =
makeRegexM :: -> m Regex
,makeRegexOptsM :: -> m Regex
一般にregex-base系の正規表現ライブラリは慣れるまでが難しいのですが, 多相性のおかげで非常に柔軟な結果を得ることが出来ます. 逆にregex-baseに既に慣れている人であれば, このライブラリをすぐに使うことができるでしょう.
このライブラリには, 後述のように日本語文字を正しく扱えないバグがあります.
使い方
型注釈を変えることで様々な結果を得られます.
> "abbbbabb" =~ "(ab*)" :: Bool True > "abbbbabb" =~ "(ab*)" :: Int 2 > "abbbbabb" =~ "(ab*)" :: String "abbbb" > "abbbbabb" =~ "(ab*)" :: [[String]] [["abbbb","abbbb"],["abb","abb"]] > "abbbbabb" =~ "(ab*)" :: MatchArray array (0,1) [(0,(0,5)),(1,(0,5))] > "abbbbabb" =~ "(ab*)" :: [MatchArray] [array (0,1) [(0,(0,5)),(1,(0,5))],array (0,1) [(0,(5,3)),(1,(5,3))]] > "abbbbabb" =~ "(ab*)" :: [MatchText String] [array (0,1) [(0,("abbbb",(0,5))),(1,("abbbb",(0,5)))],array (0,1) [(0,("abb",(5,3))),(1,("abb",(5,3)))]]
結果として得られるデータ型はまだまだあります.
=~~
はモナドを返します.
> "abbbb" =~~ "(ab*)" :: Maybe String Just "abbbb" > "bbbb" =~~ "(ab*)" :: Maybe String Nothing
pcreのオプションを使う場合は=~
が使えません. makeRegexOpts
等で正規表現をコンパイルし, match
等の関数を使う必要があります.
> let re = makeRegexOpts (compUngreedy .|. compCaseless) defaultExecOpt "(ab*)" :: Regex > match re "ABBBBABB" :: [[String]] [["A","A"],["A","A"]]
バグについて
regex-pcre-0.94.4にはString
オーバーロードが日本語の文字などを正しく扱えないというバグがあります.
[Haskell-cafe] regex-pcre is not working with UTF-8 - Google グループ
正規表現 - 日本語を含む文字列から正規表現を使って抽出する方法 - スタック・オーバーフロー
例
> let res1 = "abbbbabb" =~ "ab*" :: [[String]] > let res2 = "あばばばばあばば" =~ "あば*" :: [[String]] > mapM_ putStrLn $ concat res1 abbbb abb > mapM_ putStrLn $ concat res2 あばばばばあ -- compUTF8を付けても同様 > let re = makeRegexOpts compUTF8 defaultExecOpt "あば*" :: Regex > let res3 = re `match` "あばばばばあばば" :: [[String]] > mapM_ putStrLn $ concat res3 あばばばばあばば
これはpcreが返すオフセットおよび長さと, HaskellのString
のそれとの間のズレが修正されていないことが原因です.
以下に, 回避策を示します. いずれの場合もcompUTF8
オプションが必要です.
方法1: ByteStringを使う
String
の代わりにByteString
を使えば問題を回避できます.
> import qualified Data.ByteString.UTF8 as U > let res = re `match` (U.fromString "あばばばばあばば") :: [[ByteString]] > mapM_ (putStrLn . U.toString) $ concat res あばばばば あばば
方法2: パッチをあてる
Haskell-cafeにパッチがアップロードされています. ただしこのパッチは未完成でmatchAllText
などは正しく動作しないようです.
pcre-light
- シンプル
- 対象は
ByteString
型 - compileM =
compileM :: -> Either String Regex
使い方
-XOverloadedStrings
に注意. ダブルクォートで囲まれた文字列をByteString
やText
のリテラルとして扱ってくれるものです.
> :set -XOverloadedStrings > match (compile "(ab*)b" []) "abbbbabb" [] Just ["abbbb","abbb"]
全てのマッチを取り出すには自前のコードを書く必要がありそうです.
import Control.Monad (liftM2) import qualified Data.ByteString as BS import Text.Regex.PCRE.Light gMatch :: Regex -> BS.ByteString -> [PCREExecOption] -> Maybe [[BS.ByteString]] gMatch re bstr opts = let m = match re bstr opts in case m of Just m' -> liftM2 (:) m $ gMatch re (BS.drop (BS.length $ head m') bstr) opts Nothing -> return []
> gMatch (compile "(ab*)b" []) "abbbbabb" [] Just [["abbbb","abbb"],["abb","ab"]]
日本語文字を使う場合はData.ByteString.UTF8.fromString
で変換し, さらにutf8
オプションを付加しなければなりません.
> import qualified Data.ByteString.UTF8 as U > let res = gMatch (compile (U.fromString "(あば*)ば") [utf8]) (U.fromString "あばばばばあばば") [] > mapM_ (putStrLn . U.toString) $ concat $ fromJust res あばばばば あばばば あばば あば
pcre-heavy
- pcre-lightがベース
- 対象はString型, ByteString型, Text型*1
- 関数による置換
- QuasiQuotation
- compileM =
compileM :: -> Either String Regex
(from pcre-light)
使い方
QuasiQuotationに対応しているので正規表現がそのまま書けます.
> "123-4567-89" =~ [re|\d{3}-(\d{4})-\d{2}|] True > scan [re|\d{3}-(\d{4})-\d{2}|] "123-4567-89" [("123-4567-89",["4567"])]
グローバルな置換, 関数による置換が可能です.
> :set -XScopedTypeVariables > gsub [re|\d+|] (\(s::String) -> reverse s) "123-4567-89" "321-7654-98"
日本語もオプション無しでそのまま扱えます.
-- StringとText > import qualified Data.Text as T > :set -XOverloadedStrings > let sres = scan [re|(あば*)|] ("あばばばばあばば" :: String) > let tres = scan [re|(あば*)|] ("あばばばばあばば" :: T.Text) > mapM_ (putStrLn . fst) $ sres あばばばば あばば > mapM_ (putStrLn. T.unpack . fst) $ tres あばばばば あばば
-XOverloadedStrings
が無効な時はConvertibleStrings
のメソッドconvertString
=cs
を使ってByteString
を処理できます.
> :unset -XOverloadedStrings > let bres = scan [re|(あば*)|] (cs "あばばばばあばば" :: BS.ByteString) > mapM_ (putStrLn . U.toString . fst) $ bres あばばばば あばば
text-icu
- シンプル
- 対象は
Text
型 - compileM =
regex' :: -> Either ParseError Regex
text-icuはユニコード操作のためのパッケージ(らしい)です. 正規表現機能をその一部として含んでいます.
使い方
> find (regex [] "(ab*)") "abbbbabb" Just Match ["abbbb","abbbb"] > findAll (regex [] "(ab*)") "abbbbabb" [Match ["abbbb","abbbb"],Match ["abb","abb"]]
Match
型からは様々な情報を取り出せます.
> let m = fromJust $ find (regex [] "(\\d{3})-(\\d{4})-(\\d{2})") "123-4567-89" > group 1 m Just "123" > group 2 m Just "4567" > prefix 1 m Just "" > suffix 1 m Just "-4567-89"
日本語もそのまま扱えます.
> let res = findAll (regex [] "(あば*)") "あばばばばあばば" > T> mapM_ (\m -> putStrLn . T.unpack . fromJust $ group 1 m) res あばばばば あばば
さいごに
この記事では各ライブラリのおもだった使い方とライブラリ間の差異を紹介しました. ここで挙げたことの他にも, オプションの種類など選択基準となる要素はたくさんあります. ぜひパッケージのドキュメントをあたって, 用途に適したライブラリを探してみてください.
*1:正確には(ConvertibleStrings SBS a, ConvertibleStrings a SBS) => a