URL Normalization (HubSpot) - URLの正規化
どんな問題?
URL Normalization
http://www.checkio.org/mission/url-normalization/
以下のルールに則ってURLを正規化せよ。
- URLを小文字にする。
- %エスケープ文字は大文字にする。
- デコード可能なエスケープ文字をデコードする。
- デフォルトポートを削除。(httpなら80)
- パスから、「..」や「.」を除去する。
引数のURLを正規化して、戻り値として返す。
例題:
checkio("Http://Www.Checkio.org") == "http://www.checkio.org" checkio("http://www.checkio.org/%cc%b1bac") == "http://www.checkio.org/%CC%B1bac" checkio("http://www.checkio.org/task%5F%31") == "http://www.checkio.org/task_1" checkio("http://www.checkio.org:80/home/") == "http://www.checkio.org/home/" checkio("http://www.checkio.org:8080/home/") == "http://www.checkio.org:8080/home/" checkio("http://www.checkio.org/task/./1/../2/././name") == "http://www.checkio.org/task/2/name"
どうやって解く?
URLの分解
まず、URLを各パーツに分解する必要がある。
例えば、URLが "http://www.checkio.org:80/home/index.html" であれば、
scheme | http:// | |
host | www.checkio.org | |
port | :80 | 省略可 |
path | /home/index.html | 省略可 |
というように分解する。
正規表現を使ってみたが、かなり苦労した。どうも正規表現は苦手だなぁ。これでいけるはず。
url_pattern = re.compile(r'^(.+?)://(.+?)?(:\d+)?(/.*)?$') scheme, host, port, path = [s or '' for s in url_pattern.match(url).groups()]
re.matchで見つからない場合は、Noneが返るので、''に変換している。
%エンコードの処理
こちらも正規表現の置換(sub)を使った。%00~%FFを検索して、見つかったら、置換用の関数 normalize_octets()を呼び出して、指定範囲内ならデコード、それ以外なら大文字化を行う。
def normalize_octets(match): s = match.group() i = int(s[1:], 16) if 0x41 <= i <= 0x5A or 0x61 <= i <= 0x7A or 0x30 <= i <= 0x39 or i in [0x2D, 0x2E, 0x5F, 0x7E]: return chr(i).lower() else: return s.upper() path_pattern = re.compile(r'(%[\da-f]{2})', re.I) path = path_pattern.sub(normalize_octets, path.lower())
pathも小文字化する必要があるので、デコード前のpathと、デコード後の文字(chr)も lower()で小文字化している。
(問題文ではわかりにくいが、pathが %48%6f%6d%45 の場合は、HomEではなくhomeにしなくてはならない。)
「.」と「..」の処理
簡単に処理する方法が思いつかなかったので、ディレクトリのリストを再構築するようにした。
def remove_dot_segments(path): dirs = [] for d in path.split('/'): if d == '.': continue elif d == '..': if dirs: dirs.pop() else: dirs.append(d) return '/'.join(dirs)
まとめ
import re def normalize_octets(match): s = match.group() i = int(s[1:], 16) if 0x41 <= i <= 0x5A or 0x61 <= i <= 0x7A or 0x30 <= i <= 0x39 or i in [0x2D, 0x2E, 0x5F, 0x7E]: return chr(i).lower() else: return s.upper() def remove_dot_segments(path): dirs = [] for d in path.split('/'): if d == '.': continue elif d == '..': if dirs: dirs.pop() else: dirs.append(d) return '/'.join(dirs) def checkio(url): # Split URL url_pattern = re.compile(r'^(.+?)://(.+?)?(:\d+)?(/.*)?$') scheme, host, port, path = [s or '' for s in url_pattern.match(url).groups()] # Scheme and host to lower case scheme = scheme.lower() host = host.lower() # Remove default port if (scheme, port) == ('http', ":80"): port = '' # Normalize escape sequences path_pattern = re.compile(r'(%[\da-f]{2})', re.I) path = path_pattern.sub(normalize_octets, path.lower()) # Remove '.' and '..' path = remove_dot_segments(path) # Make normalized URL return '{}://{}{}{}'.format(scheme, host, port, path)
http://www.checkio.org/mission/url-normalization/publications/natsuki/python-3/first/
特に難しい問題でもないのだが、ルールがよくわからず、Try&Errorを繰り返すことになってしまった。