保守開発者のための正規表現入門

# 正規表現とは
正規表現で検索するとたくさん解説しているサイトや記事が出てきますが私は以下のサイトの説明が端的でわかりやすいと思いました。

> 　端的に言えば、「いくつかの文字列を一つの形式で表現するための表現方法」です。
　では、なぜこの表現方法が有名なのかといえば、この表現方法を利用すれば、たくさんの文章の中から容易に見つけたい文字列を検索することができるためです。

https://userweb.mnet.ne.jp/nakama/

よく利用するメタ文字（正規表現で文字列を表現するために利用される文字）についても事例を含めて解説されていますので、読んでみるとどういうものなのかが理解しやすいと思います。

より厳密に理解したい方や歴史について知りたい方はWikipediaの記事等を読んでみると良いと思います。

https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE

# 正規表現が使えると何がうれしいのか？

正規表現を利用するとテキストエディタ等で文字列の検索や置換を行う際にいろいろなことができるようになります。
ソースコードやログファイルを扱うエンジニアの方はもちろん、テキストファイルを扱う職種の方は覚えておいて損はないものだと思います。

# 本記事の記載内容
本記事では保守開発の現場でよくあるログの解析やデータ取得用のSQLを作成するといったシーンで正規表現を利用してこういう風に楽ができるよという事例を記載します。
初心者向けの内容です。
保守開発の現場に入って間もない方、正規表現をあまり使ったことがない方の一助になれば幸いです。

本記事に記載している正規表現はサクラエディタの検索・置換機能で動作確認をしています。
利用するツールによって表現方法が異なることがありますのでご注意ください。

# 利用例（検索編）

## ログファイルの中から複数の条件に該当する行を検索する
ログファイルの解析は保守開発者が頻繁に行う作業です。
アプリケーションから出力されるログはもちろんOSやミドルウェアが出力するログファイルを解析する必要があることもあります。

これらはそれぞれ形式が異なるものですが、大きなテキストファイルであることが多いです。
そのため文字列の検索を行って解析していくことが多いです。

例えばユーザーがアプリを利用しているときにエラーが発生し、オペレーションができなくなってしまっているというようなトラブルであれば、アプリのログから問題の発生時刻付近でエラーログやStackTraceなどが出ている箇所を探してエラーの原因を特定して問題解消をしたりします。

エラー文言がログに出力されていたり、StackTraceが出ている場合であればエラー文言やExceptionの文字列で検索するだけでうまく見つけられる場合もありますが少し込み入った条件で検索したい場合もあります。

以下のような形式で出力されるアプリケーションログがあるとします。
```
2023-09-02 08:07:23.696 [Thread-1] INFO  jp.co.hoge.HogeManager - initializing HogeManager
2023-09-02 08:07:23.697 [Thread-2] INFO  jp.co.hoge.FugaDataLoader - start loading
2023-09-02 08:07:23.825 [Thread-1] WARN  jp.co.hoge.HogeManager - not exist setting data
2023-09-02 08:07:23.826 [Thread-2] WARN  jp.co.hoge.FugaDataLoader - closing error
2023-09-02 08:07:23.827 [Thread-2] ERROR jp.co.hoge.FugaDataLoader - illeagal data state
2023-09-02 08:07:24.925 [Thread-2] INFO  jp.co.hoge.HogeManager - finish execution
・・・
```
１行の中に 
- 日付
- 時刻
- スレッド名
- ログレベル
- ログを出力したクラス
- ログメッセージ

という順番で出力される形式です。
確認したいログが `illeagal data state` と出ているもののように明確な場合は単純にその文字列で検索をかければ良いです。
しかし例えば `Thread-1` の `HogeManager` の処理を順番に追っていきたいというような場合は正規表現で検索すると便利です。

検索条件 `Thread-1.*HogeManager` とすると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/08b6baef-7c5c-1a4c-f428-3a0c5fc3733b.png)


このような検索結果になります。
記述の意味としては
`Thread-1` 「Thread-1」という文字列
`.` が任意の文字一文字 `*` が繰り返しという意味なので
`.*` で任意の文字の繰り返し
`HogeManager` 「HogeManager」という文字列
という意味で、その条件に合致した箇所が検索結果としてマーキングされています。

さらにログレベルがWARNのログのみに絞りたい場合は
検索条件 `Thread-1.*WARN.*HogeManager` のようにすれば

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/c35ec119-8266-5a14-d82e-73e1c9a40efc.png)

となります。

さらに2023-09-02 08:07のログのみに絞りこみたければ
検索条件 `2023-09-02 08:07.*Thread-1.*WARN.*HogeManager` のようにすれば
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/b6fcbb9f-608f-30dd-2234-391da7e52ec8.png)

となります。

※画像の例ではすべて2023-09-02 08:07のログで単一のファイルなので効果がわかりづらいですが、数千行、数万行ある複数のログファイルから該当の箇所を探したりする時に便利です。

正規表現は通常できるだけ長い文字列を優先してマッチするため、`.*` を多用すると余計なログが引っかかってくることがありますが、
**あまり難しく考えずに楽な方法で試してみてダメなら書き方を考える** 
というやり方が良いと思います。

ログのフォーマットに合わせて厳密に書くとすれば
`2023-09-02 08:07:[0-9]{2,2}\.[0-9]{3,3} \[Thread-1\] WARN jp\.co\.hoge\.HogeManager` 

`2023-09-02 08:07:`「2023-09-02 08:07:」という文字列
`[0-9]{2,2}` 数字を2回繰り返す
`\.` 「.」という文字列（`.`は任意の文字を表すので`\`でエスケープする必要があります）
`[0-9]{3,3}` 数字を3回繰り返す
` \[Thread-1\] WARN jp\.co\.hoge\.HogeManager` 「 [Thread-1] WARN jp.co.hoge.HogeManager」という文字列（`[`や`]`も正規表現の中でグループを表すためエスケープする必要があります）

というように書くこともできます。
厳密に書くと余計なログが引っかかることが少なくなります。

## 実行時間が記録されているログファイルから特定の時間以上かかっている行を検索する
以下のような形式でSQLの実行ごとに出力されるログファイルがあるとします。

```
2021/10/05 05:07:32.892
[do] 1(ms)
[class]jp.co.hoge.EmpDataLoader.getName(EmpDataLoader.java:83)
[sql]select emp_name from emp_id = ?
[param(1)]1=19999
-----------------------
```
１行目から順に
- 実行日付
- 実行にかかった時間
- 実行したクラス、メソッド
- SQL文
- SQL文にバインドするパラメーター

というような内容が出力される形式です。

パフォーマンス問題が起きた時にSQLの実行が怪しいとなりこのログファイルから1秒以上かかっているSQLを探したいという場合、以下のような正規表現で1000(ms)以上時間がかかっている行を検索することができます。

`[0-9]{4,}\(ms\)`

`[0-9]{4,}`この部分は0～9の文字が4回以上繰り返すという意味で
その後ろに `(ms)` がついている箇所を検索するという意味になります。
※ `(` や `)` はそのまま利用できないので `\` でエスケープしています。

少し複雑になってきますが、500ms以上かかっているSQLを検索したい場合は以下のようにOR表現を使えば検索できます。

`([0-9]{4,}|[5-9][0-9][0-9])\(ms\)`

`|` がORを表していて、意味としては
`[0-9]{4,}` 0～9の文字が4回以上繰り返している
または 
`[5-9][0-9][0-9]` 5-9の文字、0-9の文字、0-9の文字という順に並んでいる
その後ろに(ms)という文字列がある行を検索する、ということになります。

4桁以上の場合はすべて検索対象にする、3桁までの場合は百の位が5-9のもののみを検索対象にする、というような作りです。

# 利用例（置換編）
## テーブル名からSELECT文を作る  
トラブルが起きた時、ユーザーのデータベースのデータを取得して確認することがあると思います。
テーブルの一覧はあるけれど、実際にデータを取得するにはそれをSELECT文にする必要があったりします。

`EMPLOYEE` -> `SELECT * FROM EMPLOYEE ORDER BY 1;`

テーブルが1つ、2つぐらいであれば手で打ち込んだりコピペしたりできますが
20、30、、、100以上というような状況になってくると大変です。

そんな時は正規表現で置換してしまえば楽に置き換えができます。
以下のようなテーブル名のみが並んだテキストがあるとき
```
EMPLOYEE
SALARY
SECTION
POST
```

置換前 `^(.*)$`
置換後 `SELECT * FROM $1 ORDER BY 1;`
として一括置換すると以下のようなテキストに変換できます。

```
SELECT * FROM EMPLOYEE ORDER BY 1;
SELECT * FROM SALARY ORDER BY 1;
SELECT * FROM SECTION ORDER BY 1;
SELECT * FROM POST ORDER BY 1;
```
置換前の正規表現の意味としては
`^` が行の先頭
`.*` が任意の文字の繰り返し
`$` が行末
という意味です。
置換前に`()`で囲んだものが置換後の `$1` の部分に代入されて置き換えがされるため、もともとあった１行の内容を$1の部分に代入した文字列が置換後の結果となります。

`()` で囲んだ文字列が複数ある場合は `$1` `$2` のようにそれぞれ使うこともできます。
例えば以下のようにテーブル名とORDERしたいカラムがセットになっているようなファイルを作れば

```
EMPLOYEE,EMP_ID
SALARY,EMP_ID
SECTION,SECTION_ID
POST,POST_ID
```

置換前 `^(.*),(.*)$`
置換後 `SELECT * FROM $1 ORDER BY $2;`
としてやることで以下のように置換できます。

```
SELECT * FROM EMPLOYEE ORDER BY EMP_ID;
SELECT * FROM SALARY ORDER BY EMP_ID;
SELECT * FROM SECTION ORDER BY SECTION_ID;
SELECT * FROM POST ORDER BY POST_ID;
```

また置き換えは複数箇所で使うこともできるので、
置換前 `^(.*),(.*)$`
置換後 `SELECT $2 FROM $1 ORDER BY $2;`
のようにするとSELECTするカラムに指定したうえでORDERするカラムにも指定するといったこともできます。

```
SELECT EMP_ID FROM EMPLOYEE ORDER BY EMP_ID;
SELECT EMP_ID FROM SALARY ORDER BY EMP_ID;
SELECT SECTION_ID FROM SECTION ORDER BY SECTION_ID;
SELECT POST_ID FROM POST ORDER BY POST_ID;
```

## 改行やスペースが入っているSQLを1行にしてスペースを1つずつにする
GUIを持ったDBToolの中には人間が読んだり修正したりしやすいようにSQLを整形してくれるものがあります。
```
SELECT
    EMP.EMP_NAME, SAL.SALARY 
FROM 
    EMPLOYEE EMP, SALARY SAL 
WHERE 
    EMP.EMP_ID = SAL.EMP_ID AND
    SAL.YEAR = 2023 AND
    SAL.MONTH = 9 
ORDER BY
    EMP_ID;
```
どのテーブルをどう結合してどのカラムのデータを取得しているのかがわかりやすく、WHERE句の条件を変更したりするのもやりやすいです。
しかしユーザー先でCUIのDBツールで実行する場合は改行が入っていると実行できなかったりします。

この状態で修正した後に1行に戻すという作業は地味に面倒です。
正規表現の置換を使ってやってみます。

まず以下の置換で改行文字を消します。
置換前 `\r\n`
置換後 (何も入力しない)
`\r\n` は復帰改行を表します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/ec0c4528-e7a7-f4d1-90b5-2127bb4db35c.png)

すべて置換するとこうなります。

```
SELECT    EMP.EMP_NAME, SAL.SALARY FROM     EMPLOYEE EMP, SALARY SAL WHERE     EMP.EMP_ID = SAL.EMP_ID AND    SAL.YEAR = 2023 AND    SAL.MONTH = 9 ORDER BY    EMP_ID;
```

次に複数のスペースが重複している箇所を一つのスペースにまとめます。

置換前 ` {2,}` 
置換後 ` `
わかりにくいですがスペースが入っており、2つ以上のスペースが続いたものをスペース1つに置換します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1126770/b85f5295-4802-a39d-01b5-aab4f5c20eb8.png)

すべて置換すると1行で連続したスペースのないSQLになります。

```
SELECT EMP.EMP_NAME, SAL.SALARY FROM EMPLOYEE EMP, SALARY SAL WHERE EMP.EMP_ID = SAL.EMP_ID AND SAL.YEAR = 2023 AND SAL.MONTH = 9 ORDER BY EMP_ID;
```

置換を使うときのポイントは
**がんばって複雑な表現1回で置き換えようとするよりも簡単な表現を複数回使って置き換える**
ことかなと思います。

# どんどん使ってみましょう
今回取り上げた例はどれもちょっとした作業ですが、そういった作業が正規表現を使うと楽にできるということ
正規表現でこういうことができるということ、を体感していただけたのではないかと思います。

こういった作業に出会ったときに正規表現を使えないかなーと考えてやってみるとパズルを解くように楽しみながらできることが増えていくので、ぜひどんどん使ってみてください。