LoginSignup
5
3

More than 5 years have passed since last update.

URLの正規化手順

Last updated at Posted at 2017-02-13

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

Python実装

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

テストコード

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

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)
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3