pythonのライブラリurllibでURL判定したら、IPv6URLエラーでハマったはなし

Webアプリ等で、入力テキストが「URLを含んだ文字」かどうかを判定する必要があった。
正規表現だと視覚的じゃないので、何かライブラリないかな?と思ってurllibを使ってみた。
そしたら一部の表記だとIPv6URLエラーが出てアプリが落ちた。

問題の概要・原因と解決策を提示する

URL判定をライブラリでやったらハマった話

問題の概要

文字列をURLかどうか判定すると、予期しないエラーが発生した。

問題コード

from urllib import parse

def is_url(text) :
    url_param = parse.urlparse(text)
    return len(url_param.scheme) > 0

上記のコードに、以下のような条件を入力した

print(is_url("http://googcle.com")) # True
print(is_url("HTTPリクエスト"))      # False
print(is_url("abcd:efgh"))          # True
print(is_url("http://["))           # ValueError: Invalid IPv6 URL
print(is_url("http://]"))           # ValueError: Invalid IPv6 URL
print(is_url("http://[]"))          # True

実際に入力された文字

http://[

起きたエラー

ValueError: Invalid IPv6 URL

原因

urllib.parseのソースコード

ライブラリのソースコードでは意図は不明だけどこの条件が入っている。
入力文字の「http://」以降が、①または②の条件を満たす為にraise ValueError(“Invalid IPv6 URL”)となる。

  • ①[は含むが]を含まない
  • ②]は含むが[を含まない

つまり、角括弧の片方のみ含んでいる状態である。

詳細は実際のスタックトレースを参照ください(ちょっと長いです)

ValueError                                Traceback (most recent call last)
<ipython-input-19-b3b7afb7aa88> in <module>
      5     return len(url_param.scheme) > 0
      6 
----> 7 print(is_url("http://["))

<ipython-input-19-b3b7afb7aa88> in is_url(text)
      2 
      3 def is_url(text) :
----> 4     url_param = parse.urlparse(text)
      5     return len(url_param.scheme) > 0
      6 

/usr/lib/python3.5/urllib/parse.py in urlparse(url, scheme, allow_fragments)
    293     (e.g. netloc is a single string) and we don't expand % escapes."""
    294     url, scheme, _coerce_result = _coerce_args(url, scheme)
--> 295     splitresult = urlsplit(url, scheme, allow_fragments)
    296     scheme, netloc, url, query, fragment = splitresult
    297     if scheme in uses_params and ';' in url:

/usr/lib/python3.5/urllib/parse.py in urlsplit(url, scheme, allow_fragments)
    343                 if (('[' in netloc and ']' not in netloc) or
    344                         (']' in netloc and '[' not in netloc)):
--> 345                     raise ValueError("Invalid IPv6 URL")
    346             if allow_fragments and '#' in url:
    347                 url, fragment = url.split('#', 1)

ValueError: Invalid IPv6 URL

bug tracker

python bug trucker:課題33342
事象自体は本件とは別件ですが、起きているエラーは同一です。
課題として挙がっているのは、パスフレーズに[を含んだ場合であるらしい。

仕様ではどう扱われるか

公式:URL を解析して構成要素にする
公式リファレンスを覗くと、以下のように記されている。
なので、今回のValueErrorは必然であることが分かる。

netloc 属性にマッチしなかった角括弧があると ValueError を送出します。

その他の問題

ValueErrorも十分問題だが、もう1点問題がある
それは、コロンが入った文字も「URLである」という判定になってしまう事です。

本来このような文字列はURLでもなんでもないので、URLとしてはFalseとなるべきです。

print(is_url("abcd:efgh"))          # True

なぜそうなるか

urllib.parseのソースコード

またソースコードを覗きにいく。

urlparseは、内部でurlsplitを行っている。
分割する文字は「:」である為、「abcd:efgh」などの文字列でも正しくparseされる。
→schemeである[abcd]とnetlocの[efgh]へ分割

その結果、urlパラメータのスキーマ部分として、「abcd」が抽出され、実装コードのサイズ判定でTrueとなる。

    return len(url_param.scheme) > 0

問題の解決

問題なのはわかった。
ではどうすべきか。

解決コード

import re

URL_PTN = re.compile(r"^(http|https)://")
def is_url(text) :
    return URL_PTN.match(text)

これで正しく判定できるようになった。
アドレス部分は今回興味がないので省いているが、参考を元にアドレスまで見ても良いかもしれない。

この場合、文字の先頭がURLであるかどうかを判定している。

参考

正規表現でURLを削除
正規表現で参考にしました。

【Python】URLかどうか調べる方法
今回の問題ソースを書く際に参考にしました。

まとめ

URLかどうかを判定したい場合、場合にもよるがurllibを使うべきではない
単純に正規表現などで文字列比較をした方が、まだ求めたい結果が得られる。

ただ、こういうバリデーションはライブラリとかであってもいいのになぁと思う。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする