148
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PHPしか書けないザコがメールアドレス正規表現でガチ勢に挑んでみた

Last updated at Posted at 2013-08-01

【2021/10/15 追記】
この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので,過去の記事として参考程度にご覧ください。

この記事の情報は古いので,最新の情報が欲しい方は 「PHPで各種バリデーション」 をお読みください。

訂正: IPv6のメールアドレスは IPv6: プレフィクスが必要です。PHP7.1時点でこの形式に対応していることを確認しました。

  • 誤: a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]
  • 正: a@[IPv6:2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]

関数ラインナップ

私の関数

MyFunction
function validate_email($email, $strict = true) {
    $dot_string = $strict ?
        '(?:[A-Za-z0-9!#$%&*+=?^_`{|}~\'\\/-]|(?<!\\.|\\A)\\.(?!\\.|@))' :
        '(?:[A-Za-z0-9!#$%&*+=?^_`{|}~\'\\/.-])'
    ;
    $quoted_string = '(?:\\\\\\\\|\\\\"|\\\\?[A-Za-z0-9!#$%&*+=?^_`{|}~()<>[\\]:;@,. \'\\/-])';
    $ipv4_part = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])';
    $ipv6_part = '(?:[A-fa-f0-9]{1,4})';
    $fqdn_part = '(?:[A-Za-z](?:[A-Za-z0-9-]{0,61}?[A-Za-z0-9])?)';
    $ipv4 = "(?:(?:{$ipv4_part}\\.){3}{$ipv4_part})";
    $ipv6 = '(?:' .
        "(?:(?:{$ipv6_part}:){7}(?:{$ipv6_part}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){6}(?::{$ipv6_part}|:{$ipv4}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){5}(?:(?::{$ipv6_part}){1,2}|:{$ipv4}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){4}(?:(?::{$ipv6_part}){1,3}|(?::{$ipv6_part})?:{$ipv4}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){3}(?:(?::{$ipv6_part}){1,4}|(?::{$ipv6_part}){0,2}:{$ipv4}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){2}(?:(?::{$ipv6_part}){1,5}|(?::{$ipv6_part}){0,3}:{$ipv4}|:))" . '|' .
        "(?:(?:{$ipv6_part}:){1}(?:(?::{$ipv6_part}){1,6}|(?::{$ipv6_part}){0,4}:{$ipv4}|:))" . '|' .
        "(?::(?:(?::{$ipv6_part}){1,7}|(?::{$ipv6_part}){0,5}:{$ipv4}|:))" . 
    ')';
    $fqdn = "(?:(?:{$fqdn_part}\\.)+?{$fqdn_part})";
    $local = "({$dot_string}++|(\"){$quoted_string}++\")";
    $domain = "({$fqdn}|\\[{$ipv4}]|\\[{$ipv6}]|\\[{$fqdn}])";
    $pattern = "/\\A{$local}@{$domain}\\z/";
    return preg_match($pattern, $email, $matches) &&
        (
            !empty($matches[2]) && !isset($matches[1][66]) && !isset($matches[0][256]) ||
            !isset($matches[1][64]) && !isset($matches[0][254])
        )
    ;
}
  • 通常のローカル部
  • ローカル部のクオート方式
  • IPv4
  • 純正IPv6
  • IPv4混じりのIPv6

以上全てに対応しました。
strict = false にすると日本の古い携帯電話のアドレスも許容します。
今回は割愛。

PHPの filter_var 関数

詳 細 不 明

DevAchieve (和田さん)

DevAchieve'sFunction
function isValidEmailFormat($email, $supportPeculiarFormat = true){
    $wsp              = '[\x20\x09]'; // 半角空白と水平タブ
    $vchar            = '[\x21-\x7e]'; // ASCIIコードの ! から ~ まで
    $quoted_pair      = "\\\\(?:{$vchar}|{$wsp})"; // \ を前につけた quoted-pair 形式なら \ と " が使用できる
    $qtext            = '[\x21\x23-\x5b\x5d-\x7e]'; // $vchar から \ と " を抜いたもの。\x22 は " , \x5c は \
    $qcontent         = "(?:{$qtext}|{$quoted_pair})"; // quoted-string 形式の条件分岐
    $quoted_string    = "\"{$qcontent}+\""; // " で 囲まれた quoted-string 形式。
    $atext            = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]'; // 通常、メールアドレスに使用出来る文字
    $dot_atom         = "{$atext}+(?:[.]{$atext}+)*"; // ドットが連続しない RFC 準拠形式をループ展開で構築
    $local_part       = "(?:{$dot_atom}|{$quoted_string})"; // local-part は dot-atom 形式 または quoted-string 形式のどちらか
    // ドメイン部分の判定強化
    $alnum            = '[a-zA-Z0-9]'; // domain は先頭英数字
    $sub_domain       = "{$alnum}+(?:-{$alnum}+)*"; // hyphenated alnum をループ展開で構築
    $domain           = "(?:{$sub_domain})+(?:[.](?:{$sub_domain})+)+"; // ハイフンとドットが連続しないように $sub_domain をループ展開
    $addr_spec        = "{$local_part}[@]{$domain}"; // 合成
    // 昔の携帯電話メールアドレス用
    $dot_atom_loose   = "{$atext}+(?:[.]|{$atext})*"; // 連続したドットと @ の直前のドットを許容する
    $local_part_loose = $dot_atom_loose; // 昔の携帯電話メールアドレスで quoted-string 形式なんてあるわけない。たぶん。
    $addr_spec_loose  = "{$local_part_loose}[@]{$domain}"; // 合成
    // 昔の携帯電話メールアドレスの形式をサポートするかで使う正規表現を変える
    if($supportPeculiarFormat){
        $regexp = $addr_spec_loose;
    }else{
        $regexp = $addr_spec;
    }
    // \A は常に文字列の先頭にマッチする。\z は常に文字列の末尾にマッチする。
    if(preg_match("/\A{$regexp}\z/", $email)){
        return true;
    }else{
        return false;
    }
}

RFC2822

404 Blog Not Found 管理人の小飼さんが紹介されている有名な記事です。
http://blog.livedoor.jp/dankogai/archives/51189905.html
(Perlよく分からないので生成後のコードで勘弁してください)

RFC2822'sFunction
function rfc2822_func($input) {
    $pattern = 
        '/^(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!'.
        '#\$\%&\'*+\\/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|[^\\"])*")))\@(?:(?:(?:(?:'.
        '[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&\'*+\\/=?\^`'.
        "{}~|\-]+))*)|(?:\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\])))$/"
    ;
    return (bool)preg_match($pattern, $input);
}

phpspot

phpspot'sFunction
function phpspot_func($input) {
    $pattern = '/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/';
    return (bool)preg_match($pattern, $input);
}

検証

テストコード(ついでにベンチマークも取る)
<?php
$emails = <<<'EOD'
Abc@example.com
Abc.123@example.com
user+mailbox/department=shipping@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Abc@def"@example.com
"Fred\ Bloggs"@example.com
"Joe.\\Blow"@example.com
".dot_kara_hazimaru"@example.com
"I.likeyou."@example.com
"I..love...you"@example.com
Abc.@example.com
Abc..123@example.com
.dot_kara_hazimaru@example.com
I.like.you.@example.com
I..love...you@example.com
a@[0.0.0.0]
a@[255.255.255.255]
a@[255.255.255.256]
a@[001.002.003.004]
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]
a@[2001:0db8:bd05:01d2:288a::1fc0:0001:10ee]
a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee:11fe]
a@[2001:db8:20:3:1000:100:20:3]
a@[2001:db8::1234:0:0:9abc]
a@[2001:db8::9abc]
a@[::]
a@[0::0]
a@[::1]
a@[1::]
a@[1:2:3:4:5:6:7::]
a@[::255.255.255.255]
a@[::ffff:255.255.255.255]
a@[::ffff:0:255.255.255.255]
a@[2001:db8:3:4::192.0.2.33]
a@[64:ff9b::192.0.2.33]
a@[example.com]
a@[example.com:hoge]
a@0
a@a
a@0.a
a@0.0
a@a.0
a@.a
a@a-.a
a@-a.a
a@a-a.com
a@0-a.com
a@a-0.com
a@a-a.a-a
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.com
a@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678901.com
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/@example.com
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a@example.com
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"@example.com
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/a"@example.com
abcdefhghijklmnopqrstuvwxyzABC@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
abcdefhghijklmnopqrstuvwxyzABCD@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
"abcdefhghijklmnopqrstuvwxyzABC"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
"abcdefhghijklmnopqrstuvwxyzABCD"@aaaaaaaa01.aaaaaaaa02.aaaaaaaa03.aaaaaaaa04.aaaaaaaa05.aaaaaaaa06.aaaaaaaa07.aaaaaaaa08.aaaaaaaa09.aaaaaaaa10.aaaaaaaa11.aaaaaaaa12.aaaaaaaa13.aaaaaaaa14.aaaaaaaa15.aaaaaaaa16.aaaaaaaa17.aaaaaaaa18.aaaaaaaa19.aaaaaaaa20.com
EOD;

$emails = explode("\n", $emails);

$time1_sum = 0.0;
$time2_sum = 0.0;
$time3_sum = 0.0;
$time4_sum = 0.0;
$time5_sum = 0.0;

echo "<h1>Results</h1>\n";
echo "<table border=\"1\">\n";
echo "<tr>\n";
echo "<th>E-mail</th><th>My Function</th><th>PHP Function</th><th>DevAchieve's Function</th><th>RFC2822's Function</th><th>phpspot's Function</th>\n";
foreach ($emails as $email) {
    $ok = '<span style="color:green;">OK</span>';
    $ng = '<span style="color:red;font-weight:bold;">NG</span>';
    for ($i = 0; $i < 800; $i++) {
        $t1 = microtime(true);
        $r1 = validate_email($email) ? $ok : $ng;
        $t2 = microtime(true);
        $r2 = filter_var($email, FILTER_VALIDATE_EMAIL) !== false ? $ok : $ng;
        $t3 = microtime(true);
        $r3 = isValidEmailFormat($email, false) ? $ok : $ng;
        $t4 = microtime(true);
        $r4 = rfc2822_func($email) ? $ok : $ng;
        $t5 = microtime(true);
        $r5 = phpspot_func($email) ? $ok : $ng;
        $t6 = microtime(true);
        $time1_sum += $t2 - $t1;
        $time2_sum += $t3 - $t2;
        $time3_sum += $t4 - $t3;
        $time4_sum += $t5 - $t4;
        $time5_sum += $t6 - $t5;
    }
    $email = wordwrap($email, 40, "<br>\n", true);
    echo "<tr>\n";
    echo "<td style=\"font-family:Consolas, 'Courier New', Courier, Monaco, monospace;\">{$email}</td><td>{$r1}</td><td>{$r2}</td><td>{$r3}</td><td>{$r4}</td><td>{$r5}</td>\n";
    echo "</tr>\n";
}
echo "</table>\n";
echo "<h1>Benchmarks (Repeated 800 times)</h1>\n";
echo "<p>\n";
printf("My Function: %f sec<br>\n", $time1_sum);
printf("PHP Function: %f sec<br>\n", $time2_sum);
printf("DevArchive's Function: %f sec<br>\n", $time3_sum);
printf("RFC2822's Function: %f sec<br>\n", $time4_sum);
printf("phpspot's Function: %f sec<br>\n", $time5_sum);
echo "</p>\n";

results.jpg

  • 画像このままでは文字小さいので拡大してみてください(笑)
  • いろいろ情報収集しましたが、私の関数の結果が一番正しいと思います。
  • なんかIPv6対応させたら重くなっちゃったけどまぁ許容範囲!
  • Windows版のApacheだと(だけ?)よくfilter_var関数実行中に落ちたりしました。
    いろいろ値変えてやってみたけど原因不明でした。

そろそろ僕も正規表現ガチ勢名乗っていいですか。

148
141
22

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
148
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?