CLOVER🍀

That was when it all began.

Clojureで正規表現

土日とゴールデンウィークが仕事ばっかりで、全然更新できていませんでしたね…。なんとか時間が作れたので、久々の更新です。

前回のScala正規表現に続き、Clojure正規表現を使ってみようと思います。Contribは見ずに、標準関数のみです。

re-pattern(java.util.regex.Patternの構築)

re-pattern関数に、正規表現を表す文字列を渡します。

(def number-pattern (re-pattern "\\d+"))
(println (class number-pattern))  ;; => java.util.regex.Pattern

もしくは、#リーダーマクロでもOKです。

(def number-pattern #"\d+")
(println (class #"\d+"))  ;; => java.util.regex.Pattern

この場合、Java正規表現を記述する際に面倒な、バックスラッシュのエスケープが楽になります。

では、この辺りをスケープゴートにして、試していってみましょう。

(def target-str "Hello 1234 World 5678")

(def number-pattern (re-pattern "\\d+"))
(def alpha-pattern (re-pattern "[a-zA-Z]+"))
(def not-match-pattern (re-pattern ":"))

re-matcher(java.util.regex.Matcherの構築)

re-matcher関数にjava.util.regex.Patternと正規表現を適用する文字列を渡すことで、java.util.regex.Matcherを取得することができます。

(println (class (re-matcher number-pattern target-str)))  ;; => java.util.regex.Matcher
(println (class (re-matcher alpha-pattern target-str)))  ;; => java.util.regex.Matcher
(println (class (re-matcher not-match-pattern target-str)))  ;; => java.util.regex.Matcher

re-find

re-find関数を使うことで、正規表現にマッチした文字列を取得することができます。

(println (re-find number-pattern target-str))  ;; => 1234
(println (re-find alpha-pattern target-str))  ;; => Hello
(println (re-find not-match-pattern target-str))  ;; => nil

(println (re-find (re-matcher number-pattern target-str)))  ;; => 1234
(println (re-find (re-matcher alpha-pattern target-str)))  ;; => Hello
(println (re-find (re-matcher not-match-pattern target-str)))  ;; => nil

(let [m (re-matcher number-pattern target-str)]
  (loop [match (re-find m)]
    (when match
      (println (str "matched => " match))  ;; => 1回目 1234、2回目 5678
      (recur (re-find m)))))

Matcherオブジェクトをループに放り込むことで、マッチした分だけ文字列を取得していくことができます。

グループ化したパターンを渡すと、結果がちょっと変わります。

(def number-capture-pattern (re-pattern "(\\d+)"))
(println (re-find number-capture-pattern target-str))  ;; => ["1234" "1234"]
(println (re-find #"(\d+)" target-str))  ;; => ["1234" "1234"]

(let [m (re-matcher number-capture-pattern target-str)]
  (loop [match (re-find m)]
    (when match
      (println (str "matched => " match))  ;; => 1回目 ["1234" "1234"]、2回目 ["5678" "5678"]
      (recur (re-find m)))))

(def number-capture-pattern (re-pattern "(\\d+)[^0-9]+(\\d+)"))
(println (re-find number-capture-pattern target-str))  ;; => ["1234 World 5678" "1234" "5678"]
(let [m (re-matcher number-capture-pattern target-str)]
  (loop [match (re-find m)]
    (when match
      (println (str "matched => " match))  ;; => ループは1回のみ ["1234 World 5678" "1234" "5678"]
      (recur (re-find m)))))

結果としてベクタが返るようになり、ベクタの最初の要素はパターンにマッチした文字列全体、それ以降はキャプチャされた文字列が続きます。

re-seq

マッチした文字列が、リストとして返ります。

(def number-pattern #"\d+")
(def number-capture-pattern #"(\d+)")
(def alpha-number-capture-pattern #"([a-zA-Z]+) (\d+)")

(println (re-seq number-pattern target-str))  ;; => ("1234" "5678")
(println (re-seq number-capture-pattern target-str))  ;; => (["1234" "1234"] ["5678" "5678"])
(println (re-seq alpha-number-capture-pattern target-str)) ;; => (["Hello 1234" "Hello" "1234"] ["World 5678" "World" "5678"])

re-groups

re-matcher/re-findと組み合わせて使用します。java.util.regex.Matcherへre-find関数を適用した後に使うことで、マッチした文字列を取得することができます。グループ化した場合にベクタが取得できるようになるのは、re-findなどと同じです。違いは、Matcherに対してre-findを適用してマッチ位置を進めない限り、何度実行しても同じマッチ結果が取得できることです。

(def number-pattern #"\d+")
(def number-capture-pattern #"(\d+)")
(def alpha-number-capture-pattern #"([a-zA-Z]+) (\d+)")

(def number-matcher (re-matcher number-pattern target-str))
(def number-capture-matcher (re-matcher number-capture-pattern target-str))
(def alpha-number-capture-matcher (re-matcher alpha-number-capture-pattern target-str))

(re-find number-matcher)
(re-find number-capture-matcher)
(re-find alpha-number-capture-matcher)

(println (re-groups number-matcher))  ;; => 1234
(println (re-groups number-capture-matcher))  ;; => ["1234" "1234"]
(println (re-groups alpha-number-capture-matcher))  ;; => ["Hello 1234" "Hello" "1234"]

(re-find number-matcher)
(re-find number-capture-matcher)
(re-find alpha-number-capture-matcher)

(println (re-groups number-matcher))  ;; => 5678
(println (re-groups number-capture-matcher))  ;; => ["5678" "5678"]
(println (re-groups alpha-number-capture-matcher))  ;; => ["World 5678" "World" "5678"]

なお、Matcherに対してre-findを適用せずにre-groupsを適用すると、IllegalStateExceptionが飛んできます。

(try
  (println (re-groups (re-matcher number-pattern target-str)))
  (catch IllegalStateException e (println (.toString e))))  ;; => java.lang.IllegalStateException: No match found

re-matches

java.util.regex.Matcher#matchesを使っての正規表現の適用となります。結果は、re-groupsと同じです。

(def number-pattern #"\d+")
(println (re-matches number-pattern target-str))  ;; => nil

(def matches-pattern #"[a-zA-Z]+ \d+ [a-zA-Z]+ \d+")
(println (re-matches matches-pattern target-str))  ;; => Hello 1234 World 5678

(def matches-capture-pattern #"([a-zA-Z]+) (\d+) ([a-zA-Z]+) (\d+)")
(println (re-matches matches-capture-pattern target-str))  ;; => ["Hello 1234 World 5678" "Hello" "1234" "World" "5678"]

clojure.string/replace-first

正規表現にマッチした最初の文字または文字列の置換を行います。

(def target-str "Hello 1234 World 5678")

(use '[clojure.string :only (replace-first)])
(println (replace-first target-str #"\d+" "NumberReplaced"))  ;; => Hello NumberReplaced World 5678

グループ化を使用した場合は、「$n」で後方参照を行うこともできます。

(println (replace-first target-str #"(\w+) \d+ (\w+)" "$1 $2")) ;; => Hello World 5678

第2引数は、java.util.regex.Patternでなくても構いません。charやStringでもOKです。

(println (replace-first target-str "World" "Clojure"))  ;; => Hello 1234 Clojure 5678
(println (replace-first target-str \o \O))  ;; => HellO 1234 World 567

(?i)を付けると、Case-Insensitiveになります。

(println (replace-first target-str #"(?i)world" "Clojure")) ;; => Hello 1234 Clojure 5678

ちなみに、ClojureDocsには関数を引数に取る以下のような例も載っていましたが、今回は試してません…。

user=> (import '(java.util.regex Matcher))
java.util.regex.Matcher

user=> (defn re-qr [replacement]
         (Matcher/quoteReplacement replacement))
#'user/re-qr

user=> (str/replace-first "fabulous fodder foo food" #"f(o+)(\S+)" (re-qr "$2$1"))
"fabulous $2$1 foo food"

clojure.string/replace

使い方は、ほぼclojure.string/replace-firstと同じです。違いは、replace-firstが最初の1回しか置換しないのに対し、replaceはマッチした分だけ置換を行います。

(println (clojure.string/replace target-str #"\d+" "NumberReplaced"))  ;; => Hello NumberReplaced World NumberReplaced
(println (clojure.string/replace target-str #"[a-zA-Z]+" "Clojure"))  ;; => Clojure 1234 Clojure 5678
(println (clojure.string/replace target-str #"(\w+) \d+ (\w+)" "$1 $2")) ;; => Hello World 5678

なお、useするとclojure.core/replaceを隠してしまうので、警告が出てしまいます。

(use '[clojure.string :only (replace)])  ;; WARNING: replace already refers to: #'clojure.core/replace in namespace: user, being replaced by: #'clojure.string/replace 

よって、

(clojure.string/replace target-str pattern replace-string))

という形になります。…ちょっと微妙。

clojure.string/split

与えられた正規表現で、文字列をベクタに分割します。

(use '[clojure.string :only (split)])
(println (split target-str #"\s+"))  ;; => ["Hello" "1234" "World" "5678"]

これのオマケで、\nまたは\r\nで分割するsplit-linesという関数もあります。

(use '[clojure.string :only (split-lines)])
(println (split-lines "The\nQuick\r\nBrown\nFox"))  ;; => ["The" "Quick" "Brown" "Fox"]

以上、正規表現一巡りでした〜。