リモート開発メインのソフトウェア開発企業のエンジニアブログです

Spark での regex_replace

前回の記事で「軽く書いていきます」と言いつつ長文を書いてしまったので、今度こそ軽くまとめたいと思います。)

regex_replace の説明(本題)

基本

Spark SQL には、regex_replace という関数があります。関数名から想像が付くかと思いますが、DataFrame のカラム等の文字列の一部を、正規表現を使って別の文字列に置き換えるものです。

ドキュメントは以下のページにあります。

Functions – Spark SQL, Built-in Functions

上述のドキュメントによれば、以下のように使うそうです。

> SELECT regexp_replace('100-200', '(\d+)', 'num');
 num-num

ここまでは問題ないと思います。

キャプチャーした文字列を使うには?

上の例では、文字列中に現れる数値( \d+)を num という文字列に置き換えました。

では別の例として、6個の数字が並んでいたら(それを yyyyMM だと解釈して)、6個の数字のうち4番目の数字の後ろに を入れて、6番目の数字の後ろに を入れるという置換をしたいとします。202201 〜 202203 という文字列があれば 2022年01月 〜 2022年03月 という文字列に置換するという形です。

この場合、どのように書けば良いでしょうか。他の言語の経験があれば、 $1\1 とかを使うんだろうというのは容易に想像が付きますが、$ と \ のどっちなのか、エスケープは必要なのか、などで悩むと思います。

結論から書くと、(Scala の場合)以下の通り $1, $2, … を使います。

import org.apache.spark.sql.functions.regexp_replace

df.select(
  regex_replace(df("col1"), "(\\d{4})(\\d{2})", "$1年$2月")
)

困ったことに上述のドキュメントには記載がありませんし、Stack Overflow でもいくつか違った答えが見つかり、何度か実際に試してみて正解にたどり着いたので、今回の記事を書くことにしました。後から考えると、以下の答えが正解だったわけですが。

regex – Back-reference in Spark DataFrame regexp_replace – Stack Overflow

解説・補足

ソースを見てみた

regex_replace のソースを軽く見てみました。すると、正規表現による置換処理には java.util.regex.Matcher#appendReplacement が使われていましたことが分かりました。 Matcher のドキュメントは以下にあります。

Matcher (Java Platform SE 8 )

ドキュメントに記載の通り、キャプチャーグループへの参照は $g だけでなく ${name} も使えるので、 regex_replace でも ${name} は使えるものと思われます。(試していない)

正規表現が面倒なわけ

プログラムで正規表現を扱う場面というのは多々あると思いますが、いろいろな面倒な点もあるので、それについて少し書きます。

色々な方言がある

語弊を恐れずに言うならば、正規表現が広く使われるようになったのは Perl の功績が大きいです。Perl においては、正規表現は言語の中心的な機能であり、昔は(20年以上前)正規表現による文字列処理が書きやすいからという理由で Perl を使うことが多かったです。

その後に出てきた様々な言語でも当然正規表現をサポートしていますが、「Perl 互換」あるいは Perl とほぼ同様の正規表現をサポートする言語・ライブラリーが多くあります。

Perl 互換のもので有名なのは PCRE (Perl Compatible Regular Expressions) で、プログラム言語では PHP などが PCRE を使っています。ただし、通常使う分には殆ど問題になりませんが、PCRE は「Perl 互換」と言いつつも Perl との細かい差異もあります。

また、Perl と「ほぼ」同様の正規表現をサポートする言語も多いです。例えば、Ruby で使われている Onigmo (Oniguruma-mod) という正規表現エンジンは、Perl 互換とは謳っていないようですが、Perl の新しいバージョンで導入された機能を取れ入れたりしていることから、Perl 互換を意識しているように見えます。

一方、Perl とは別の一大潮流が POSIX の BRE (Basic Regular Expressions = 標準正規表現) と ERE (Extended Regular Expressions = 拡張正規表現) で、bash, grep, sed 等の各種 UNIX 系コマンドで使えます。仕様については以下のドキュメントを参照してください。

Regular Expressions

BRE と ERE でわかりにくいのは、BRE の場合グルーピングを \(aaaa\) のようにするのに対して、ERE の場合は (aaaa) のようにバックスラッシュが要らないことです。また、各種コマンドによって BRE が使われたり ERE が使われたり、あるいはオプションによってそれらが切り替えられたり、grep だと PCRE が使えるオプションもあったりと、とにかく面倒です。

エスケープとか

正規表現が各種プログラム言語でどういう扱いなのかも大きく異なります。

Perl では正規表現は言語の中心的な機能であり、正規表現を扱うための独自の構文などが存在します。

$a = "123";
if ($a =~ /^\d+$/) {
  print "number!"
}

JavaScript も正規表現は変数などとは異なった扱いで、いわゆる first class citizen です。

一方、Java などで正規表現を扱うには、正規表現が書かれた文字列を用意し、それを正規表現のクラス・メソッドに渡すという形です。Perl で /^\d+$/ と書く正規表現は、Java では "^\\d+$" というようにバックスラッシュをエスケープした文字列を使う必要があります。

書いていてまた長くなりそうだったので、この辺にしておきます。

まとめ

Spark の regex_replace は、名前から分かるとおり、正規表現を使って文字列を置換するものです。内部的には Java の正規表現関連のクラスが使われているため、グループ化したパターンを置換文字列中で $1, $2 などで参照することが出来ます。

regex_replace のドキュメントにはごく簡単な使用例しか書かれていませんが、正規表現には色々な方言などがあるので、regex_replace のような正規表現を使える機能・クラス・メソッドの場合、どういった正規表現の機能が使えるのかについて説明して欲しいところです。

Tags

← 前の投稿

macOS で Java を使う方法 ver. 2022

次の投稿 →

Python での日付・時刻の扱い方

コメントを残す