重複パラメーターと入力の正規化
正規化する順序と回数を決め、検証済みの値をそのまま利用する設計を学びます。
説明
検証側と利用側が同じ要求を違う規則で解釈すると、検査を通った値と実際に使う値が食い違います。重複キーを先頭優先・末尾優先で扱う差や、URLの二重デコードが代表的な原因です。
一つの入力を標準のパーサーで解析し、解釈の違いを比較します。重複の拒否、一度だけのデコード、Unicodeの正規化を行い、保存する値まで同じ表現を使う流れを確認します。
正規化する順序と回数を決め、検証済みの値をそのまま利用する設計を学びます。
1同名パラメーターを一覧として読む
数量を二つ含むクエリーです。解析直後は重複を保持する形式で確認します。
from urllib.parse import parse_qsl,unquote
from sd01 import parse_once,unique
import json,unicodedata,sqlite3
query = "quantity=1&quantity=9"
pairs = parse_qsl(query,keep_blank_values=True)重複と空値を保持して解析する
print(pairs)quantityが1と9の二つあります。ここでdictへ変換すると、どちらか一つしか残りません。
2先頭優先と末尾優先を比較する
検査側が先頭の1、利用側がdict変換後の9を使う場合です。
checked = pairs[0][1]先頭の値を検査側の値にする
used = dict(pairs)["quantity"]末尾が残る変換で利用側の値を取得する
print(checked,used,int(checked)<=5,int(used)<=5)検査した1は範囲内ですが、使う9は範囲外です。各段階の規則が違うことが原因です。
3単一値の項目では重複を拒否する
parse_onceは一覧を受け、同名キーがあれば辞書へ変換する前に拒否します。
print(parse_once(query)重複したキーを拒否する)
print(parse_once("quantity=1"))
print(parse_once("quantity=")空値を保持して解析する)重複はduplicate:quantity、単一の値は辞書になります。空文字を保持することで、欠落と空の値も後で別に検証できます。
4二重に符号化されたパスを比較する
%25は%の符号化です。二回のデコードで初めて..が現れる試料を使います。
raw = "%252e%252e%252fnotes.txt"
once = unquote(raw)一回目のデコードを行う
twice = unquote(once)二回目との違いを確認する
print(raw,once,twice)
print(".." in once,".." in twice)一回目は%2e%2e%2f、二回目は../です。検査後にもう一度デコードする構成では、利用時に別のパスへ変わってしまいます。
5JSONの重複キーも保持して調べる
JSONの同名キーを通常のloadsで読むと、ここでは末尾が残ります。object_pairs_hookで解析直後の対を受け取れます。
raw_json = '{"quantity":1,"quantity":9}'
print(json.loads(raw_json)通常の辞書変換を確認する)
print(json.loads(raw_json,object_pairs_hook=list重複した対を保持する))
try: json.loads(raw_json,object_pairs_hook=unique重複を拒否して変換する)
except ValueError as error: print(str(error))通常は9だけが残り、重複拒否ではduplicate:quantityです。入力形式が変わっても解釈の一致を確かめる必要があります。
6Unicodeの見た目とコードポイントを比較する
éの合成済み表現と、e+結合アクセントの表現を使います。
a = "é"
b = "é"
print(a==b元のコードポイント列を比較する,len(a),len(b),a.encode().hex(),b.encode().hex())
print(unicodedata.normalize("NFC",a)合成済みの正規形へそろえる==unicodedata.normalize("NFC",b))元は1文字分と2コードポイントで異なりますが、NFC後は一致します。大小文字やすべての見た目の類似までNFCが統一するわけではありません。
7正規化した値を一意に保存する
このシステムはNFCで同じ名前を同一と扱う仕様にします。検査後も同じ正規化済み値を保存します。
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE names(value TEXT UNIQUE保存値の一意性を設定する)")
print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",a)正規化済みの値を保存する,)).rowcount)
print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",b),)).rowcount)
print(db.execute("SELECT value FROM names").fetchall())初回は1、二つ目は0で一行です。正規化の規則を仕様として定め、検証・保存・検索で同じ表現を使います。
まとめ
解析段階で重複を把握し、単一値項目の扱いを決めます。
検証後の再デコードで値を変えない構成にします。
正規化した値を、検証から保存まで一貫して使います。
操作履歴と解説を振り返れます。
Python 3.15 / HTTP・SQLite・標準パーサー / 固定の要求
from urllib.parse import parse_qsl,unquote
from sd01 import parse_once,unique
import json,unicodedata,sqlite3
query = "quantity=1&quantity=9"
pairs = parse_qsl(query,keep_blank_values=True)
print(pairs)[('quantity', '1'), ('quantity', '9')]checked = pairs[0][1]
used = dict(pairs)["quantity"]
print(checked,used,int(checked)<=5,int(used)<=5)1 9 True False
print(parse_once(query))
print(parse_once("quantity=1"))
print(parse_once("quantity="))duplicate:quantity
{'quantity': '1'}
{'quantity': ''}raw = "%252e%252e%252fnotes.txt"
once = unquote(raw)
twice = unquote(once)
print(raw,once,twice)
print(".." in once,".." in twice)%252e%252e%252fnotes.txt %2e%2e%2fnotes.txt ../notes.txt False True
raw_json = '{"quantity":1,"quantity":9}'
print(json.loads(raw_json))
print(json.loads(raw_json,object_pairs_hook=list))
try: json.loads(raw_json,object_pairs_hook=unique)
except ValueError as error: print(str(error)){'quantity': 9}
[('quantity', 1), ('quantity', 9)]
duplicate:quantitya = "é"
b = "é"
print(a==b,len(a),len(b),a.encode().hex(),b.encode().hex())
print(unicodedata.normalize("NFC",a)==unicodedata.normalize("NFC",b))False 1 2 c3a9 65cc81 True
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE names(value TEXT UNIQUE)")
print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",a),)).rowcount)
print(db.execute("INSERT OR IGNORE INTO names VALUES(?)",(unicodedata.normalize("NFC",b),)).rowcount)
print(db.execute("SELECT value FROM names").fetchall())1
0
[('é',)]演習完了