JavaScript Tips | 文字列操作:文字列をURL用にエンコードする

JavaScript JavaScript
スポンサーリンク
スポンサーリンク
  1. 業務・実務で使える JavaScript ユーティリティ:文字列をURL用にエンコードする方法
  2. URLエンコードとは何か
    1. URLの構造と、データとしての文字を区別する
    2. 日本語はUTF-8のバイト列をもとに変換される
  3. ステップ1:URLのどこへ入れるかを決める
  4. ステップ2:encodeURIComponent()の基本を理解する
    1. URLの一部分として使う値をエンコードする
    2. すべての記号が変換されるわけではない
  5. ステップ3:複数のクエリパラメーターはURLSearchParamsで作る
    1. 値を事前にエンコードせず、そのまま渡す
    2. 空白はプラス記号になる
  6. ステップ4:URL全体はURLオブジェクトで組み立てる
    1. 既存のクエリやフラグメントを壊さず更新する
  7. ステップ5:encodeURI()との違いを理解する
  8. 業務で使えるテンプレート:検索URLを安全に組み立てる
    1. 前処理を自動で増やしすぎない
  9. 構成要素だけを変換するテンプレート
    1. パスへ入れる場合は、サーバー側の扱いも確認する
  10. 重要:二重エンコードを防ぐ
    1. エンコード済み文字列を再入力しない
    2. パーセント記号があるだけで「変換済み」と判断しない
  11. デコードするときの注意点
    1. 構成要素の復元にはdecodeURIComponent()を使う
    2. クエリの取得はsearchParams.get()へ任せる
  12. Unicodeと例外処理で注意すること
  13. 情報セキュリティ上の重要な注意点
    1. URLエンコードは暗号化ではない
    2. エンコードは接続先の安全性を保証しない
    3. HTMLエスケープとは別の処理として扱う
  14. テストで正常系と境界条件を確認する
    1. 変換結果と復元結果を確認する
    2. URLを解析し直して、元の値が取得できるか確認する
  15. まとめ

業務・実務で使える JavaScript ユーティリティ:文字列をURL用にエンコードする方法

JavaScriptで検索キーワードや商品名などの文字列をURLへ組み込むには、用途に応じて encodeURIComponent() や URLSearchParams を使います。特に、検索条件などのクエリパラメーターを作る場合は、URL と URLSearchParams を組み合わせる方法が実務で扱いやすい選択肢です。

ただし、URL全体を処理するのか、URLの一部分を処理するのかによって、適切な方法は変わります。また、URLエンコードは暗号化でも、接続先の安全性を検証する処理でもありません。

この記事では、URLエンコードの仕組みを確認したうえで、検索キーワード、複数のパラメーター、パスの一部分を扱う方法を段階的に説明します。最後に、再利用できるテンプレートとテスト例を紹介します。

URLエンコードとは何か

URLの構造と、データとしての文字を区別する

URLには、特別な役割を持つ記号があります。たとえば、? はクエリ部分の開始、& はクエリパラメーターの区切り、# はフラグメントの開始に使われます。

そのため、利用者が入力した文字列をそのままURLへ連結すると、データの一部だった記号が、URLの構造として解釈されてしまうことがあります。

// 検索したい文字列
const keyword = "紅茶&コーヒー";

// 単純な連結では、入力中の & が区切りとして扱われてしまう
const url = "https://example.com/search?q=" + keyword;

console.log(url);
// "https://example.com/search?q= 紅茶&コーヒー"
JavaScript

このURLでは、検索語全体が1つの値として扱われるとは限りません。& より後ろが別のパラメーターとして解釈される可能性があるためです。

URLエンコードでは、このような記号や日本語などを、必要に応じて % と16進数の組み合わせへ変換します。この表現をパーセントエンコーディングと呼びます。

日本語はUTF-8のバイト列をもとに変換される

encodeURIComponent() は、対象の文字をUTF-8のバイト列へ変換し、必要な部分をパーセント表記にします。そのため、日本語1文字が %XX 1個になるとは限りません。

// 日本語1文字が、複数のバイトを表す表記になる
console.log(encodeURIComponent("猫"));
// "%E7%8C%AB"

// 空白や記号も、用途に応じた表記へ変換される
console.log(encodeURIComponent("A B&C"));
// "A%20B%26C"
JavaScript

元の文字列より長くなっても、異常ではありません。見た目の文字数と、エンコード後のURLの長さは別のものとして扱います。

ステップ1:URLのどこへ入れるかを決める

最初に決めるべきことは、文字列をURLのどの部分へ入れるかです。URL全体と、その中の値を同じように処理しないことが重要です。

  • 検索キーワードなどのクエリパラメーターは、URLSearchParams で組み立てます。
  • URLの構成要素を単独でエンコードする場合は、encodeURIComponent() を使います。
  • パスへ識別子などを入れる場合は、区切りのスラッシュとデータ部分を分けて考えます。
  • 既存URLのパラメーターを変更する場合は、URL で解析してから操作します。

文字列を手作業で連結するより、構造を扱えるAPIへ任せるほうが、区切り記号や既存パラメーターの扱いを整理しやすくなります。

ステップ2:encodeURIComponent()の基本を理解する

URLの一部分として使う値をエンコードする

encodeURIComponent() は、文字列をURLの構成要素として扱うための関数です。クエリ値を手動で組み込む場合は、値の部分だけへ適用します。

// 生の検索キーワード
const keyword = "紅茶&コーヒー";

// 検索値だけをエンコードする
const encodedKeyword = encodeURIComponent(keyword);

// URLの構造を表す記号は、固定部分として残す
const url = `https://example.com/search?q=${encodedKeyword}`;

console.log(url);
// "https://example.com/search?q=%E7%B4%85%E8%8C%B6%26%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC"
JavaScript

入力に含まれる & は %26 になり、パラメーターの区切りと区別されます。= や # なども変換されるため、値がURLの構造へ影響しにくくなります。

すべての記号が変換されるわけではない

encodeURIComponent() は、英数字に加え、-、_、.、!、~、*、'、(、) を変換せずに残します。

// これらの文字は、そのまま残る
console.log(encodeURIComponent("AZaz09-_.!~*'()"));
// "AZaz09-_.!~*'()"
JavaScript

したがって、「記号をすべて消す関数」「HTML属性へ安全に埋め込む関数」と考えてはいけません。外部APIの署名処理などで、より厳密なエンコード規則が指定されている場合は、その仕様に合わせた処理が必要です。

ステップ3:複数のクエリパラメーターはURLSearchParamsで作る

値を事前にエンコードせず、そのまま渡す

検索語、分類、ページ番号などを組み合わせる場合は、URLSearchParams を利用します。パラメーター名と値を渡すだけで、区切りやエンコードを処理できます。

// パラメーターの構造をAPIで管理する
const params = new URLSearchParams();

// エンコード前の値を渡す
params.set("q", "紅茶&コーヒー");
params.set("category", "飲料");
params.set("page", "1");

// クエリ文字列へ変換する
const query = params.toString();

console.log(query);
// "q=%E7%B4%85%E8%8C%B6%26%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC&category=%E9%A3%B2%E6%96%99&page=1"
JavaScript

URLSearchParams へは、原則としてエンコード前の値を渡します。先に encodeURIComponent() を実行すると、パーセント記号がさらにエンコードされるためです。

空白はプラス記号になる

URLSearchParams の文字列化では、空白は + になります。一方、encodeURIComponent() では %20 になります。

// encodeURIComponent() では空白が %20 になる
console.log(encodeURIComponent("A B"));
// "A%20B"

// URLSearchParams では空白が + になる
const params = new URLSearchParams();
params.set("q", "A B");

console.log(params.toString());
// "q=A+B"
JavaScript

これは、URLSearchParams がフォーム形式のシリアライズ規則を使うためです。一般的なクエリ処理で利用できますが、外部APIが表記を厳密に指定している場合は、要求された方式を確認します。

本来のプラス記号は区別される

値に含まれるプラス記号は %2B へ変換されるため、空白を表す + と区別できます。

// 空白とプラス記号が混在する入力
const params = new URLSearchParams();
params.set("q", "A+B C");

console.log(params.toString());
// "q=A%2BB+C"

// APIで読み出せば、元の値を取得できる
console.log(params.get("q"));
// "A+B C"
JavaScript

このため、入力値を連結して new URLSearchParams("q=" + value) とするより、set() で値を設定するほうが確実です。

ステップ4:URL全体はURLオブジェクトで組み立てる

既存のクエリやフラグメントを壊さず更新する

既存URLへ単純に "?q=" を追加すると、すでにクエリがある場合に構造を壊してしまいます。URL オブジェクトの searchParams を使うと、既存の構造を保ちながら操作できます。

// 既存のクエリとフラグメントを持つURL
const url = new URL(
  "https://example.com/search?sort=price#results"
);

// 生の検索値を設定する
url.searchParams.set("q", "A&B");
url.searchParams.set("page", "2");

console.log(url.toString());
// "https://example.com/search?sort=price&q=A%26B&page=2#results"
JavaScript

set() は、同じ名前の値を置き換えます。同じ名前のパラメーターを複数送信する必要がある場合は、append() を使います。

// 同じ名前のパラメーターを複数設定する
const url = new URL("https://example.com/search");

url.searchParams.append("tag", "JavaScript");
url.searchParams.append("tag", "初心者");

// 同じ名前の値をすべて取得する
console.log(url.searchParams.getAll("tag"));
// ["JavaScript", "初心者"]
JavaScript

ただし、同名パラメーターを受信側がどう扱うかは、サーバーの仕様によります。配列として受け取るのか、最初や最後の値だけを使うのかを確認します。

ステップ5:encodeURI()との違いを理解する

encodeURI() は、URL全体の構造に使う記号を多く残します。そのため、クエリ値に含まれる & や # を区切りと区別したい用途には適しません。

// 同じ入力を、それぞれの関数で変換する
const value = "A&B#C";

// URLの構造に使う記号が残る
console.log(encodeURI(value));
// "A&B#C"

// 構成要素として使うため、区切り記号も変換される
console.log(encodeURIComponent(value));
// "A%26B%23C"
JavaScript

反対に、完成したURL全体へ encodeURIComponent() を使うと、: や / まで変換され、通常のリンクとして使うURLとは異なる文字列になります。

// URL全体を構成要素としてエンコードした例
const encoded = encodeURIComponent("https://example.com/search?q=tea");

console.log(encoded);
// "https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dtea"
JavaScript

ただし、別のURLをクエリ値として送る場合には、URL全体が「1つの値」になります。この場合も、外側のURLの searchParams.set() へ元のURLを渡せば、必要な処理を任せられます。

業務で使えるテンプレート:検索URLを安全に組み立てる

次の関数は、検索語とページ番号を受け取り、固定された検索先のURLを返します。接続先を関数内で固定し、入力値だけをクエリへ設定する設計です。

/**
 * 検索画面のURLを作ります。
 *
 * @param {string} keyword エンコード前の検索語
 * @param {number} page 1以上の安全な整数
 * @returns {string} 組み立てたURL
 * @throws {TypeError} 引数の型が不正な場合
 * @throws {RangeError} ページ番号が範囲外の場合
 */
function buildSearchUrl(keyword, page = 1) {
  // 欠損値などを自動で文字列化しない
  if (typeof keyword !== "string") {
    throw new TypeError("keyword は文字列で指定してください");
  }

  // 数値以外を受け付けない
  if (typeof page !== "number") {
    throw new TypeError("page は数値で指定してください");
  }

  // 小数、負数、Infinity、大きすぎる整数などを除外する
  if (!Number.isSafeInteger(page) || page < 1) {
    throw new RangeError("page は1以上の安全な整数で指定してください");
  }

  // 実際のシステムでは、固定された検索先へ変更する
  const url = new URL("https://example.com/search");

  // エンコード前の値を渡す
  url.searchParams.set("q", keyword);
  url.searchParams.set("page", String(page));

  // 完成したURLを返す
  return url.toString();
}

// 日本語と区切り記号を含む検索語
console.log(buildSearchUrl("紅茶&コーヒー", 2));

// プラス記号と空白を含む検索語
console.log(buildSearchUrl("A+B C"));
// "https://example.com/search?q=A%2BB+C&page=1"

// 空文字も、この関数では許可する
console.log(buildSearchUrl(""));
// "https://example.com/search?q=&page=1"
JavaScript

前処理を自動で増やしすぎない

この関数では、検索語の前後の空白除去や、大文字小文字の統一を行いません。入力をどう整形するかと、URLへ格納する処理を分けるためです。

検索語の未入力を禁止したい場合は、その要件を別途追加します。空文字を送ることと、パラメーター自体を送らないことも、サーバー側では異なる意味になる場合があります。

構成要素だけを変換するテンプレート

URLの一部分を単独で変換する必要がある場合は、入力の型を確認する小さな関数を用意できます。

/**
 * 生の文字列をURLの構成要素用にエンコードします。
 *
 * @param {string} text エンコード前の文字列
 * @returns {string} エンコード後の文字列
 * @throws {TypeError} 入力が文字列ではない場合
 * @throws {URIError} 不正なサロゲートが含まれる場合
 */
function encodeUrlComponent(text) {
  // 意図しない自動文字列化を防ぐ
  if (typeof text !== "string") {
    throw new TypeError("text は文字列で指定してください");
  }

  // 不正な文字列によるURIErrorは、呼び出し側へ伝える
  return encodeURIComponent(text);
}

// 区切り記号を含む値
console.log(encodeUrlComponent("A/B?C"));
// "A%2FB%3FC"

// 空文字も正常に処理できる
console.log(encodeUrlComponent(""));
// ""
JavaScript

パスへ入れる場合は、サーバー側の扱いも確認する

たとえば、ファイル名をパスの1区間として扱う場合は、そのデータ部分だけをエンコードします。URL全体や、区切りとして使うスラッシュまでまとめて変換しないようにします。

// ファイル名をパスの1区間として組み込む
const fileName = "月次 報告書.pdf";

// 固定されたパスへ、変換したファイル名を追加する
const url = new URL(
  "/files/" + encodeUrlComponent(fileName),
  "https://example.com"
);

console.log(url.toString());
JavaScript

ただし、スラッシュを %2F に変換しても、サーバーやプロキシがデコードして区切りとして扱う場合があります。また、. や .. のような特殊なパス区間にも注意が必要です。

エンコードは、パスの権限確認やディレクトリ逸脱対策の代わりにはなりません。可能であれば、公開する識別子の形式を限定し、サーバー側でも検証します。

重要:二重エンコードを防ぐ

エンコード済み文字列を再入力しない

エンコード後の文字列をもう一度処理すると、パーセント記号が %25 へ変換されます。

// 生の値
const original = "A B";

// 1回目の変換
const once = encodeURIComponent(original);

// 2回目の変換
const twice = encodeURIComponent(once);

console.log(once);  // "A%20B"
console.log(twice); // "A%2520B"
JavaScript

同じ問題は、先にエンコードした値を URLSearchParams へ渡した場合にも起こります。

// 先にエンコードしてしまった例
const encodedValue = encodeURIComponent("A&B");

const params = new URLSearchParams();

// % がさらにエンコードされる
params.set("q", encodedValue);

console.log(params.toString());
// "q=A%2526B"
JavaScript

基本方針は、元データを生の文字列として保持し、URLへ組み込む段階で必要な変換を行うことです。URLSearchParams を使うなら、そのAPIへ生の値を渡します。

パーセント記号があるだけで「変換済み」と判断しない

"達成率100%" や "%20という表記" のように、パーセント記号そのものが元データに含まれる場合があります。文字列の見た目から変換済みかを推測するのではなく、変数名や関数の契約で管理します。

デコードするときの注意点

構成要素の復元にはdecodeURIComponent()を使う

encodeURIComponent() の結果は、通常 decodeURIComponent() で元の文字列へ戻せます。

// 構成要素の変換と復元
const original = "日本語 A&B";
const encoded = encodeURIComponent(original);
const decoded = decodeURIComponent(encoded);

console.log(decoded === original); // true
JavaScript

ただし、不正なパーセント表記やUTF-8として不正な並びを渡すと、URIError になります。外部から受け取った文字列をデコードする場合は、例外処理を検討します。

クエリの取得はsearchParams.get()へ任せる

decodeURIComponent() は、プラス記号を空白へ変換しません。フォーム形式のクエリを読む場合は、URLSearchParams を使います。

// decodeURIComponent() は + をそのまま残す
console.log(decodeURIComponent("A+B"));
// "A+B"

// クエリとして解析すると、+ は空白になる
const url = new URL("https://example.com/search?q=A+B");

console.log(url.searchParams.get("q"));
// "A B"
JavaScript

searchParams.get() が返す値は、クエリの規則でデコード済みです。さらに decodeURIComponent() を呼ぶと、元データの意味を変えたり、例外になったりする可能性があります。受信するフレームワークでも、デコード済みの値を返していないか確認します。

Unicodeと例外処理で注意すること

通常の日本語や絵文字は encodeURIComponent() で処理できます。一方、JavaScriptの文字列に、不正な単独サロゲートが含まれていると URIError になります。

// 通常の絵文字は処理できる
console.log(encodeURIComponent("😀"));
// "%F0%9F%98%80"

try {
  // 単独の上位サロゲートは正しいUnicodeの文字を表さない
  encodeURIComponent("\uD800");
} catch (error) {
  console.log(error instanceof URIError); // true
}
JavaScript

また、URLSearchParams は、このような不正なサロゲートを置換文字へ変えるため、元の文字列と完全に一致しなくなる場合があります。文字列を途中で切り出す処理などがある場合は、入力の生成方法も確認します。

不正な文字を補正するのか、入力不備として拒否するのかは業務要件次第です。特に識別子や署名対象の値では、知らないうちに別の値へ変えないことが重要です。

情報セキュリティ上の重要な注意点

URLエンコードは暗号化ではない

パーセント表記は簡単に復元できます。パスワード、認証情報、個人情報などを、見た目が読みにくくなったという理由でURLへ入れてはいけません。

URLは、ブラウザーの履歴、アクセスログ、監視システムなどに残る可能性があります。機密情報の送信方法や保存範囲は、エンコードとは別に設計します。

エンコードは接続先の安全性を保証しない

文字列をエンコードしても、そのURLが信頼できる接続先かどうかは判断できません。外部入力をリンク先やリダイレクト先に使う場合は、スキーム、ホスト名、必要に応じてポートやパスなどを検証します。

今回の検索URLテンプレートで接続先を固定しているのは、外部入力へURL全体の構造を委ねないためです。ただし、受信側で検索条件の検証や権限確認が不要になるわけではありません。

HTMLエスケープとは別の処理として扱う

URLエンコードした文字列を、HTML属性へ安全に埋め込めるとは限りません。URLを作る処理と、HTMLへ出力する処理は別の段階です。

ブラウザー上でリンクを作る場合は、検証済みのURLをDOMのプロパティへ設定し、表示する文字列は textContent へ設定すると、HTML文字列の連結を減らせます。

// 固定された検索先を使ってURLを生成する
const searchUrl = buildSearchUrl("紅茶&コーヒー");

// リンク要素を作る
const link = document.createElement("a");

// URLと表示文字列を、それぞれのプロパティへ設定する
link.href = searchUrl;
link.textContent = "検索結果を開く";

// 画面へ追加する
document.body.append(link);
JavaScript

テストで正常系と境界条件を確認する

変換結果と復元結果を確認する

日本語だけでなく、空白、プラス記号、区切り記号、パーセント記号、空文字をテストします。以下は、先ほどの encodeUrlComponent() に対するテスト例です。

// [入力値, 期待するエンコード結果]
const testCases = [
  ["ABC", "ABC"],
  ["", ""],
  ["A B", "A%20B"],
  ["A+B", "A%2BB"],
  ["A&B", "A%26B"],
  ["A=B", "A%3DB"],
  ["A#B", "A%23B"],
  ["A/B", "A%2FB"],
  ["100%", "100%25"],
  ["猫", "%E7%8C%AB"],
  ["😀", "%F0%9F%98%80"]
];

// 変換と復元の両方を確認する
for (const [input, expected] of testCases) {
  const actual = encodeUrlComponent(input);
  const restored = decodeURIComponent(actual);

  console.log({
    input,
    expected,
    actual,
    encodingPass: actual === expected,
    roundTripPass: restored === input
  });
}
JavaScript

URLを解析し直して、元の値が取得できるか確認する

検索URLのテストでは、文字列全体の表記だけでなく、パラメーターとして正しく復元できるかを確認すると実用的です。

// 記号や日本語を含む入力をまとめて確認する
const keywords = [
  "紅茶&コーヒー",
  "A+B C",
  "達成率100%",
  "分類=飲料#おすすめ",
  ""
];

for (const keyword of keywords) {
  // URLを生成する
  const result = buildSearchUrl(keyword, 2);

  // URLとして解析し直す
  const parsed = new URL(result);

  console.log({
    keyword,
    restored: parsed.searchParams.get("q"),
    keywordPass: parsed.searchParams.get("q") === keyword,
    pagePass: parsed.searchParams.get("page") === "2",
    destinationPass: parsed.origin === "https://example.com"
  });
}
JavaScript

加えて、文字列以外の検索語、0や小数のページ番号、不正なUnicode文字列など、関数が許可しない入力も確認します。URLが作れたことだけでなく、意図した値と接続先が保たれていることが重要です。

まとめ

JavaScriptで文字列をURL用にエンコードする場合は、URL全体と、その中のデータを分けて考えます。検索条件を組み立てるなら、URL と URLSearchParams の組み合わせが基本です。

  • URLの構成要素を単独で変換する場合は、encodeURIComponent() を使います。
  • クエリパラメーターは、エンコード前の値を URLSearchParams へ渡します。
  • encodeURI() は、クエリ値のエンコードの代わりにはなりません。
  • 二重エンコードや、デコード済みの値の再デコードを避けます。
  • 空白、プラス記号、不正なUnicodeの扱いを確認します。
  • URLエンコードを、暗号化、接続先の検証、HTMLエスケープと混同しません。

実務で重要なのは、文字列を変換することだけではなく、「URLのどの部分へ、どのような値を渡すのか」を明確にすることです。構造を扱うAPIと入力検証を組み合わせ、生成後のURLをテストすることで、保守しやすいユーティリティになります。

タイトルとURLをコピーしました