👌

URLからファイル名を取得

2024/04/27に公開

はじめに

axiosを使用してダウンロードしたファイルからファイル名を取得する際に、正規表現を使った方法を目にしました。
「こんな意味なんだな」というのを少し時間を割いて調べてみたので記事として残したいと思います。

正規表現を使用したのコード

実際に目にした正規表現は次のようなコードでした。

const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;

見慣れている方なら、すぐに判断できたと思います。
私は全く慣れていないので少しずつ分割しながら理解を進めました。
次から一緒に見ていきましょう!

サンプルヘッダー

今回はchatGPTの力を借りてサンプルのヘッダーを作成してもらいました。

const sampleHeader = 'attachment; filename="example.txt"';

目で確認するとファイル名はmyfile.txtであることがすぐわかりますね。

正規表現を使ったファイル名の取得

では改めて今回の正規表現を使ったコードを見てみましょう。

const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;

まず、JavaScriptやTypeScriptでは//で囲まれた内容が正規表現のパターンとなります。

パターン:filename

filenamefilenameという文字列にマッチする値を絞り込みます。

const sampleHeader = 'attachment; filename="example.txt"';
const regexPattern = /filename/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: filename

パターン:[^;=\n]*

このパターンは、;=、改行以外の文字が0文字以上続くことを示しています。

const sampleHeader = 'attachment; filename="example.txt"';
const regexPattern = /filename[^;=\n]*/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: filename

結果が先ほどのパターン(filename)と同じになりました。
[^;=\n]*;=、改行以外の文字が0文字以上続く ことを示しているので、filename直後の=は正規表現のパターンとマッチしていません。
URLに少し手を加えてみましょう。

- const sampleHeader = 'attachment; filename="example.txt"';
+ const sampleHeader = 'attachment; filenameABC123="example.txt"';
const regexPattern = /filename[^;=\n]*/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: filenameABC123

パターンにマッチしたファイル値を取得することができました。

パターン:=

=は文字列=の後に書かれているパターンにマッチする値を絞り込みます。

パターン:(['"]).*?\2

((['"]).*?\2は、いくつかの意味を持っています。
(['"])は、引用符'(シングルコーテーション)または"(ダブルコーテーション)で囲まれている文字列をマッチします。
.*?は、0文字以上の任意の文字列を最小の値でマッチします。
\2は、2番目の括弧で囲まれたグループにマッチする文字列を示します。
最初のパターン((['"]))でマッチした文字列を再度マッチします。これによって、引用符'(シングルコーテーション)または"(ダブルコーテーション)で囲まれた文字列全体を精査することができます。

const sampleHeader = 'attachment; filename="example.txt"';
const regexPattern = /filename[^;=\n]*=((['"]).*?\2)/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: "filename=\"example.txt\"",
    "\"example.txt\"",
    "\"" 

結果が3件となりました。
これは、ファイル名が引用符で囲まれている場合に、その引用符も含めてマッチしているからになります。
分かりやすくするために、少し削って再度マッチしてみましょう。

- const sampleHeader = 'attachment; filename="example.txt"';
- const regexPattern = /filename[^;=\n]*=((['"]).*?\2)/;
+ const regexPattern = /((['"]).*?\2)/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: "\"example.txt\"",
    "\"example.txt\"",
    "\"" 

同様に、引用符で囲まれた3件がマッチしました。
次のようなイメージでマッチしていると考えるのがいいかと思います。

  1. 'attachment; filename="example.txt"'からシングルコーテーションで囲まれた"example.txt"がマッチ。
  2. 1の結果から、ダブルクォーテーションで囲まれた"example.txt"がマッチ。
  3. 2の結果から、ダブルクォーテーションで囲まれた"\""がマッチ。
    となっています。(\が含まれていますが、コンソールなどで実行すると表示されないかと思います。)

パターン:[^;\n]*

[^;\n]* は、; または改行以外の文字が 0 文字以上続くことを示します。

const sampleHeader = 'attachment; filename="example.txt"';
const regexPattern = /[^;\n]*/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: attachment

; または改行以外の文字をマッチしているため、最初にマッチするattachmentのみが結果になります。

(['"]).*?\2[^;\n]* を合わせたパターン: ((['"]).?\2|[^;\n])

(['"]).*?\2[^;\n]*|によって区切られています。
これは、いずれかのパターンにマッチした場合に結果を返します。
つまり今回の例では、ファイル名が引用符で囲まれている場合はその内側を、引用符で囲まれていない場合はセミコロンや改行文字が現れるまでをマッチングします。

最後に

改めてコードを見てみましょう。

const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;

ここまでくればなんとなく結果が分かるようになったのではないでしょうか。結果は以下の通りです。

const sampleHeader = 'attachment; filename="example.txt"';
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = sampleHeader.match(regexPattern);

console.log(`結果: ${matches}`)
→ 結果: example

もし分からなくなったら少しずつ分解してみることをおすすめします。
いじょう。

Discussion