スパムに効く正規表現2-URLの分解とドメイン名の抽出

ウチのCGI掲示板には、スパムと判定された投稿からURLのドメイン名を抽出し、投稿禁止ワードとして一定期間保持する機能を持たせています。他のスパム判定フィルタと合わせることでモレを少なくする効果を狙ったものですが、特にスパム対策が未熟だった頃、この機能に引っかかったスパム投稿も少なくありませんでした。

さて、正規表現 URL 分解 を Google で検索する と第1位に「正規表現サンプル(URLを分解する)」が挙がり、次のような正規表現が紹介されています。

^(.+?)://(.+?):?(d+)?(/.*)?$

この正規表現は、左側のカッコから順に スキームホストポートパス へと分解してくれるのですが、http://127.0.0.1/ の様にIPアドレスで構成されたURLの場合、ホストが 127.0.0.、ポートが 1 となってしまうという問題点があります。

もちろんこれを改善する正規表現を作ることは出来るのですが、ここでは RFC2396日本語訳) に従ってURIを分解する正規表現を検証してみたいと思います。

1.URI について

RFC2396 の第3章ではURIの一般構文が、第4章では(絶対URIや相対URIを含むリソース識別子としての)URI参照が解説されています。ここでは以降の解説に必要最低限な項目だけをまとめておきます。

  • 絶対 URI の一般形
    • <scheme>:<scheme-specific-part>
  • 階層構造を表す URI の4つのコンポーネント
    • <scheme>://<authority>/<path>?<query>
  • scheme
    • リソースへアクセスするための方法を表す。
  • authority
    • リソースを管理する機関の最上位階層を表す。インターネット上の特定サーバーに対するURLスキームでは <userinfo>@<host>:<port> の成分を持つ。 userinfoport は省略しても良い。
  • path
    • スキーム及びリソースを管理する機関内で、リソースを特定するためのデータを表す。
  • query
    • リソースによって解釈される文字列情報を表す。
  • fragment
    • これはURIの一部分ではないが、しばしばURIとともに用いられる。リソースの取得成功後にユーザーエージェント(例えばブラウザなど)が解釈する付加的な参照情報を表す。

2.URI を分解する正規表現

次の正規表現は RFC2396 の付録 B に掲載されたURIを解釈する POSIX 準拠の 正規表現 です。あくまで 分解 が目的であり、URIの正当性を評価するものではないことに注意してください。

^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
 12            3  4          5       6  7        8 9

2行目のカッコの下の数字は、各 URI 要素を参照するためのインデックスを表しています。例えば次の Perl コードでは、分解された URI の各要素が配列 @uri に格納されます。試してはいませんが、PHP でも関数 preg_match() で同様のことができると思います。

$uri = "scheme://authority/path?query#fragment";
@uri = $uri =~ m|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|;

for (my $i = 0; $i < @uri; $i++) {
	print "$i : $uri[$i]n";
}

実行結果は次の通りです。

0 : scheme:
1 : scheme
2 : //authority
3 : authority
4 : /path
5 : ?query
6 : query
7 : #fragment
8 : fragment

ただし見てお分かりのように、この正規表現は () による文字列のグループ化により URI 要素への分解が多少冗長となっています。そこで後方参照を伴わないグループ化 (?:...) を用いて Perl のサブルーチンを次のように定義すれば、リストコンテキストで呼び出すことにより scheme、authority、path、query、fragment が順に格納された配列を得ることが出来ます。

sub parse_uri() {
	# $1 : scheme, $2 : authority, $3 : path, $4 : query, $5 : fragment
	$_[0] =~ m|^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|;
}

JavaScript 版は、以下をご参照ください。

3.各スキームに対する適用例

RFC2396 の1.3章には http、ftp、telnet だけではなく、mailto、news、gopher といったスキームに対するURIの具体例が挙げられています。これらの例に倣ったURIを、先に定義したPerl関数 parse_uri() で処理した結果を示します。冒頭に挙げた正規表現例の様に ポート は分離しませんが、ホスト に相当する部分は正しく分離出来ていることがわかります。通常はポート番号の分離自体にそれほどの利用価値があるとは思えませんので、必要かつ十分だと思います。

  • http://127.0.0.1/
    • scheme http
      authority 127.0.0.1
      path /
      query
      fragment
  • http://www.example.com:8080/search?lng=jp&q=test#top
    • scheme http
      authority www.example.com:8080
      path /search
      query lng=jp&q=test
      fragment top
  • telnet://user:password@host:80/
    • scheme telnet
      authority user:password@host:80
      path /
      query
      fragment
  • ftp://anonymous@ftp.example.com/text.txt
    • scheme ftp
      authority anonymous@ftp.example.com
      path /text.txt
      query
      fragment
  • file://Users/myname/Documents/text.txt
    • scheme file
      authority Users
      path /myname/Documents/text.txt
      query
      fragment
  • mailto:myname@example.com
    • scheme mailto
      authority
      path myname@example.com
      query
      fragment

2011年10月2日 追記

jsdo.it で JavaScript 版を公開 しています。ご参考まで。

4.テキストからドメイン名を抽出する

次のPerlプログラムは、テキストからURLを抜き出し、先の関数 parse_uri()authority を抽出します。URLを抜き出すための関数 extract_url() 中の正規表現 $regexp は、Perlメモ – http URL の正規表現 の簡易表現を使わせてもらっています。掲示板CGIで用いる場合にはサニタイジング処理の影響がありますので、ぜひ スパムに効く正規表現1-aタグのURLを正しく自動リンクする もご参照ください。

# テキスト中に記述されたURLのリストを得る
my @list = &extract_url($text);

# 重複したURLを削除する
my %tmp = ();
@list = grep(!$tmp{$_}++, @list);

foreach (@list) {
	my @url = &parse_uri($_);

	# authority に対する処理例
	print $url[1];
}

sub extract_url() {
	my $regexp = 's?https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+';
	$_[0] =~ /($regexp)/go;
}

さて、抽出された authority をドメイン名として投稿禁止ワードに登録しても良いのですが、その先頭には慣例としてサーバー名が含まれている場合があります。その場合、例えば s1.example.com を投稿禁止ワードとすると s2.example.com を含むURLを引っ掛けることが出来ません。事実、ホームのURLと記事中のURLを巧妙に使い分けて投稿してくる例が過去にはありました。

そこでウチの掲示板で使っている、authority からドメイン名のみを抽出する(サーバー名を削除する)処理について、最後に少しだけ説明をしておきます。

URLの場合、authority はホスト名と同値です。ホスト名(IPアドレスを持つ)とドメイン名(DNSに登録されている)は、場合によって同値となりますが、ここでは <host> = <server> . <domain> であると仮定をします。

ドメイン名の構成

ドメイン名の構成

一般にドメイン名は、その階層構造を表す要素が . (ドット) で連結された構成となっています。各要素は右から順にトップレベルドメイン (TLD)、2nd レベルドメイン (SLD)、3rd レベルドメイン、… と命名されますが、最左端の要素が特定のサーバー名を表しているかどうかを正規表現だけで判定することは出来ません。

そこで次の様なヒューリスティックなルールにより、サーバー名を削除します。

  1. authority. (ドット)で分割する。
  2. 3rd レベルドメイン以上の要素があれば、次の2つを満たす場合に最左端の要素をサーバー名とする。
    • 2nd レベルドメインの文字数が n 文字以上ある(n は3が適当)。
    • トップレベルドメインがIPアドレスの一部ではない。

ルール 2. は、http://jprs.jp/ などの 汎用JPドメイン名 を不必要に短くしてしまわないための措置です。また .co.ac 等の国コード 2nd レベルドメインは、国コードトップレベルドメインごとに決められており、現在 .jp では9種類、.br (ブラジル)に至っては66種類もあり、その全てをチェックすることは非現実的です。そこで、そのほとんどが3文字以下であることを利用します。

もちろんこのようなヒューリスティックな処理は正しくなく、(サーバー名ではない)ドメイン名の要素を削除してしまう可能性があります。本来は DNS などを引くべきでしょう。しかし、「投稿禁止ワードとしてドメイン名を登録する」という目的では逆に好ましい結果となるため、私的にはOKとしています 😉 。

5.参考情報

Wikipedea におけるドメイン名やホスト名に関する解説は、英文のほうが詳しくて分かり易いです。日本語訳を参照したい場合には、Wikipedia の言語メニューから「日本語」を選択してください。

スパムに効く正規表現2-URLの分解とドメイン名の抽出」への3件のフィードバック

  1. 通りすがり

    ¥記号がエンコードかなにかで消えてしまっていて記事中の正規表現が壊れてしまっているようです。

    誤 ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(?([^#]*))?(#(.*))?
    正 ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?

    返信
    1. tokkonoPapa 投稿作成者

      通りすがりさま

      6年もののバグでした 😉 記事中の3カ所を修正しました。ご指摘、ありがとうございました m_(. .)_m

      返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません