Scalaの正規表現で気楽にテキスト処理したい人向け情報です。
Scala 2.12.xの場合で説明します。
要約
言いたいことは結局以下の2点です。
- Scalaいいよね
- Scalaの正規表現は使いやすい
何故Scala?
書いていて楽しい。これが一番です。
機能的にも、以下の3点を全て満たしているのがいいですね。
- 型安全性
- コンパイル時に型エラーを全て報告
- エラーを発見しやすい
- ゴリゴリの関数型言語的使い方をしなければ保守が簡単
- 実行速度
- JVM上で動くので、Java同様起動すれば早い
- 正規表現の使いやすさ
- 正規表現初心者に優しい
- scala-parser-combinators と組み合わせることで
関数型言語が分かれば楽々構文解釈できる
Python ではダメなのか
これ以降の話ならPythonでだいたい同じことできるよ、と思われる方も多いでしょう。存じております。私も一部使用しています。Pythonもいい言語です。
ただ、以下の理由でメイン利用はしていません。
- 実行速度に難あり
- Pythonはデバッグが大変
- 静的型付けではないため、型エラーを起こしやすい
- 結局自分で型チェックをすることになる
- オフサイドルールが意外とバグの温床になる
インデント間違えるのが悪いんだけど、人間だもの
Javaではダメなのか
実行速度、型安全性の観点から言って、Javaは非常に優秀です。
Javaは最初のJVM起動こそ遅いですが、一度起動さえすれば実行速度は早いです。
私自身、かつてはJavaの堅牢さとinterface/abstract class回りの設計の素晴らしさに心酔していました。Javaは本当によく練られた言語です。
ただし、Javaは単純な処理でもソースコードが長くなる傾向にあります。Scalaだと1行(なおかつメソッド1, 2個のチェイン。Java servletにありがちなメソッドチェイン地獄ではない)で終わる操作をJavaで実装すると、場合によってはその長さに真顔になります。特に一番使う文字列/コレクションの処理で言語側の機能差が如実に出ます。
Javaも 8 からScalaから関数リテラルが輸入されましたが、正直使い辛いです。そもそもJavaは全体的にIDEないと無理です。
何より、個人的に多用している正規表現が使っていて辛いです。バックスラッシュ地獄です。
バックスラッシュ地獄
Javaの正規表現はJava自身の特殊文字(バックスラッシュ\
が代表的)もエスケープする必要があります。
JDK13 にRaw String LiteralがPreviewとして入ったらしいですが、現状のJavaで正規表現を使うとストレスがマッハです。
例えばWindowsのファイルパスは区切り文字がバックスラッシュです。そういったバックスラッシュを文字としてを含む文字列を処理しようとすれば、最早バックスラッシュ地獄です。Windowsもいい加減正式にスラッシュ/
をファイルパスの区切り文字にしてください。
Scalaで正規表現を使用する方法
1. 正規表現の型
正規表現は型scala.util.matching.Regex
で表します。
import scala.util.matching.Regex
型を明記せずに型推論に任せきりにするなら、importは不要です。
ただし、一ヶ月後に自分で読んでも型が分からなくなる場合があるので、型を書きましょう。
2. 正規表現の書き方
2.1. 基本的な使い方
文字列リテラルに.r
をつけるだけです。
例えば、a
で始まってz
で終わる最短一致の場合、以下のように書きます。
val pattern:Regex = "a.*?z".r
2.2. triple quotes でエスケープを気にせず記入できる
Scalaのtriple quotesを使用することで、プログラミング言語側の特殊記号を無視して正規表現をそのまま記述できます。
"a.*?\\z".r // abcd\z にマッチしない(エスケープにより「\\」が「\」一文字扱いされる)
"""a.*?\\z""".r // abcd\z にマッチ(\\は正規表現の\\としてそのまま機能する)
Javaで正規表現を扱う時に心が折れる難問を楽々突破です。これで無敵ですね!
Scalaのtriple quotes は以下のような複数行の記述も可能とします。
"""abc
def
ghi
"""
PythonでRaw stringとtriple quotesを同時に使った場合と同じ動作をすると思えばいいかと思います。
無視できない特殊文字
無視できない特殊文字もあります。
例えば\u
、つまりユニコード文字を番号で指定する場合です。
おなじみ、\u0041
(10進数では65)はA
です。
例えばLaTeXのプリアンブルでパッケージを追加する文法\usepackage{パッケージ名}
を記述する場合、以下の書き方では\u
部分がユニコード文字記述用の命令として解釈され、エラーになります。
val str:String = """\usepackage{hyperref}"""
scala> val str:String = """\usepackage{hyperref}"""
<console>:1: error: error in unicode escape
val str:String = """\usepackage{hyperref}"""
この問題は、Scalaのコンパイル/実行時のオプションに-Xno-uescape
をつけることで対応できます。
# コンパイルする場合 (実行時には不要)
scalac -Xno-uescape RegexCheck.scala
# Scala REPLを実行する場合
scala -Xno-uescape
fat jarでアプリケーションを配布したい(つまりjavaコマンドで実行させたい)場合も、
Scalaで書いたソースコードのコンパイル時だけ-Xno-uescape
オプションをつければ動作します。
3. 正規表現に一致した特定の範囲を抽出
3.1. 書き方
以下のように()で囲むことで、抽出したい部分を指定できます。
"前(抽出したい部分)後ろ".r
()の数は何個でも増やせます。
抽出したい部分の数に合わせてください。
"""ab(c.*?f)g.*?l(m.*?z)""".r
注意点
- ()の中に()を入れることはできません
- 文字列としての()はバックスラッシュつき
\(
,\)
で示します - ()の数と、次の3.2. 抽出の仕方のcase文中での抽出子の数は一致しないといけません
- コンパイルエラーにはなりませんが、実行時にエラーになります
3.2. 抽出の仕方
ここでは、一行ずつのマッチを例に話します。
複数行マッチも可能ですが、そちらは今回は省きます。
3.2.1. 全体一致(従来)
Scalaの正規表現で一行中の特定の一部だけが欲しい場合でも、以下のように一行全体に一致させる必要があります。
val pattern:Regex = "^a(b.*?d)z$".r
val line:String = "abcd----dog-data-xyz"
line match{
case pattern(x) => x // ここでの抽出子 x は 任意の名前でいい。正規表現中の()内に相当
case _ => "None!!"
}
// 上の例では "bcd" を返す
3.3.2. 部分一致
型Regex
のオブジェクトには、unanchored
メソッドがあります。
このメソッドを実行すると、型UnanchoredRegex
のオブジェクトが作成されます。
Regex
と違い、文字列の一部だけでも正規表現にマッチすればいいです。
以下、公式ドキュメントからの引用です。
def unanchored: UnanchoredRegex
Create a new Regex with the same pattern, but no requirement that the entire String matches in extractor patterns and Regex#matches.
以下、例です。
val pattern:Regex = "a(b.*?d)xyz".r
val pattern2:Regex = pattern.unanchored
val line:String = "!:dwabcd----dog-data-dxyz:^-/a;dw"
line match{
case pattern(x) => x
case _ => "None!!"
}
// "None!!" を返す
line match{
case pattern2(x) => x
case _ => "None!!"
}
// "bcd----dog-data-d" を返す
scala> import scala.util.matching.Regex
import scala.util.matching.Regex
scala> val pattern:Regex = "a(b.*?d)xyz".r
pattern: scala.util.matching.Regex = a(b.*?d)xyz
scala> val pattern2:Regex = pattern.unanchored
pattern2: scala.util.matching.Regex = a(b.*?d)xyz
scala> val line:String = "!:dwabcd----dog-data-dxyz:^-/a;dw"
line: String = !:dwabcd----dog-data-dxyz:^-/a;dw
// Regex では文字列全体にマッチしないといけない。
// 今回の場合、文字列全体とはマッチしないので、"None!!"を返す
scala> line match{
| case pattern(x) => x
| case _ => "None!!"
| }
res0: String = None!!
// UnanchoredRegex では文字列の一部にマッチすればいい。
// 今回の場合、文字列の一部にマッチしたので、()で囲った部分を返す。
scala> line match{
| case pattern2(x) => x
| case _ => "None!!"
| }
res1: String = bcd----dog-data-d
参考元
scala.util.matching.Regex (Scala API Document)
抽出子オブジェクト(TOUR OF SCALA)