Ruby使用正则表达式进行电子邮件验证

75

我有一个很长的电子邮件列表正在进行中,其中许多电子邮件存在拼写错误。我正试图构建一个字符串来检查有效的电子邮件。

以下是我的正则表达式。

def is_a_valid_email?(email)
  (email =~ /^(([A-Za-z0-9]*\.+*_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\+)|([A-Za-z0-9]+\+))*[A-Z‌​a-z0-9]+@{1}((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,4}$/i)
end

如果电子邮件中只有下划线和一个点,则可以通过验证。我有很多电子邮件在名称本身中有多个点。如何在正则表达式中检查它。

如果电子邮件中只有下划线和一个点,则可通过验证。我有很多电子邮件的名称中含有多个点,想要使用正则表达式来检查该情况。

hello.me_1@email.com # <~~ valid
foo.bar#gmail.co.uk # <~~~ not valid
f.o.o.b.a.r@gmail.com # <~~~valid 
f...bar@gmail.com # <~~ not valid 
get_at_m.e@gmail  #<~~ valid

有人可以帮我重写正则表达式吗?


可能是 https://dev59.com/uHVC5IYBdhLWcg3wtzut?rq=1 的重复问题。 - CAustin
请参考这里来创建您的正则表达式。 - tenub
16个回答

142
这个功能至少从2.2.1版本开始已经内置在标准库中了。
URI::MailTo::EMAIL_REGEXP

*警告:上述内容认为a@b是一个有效的电子邮件地址。

19
无法处理这种情况。 'aa@aaa' =~ URI::MailTo::EMAIL_REGEXP - Benjamin
2
如果正则表达式中有一个句点,但是句点后面没有任何内容,它将返回nil。如果句点后面有任何内容或者根本没有句点,它将通过。"这个要求是RFC 5322的故意违规,该标准定义了一种电子邮件地址的语法,同时过于严格(在“@”字符之前),过于模糊(在“@”字符之后),以及过于宽松(允许注释、空白字符和引用字符串以大多数用户不熟悉的方式)在这里没有实际用途。" html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - Joshua Hunter
[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@a-zA-Z0-9?(?:.a-zA-Z0-9?)+(我删掉了尾随的?并将其替换为+,然后它按预期工作) - Darpan
谢谢@Benjamin,我需要知道如何实现Josh Hunter的回答,并不知道正确的比较电子邮件的方法。 - CWarrington
@JoshuaHunter 是的,代码是正确的。 然而,由于人们输入太多错误,似乎在现实世界中检查该代码是不足够的。 - Benjamin

126

简而言之:

感谢@joshuahunter(下面的回答),本文将其包含在内以便人们看到。

URI::MailTo::EMAIL_REGEXP

Old TL;DR

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

最初的回答

看起来你把事情复杂化了,我会简单地使用:

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

这段内容摘自迈克尔·哈特尔的Rails书
既然这不符合您的点要求,可以简单地进行修改,如下所示: "最初的回答"。
VALID_EMAIL_REGEX = /\A([\w+\-]\.?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

如CAustin所提到的那样,还有许多其他解决方案。

编辑:

@installero指出原始版本在具有连字符子域的情况下失败,这个版本将起作用(不确定为什么第一次缺少数字和连字符字符类)。

"Original Answer"翻译成中文是"最初的回答"。

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

我该如何为 email_field 添加验证?目前,它只检查 @ 的存在。我希望它也能验证 . 的存在。 - sshah
@sshah你所说的email_field是什么意思?这个正则表达式检查电子邮件是否为something_valid@somewhere.tld,(请参见正则表达式第二部分中的\.部分)。 - Mike H-R
@MikeH-R 嗯,那个正则表达式(Michael Hartl的)只对@有效。那是一个有效的电子邮件吗? - Mohamad
@Mohamad,正则表达式不应该只匹配单独的@(尽管可以争论说这样更准确地匹配电子邮件,如John Carney所述)。所有带有+的组都需要一个或多个匹配项。例如,以[\w+\-.]+开头的模式将匹配aaaaaa+b.,但不会匹配空字符串。请参见此处的演示 - Mike H-R
@installero,你是对的。请查看编辑后的版本,它应该可以工作。有趣的是,它也会对 "hello@sub.domain9.com" 失败。 - Mike H-R
显示剩余8条评论

28

这是一篇由David Celis撰写的精彩文章,解释为什么包括Mike发布的所有用于验证电子邮件地址的正则表达式都是错误的。

从文章中可以得到:

  

本地字符串(在@之前的部分)可以包含以下字符:

    `! $ & * - = ` ^ | ~ # % ' + / ? _ { }` 

你猜怎么着?如果你用引号括起来,你几乎可以使用任何字符,如"看看这些空格!"@example.com是一个有效的电子邮件地址。很好。

如果您需要进行基本检查,则最佳正则表达式只是/@/


4
大致猜测...虽然电子邮件可以包含几乎任何东西,只要它被正确引用,但实际上99.99%的电子邮件遵循相当标准的格式,许多系统将在接收到无法识别为有效的地址时出错(即使它是有效的)。如果您有这样的组件,则确保电子邮件地址合理且有效非常重要-特别是如果它是遗留系统的一部分或无法更改/更新的内容。 - Dave Smylie
3
这很公平,但如果你的电子邮件地址中有空格或美元符号,我不在乎你是否能使用我的系统。而且我怀疑你在这样做时知道自己在做什么。 - Grant Birchmeier
那么,你的解决方案就是“别费力了”吗? - undefined
我没有说过那个。再读一遍我的评论和链接的文章。 - undefined

21

这个更短且更安全:

/\A[^@\s]+@[^@\s]+\z/

在Devise gem中使用了regular表达式,但对于以下这些值存在一些漏洞:

  ".....@a....",
  "david.gilbertson@SOME+THING-ODD!!.com",
  "a.b@example,com",
  "a.b@example,co.de"

我更喜欢使用Ruby库中的正则表达式URI::MailTo::EMAIL_REGEXP

有一个用于电子邮件验证的gem

Email Validator


7
感谢您指出 URI::MailTo::EMAIL_REGEXP!这似乎是最佳方法,因为它可能比在代码库中倾泻自定义正则表达式更易于维护。 - Carsten
/\A[^@\s]+@[^@\s]+\z/.match?("r<<r@r.r.com") returns true - 7urkm3n

15

现在,Ruby的标准库提供了电子邮件验证正则表达式。您可以在URI::MailTo模块中找到它,它是URI::MailTo::EMAIL_REGEXP

在Ruby 2.4.1中,它评估为

/\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/

但我会直接使用常量本身。


这是3年前写的,如果我没记错的话,当时我还在使用Ruby1.9。可能这就是我不知道它的原因吧?不过还是谢谢你的一行代码。 - T0ny lombardi
是的,但三年后人们仍然用他们自己的正则表达式回答。无论如何,我并不打算攻击你或任何其他人。我已经相应地改变了我的回答语气。 - kaikuchn
感谢您。在测试正则表达式一小时后,我才找到了这个。 - gl03

7
接受的答案建议使用URI :: MailTo :: EMAIL_REGEXP

然而,这个正则表达式认为1234@1234是有效的电子邮件地址,在实际应用程序中可能不希望出现这种情况(例如,如果您尝试向此类地址发送电子邮件,AWS SES将引发异常)。

正如Darpan在评论中指出的那样,您只需将该正则表达式中的尾随?更改为+,它就会按预期工作。 得到的正则表达式是:

/\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+\z/

自从原始的URI::MailTo正则表达式,虽然在规范上技术上是有效的,但在我们的需求上毫无用处,因此我们在Devise初始化程序中“修复”它。
# in config/initializers/devise.rb, put this at the beginning of the file
URI::MailTo.send(:remove_const, :EMAIL_REGEXP)
URI::MailTo.const_set(:EMAIL_REGEXP, /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+\z/)

# And then find `config.email_regexp` (it will already be there in the file) and change it to:
config.email_regexp = URI::MailTo::EMAIL_REGEXP

如果你想知道为什么这个猴子补丁没有放在单独的初始化文件中,那么你必须将初始化文件命名为00_xxx.rb,以使其在设备初始化程序之前加载。这违反了Rails文档的建议,实际上建议你在这种情况下使用单个初始化程序:

如果一个初始化程序有依赖于另一个初始化程序的代码,可以将它们合并为一个初始化程序。这使得依赖关系更加明确,并且可以帮助揭示应用程序中的新概念。Rails还支持初始化文件名称的编号,但这可能会导致文件名称的变化。


1
这是最好的答案。我进一步简化了设置Devise email_regexp到更新的正则表达式直接:config.email_regexp = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_{|}~-]+@a-zA-Z0-9?(?:.a-zA-Z0-9?)+\z/` - Kobius
我现在遇到了电子邮件"info@кириллическийдомен.рф",它可能是有效的,但与此常量不匹配。 - Nakilon

5

简而言之

互联网上的任何自定义正则表达式,包括URI::MailTo::EMAIL_REGEXP,都是错误的。

这里是你应该使用的内容:

# The closest thing to RFC_5322
RFC_5322 = /\A(?:[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])\z/i

# Lighter more practical version RFC_5322 that will be more useful in real life
RFC_5322_light = /\A[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/i

# Same as the light version but with length limit enforcing
RFC_5322_with_length = /\A(?=[a-z0-9@.!#$%&'*+\/=?^_‘{|}~-]{6,254}\z)(?=[a-z0-9.!#$%&'*+\/=?^_‘{|}~-]{1,64}@)[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_‘{|}~-]+)*@(?:(?=[a-z0-9-]{1,63}\.)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?=[a-z0-9-]{1,63}\z)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/i

细节

最后一个定义电子邮件地址格式的RFC是RFC5322 - 互联网消息格式

您可以查看3.4.1. Addr-Spec规范。如果我们只看第一部分,@将本地部分(左侧)和域名(右侧)分开。

addr-spec = local-part "@" domain

local-part = dot-atom / quoted-string / obs-local-part

例如,本地部分可以包含在此处定义的点原子引用字符串中:

这有点复杂,但是您的电子邮件地址可能包含许多ASCII特殊字符,这些字符被许多正则表达式(如#$&等)排除在外。

另一方面,URI::MailTo::EMAIL_REGEXP在{{link3:ruby/lib/uri/mailto.rb}}中定义为以下正则表达式:

EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/

这个正则表达式上方的注释表明他们遵循了 https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address 的建议。
但是 WHATWG 规范添加了以下非常重要的注释:
这个需求是故意违反 RFC 5322 的规定,它定义了一种电子邮件地址的语法,同时太严格(在“@”字符之前),太模糊(在“@”字符之后),并且过于松散(允许以大多数用户不熟悉的方式使用注释、空白字符和带引号的字符串),因此在此处没有实际用途。
所以,WHATWG 告诉我们他们没有遵守标准化电子邮件地址格式的 RFC。他们说 RFC 5322 中的域部分太模糊了,但 RFC 5322 给出了这个注释,告诉我们必须检查其他 RFC 来获取更完整的域格式规范:
注意:此处提供了地址规范的自由语法。然而,域部分包含由其他协议(例如[RFC1034]、[RFC1035]、[RFC1123]、[RFC5321])指定和使用的寻址信息。因此,实现必须符合它们所用上下文中地址的语法。
WHATWG 还告诉我们,RFC 5322 中的本地部分过于严格。但是,请看遵循 WHATWG 规范的 URI::MailTo::EMAIL_REGEXP:
URI::MailTo::EMAIL_REGEXP.match?('.@toto.fr') # => true
URI::MailTo::EMAIL_REGEXP.match?('-@z') # => true
URI::MailTo::EMAIL_REGEXP.match?('++++++++.........@z') # => true

相反,WHATWG规范(以及URI::MailTo::EMAIL_REGEXP)过于宽松。

因此,我在https://emailregex.com/找到了一个通用电子邮件正则表达式(RFC 5322官方标准)(请参见summary)。

解释和替代方案可以在https://www.regular-expressions.info/email.html中找到。

# Blind RFC 5322
\A(?:[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])\z

# RFC 5322, practical version (omit IP addresses, domain-specific addresses, the syntax using double quotes and square brackets)
\A[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z

# RFC 5322, practical version (similar as previous + length limits enfocing)
\A(?=[a-z0-9@.!#$%&'*+/=?^_‘{|}~-]{6,254}\z)(?=[a-z0-9.!#$%&'*+/=?^_‘{|}~-]{1,64}@)[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*@(?:(?=[a-z0-9-]{1,63}\.)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?=[a-z0-9-]{1,63}\z)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z

正如您在下面的截图中所看到的,WHATWG / URI::MailTo::EMAIL_REGEXP 接受的地址都是无效的。

invalid email addresses

让我们在本地做同样的事情:

RFC_5322 = /\A(?:[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])\z/i

现在我们可以比较两者(在 Ruby 3.2.0 上):

# WHATWG
## Invalid cases
URI::MailTo::EMAIL_REGEXP.match?('.@toto.fr') # => true
URI::MailTo::EMAIL_REGEXP.match?('-@z') # => true
URI::MailTo::EMAIL_REGEXP.match?('++++++++.........@z') # => true
URI::MailTo::EMAIL_REGEXP.match?('invalíd@mail.com') # => false
URI::MailTo::EMAIL_REGEXP.match?('invalid%$£"@domain.com') # => false
URI::MailTo::EMAIL_REGEXP.match?('invalid£@domain.com') # => false
URI::MailTo::EMAIL_REGEXP.match?('invali"d@domain.com') # => false
URI::MailTo::EMAIL_REGEXP.match?('.dot..dot.@example.org') # => true
URI::MailTo::EMAIL_REGEXP.match?('!#$%’*+-/=?^_`{|}~@example.org') # => false
## Valid cases
URI::MailTo::EMAIL_REGEXP.match?('sometest@gmail.com') # => true
URI::MailTo::EMAIL_REGEXP.match?('some+test@gmail.com') # => true
URI::MailTo::EMAIL_REGEXP.match?('stuart.sillitoe@prodirectsport.net') # => true
URI::MailTo::EMAIL_REGEXP.match?('_valid@mail.com') # => true
URI::MailTo::EMAIL_REGEXP.match?('valid%$@domain.com') # => true
URI::MailTo::EMAIL_REGEXP.match?('"valid"@domain.com') # crash with error NameError

# RFC 5322
## Invalid cases
RFC_5322.match?('.@toto.fr') # => false
RFC_5322.match?('-@z') # => false
RFC_5322.match?('++++++++.........@z') # => false
RFC_5322.match?('invalíd@mail.com') # => false
RFC_5322.match?('invalid%$£"@domain.com') # => false
RFC_5322.match?('invalid£@domain.com') # => false
RFC_5322.match?('invali"d@domain.com') # => false
RFC_5322.match?('.dot..dot.@example.org') # => false
RFC_5322.match?('!#$%’*+-/=?^_`{|}~@example.org') # => false
## Valid cases
RFC_5322.match?('sometest@gmail.com') # => true
RFC_5322.match?('some+test@gmail.com') # => true
RFC_5322.match?('stuart.sillitoe@prodirectsport.net') # => true
RFC_5322.match?('_valid@mail.com') # => true
RFC_5322.match?('valid%$@domain.com') # => true
RFC_5322.match?('"valid"@domain.com') # => true

# RFC 5322 light (same results with RFC_5322_with_length)
## Invalid cases
RFC_5322_light.match?('.@toto.fr') # => false
RFC_5322_light.match?('-@z') # => false
RFC_5322_light.match?('++++++++.........@z') # => false
RFC_5322_light.match?('invalíd@mail.com') # => false
RFC_5322_light.match?('invalid%$£"@domain.com') # => false
RFC_5322_light.match?('invalid£@domain.com') # => false
RFC_5322_light.match?('invali"d@domain.com') # => false
RFC_5322_light.match?('.dot..dot.@example.org') # => false
RFC_5322_light.match?('!#$%’*+-/=?^_`{|}~@example.org') # => false
## Valid cases
RFC_5322_light.match?('sometest@gmail.com') # => true
RFC_5322_light.match?('some+test@gmail.com') # => true
RFC_5322_light.match?('stuart.sillitoe@prodirectsport.net') # => true
RFC_5322_light.match?('_valid@mail.com') # => true
RFC_5322_light.match?('valid%$@domain.com') # => true
RFC_5322_light.match?('"valid"@domain.com') # => false (difference with "pure" version)

警告,此测试尚未完成,且未涵盖所有情况。


5

我觉得书中的例子可以改进,以匹配带有-的子域名的电子邮件。

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

例如:

> 'some@email.with-subdomain.com' =~ VALID_EMAIL_REGEX
=> 0

1
啊,直到现在我才看到你的回答,这就是我添加到我的答案中的内容。 - Mike H-R

2
你的问题确实很复杂。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

上述代码应该足够。
为了澄清,上述表达式中的每个部分的解释如下:
正则表达式的开头:
/

匹配字符串的开头:

\A

至少一个字母、加号、连字符或者点:

[\w+\-.]+

一个字面上的“at符号”:
@

一个字面上的点:
\.

至少一个字母:

[a-z]+

匹配字符串的结尾:

\z

正则表达式结束:
/

不区分大小写:

i

重新组装:
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

请查看Rubular,以便在编写表达式时方便地测试它们。


不错,但应禁止非拉丁字符。例如,ProtonMail 不允许我创建这个电子邮件:"helloworld\u20131234@protonmail.com"(helloworld–1234@protonmail.com)。但是你的正则表达式将其验证为有效电子邮件。 - 15 Volts

2
如果您正在使用Devise,您也可以通过以下方式使用它们包含的正则表达式:
Devise.email_regexp

返回:

/\A[^@\s]+@[^@\s]+\z/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接