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

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

この記事の情報は古いので，最新の情報が欲しい方は [「PHPで各種バリデーション」](http://qiita.com/mpyw/items/346f1789ad0e1b969ebc) をお読みください。

**訂正: IPv6のメールアドレスは `IPv6:` プレフィクスが必要です。PHP7.1時点でこの形式に対応していることを確認しました。**
- 誤: `a@[2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]`
- 正: `a@[IPv6:2001:0db8:bd05:01d2:288a:1fc0:0001:10ee]`

関数ラインナップ
============

私の関数
-----------

```php: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

```php: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よく分からないので生成後のコードで勘弁してください)

```php: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

```php: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:テストコード(ついでにベンチマークも取る)
<?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](https://qiita-image-store.s3.amazonaws.com/0/25060/51c24873-acc5-f8f5-16d2-e5c61fa6499c.jpeg)

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

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