PythonでURLクエリの重複パラメータを見つける:空値と%エンコードも安全に確認

プログラミング・Web開発カテゴリを表すパンダのイラスト プログラミング・Web開発

この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。 Python 3.13の公式urllib.parse資料を確認し、ネットワークに接続しない架空のURLクエリ検査サンプルを作りました。Linux / Python 3.13.5で単体テスト18件、構文検査、正常系と異常系のCLI出力を確認しています。

検証ステータス:🧪 Linux / Python 3.13.5でローカル実行・18テスト成功。Windows/macOS未確認。GitHub Actions・Codex CLI未実行。

対応環境・できること・制限事項

項目内容
対象Python 3.10以降(標準ライブラリ)
できること架空のクエリの重複キーと空値を数える
通信・費用通信なし、追加費用なし
実測・制限Linux / Python 3.13.5。Windows/macOS未確認

「URLの末尾に同じ項目が2回入っているけれど、どちらが使われるのだろう?」。WebアプリやAPIの動作を調べていると、こんな疑問に出会います。たとえば次の文字列は、tagが2回、emptyが1回出現しています。

tag=one&tag=two&empty=

ここで大切なのは、重複を見つけることと、サーバーがどちらの値を採用するかは別の話だという点です。今回はPython標準ライブラリのurllib.parse.parse_qsl()を使い、キーと値の組を失わずに数えます。実在するWebサービスへ通信せず、キー名や値を画面へ表示しない小さな検査プログラムです。

辞書に変換する前にペアを調べる

parse_qsl()はクエリ文字列を「キーと値のペアのリスト」に変換します。一方、parse_qs()はキーごとに値をリストにまとめた辞書を返します。どちらも用途がありますが、元の出現順と重複回数を確認したい場合はparse_qsl()が分かりやすいです。

公式の説明はPython 3.13 urllib.parseドキュメントにあります。keep_blank_valuesは空の値を残すかどうか、strict_parsingは解析できない項目をどう扱うかを決める引数です。Python 3.10以降はseparator='&'を明示でき、今回の教材も&だけを区切りにします。

用意するもの

Python 3.10以降と、audit_query.py、test_audit_query.py、READMEを使います。管理者権限や追加ライブラリは不要です。今回の実測はLinux / Python 3.13.5で、WindowsとmacOSの個別実行は未確認です。

完全版のコードとテストは末尾のDaily Code Samplesにあります。まずは新しい作業フォルダへ3ファイルを置いてください。Windowsではpy -3、Linux/macOSではpython3など、手元の環境に合わせてPythonの起動コマンドを選びます。

注意:実際の認証URLやトークンを--queryに渡さないでください。 このサンプルは出力に値を含めませんが、コマンドラインの引数自体はシェル履歴やOSのプロセス一覧に残る場合があります。以下はすべて架空の文字列です。

まず動かす:同じキーが2回ある例

サンプルのディレクトリで、次を実行します。

python3 audit_query.py --query 'tag=one&tag=two&empty='

Linux / Python 3.13.5で実際に確認した表示は次のとおりです。終了コードは0でした。

STATUS=VALID FIELDS=3 UNIQUE_KEYS=2 DUPLICATE_KEYS=1 EXTRA_OCCURRENCES=1 BLANK_VALUES=1

数字の意味を分けて読むと、FIELDS=3はキーと値の組が3個あること、UNIQUE_KEYS=2は異なるキー名が2種類あることです。DUPLICATE_KEYS=1は「2回以上現れたキー名」が1種類あるという意味です。EXTRA_OCCURRENCES=1は、各キーの最初の1回を除いた余分な出現回数の合計です。BLANK_VALUES=1は値が空文字列の組が1個あることを示します。

たとえばtag=one&tag=two&tag=threeなら、重複したキー名は1種類でも、余分な出現は2回です。この2つを分けておくと、「何種類のキーに問題があるか」と「余分な項目が合計何個あるか」を混同しません。

仕組み:何を指定して解析しているのか

完全版の中心は次の部分です。これは説明用の抜粋であり、エラー処理と入力制限を含む実行可能な完成版はGitHub側を正本とします。

from urllib.parse import parse_qsl

pairs = parse_qsl(
    'tag=one&tag=two&empty=',
    keep_blank_values=True,
    strict_parsing=True,
    encoding='utf-8',
    errors='strict',
    max_num_fields=100,
    separator='&',
)
print(len(pairs))  # 3

keep_blank_values=Trueがないと、empty=のような項目は既定の解析では除外され得ます。業務の入力検査では「項目がなかった」のか「項目はあったが空だった」のかが違う意味を持つことがあります。そこで空値を残して数えています。

errors='strict'は、パーセントエンコードから復元したバイト列がUTF-8として不正なら、置換文字で黙って続けずエラーにする指定です。またmax_num_fields=100は極端に多い項目の解析を抑えるための教材上の上限であり、URLの規格そのものが100件までと決めているわけではありません。

条件を1つ変える:壊れた%エンコード

次は%の後ろが16進数2桁になっていない例です。

python3 audit_query.py --query 'a=%ZZ'

実際の出力は次のとおりで、終了コードは2でした。

STATUS=INVALID ISSUE=INVALID_PERCENT_ESCAPE

ここは意外な落とし穴です。Pythonのparse_qsl()は、壊れたパーセントエスケープを常にエラーにする検証器ではありません。そこで完成版では解析前に%の直後に16進数が2桁続くかを独自に調べています。%FFのように形式上は2桁でもUTF-8として不正な場合は、別のINVALID_UTF8として扱います。

この区別によって、「エンコードの書き方がおかしい」と「エンコード後のバイト列をUTF-8として読めない」を切り分けられます。修復や自動再送は行わず、入力を変える前に原因を確認するための教材です。

+、%2B、大文字小文字で結果が変わる

URLクエリでは、+がスペースとして扱われ、%2Bは文字としてのプラス記号になります。またa=1&%61=2は、デコード後には同じaというキーが2回現れた形です。キー名の大小文字は今回のコードでは区別します。つまりTag=1&tag=2は異なる2種類のキーとして数えます。

ただし、実際のWebアプリがキー名の大小文字を区別するか、重複した値の最初・最後・全部のどれを使うかは別問題です。サーバー、フレームワーク、アプリケーション側の仕様を確認してください。今回のプログラムは、その採用規則を推測しません。

失敗したときの切り分け

表示主に調べること
INVALID_PERCENT_ESCAPE%の後ろが16進数2桁になっているか
INVALID_UTF8%FFなど、UTF-8として不正なバイト列がないか
INVALID_FIELDa=1&bや&&など、形式が崩れていないか
EMPTY_KEY=valueのようにキー名が空になっていないか
FIELD_LIMIT100項目を超えていないか(教材独自制限)
QUERY_TOO_LONG4096文字を超えていないか(教材独自制限)

入力が無効なら終了コード2、有効なら0です。検査に失敗したのに成功と表示して次の処理へ渡すことを避けます。なお、--query自体が指定されないなどCLI引数の誤りはargparseの扱いになり、同じ2でもプログラム本体の入力検査とは区別が必要です。

18件のテストで何を確認したか

実行したコマンドは次のとおりです。

python3 -m unittest discover -p 'test_*.py' -v
python3 -m py_compile audit_query.py test_audit_query.py

Linux / Python 3.13.5で18件すべてPASS、構文検査PASSを確認しました。重複キー、空値、%61とa、+と%2B、UTF-8不正、フィールド数・文字数制限、CLIの正常系と異常系をテストしています。出力にダミー値が含まれないことも単体テストで確認しました。Codex CLIはこの環境に存在せず、実行していません。GitHub Actionsも請求制限のため実行していません。Windows/macOSの結果は推測で補っていません。

実務では「安全なURL」判定と混同しない

この教材はクエリの形式と出現回数を調べるだけです。認証・認可・署名・URLの危険性・サーバー側の解釈が正しいことは証明しません。クエリ文字列には、個人情報や認証情報が含まれる場合があります。実際の業務データを検査する仕組みへ拡張するなら、コマンドラインに秘密を渡さない入力経路、ログの扱い、アクセス権、データ保存期間を別途設計する必要があります。

また、PythonのURL解析に関するセキュリティ上の注意も参照してください。標準ライブラリの解析結果をそのまま「安全な入力」と認定してはいけません。今回のサンプルも、不正なURL全般を検出するものではありません。

まとめ:重複キーの検査は、まず小さく試す

parse_qsl()でキーと値の組を保持し、空値を残して数える。形式エラーは終了コードで区別し、入力値は表示しない。この3点だけでも、URLを文字列として眺めるより原因を整理しやすくなります。最初は架空の3項目で動かし、tagを3回に増やすなど1条件ずつ変えて、数え方の違いを確認してみてください。

GitHubサンプル・公式情報

文書情報

記事タイトル
PythonでURLクエリの重複パラメータを見つける:空値と%エンコードも安全に確認
作成日
更新日
Source URL
https://papanda925.com/?p=18372

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

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