Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事の情報は古いので,最新の情報が欲しい方は 「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 (和田さん)

http://wada811.blogspot.com/2013/03/best-email-format-check-regex-in-php.html

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

http://phpspot.net/php/pg%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE%EF%BC%9A%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E8%AA%BF%E3%81%B9%E3%82%8B.html

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関数実行中に落ちたりしました。 いろいろ値変えてやってみたけど原因不明でした。

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

mpyw
古い記事はそのまま参考にしないようにご注意ください
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away