URLの正規化手順

Webクロールを行う場合、収集したURLには大抵表記ゆれがあることから、単純な文字列の比較だけでは検証漏れが発生し、同じページを複数回クロールすることがあります。

URLを正規化する手順が存在するか確認してみたのですが、どうやら一定の手順はあっても仕様として確立したものは見当たりませんでした。
OAuthのパラメータの正規化手順もあくまでInformationalなのが気になります…。

ひとまずURIの正規化（[RFC3986 section-6]）とOAuthにおけるパラメータ順序の正規化（[RFC5849 section-3.4.1.3.2]）を組み合わせることで次のようなURLの正規化手順を組み立てました。

1. schemeとnetlocをlowercase化（[RFC3986 secion-6.2.2.1]）
2. pathとqueryのパーセントエンコーディングをuppercase化（RFC3986 secion-6.2.2.1）
3. [0-9A-Za-z-._~]の範囲のパーセントエンコーディングを解除（[RFC3986 secion-6.2.2.2]）
4. 既知のプロトコルポートの除去（[RFC3986 secion-6.2.3]）
5. 相対パスの正規化（[RFC3986 secion-6.2.2.3]）
6. パラメータの順番の整列（[RFC5849 section-3.4.1.3.2]）

[RFC3986 section-6]: https://tools.ietf.org/html/rfc3986#section-6 "6. Normalization and Comparison"
[RFC3986 secion-6.2.2.1]: https://tools.ietf.org/html/rfc3986#section-6.2.2.1 "6.2.2.1.  Case Normalization"
[RFC3986 secion-6.2.2.2]: https://tools.ietf.org/html/rfc3986#section-6.2.2.2 "6.2.2.2.  Percent-Encoding Normalization"
[RFC3986 secion-6.2.2.3]: https://tools.ietf.org/html/rfc3986#section-6.2.2.3 "6.2.2.3.  Path Segment Normalization"
[RFC3986 secion-6.2.3]: https://tools.ietf.org/html/rfc3986#section-6.2.3 "6.2.3.  Scheme-Based Normalization"
[RFC5849 section-3.4.1.3.2]: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 "3.4.1.3.2. Parameters Normalization"

## Python実装

以上の手順の他、細々したものを実装したのがpython-url-c14nになります。

<https://github.com/info-labs/python-url-c14n>

### テストコード

他に考慮すべき要素などがありましたらissueやpull-reqを投げていただけると助かります。

```python
class URLC14NTests(unittest.TestCase):
    def check(self, source, c14ned):
        self.assertEqual(url_c14n(source), c14ned)

    def test_upper_scheme(self):
        self.check('HTTP://example.com/', 'http://example.com/')

    def test_upper_host(self):
        self.check('http://EXAMPLE.COM/HOGE', 'http://example.com/HOGE')

    def test_capitalize_escape_sequence(self):
        source = 'http://example.com/%e3%81%a6%e3%81%99%e3%81%a8'
        c14ned = 'http://example.com/%E3%81%A6%E3%81%99%E3%81%A8'
        self.check(source, c14ned)

    def test_unreserved_escape(self):
        source = 'http://example.com/%61%62%63'
        c14ned = 'http://example.com/abc'
        self.check(source, c14ned)

    def test_remove_default_port(self):
        self.check('http://example.com:80/', 'http://example.com/')
        self.check('https://example.com:443/', 'https://example.com/')

    def test_root_url(self):
        self.check('http://example.com', 'http://example.com/')

    def test_normalize_path(self):
        self.check('http://example.com/a/./b/../c', 'http://example.com/a/c')

    def test_duplicate_slash(self):
        self.check('http://example.com/a//b/////c', 'http://example.com/a/b/c')

    def test_normalize_path_last_slash(self):
        self.check('http://example.com/a/./b/../c/', 'http://example.com/a/c/')

    def test_query_order(self):
        source = 'http://example.com/?a=x&c=y&b=z'
        c14ned = 'http://example.com/?a=x&b=z&c=y'
        self.check(source, c14ned)

    def test_empty_parameter(self):
        source = 'http://example.com/?a&b=&c=z'
        c14ned = 'http://example.com/?a=&b=&c=z'
        self.check(source, c14ned)

    def test_empty_parameter(self):
        source = 'http://example.com/?a&b=&c=z'
        c14ned = 'http://example.com/?a=&b=&c=z'
        self.check(source, c14ned)
```