📌

Documentの中にあるURLを抽出する

に公開

やること

Google botのようなクローラーはコンテンツ中のURLをどのように抽出するのでしょうか?
今回は、シンプルで軽量なWebスパイダーである「gospider」のリンク抽出を参考にこの疑問に迫ってみます。

https://github.com/jaeles-project/gospider

該当ソースコード

今回見ていくプログラムは以下の箇所です。
受け取ったHTML等のコンテンツ中からURLを抽出する関数です。

https://github.com/jaeles-project/gospider/blob/master/core/linkfinder.go#L8-L29

8行目の正規表現ですが、見やすく改行してみます。

(?:"|')
(
  ((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})
  |
  ((?:/|\.\./|\./)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})
  |
  ([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|#][^"|']{0,}|))
  |
  ([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{3,}(?:[\?|#][^"|']{0,}|))
  |
  ([a-zA-Z0-9_\-]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:[\?|#][^"|']{0,}|))
)
(?:"|')

ダブルクオートとシングルクオートで囲まれた箇所をみています。
URLには4パターンあるようです。

  1. ((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}.[a-zA-Z]{2,}[^"']{0,})

http://やhttps://のようにプロトコルが含まれるパターンです。

  1. ((?:/|../|./)[^"'><,;| *()(%%$^/\[]][^"'><,;|()]{1,})

相対パスのパターンです。

  1. ([a-zA-Z0-9_-/]{1,}/[a-zA-Z0-9_-/]{1,}.(?:[a-zA-Z]{1,4}|action)(?:[?|#][^"|']{0,}|))

file.php や file.html のようなファイル名が含まれるパターンです。

  1. ([a-zA-Z0-9_-/]{1,}/[a-zA-Z0-9_-/]{3,}(?:[?|#][^"|']{0,}|))

/path/to/file.php?param=value のようなクエリパラメータが含まれるパターンです。

ビジュアライズする

reex-vis.comを使って可視化してみます。

初見でもわかる程度には見やすくなりました。

https://regex-vis.com/?r=(%3F%3A"|')(((%3F%3A[a-zA-Z]{1%2C10}%3A%2F%2F|%2F%2F)[^"'%2F]{1%2C}\.[a-zA-Z]{2%2C}[^"']{0%2C})|((%3F%3A%2F|\.\.%2F|\.%2F)[^"'><%2C%3B|+*()(%25%25%24^%2F\\\[\]][^"'><%2C%3B|()]{1%2C})|([a-zA-Z0-9_\-%2F]{1%2C}%2F[a-zA-Z0-9_\-%2F]{1%2C}\.(%3F%3A[a-zA-Z]{1%2C4}|action)(%3F%3A[\%3F|%23][^"|']{0%2C}|))|([a-zA-Z0-9_\-%2F]{1%2C}%2F[a-zA-Z0-9_\-%2F]{3%2C}(%3F%3A[\%3F|%23][^"|']{0%2C}|))|([a-zA-Z0-9_\-]{1%2C}\.(%3F%3Aphp|asp|aspx|jsp|json|action|html|js|txt|xml)(%3F%3A[\%3F|%23][^"|']{0%2C}|)))(%3F%3A"|')

RFCと照らし合わせる

URIはRFC3986で定義されています。
RFC3986に準拠したURLの正規表現を以下を参考に照らし合わせてみます。

https://qiita.com/shimataro999/items/fced9665fa970c009c1e

/^[a-z]([a-z]|[0-9]|[+\-.])*:(\/\/((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|:)*@)?(\[((([0-9a-f]{1,4}:){6}([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|::([0-9a-f]{1,4}:){5}([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|([0-9a-f]{1,4})?::([0-9a-f]{1,4}:){4}([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::([0-9a-f]{1,4}:){3}([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::([0-9a-f]{1,4}:){2}([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::([0-9a-f]{1,4}:[0-9a-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})|(([0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(([0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+\.(([a-z]|[0-9]|[-._~])|[!$&'()*+,;=]|:)+)]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3}|(([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=])*)(:\d*)?(\/((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@]))*)*|\/(((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@]))+(\/((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@]))*)*)?|((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@]))+(\/((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@]))*)*|)(\?((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@])|[\/?])*)?(#((([a-z]|[0-9]|[-._~])|%[0-9a-f][0-9a-f]|[!$&'()*+,;=]|[:@])|[\/?])*)?$/i

1番のパターンだけでも恐ろしいくらいの制約があります。 regex-vis.com
ただ大まかな構造としては、${SCHEME}:${HIER_PART}(\\?${QUERY})?(#${FRAGMENT})? という形になっていることがわかります。

まとめ

RFC1808がURLの構造について分かりやすくまとめられています。
Browserの場合はnet_locを含まなかったりもするのでその辺りも考慮する必要があります。
URL抽出を行う際はちゃんとRFCを見た上で要件に合わせたパフォーマンスの考慮を取り入れましょう。

https://datatracker.ietf.org/doc/html/rfc1808

Discussion