Fastly  で正規表現を使った書き換え処理


```
特に記載がない限り本記事の記載内容は Fastly のデフォルト設定での挙動となります。
Fastly の正式なサポート内容ついては以下のドキュメントをご参照下さい。
サポートページ: https://docs.fastly.com/
日本語(一部のみ): https://docs.fastly.com/ja/
```


# この記事の内容について

Fastly では正規表現を利用してリクエストパスやクエリストリングなどの値を柔軟に操作することができます。この記事では 正規表現を利用した値の取得やリクエストの書き換えの基本的な説明とよく使われる方法をご紹介したいと思います。

なお、Fastly では Perl 互換の正規表現 (Perl Compatible Regular Expression - PCRE) 構文を利用しています。大文字小文字は区別され、スラッシュ(/) はエスケープする必要はありません。


# Fastly で利用可能な正規表現

正規表現はメタキャラクタを利用して様々な文字列をマッチさせたり取り出したりすることが出来ます。
具体的な方法は分かりやすく説明してくれているサイトが沢山あるので、ここでは Fastly でパスなどから必要な情報を取得する際によく利用されるものをメモ程度に書いておきたいと思います。

**`(?i)` : 大文字小文字を区別しない**
正規表現記述の最初に(?i)を含めることで大文字小文字を区別せずに一致。
例: `(?i)jpg` は "jpg" はもちろん "Jpg", "JPG", "jpG" すべて一致

**`^` : 行頭**
例: `^abc` は行頭の"abc"と一致。"aabc", "xxabc" などとは一致しない。

**`$` : 文末**
例: `abc$` は文末の"abc"と一致。"abca", "abcx" などとは一致しない。

**`.` : 任意の1文字**
なんでもいいので一文字入ることを意味します

**`[abc]` : 指定文字と一致**
指定された文字のいずれかと一致。この場合は a か b か c。

**`[a-z]` : 指定された範囲の文字と一致**
この場合、aからzまでのアルファベットと一致。[0-9]なら数字と一致。

**`[^a-z]` : 指定範囲以外の文字と一致**
この場合、aからzまでのアルファベット以外の文字と一致。

**`abc|xyz` : 指定パターンのどちらかと一致**
この場合、"abc" または "xyz" と一致。

**`{N}` : 直前の文字の N 回 繰り返し**
例: `.{2}`は`.` は任意の1文字なので、"aa", "ab", "xa" などなんでもよいのでとにかく2文字と一致。

**`{N,X}` : 直前の文字の N 回 以上の X 回以下の繰り返し**
例: `^(.{2,3})` だと
対象文字列が 'abcde' の場合 'abc',
対象文字列が 'ab' の場合 'ab',
対象文字列が 'a' の場合は 'null' になる （文字が2文字ないため）

**`*` : 直前の文字の0回以上の繰り返し**
例: `be*` は "b" とも "bee" とも "beee" とも一致

**`?` : 直前の文字0回、または1回と一致**
例: `bus?` は "bu" または "bus"と一致。

**`+` : 直前の文字の1回以上と一致**
例: `be+` は "be" や "bee" とは一致。"b" とは一致しない。


# 正規表現のテスト

作成した正規表現は以下のようなサイトで挙動を確認したりテストしたりすることが出来ます。

https://regexper.com/
正規表現を記載してDisplay ボタンをクリックすると、構成を分析して表示してくれます。

https://regex101.com/
正規表現を記載し、テスト用の文字列を記載すると分析結果と、group でマッチする内容を表示してくれます。

他にも同様のサービスを提供しているサイトは色々とあるので、使いやすいと思うものをご利用下さい。

なお、上記の2サイトで正規表現をテストする場合、`/`(スラッシュ)は`\/`というようにエスケープする必要がありますのでご注意下さい。


# Fastly で特定の文字列を取り出す

それでは正規表現を利用して URL などから特定の情報を取得したい場合の基本的な手順についてみてみます。

####サンプル1: キャプチャの基本的な使い方

```:vcl_deliver
set req.http.Foo = "abbbccccc";
if (req.http.Foo ~ "^(a+)(b+)(c+)") {
  set resp.http.match0 = re.group.0; # now equals 'abbbccccc'
  set resp.http.match1 = re.group.1; # now equals 'a'
  set resp.http.match2 = re.group.2; # now equals 'bbb'
  set resp.http.match3 = re.group.3; # now equals 'cccccc'
}
```

例えば上記のコードが vcl_deliver にあったとします。

re.group.[0-9] オブジェクトを利用すると、マッチした内容を取得することが出来ます。re.group.0 は一致した文字列全体を含みます。re.group.1 は最初の()に一致した文字列、re.group.2は2つ目の()に一致した文字列、re.group.3は3つ目の()に一致した文字列、と続いていきます。

では上の内容をもう少し詳しく見てみます。文字列を取り出したい対象となる元の文字列は "abbbccccc" です。この文字列を最初に Foo というヘッダーに格納しています。

正規表現の部分を見てみると `^(a+)(b+)(c+)` となっています。
^ は文字列の最初です。
ひとつめのカッコは (a+) ですね。+ は直前の１文字の１回以上の繰り返しです。つまりaが続く限り一致し続けます。
(b+) も同様なので a の連続が終了し、 b が始まった箇所から b が続く限り一致し続けます。
(c+) も同様です。

set reap.http.matchX に re.group.X で取得した値をそれぞれ代入しているので、上記のコードの実行すると matchX にはそれぞれ以下のような値が代入されていることになります。

resp.http.match0 = re.group.0 =  'abbbccccc'
resp.http.match1 = re.group.1 =  'a'
resp.http.match2 = re.group.2 = 'bbb'
resp.http.match3 = re.group.3 = 'cccccc'

Fastly のテストツールである Fastly fiddle で上記のコードの動作確認できます。
Fastly Fiddle link: https://fiddle.fastlydemo.net/fiddle/a462a44d


####サンプル2: リクエストパスから特定の箇所を取得

`http://www.example.com/abc/xxx/def/index.html` という URL から `xxx` の部分を取得したいとします。

```:vcl_deliver
if (req.url ~ "/abc/(.*)/def") {
  set resp.http.X-target = re.group.1;
}
```
リクエストパスは `req.url` に含まれています。そこから上記のような正規表現で "/abc/" と "/def" の間の箇所をカッコ `()` でグルーピングして取得しています。
ここでサンプル1のように複数のカッコを使って正規表現を `^(/abc/)(.*)(/def)` とすることも可能です。その場合は `re.group2` に2階層目の内容が含まれることになります。

上記のコードでは取得した内容をレスポンスに `x-target` という名前のカスタムヘッダーとして追加しています。

Fastly Fiddle link: https://fiddle.fastlydemo.net/fiddle/921021e4

####サンプル3: リクエストパスから特定の階層を取得
サンプル2ではパスの中のディレクトリ名が固定でした。今度はディレクトリ名がどのような名前か分からない場合に2階層目の文字列を取得してみます。

```:vcl_deliver
if (req.url ~ "^/[^/]*/([^/]*).*") {
  set resp.http.X-target = re.group.1;
}
```
正規表現の部分を最初から順番にみてみます
req.url には `/abc/xxx/def/index.html` という文字列が含まれています。

`^/`　= 最初の / (スラッシュ)
`[^/]*` = スラッシュ以外の文字列が続く限り取得(1階層目の名前)
`/` = 1階層目と2階層目の間のスラッシュ。
`([^/]*)` = スラッシュ以外の文字列が続く限り取得(2階層目の名前)。カッコに含まれているので `re.group.1` にこの値が取得される
`.*` = 残りの文字列すべて

上記のコードでパスの内容が何であれ2階層目のディレクトリ名を `re.group.1` から取得出来ます。

Fastly Fiddle link: https://fiddle.fastlydemo.net/fiddle/5c6ee430

####サンプル4: 取得するパスを変更
サンプル3までは情報を取得しているだけでしたが、次にリクエストされたパスを変更してみたいと思います。変更したい対象のディレクトリ名がわかっている場合、`regsub`　を利用することが出来ます。

```:vcl_recv
set req.url = regsub(req.url, "image", "new-image");
```

上記のコートで `req.url` に含まれる `image` という文字列を `new-image` に書き換えます。（正確には書き換えた内容で上書きしています）

つまり以下のような動作となります。

変換前: www.example.com/image/aaa.jpg
　↓
変換後: www.example.com/new-image/aaa.jpg

なお、もし特定のディレクトリをパスから削除したい場合は
`regsub(req.url, "/image", "");`
と記述することで実現できます。この処理では `/image` を "(null)" に置き換えます。

Fastly Fiddle link: https://fiddle.fastlydemo.net/fiddle/faa8cca1


####サンプル5: 特定の階層をリクエストパスから削除
サンプル4では固定のディレクトリをパスを変更しただけなのですが、今度は特定の階層を削除してみます。
ディレクトリ名は決まっていないので `regsub` は使うことが出来ません。

この場合、サンプル3で利用した正規表現をアレンジして利用することが出来ます。


```:vcl_recv
if (req.url ~ "^(/[^/]*)(/[^/]*)(.*)") {
  set req.url = re.group.1 + re.group.3;
}
```

サンプル3の正規表現 : `^/[^/]*/([^/]*).*`
サンプル5の正規表現 : `^(/[^/]*)(/[^/]*)(.*)`

まずサンプル3に比べてカッコの数が増えています。またサンプル3ではカッコの外にあった / が2つ目のカッコの中に移動しています。
これはリクエストパスから特定のディレクトリを削除する場合、ディレクトリ名だけでなくその前後どちらかのスラッシュも合わせて削除する必要があるためです。

`req.url` の中身が `/abc/xxx/def/index.html` だとすると、各groupに含まれる内容は以下のようになっています。

re.group.1 = /abc
re.group.2 = /xxx
re.group.3 = /def/index.html

`req.url` には group.1 と group.3 を代入しているので、このコードが処理されるとURLは /xxx が削除され、

変換前: /abc/xxx/def/index.html
　↓
変換後: /abc/def/index.html

に変換されます。

Fastly Fiddle link: https://fiddle.fastlydemo.net/fiddle/cad4fd54


# まとめ
Fastly では上記のように正規表現を利用してパスやリクエストヘッダー、クッキーなどから自由に情報を取り出すことが出来ます。なお、ある文字列から特定の文字列を取得するための正規表現は複数存在します。

この記事で説明している方法以外にもっと効率的かつスマートに目的の文字列を取得する方法などはたくさんあると思いますので、必要に応じた表現で実施されたい処理を実現して頂ければと思います。