実用的な正規表現

# はじめに

普段テキストファイルなどから特定文字を探したい時に単純な文字列検索では多数ヒットしてしまう、あいまい検索したいと思った時に使う正規表現ですが、いざ使おうと思うと忘れてしまっていたりするので、備忘録を兼ねて記事にしました。

自分で使って使えそうなものがあれば随時更新したいと思います。

# 正規表現の基本

誤解を恐れずにザックリと言うと、ある文字列に合致する文字のパターンを宣言し、一連の文字列に適用していく、文字列用のクエリ言語と言った所でしょうか。

より詳しくは、[正規表現−Wikipedia](https://ja.m.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE) などを参照してください。

記法などは以下が参考になると思いますが、実装や言語によって多少違う部分もあるので、実際に使用される正規表現のドキュメントなどを参照するのが良いかと思います。

* [javascriptの正規表現](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions)
* [Rubyの正規表現](https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html)
* [Pythonの正規表現](https://docs.python.org/ja/3/library/re.html)
* [.Net(C#, VB.Net, F#)の正規表現](https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expression-language-quick-reference)

ドキュメントは.NetかPythonが正規表現の記法を知るにはわかりやすいかと思います。

# 気軽に試せるOnline正規表現テスター

実際に検索が思った通りに動作するか確認したいときに以下のサイトで実際に試してみることができます。

* [Online regex tester and debugger](https://regex101.com)

# 文字列の検索
文字列から○○を含む文字列とか、行の先頭が○○で始まる文字列を検索したい事がよくあります。
そういった時に正規表現の検索/Grep(テキストエディタ)や、Match(プログラム言語)などを使用して検索が出来ます。

## 基本
たとえば、以下のようなCSVに対して検索することを考えてみます。

```
id,name,age
1,Joe,26
2,Alice,22
3,Mike,nil
```

例えば、３列目に数字以外が含まれているかを検索したい場合は、以下の正規表現を使って検索できます。

```ruby:regex_match.rb
# 検証するデータ
test_data = <<-EOS
1,Joe,26
2,Alice,22
3,Mike,nil
EOS
# 正規表現で行頭から２つまでのカンマまでは何でも良く(([^,]*,){2})
# ３列目に数値以外にマッチするようにする(?:[^0-9]+)
/^(([^,]*,){2})(?:[^0-9]+)$/.match(test_data)
#=> #<MatchData "3,Mike,nil\n" 1:"3,Mike," 2:"Mike,">
```
この場合、3行目の文字列とマッチします。
正規表現が使えるテキストエディタで検索する場合は`^(([^,]*,){2})(?:[^0-9]+)$`の部分を入力すれば、同様の検索が可能です。

４列目やn列目を対象に検索する場合は、`{2}`を`{3}`や`{n-1}`に変更することで応用ができます。
行内の間の列を検索をしたい場合は、`^(([^,]*,){2})(?:[^0-9]+)$`を`^(([^,]*,){2})(?:[^0-9]+?)(,.*)?$`に置換えることで任意の列の検索ができるようになります。
量指定子と呼ばれる任意の繰返しが使いこなせるようになれば、CSVなどのテキスト情報も簡単に扱えるようになるので、是非正規表現の記法は頭に入れるか、直ぐにドキュメントを参照できるようにしてください。


# 文字列の置換

文字列の検索で何でカッコ`()`で囲んでいるのか理由が分かる方は多分この記事を見てもあまり参考にならないので、より詳細に解説されているサイトへ行くのをお勧めします。
検索できたのはいいけど、検索した文字列を別な文字列で置換えたいと思うことが大半だと多います。
そこで、文字列の検索で使用したデータを使って、ageに`nil`が設定されているデータを`-1`に置換えてみましょう。

```ruby:regex_replace.rb
# 検証するデータ
test_data = <<-EOS
1,Joe,26
2,Alice,22
3,Mike,nil
EOS
# Rubyで置換をする場合は、gsubメソッドまたはgsub!を使用する
# 使い方：<文字列>.gsub(<検索条件>, <置換文字列>)
# 他の言語では、replaceやreplaceAll等のメソッドが用意されていることが多い
test_data.gsub(/^(([^,]*,){2})(?:[^0-9]+)$/, '\1-1')
#=> "1,Joe,26\n2,Alice,22\n3,Mike,-1"
```

コードを見て`\1`って何？と思った方もいると思うので、簡単に説明します。
冒頭で正規表現内でカッコ`()`で囲んでいることについて少し触れましたが、ほとんどの正規表現実装では、カッコで囲んだ正規表現をグループとしてキャプチャすることができ、その条件に一致した文字列を置換文字列として`$1`や`\1`等で再利用できるようになっています。
中にはグループに任意の名前を割当てて明示的に参照できるものもあります。
検索の際に出力された、`#<MatchData "3,Mike,nil\n" 1:"3,Mike," 2:"Mike,">`の`1:"3,Mike,"`が`\1`の代わりに代入されて、その後に`-1`を出力せよという意味で、置換文字列を`\1-1`としています。

# 文字列の分割

置換と考え方は同じですが、より実践的に使う例を紹介します。
## CSV（カンマ区切り）からTSV（タブ区切り）に変換
### 列内にカンマが含まれない場合

列にカンマが含まれていない場合は単純にカンマをタブに置き換えるだけなので、簡単ですね。
データはこれまでと同じものを使用して例を示します。

```ruby:regex_split.rb
# 検証するデータ
test_data = <<-EOS
1,Joe,26
2,Alice,22
3,Mike,nil
EOS
# Rubyで置換をする場合は、gsubメソッドまたはgsub!を使用する
test_data.gsub(/,/, '\t')
#=> "1\\tJoe\\t26\n2\\tAlice\\t22\n3\\tMike\\tnil\n"
```

これは簡単ですね。カンマを条件にタブ（`\t`）を置換文字に割当てるだけでOKです。

### 列内にカンマが含まれる場合

では、列内にカンマが含まれる（"Joe, Doe"）場合はどうでしょうか。
そういったCSVはダブルクォート（`"`）で囲まれているのが一般的です。
データを一部変更して試してみましょう。


```ruby:regex_split_with_dquote.rb
# 検証するデータ
test_data = <<-EOS
1,"Joe, Doe",26
2,Alice,22
3,Mike,nil
EOS
# Rubyで置換をする場合は、gsubメソッドまたはgsub!を使用する
test_data.gsub(/,(?="[^"]*?")|,(?<!")(?![^"]*?")/, '\t')
#=> "1\\t\"Joe, Doe\"\\t26\n2\\tAlice\\t22\n3\\tMike\\tnil\n"
```

実はこれ、テキストエディタ等でも試せるように置換を使っていますが、一行に複数ダブルクォートで囲まれた場合などは意図した通りに機能しません。
列の数が分かっている場合などはデータに合わせて正規表現を書き換えてあげる必要があります。

JavaやPythonといったプログラム言語ではこういったCSVなどを扱うための便利なライブラリが用意されていますので、そちらを使った方が楽に扱うことが出来ますが、一度正規表現の使い方を覚えてしまえばいろいろなことに応用できるので、まだ使い慣れていないという方は試してみてはいかがでしょうか。