シンプル図解: 正規表現の (?= ) とか (?! ) とか (?<= ) とか (?<! ) とか

:::note info
この記事は、不動産情報サイト [LIFULL HOME'S](https://www.homes.co.jp/) などを運営する[株式会社LIFULL](https://lifull.com/) の **[LIFULL Advent Calendar 2022](https://qiita.com/advent-calendar/2022/lifull)** の記事です。
:::

入社1年目、趣味は整理整頓、 @pal4de です。
正規表現が大好きです。

先日投稿した記事が好評いただき本当にうれしかったです。もっとたくさんの人に読んでもらいたい！！！

https://qiita.com/pal4de/items/55a1e1cfb0f17624e595

さて、上の記事で紹介したのは単語境界 `\b` でしたが、これは先読み/後読みを駆使して下記の通りに表せると紹介しました。

```
(?<=\W|^)(?=\w)|(?<=\w)(?=\W|$)
```

便利な`\b`ですらショートハンドに過ぎず、真に強力なものは 先読み`(?=...)`と後読み`(?<=...)` だということを主張したい！！！
わかりすい図解も添えましたので、是非お楽しみください🏝️

# 正規表現の記号は4種類

先読みの紹介に入る前に、**改めて正規表現を俯瞰**してみましょう。
誤解を恐れずに言えば、正規表現に出てくる**記号の種類は4つしかない**といえます[^4種類] [^社内勉強会]。

[^社内勉強会]: LIFULL Tech Hubという社内の技術関連の発表会のスライドの図を流用しています

[^4種類]: 左から順に 文字クラス・数量子・言明(アンカー) と名前が付いています。 ４つ目は実際は雑にその他をまとめただけ............

![IMG_2203.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337174/545c2c8c-2dcd-7343-41ac-368fb3f9a5ea.jpeg)

このようにとらえてみると、正規表現と向き合うのがだいぶ楽になります。体系化された理解への第一歩ですね 🗿
図にもある通り、 行頭 `^` も行末 `$` も単語境界 `\b` も先読み `(?=...)` も後読み `(?<=...)` も **<font color="ab7c9d">文字の前後の条件を表すもの</font>** に分類されます。

### 「文字の前後の条件を表すもの」の特徴

この **<font color="ab7c9d">文字の前後の条件を表すもの</font>** の特徴は次の通りにとらえましょう。

- **1文字の前後の条件**に関する特殊な表現
- 文字と文字の間にマッチする
- **0文字分の幅**しか持たない (どれほど長く記述されていても)

### 例を見てみる

試しに `const [level, setLevel] = useState(1);` に対して **<font color="ab7c9d">文字の前後の条件を表すもの</font>** を使ってみた例も見てみましょう[^ignore-case]。

[^ignore-case]: 大文字小文字の区別はしないものとします

| 正規表現       | マッチの結果                            | 解説 |
|:---------------|:---------------------------------------|:---|
| `^`            | <b><font color="ed6103">\|</font></b>const [level, setLevel] = useState(1); | 行頭にマッチ |
| `$`            | const [level, setLevel] = useState(1);<b><font color="ed6103">\|</font></b> | 行末にマッチ |
| `\b`           | <b><font color="ed6103">\|</font></b>const<b><font color="ed6103">\|</font></b> [<b><font color="ed6103">\|</font></b>level<b><font color="ed6103">\|</font></b>, <b><font color="ed6103">\|</font></b>setLevel<b><font color="ed6103">\|</font></b>] = <b><font color="ed6103">\|</font></b>useState<b><font color="ed6103">\|</font></b>(<b><font color="ed6103">\|</font></b>1<b><font color="ed6103">\|</font></b>); | 単語境界にマッチ |
| `(?=,)` | const [level<b><font color="ed6103">\|</font></b>, setLevel] = useState(1); | **右側**に `,` がある場所にマッチ |
| `(?<=set)` | const [level, set<b><font color="ed6103">\|</font></b>Level] = useState(1); | **左側**に `set` がある場所にマッチ |

どれも**文字と文字の間にマッチしている**イメージがつかめるはずです。
これに、ただの文字列`level`を組み合わせると このグループの底力が垣間見えてきます。

| 正規表現       | マッチの結果                            |
|:---------------|:---------------------------------------|
| `level` | const [<b><font color="ed6103">level</font></b>, set<b><font color="ed6103">Level</font></b>] = useState(1); |
| `level(?=,)` | const [<b><font color="ed6103">level</font></b>, setLevel] = useState(1); |
| `(?<=set)level` | const [level, set<b><font color="ed6103">Level</font></b>] = useState(1); |

# 先読みとは

本題です。

正規表現の先読み `(?=...)` とは、記述された部分よりも **右側の条件を表す** 記号です。

先述の通り、**<font color="ab7c9d">文字の前後の条件を表すもの</font>** は0文字分の幅しか持ちません。
`level(?=,)`の例をビジュアルで表現するならば次のようなイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337174/10ab356f-80f9-0e35-ca88-aaed3b154efe.png)

我ながらよくできた図だと思います。

## 否定先読み

先ほど紹介した 先読み `(?=...)` の逆、すなわち **右側の条件を満たさないことを表す** 記号が 否定先読み `(?!...)` です[^否定の記号と肯定の記号]。

[^否定の記号と肯定の記号]: 先読み/後読みの世界では `=` の否定が `!` です。

`const [level, setLevel] = useState(1);`

| 正規表現       | マッチの結果                            | 解説 |
|:---------------|:---------------------------------------|:--|
| `(?!,)` | <font color="ed6103">\|</font>c<font color="ed6103">\|</font>o<font color="ed6103">\|</font>n<font color="ed6103">\|</font>s<font color="ed6103">\|</font>t<font color="ed6103">\|</font> <font color="ed6103">\|</font>[<font color="ed6103">\|</font>l<font color="ed6103">\|</font>e<font color="ed6103">\|</font>v<font color="ed6103">\|</font>e<font color="ed6103">\|</font>l,<font color="ed6103">\|</font> <font color="ed6103">\|</font>s<font color="ed6103">\|</font>e<font color="ed6103">\|</font>t<font color="ed6103">\|</font>L<font color="ed6103">\|</font>e<font color="ed6103">\|</font>v<font color="ed6103">\|</font>e<font color="ed6103">\|</font>l<font color="ed6103">\|</font>]<font color="ed6103">\|</font> <font color="ed6103">\|</font>=<font color="ed6103">\|</font> <font color="ed6103">\|</font>u<font color="ed6103">\|</font>s<font color="ed6103">\|</font>e<font color="ed6103">\|</font>S<font color="ed6103">\|</font>t<font color="ed6103">\|</font>a<font color="ed6103">\|</font>t<font color="ed6103">\|</font>e<font color="ed6103">\|</font>(<font color="ed6103">\|</font>1<font color="ed6103">\|</font>)<font color="ed6103">\|</font>;<font color="ed6103">\|</font> | 右に`,`がない場所 |
| `level(?!,)` | const [level, set<b><font color="ed6103">Level</font></b>] = useState(1); | 右に`,`がない`level` |

# 後読みとは

先読みが「記述された部分よりも右側の条件を表す」ものでした。

後読み`(?<=...)`は当然、記述された部分よりも**左側**の条件を表します。
`<`が視覚的にも左向きなイメージを与えます。

`(?<=set)level`の例をビジュアルで表現するならば次のようなイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337174/a024cfc6-1f85-3ea5-7989-60e9a3f31263.png)

:::note warn
パフォーマンス上の理由で、後読みは多くの環境で**有限長のパターン**しか使えません。
⭕ `(?<=[0-9]hoge)fuga` (後読み内パターン長が5文字で確定する)
❌ `(?<=hoge.*)fuga` (後読み内パターン長が無制限の可能性がある)

ただし、OR `|` で複数の有限長パターンを使えることがあります。
⭕ `(?<=hoge|hogera)fuga` (後読み内パターン長が4文字か6文字で確定する)
:::

## 否定後読み

肯定先読み `(?=...)` と否定先読み `(?!...)` の関係と同様に、肯定後読み `(?<=...)` の否定版として 否定後読み `(?<!...)` があります。

# 応用例

簡単な応用例は省いて、ちょっとトリッキーな例をちょっと示してみます。

## 〇〇を含まない行

例えば、「`rem`を含む行」は `^.*rem.*$`と表せる一方、「`rem`を含まない行」のような条件は難しかったりします。
否定先読み `(?!...)` を使うと次のように書き表せます[^含まない行別パターン]。

[^含まない行別パターン]: この場合だと`^(?!.*rem).*$`と書いても等価です。

```
^((?!rem).)*$
```

日本語に意訳するなら「`rem`が続かないような一文字が、最初から最後まで、任意長続く」ぐらいの意味になります。
この一文字ずつ先読みしていく方法は非常に応用が利くので覚えておくと便利です。

## 単語境界 (ケバブケース対応版)

単語境界 `\b` では、 ケバブケースの単語に対する検索結果は次のような結果となってしまいます。

| 正規表現       | 検索対象 | マッチの結果                            |
|:---------------|:-|:---------------------------------------|
| `\b` | user-icon | <font color="ed6103">\|</font>user<font color="ed6103">\|</font>-<font color="ed6103">\|</font>icon<font color="ed6103">\|</font> |
|||
| `\buser-icon\b` | user-icon | <b><font color="ed6103">user-icon</font></b> |
| `\buser-icon\b` | user-icon-badge | <b><font color="ed6103">user-icon</font></b>-badge |
| `\buser-icon\b` | large-user-icon | large-<b><font color="ed6103">user-icon</font></b> |

`(?<!-)\buser-icon\b(?!-)`のように、「単語境界より左にハイフンが無い `(?<!-)`」「単語境界より右にハイフンが無い `(?!-)`」というニュアンスを両端に付け加えることで、スネークケースと同様の結果を得られます。

| 正規表現       | 検索対象 | マッチの結果                            |
|:---------------|:-|:---------------------------------------|
| `(?<!-)\buser-icon\b(?!-)` | user-icon | <b><font color="ed6103">user-icon</font></b> |
| `(?<!-)\buser-icon\b(?!-)` | user-icon-badge | user-icon-badge |
| `(?<!-)\buser-icon\b(?!-)` | large-user-icon | large-user-icon |

**<font color="ab7c9d">文字の前後の条件を表すもの</font>** は0文字分の幅しか占めないため、ANDのような感覚で条件の重ね掛けができるのがおもしろいんです

# まとめ

- 先読み/後読み は **<font color="ab7c9d">文字の前後の条件を表すもの</font>** です。
- 先読み/後読み はどれだけ長くとも0文字分の幅しか占めません。
- 先読み `(?=...)` により、その場所よりも**右側が満たすべき条件**を記述できます。 (`(?!...)`で否定の意味)
- 後読み `(?<=...)` により、その場所よりも**左側が満たすべき条件**を記述できます。 (`(?<!...)`で否定の意味)

# オマケ

### 「先読み」という名前について

私はかつて `(?=...)` が先読みなのか、後読みなのかよく混乱していました。
正直なところ正確に覚える必要なんてないと思ったりしますが、強いて言えば次のように覚えることになるのでしょうか。

> 正規表現は左から右に進んでいくものである。
> `(?=...)`は「ここより右側の条件を指定するもの」であるので、進行方向の**先を予め見ている**。
> よって、`(?=...)`は先読み。

### 処理系について

[はてブ](https://b.hatena.ne.jp/entry/s/qiita.com/pal4de/items/7e2292cfd7ac424e0836)で言及頂いたので追記します。

簡単のため処理系については言及しておりませんが、主題がコード検索であるため VSCode での挙動を念頭に置いています。
VSCodeでの具体的なFlavorについては今日まで知らなかったのですが、[複数の処理系が併用されている](https://stackoverflow.com/questions/42179046/what-flavor-of-regex-does-visual-studio-code-use)ようです。

先読み `(?= )` `(?! )` はおおよそどこでも使え、後読み `(?<= )` `(?<! )` は使えないことも多い、という程度の肌感でいます。
目立った仕様の違いもないように感じています。

---

ここまでご覧いただきありがとうございました！

[弊社カレンダー](https://qiita.com/advent-calendar/2022/lifull)の他の記事もぜひご覧ください😉

https://qiita.com/advent-calendar/2022/lifull