作者
Patrik Nyblom <pan(at)erlang(dot)org>
状态
已接受/R12B-3u 提案已在 OTP R12B-3 版本中实现,除了根据 EEP 10 的 Unicode 支持
类型
标准跟踪
创建
2008-06-04
Erlang 版本
OTP_R12B-5
发布历史
1970-01-01

EEP 11:Erlang 中的内置正则表达式 #

摘要 #

此 EEP 建议如何将外部正则表达式库集成到 Erlang 虚拟机中。

动机 #

正则表达式被广泛使用。无论如何使用语言的其他功能来匹配或挑出字符串的各个部分,许多程序员都喜欢正则表达式语法,并希望在现代语言中使用正则表达式。

Perl 编程语言已将正则表达式直接集成到语法中,Perl 程序员通常非常擅长编写复杂的正则表达式,例如解析文本文件、HTTP 请求或简单的用户输入。Perl 对通用正则表达式的扩展广为人知,许多现代编程语言都支持类似的功能。

Erlang 目前有一个最简化的正则表达式模块(STDLIB 中的 regexp 模块),它缺少其他实现中常用的功能。与在其他语言中使用的原生 C 库相比,当前的库也慢得令人痛苦。

Erlang 需要以不破坏虚拟机属性的方式与现代正则表达式库进行交互。

理由 #

前提条件 #

已经尝试过完全用 Erlang 编写更高效的正则表达式库,但到目前为止,还没有提出真正有效的实现,并且创建该实现所涉及的工作被认为是大量的。

另一方面,已经提出了几种或多或少成功的将外部正则表达式库集成到虚拟机中的尝试。然而,它们都没有解决长时间运行的正则表达式使调度程序停顿的问题。

Erlang VM 中的内置函数需要在运行一定数量的迭代后停止执行,以避免调度程序停顿,从而使系统中的其他进程陷入饥饿状态。当 Erlang 进程再次被调度时,内置函数会重新启动,并提供某种存储其当前状态的方法,以便该函数的执行可以在上次离开的位置继续。

正则表达式匹配的执行在许多方面类似于虚拟机执行普通 beam 代码,但是可用的库(由于显而易见的原因)没有准备好暂时放弃执行以允许其他进程执行。对大量主题数据的复杂正则表达式可能需要数秒甚至数分钟才能执行。在真正的并行系统中,让 VM 中的一个调度程序停顿这么长的时间是不可行的。由于建议的外部库接口从未解决这个问题,因此没有一个被接受和/或集成到 Erlang 的主要发行版中。

堆栈使用是另一个很少被提及的问题。Erlang 虚拟机可以运行许多调度程序线程,尤其是在具有大量核心的处理器上。多线程应用程序需要注意堆栈的使用,因此最好避免递归 C 例程。Erlang 虚拟机避免在 C 代码中使用递归,因此链接的库也应该这样做。当涉及到实时操作系统时,避免在 C 代码中使用递归的必要性更加明显。用于 Erlang 正则表达式的库绝对不能在 C 堆栈上递归,至少不能以无法在编译时确定堆栈使用的方式进行递归。

多线程与可中断执行 #

当应该调度另一个 Erlang 进程时,中断正则表达式(或其他长时间操作)执行的问题有两种明显的解决方案

  1. 计算正则表达式匹配中的迭代次数,在一定数量的迭代次数(或一定时间)后存储状态,并在超出执行时间段时将控制权返回给调度程序。

  2. 让操作系统通过在单独的内核线程中执行正则表达式匹配来解决问题。

在虚拟机的的文件驱动程序中,使用了第二种方法,引入了异步线程池的概念。然而,文件 I/O 的情况很特殊,因为 I/O 操作本身通常比使用异步线程时涉及的线程间通信和任务切换的运行时间要长得多。此外,I/O 根本没有其他解决方案,因此 OS 线程是这种情况下的唯一解决方案。

如果正则表达式要在单独的线程中执行,即使是非常小而简单的表达式也必须承受操作系统级别任务切换和通信的额外负担。

虚拟机中的其他长时间操作使用第一种方法,即自愿中断和重新调度。在涉及外部库的情况下,例如 IP 通信,模拟器通过提供 I/O 多路复用(select/poll)的接口来提供被动等待事件的方式。这是在大多数驱动程序中避免阻塞调度程序的方法。异步线程仅在根本没有其他选择的情况下使用,例如在文件 I/O 中(不能使用 I/O 多路复用)。

在驱动程序或 BIF 中接口外部库时使用第一种解决方案,需要找到一个可以中断和重新启动执行的库,或者修改现有库以支持此功能。

尽管修改库会使库的升级和修补更加困难,但好处是显着的。当执行例如正则表达式时,将使用实际执行 beam 代码的同一个线程,因此通常将设置时间和开销保持在最低限度。当然,regexp 本身的执行时间会稍微长一些,因为实现需要跟踪已执行的迭代次数,并且需要准备好存储当前状态以供稍后执行唤醒。但是,当涉及到较小的正则表达式时(或者更确切地说,涉及到少量循环的表达式时),较小的设置时间预计将占主导地位。人们还必须记住,此解决方案对操作系统调度程序施加的负载要小得多,这对大型和/或嵌入式系统来说是一件好事。

对于没有内核线程可用的操作系统,第一种解决方案是唯一可接受的。纯用户空间代码执行的单独线程会对 Erlang 系统的实时属性造成弊大于利。

选择适合集成的库 #

理想情况下,要集成到虚拟机中的库应满足以下要求

  • 可中断,正则表达式匹配的执行应在一定数量的迭代后停止,并且应在稍后时间可重新启动。
  • 该库应以纯 C 语言实现,而不是任何其他语言或扩展。
  • C 实现应该是非递归的。
  • 该库应实现现代(类似 Perl)的正则表达式语法。
  • 该库应高效。
  • 该库应提供 Unicode 支持。

目前没有可用的正则表达式库能提供完美匹配。最好的可用库是 PCRE 库,它具有不使用 C 堆栈的编译时选项,与 Perl(和 Python)兼容的正则表达式,并且还以结构良好的方式编写,使其适合集成、移植和实现在 Erlang 案例中所需的扩展。

其他替代方案包括 rxlib(不再维护)、Tcl/Tk 正则表达式实现、GNU regex、Jakarta 和 Onigurama 等。其中,Tcl/Tk 实现看起来最有希望,尤其是因为它在许多情况下比其他实现快得多。然而,算法和代码非常难以理解,正则表达式风格也不是最流行的。

在仔细研究了替代方案之后,我得出的结论是,PCRE 是最佳选择,原因如下

  • 代码是维护的,非常可读且易于使用。
  • 该库速度很快,尽管不是最快的。
  • 广泛的测试套件。
  • 与 Perl 兼容的语法。
  • 广泛传播:在 Apache、PHP、Apple Safari 等中使用。
  • regexp 引擎是纯 C 代码。
  • Unicode 支持 (UTF-8),它非常适合 Erlang 中建议的 Unicode 表示 (EEP 10)。
  • 可以避免 C 堆栈上的递归。
  • 该库具有大多数用于表达式可中断执行的基础结构,尽管尚未(尚未)实现中断匹配的重新启动。

尽管关于代码可读性的主观推理可能看起来有些不合时宜,但 PCRE 代码库使得库的更新更容易集成,因为只需对库进行相对较少且易于理解的更改即可使其适合虚拟机。能够维护库很重要,并且能够理解代码至关重要。

该库最吸引人的功能是广泛支持与 Perl 兼容的正则表达式。PCRE 无疑是最强大的库之一,习惯于 Perl 正则表达式的 Erlang 程序员会感到宾至如归。

程序员接口 #

在 Perl 中,正则表达式集成到语言本身中。当然,这也可以在 Erlang 中完成。但是,Erlang 已经有用于匹配结构化数据以及二进制数据的语法。引入用于使用正则表达式进行字符串匹配的新原语似乎不合适。Erlang 也不是一种像 Perl 那样设计用于处理文本数据的语言,而是一种可以处理复杂的结构化数据的语言。然而,位语法可能有一天会受益于正则表达式扩展,但这超出了此 EEP 的范围。

在 Erlang 中,通过内置函数与库接口进行交互的正则表达式模块是常用的方法,这也是本 EEP 建议的方式。由于模块名 regexp 已经被占用,因此选择缩写 “re” 作为模块名似乎是个不错的选择。

作为基本实现,我建议该模块包含两个基本函数:一个用于将正则表达式预编译为用于正则表达式匹配执行的“字节码”;另一个用于实际运行正则表达式匹配。运行匹配的函数应该接受已编译的正则表达式或正则表达式的源代码作为输入(以及目标字符串和执行选项)。

围绕这两个建议的函数,可以在 Erlang 中实现功能,以模拟现有的正则表达式库或实现新功能。

当前的 regexp 模块除了匹配之外,还可以根据正则表达式拆分字符串(功能类似于 Perl 内置函数 split)并基于正则表达式匹配执行子字符串的替换(类似于 Perl 或 awk 中的 s///表达式)。通过“re”模块中的相应函数,新模块将提供旧模块的所有功能。

函数名称应尽可能选择避免与当前的 regexp 库函数混淆,因此我建议使用 “compile”、“run” 和 “replace” 作为正则表达式编译、执行和替换的名称。由于没有出现“split” 的好的同义词,因此在新模块中保留该名称。

以下是建议的手册页的一部分

建议的手册页摘录 #

数据类型 #

iodata() = iolist() | binary()
iolist() = [char() | binary() | iolist()]
           % a binary is allowed as the tail of the list

mp() = Opaque datatype containing a compiled regular expression.

导出 #

compile(Regexp) -> {ok, MP} | {error, ErrSpec} #

类型

Regexp = iodata()

与 compile(Regexp,[]) 相同

compile(Regexp,Options) -> {ok, MP} | {error, ErrSpec} #

类型

Regexp = iodata()
Options = [ Option ]
Option = anchored | caseless | dollar_endonly | dotall | extended |
         firstline | multiline | no_auto_capture | dupnames |
         ungreedy | {newline, NLSpec}
NLSpec = cr | crlf | lf | anycrlf
MP = mp()
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

此函数使用以下描述的语法将正则表达式编译为内部格式,以便稍后用作 run/2,3 函数的参数。

如果在程序的生命周期中,相同的表达式将用于匹配多个目标,则在匹配之前编译正则表达式会很有用。编译一次并执行多次比每次想要匹配时都编译要高效得多。

选项具有以下含义

  • anchored
    该模式被强制为“锚定”,也就是说,它被约束为仅在被搜索字符串(“目标字符串”)中的第一个匹配点进行匹配。此效果也可以通过模式本身中的适当构造来实现。

  • caseless
    模式中的字母匹配大小写字母。它等效于 Perl 的 /i 选项,并且可以通过 (?i) 选项设置在模式内进行更改。大写字母和小写字母的定义与 ISO-8859-1 字符集中相同。

  • dollar_endonly
    模式中的美元元字符仅在目标字符串的末尾匹配。如果没有此选项,则美元符号也会在字符串末尾的换行符之前立即匹配(但不会在任何其他换行符之前匹配)。如果给定了 multiline,则忽略 dollar_endonly 选项。Perl 中没有等效的选项,也没有办法在模式内设置它。

  • dotall
    模式中的点号匹配所有字符,包括表示换行符的字符。如果没有此选项,则当当前位置位于换行符时,点号不会匹配。此选项等效于 Perl 的 /s 选项,并且可以通过 (?s) 选项设置在模式内进行更改。诸如 [^a] 之类的否定类始终匹配换行符,而与此选项的设置无关。

  • extended
    模式中的空白数据字符将被忽略,除非它们被转义或位于字符类中。空白字符不包括 VT 字符(ASCII 11)。此外,字符类之外的未转义 # 和下一个换行符(包括换行符)之间的字符也将被忽略。这等效于 Perl 的 /x 选项,并且可以通过 (?x) 选项设置在模式内进行更改。此选项可以使在复杂的模式中包含注释成为可能。但是请注意,这仅适用于数据字符。空白字符永远不会出现在模式中的特殊字符序列中,例如在引入条件子模式的序列 (?( 中。

  • firstline
    未锚定的模式必须在目标字符串中的第一个换行符之前或在第一个换行符处匹配,尽管匹配的文本可能会继续跨越换行符。

  • multiline
    默认情况下,PCRE 将目标字符串视为由一行字符组成(即使它实际上包含换行符)。“行首”元字符 (^) 仅在字符串的开头匹配,而“行尾”元字符 ($) 仅在字符串的末尾匹配,或在终止换行符之前匹配(除非给定了 dollar_endonly)。这与 Perl 相同。

    当给定 multiline 时,“行首”和“行尾”构造分别在目标字符串中的内部换行符之后或之前立即匹配,以及在最开始和结尾处匹配。这等效于 Perl 的 /m 选项,并且可以通过 (?m) 选项设置在模式内进行更改。如果目标字符串中没有换行符,或者模式中没有 ^$ 的出现,则设置 multiline 无效。

  • no_auto_capture
    禁用模式中编号的捕获括号的使用。任何没有跟 ? 的开括号的行为就像它后面跟了 ?:,但命名的括号仍然可以用于捕获(并且它们以通常的方式获取编号)。Perl 中没有与此选项等效的选项。

  • dupnames
    用于标识捕获子模式的名称不必唯一。当已知命名的子模式的实例只有一个可以被匹配时,这对于某些类型的模式很有帮助。下面有关于命名子模式的更多详细信息。

  • ungreedy
    此选项反转量词的“贪婪性”,使它们默认情况下不贪婪,但如果后面跟 ? 则变为贪婪。它与 Perl 不兼容。它也可以通过模式内的 (?U) 选项设置进行设置。

  • {newline, NLSpec}
    覆盖目标字符串中换行符的默认定义,在 Erlang 中为 LF (ASCII 10)。

    • cr
      换行符由单个字符 CR (ASCII 13) 表示
    • lf
      换行符由单个字符 LF (ASCII 10) 表示,默认值
    • crlf
      换行符由双字符 CRLF (ASCII 13 后跟 ASCII 10) 序列表示。
    • anycrlf
      应识别上述三个序列中的任何一个。
run(Subject,RE) -> {match, Captured} | nomatch | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
Captured = [ CaptureData ]
CaptureData = {int(),int()} | string() | binary()
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

与 run(Subject,RE,[]) 相同。

run(Subject,RE) -> {match, Captured} | match | nomatch | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
Options = [ Option ]
Option = anchored | global | notbol | noteol | notempty | {offset, int()} |
         {newline, NLSpec} | {capture, ValueSpec} |
         {capture, ValueSpec, Type} | CompileOpt
Type = index | list | binary
ValueSpec = all | all_but_first | first | ValueList
ValueList = [ ValueID ]
ValueID = int() | string() | atom()
CompileOpt = see compile/2 above
NLSpec = cr | crlf | lf | anycrlf
Captured = [ CaptureData ] | [ [ CaptureData ] ... ]
CaptureData = {int(),int()} | string() | binary()
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

执行正则表达式匹配,返回 match / {match, Captured}nomatch。正则表达式可以作为 iodata() 给出,在这种情况下,它会被自动编译(如 re:compile/2)并执行,或者作为预编译的 mp() 给出,在这种情况下,它会直接针对目标执行。

当涉及编译时,该函数可能会返回与单独编译时相同的编译错误 ({error, {string(),int()}});当仅匹配时,不会返回错误。

如果正则表达式是先前编译的,则选项列表只能包含选项 anchoredglobalnotbolnoteolnotempty{offset, int()}{newline, NLSpec}{capture, ValueSpec} / {capture, ValueSpec, Type}。否则,re:compile/2 函数的所有有效选项也都是允许的。匹配的编译和执行都允许的选项,即 anchored{newline, NLSpec},如果与非预编译的正则表达式一起出现,将同时影响编译和执行。

{capture, ValueSpec} / {capture, ValueSpec, Type} 定义了在成功匹配后从函数返回的内容。捕获元组可以包含一个值规范,指示要返回哪些捕获的子字符串,以及一个类型规范,指示如何返回捕获的子字符串(作为索引元组、列表或二进制)。capture 选项使该函数非常灵活和强大。不同的选项将在下面详细描述

如果捕获选项描述根本不进行子字符串捕获 ({capture, none}),则该函数将在成功匹配时返回单个原子 match,否则返回元组 {match, ValueList}。可以通过指定 none 或空列表作为 ValueSpec 来禁用捕获。

下面是所有与执行相关的选项的描述

  • anchored
    re:run/3 限制为在第一个匹配位置匹配。如果模式是用 anchored 编译的,或者由于其内容而被锚定,则无法在匹配时使其取消锚定,因此没有 unanchored 选项。

  • global
    实现全局(重复)搜索,如 Perl 中的 /g 标志。每个找到的匹配项都作为单独的 list() 返回,其中包含特定匹配项以及任何匹配的子表达式(或由 capture 选项指定)。因此,当给定此选项时,返回值的 Captured 部分将是 list() 的 list()。

    当正则表达式匹配空字符串时,该行为可能看起来不直观,因此该行为需要一些澄清。使用 global 选项,re:run/3 以与 Perl 相同的方式处理空匹配,这意味着在任何点匹配给出空字符串(长度为 0)都将使用选项 [anchored, notempty] 再次尝试。如果该搜索给出的结果长度 > 0,则该结果将包含在内。一个例子

      re:run("cat","(|at)",[global]).
    

    匹配将按以下方式执行

    • 偏移量 0
      正则表达式 (|at) 将首先在字符串 cat 的初始位置匹配,给出结果集 [{0,0},{0,0}] (第二个 {0,0} 是由于括号标记的子表达式)。由于匹配的长度为 0,因此我们暂时不前进到下一个位置。

    • 偏移量 0,使用 [anchored, notempty]
      使用选项 [anchored, notempty] 在同一位置重新尝试搜索,这不会给出任何更长长度的有意义的结果,因此搜索位置现在前进到下一个字符 (a)。

    • 偏移量 1
      现在搜索的结果是 [{1,0}, {1,0}],这意味着此搜索也将使用额外选项重复。

    • 偏移量 1,使用 [anchored, notempty]
      现在找到了 ab 替代方案,结果将是 [{1,2}, {1,2}]。结果将添加到结果列表中,搜索字符串中的位置将前进两步。

    • 偏移量 3
      现在搜索再次匹配空字符串,给出 [{3,0}, {3,0}]

    • 偏移量 1,使用 `[anchored, notempty]
      这将不会给出长度 > 0 的结果,并且我们处于最后一个位置,因此全局搜索完成。

    调用的结果是

    {match,[[{0,0},{0,0}],[{1,0},{1,0}],[{1,2},{1,2}],[{3,0},{3,0}]]}

  • notempty
    如果给定此选项,则空字符串不被视为有效匹配项。如果模式中存在替代项,则会尝试这些替代项。如果所有替代项都匹配空字符串,则整个匹配将失败。例如,如果模式

      a?b?
    

    应用于不以“a”或“b”开头的字符串,它将匹配主题开头的空字符串。如果指定 notempty,则此匹配无效,因此 re:run/3 会在字符串中进一步搜索 “a” 或 “b” 的出现。

    Perl 没有直接等效于 notempty 的选项,但是它在其 split() 函数中以及使用 /g 修饰符时,对空字符串的模式匹配进行了特殊处理。可以在匹配空字符串后模拟 Perl 的行为,方法是首先使用 notempty 和 anchored 在同一偏移量再次尝试匹配,如果失败,则通过前进起始偏移量(见下文)并再次尝试普通匹配。

  • notbol
    此选项指定主题字符串的第一个字符不是一行的开头,因此脱字符元不应在其之前匹配。在没有 multiline(在编译时)的情况下设置此项会导致脱字符永不匹配。此选项仅影响脱字符元的行为。它不影响 \A

  • noteol
    此选项指定主题字符串的结尾不是一行的结尾,因此美元符号元字符不应匹配它,也不应匹配(除非在多行模式下)它之前的换行符。在没有多行(在编译时)的情况下设置此项会导致美元符号永不匹配。此选项仅影响美元符号元字符的行为。它不影响 \Z\z

  • {offset , int()}
    在主题字符串中给定的偏移量(位置)处开始匹配。偏移量是基于零的,因此默认值为 {offset,0} (整个主题字符串)。

  • {newline, NLSpec}
    覆盖目标字符串中换行符的默认定义,在 Erlang 中为 LF (ASCII 10)。

    • cr
      换行符由单个字符 CR (ASCII 13) 表示。
    • lf
      换行符由单个字符 LF (ASCII 10) 表示,这是默认值。
    • crlf
      换行符由双字符 CRLF (ASCII 13 后跟 ASCII 10) 序列表示。
    • anycrlf
      应识别以上三个序列中的任何一个
  • {capture, ValueSpec} / {capture, ValueSpec, Type}
    指定返回哪些捕获的子字符串以及以何种格式返回。默认情况下,re:run/3 捕获子字符串的整个匹配部分以及所有捕获子模式(自动捕获所有模式)。默认返回类型是字符串的捕获部分的(基于零的)索引,以 {Offset,Length} 对的形式给出(捕获的索引类型)。

    作为默认行为的一个示例,以下调用

      re:run("ABCabcdABC","abcd",[]).
    

    返回,作为第一个也是唯一捕获的字符串,主题的匹配部分(中间的“abcd”)作为索引对 {3,4},其中字符位置是基于零的,就像在偏移量中一样。上面调用的返回值将是

      {match,[{3,4}]}
    

    另一个(非常常见的)情况是正则表达式匹配整个主题,如下所示

      re:run("ABCabcdABC",".*abcd.*",[]).
    

    其中返回值将相应地指出整个字符串,从索引 0 开始,长度为 10 个字符

      {match,[{0,10}]}
    

    如果正则表达式包含捕获子模式,如下例所示

      re:run("ABCabcdABC",".*(abcd).*",[]).
    

    将捕获所有匹配的主题,以及捕获的子字符串

      {match,[{0,10},{3,4}]}
    

    完整的匹配模式始终给出列表中的第一个返回值,其余的子模式按照它们在正则表达式中出现的顺序添加。

    捕获元组的构建方式如下

    • ValueSpec
      指定要返回哪些捕获的(子)模式。ValueSpec 可以是描述预定义返回值的原子,也可以是包含要返回的特定子模式的索引或名称的列表。

      子模式的预定义集为

      • all
        所有捕获的子模式,包括完整的匹配字符串。这是默认值。

      • first
        仅第一个捕获的子模式,它始终是主题的完整匹配部分。所有显式捕获的子模式都将被丢弃。

      • all_but_first
        除第一个匹配的子模式以外的所有子模式,即所有显式捕获的子模式,但不包括主题字符串的完整匹配部分。如果正则表达式整体匹配主题的很大一部分,但您感兴趣的部分在显式捕获的子模式中,这将很有用。如果返回类型为 list 或 binary,则不返回您不感兴趣的子模式是优化的一种好方法。

      • none
        根本不返回匹配的子模式,在成功匹配时,生成单个原子匹配作为函数的返回值,而不是 {match, list()} 返回值。指定一个空列表会产生相同的行为。

      值列表是要返回的子模式的索引列表,其中索引 0 表示所有模式,1 表示正则表达式中第一个显式捕获的子模式,依此类推。当在正则表达式中使用命名捕获的子模式(见下文)时,可以使用 atom()string() 来指定要返回的子模式。这需要一个示例,考虑以下正则表达式

        ".*(abcd).*"
      

      与字符串 "ABCabcdABC" 匹配,仅捕获 "abcd" 部分(第一个显式子模式)

        re:run("ABCabcdABC",".*(abcd).*",[{capture,[1]}]).
      

      调用将产生以下结果

        {match,[{3,4}]}
      

      因为第一个显式捕获的子模式是 "(abcd)",与主题中的 "abcd" 匹配,在(基于零的)位置 3,长度为 4。

      现在考虑相同的正则表达式,但子模式显式命名为 'FOO'

        ".*(?<FOO>abcd).*"
      

      使用此表达式,我们仍然可以使用以下调用给出子模式的索引

        re:run("ABCabcdABC",".*(?<FOO>abcd).*",[{capture,[1]}]).
      

      产生与之前相同的结果。但是,由于子模式已命名,我们也可以在值列表中给出其名称

        re:run("ABCabcdABC",".*(?<FOO>abcd).*",[{capture,['FOO']}]).
      

      这将产生与早期示例相同的结果,即

        {match,[{3,4}]}
      

      值列表可能会指定正则表达式中不存在的索引或名称,在这种情况下,返回值会因类型而异。如果类型是 index,则为正则表达式中没有相应子模式的值返回元组 {-1,0},但是对于其他类型(二进制和列表),值分别为空二进制或列表。

    • 类型
      可选地指定如何返回捕获的子字符串。如果省略,则使用默认的 index。Type 可以是以下之一

      • index
        将捕获的子字符串作为字节索引对返回到主题字符串中,以及主题中匹配字符串的长度(就像在匹配之前使用 iolist_to_binary 将主题字符串展平一样)。这是默认值。

      • list
        将匹配的子字符串作为字符列表(Erlang string())返回。

      • binary
        将匹配的子字符串作为二进制文件返回。

    通常,在匹配中未分配值的子模式在类型为 index 时会作为元组 {-1,0} 返回。对于其他返回类型,未分配的子模式分别返回为空二进制或列表。考虑正则表达式

      ".*((?<FOO>abdd)|a(..d)).*"
    

    有三个显式捕获的子模式,其中左括号位置确定结果中的顺序,因此 "((?<FOO>abdd)|a(..d))" 是子模式索引 1,"(?<FOO>abdd)" 是子模式索引 2,"(..d)" 是子模式索引 3。当与以下字符串匹配时

      "ABCabcdABC"
    

    索引 2 处的子模式将不匹配,因为字符串中不存在 "abdd",但完整的模式会匹配(由于替代项 "a(..d)")。因此,索引 2 处的子模式未分配,默认返回值为

      {match,[{0,10},{3,4},{-1,0},{4,3}]}
    

    将捕获类型设置为二进制将给出以下结果

      {match,[<<"ABCabcdABC">>,<<"abcd">>,<<>>,<<"bcd">>]}
    

    其中空二进制(<<>>)表示未分配的子模式。在二进制情况下,会丢失一些关于匹配的信息,<<>> 可能也是捕获的空字符串。

    如果需要在空匹配和不存在的子模式之间进行区分,请使用 index 类型,并在 Erlang 代码中转换为最终类型。

    当给定选项 global 时,捕获规范会单独影响每个匹配项,因此

      re:run("cacb","c(a|b)",[global,{capture,[1],list}]).
    

    给出结果

    {match,[["a"],["b"]]}

仅影响编译步骤的选项在 re:compile/2 函数中描述。

replace(Subject, RE, Replacement) -> iodata() | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
Replacement = iodata()
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

与 replace(Subject, RE, Replacement,[]) 相同。

replace(Subject, RE, Replacement, Options) -> iodata() | binary() | list() | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
Replacement = iodata()
Options = [ Option ]
Option = anchored | global | notbol | noteol | notempty |
         {offset, int()} | {newline, NLSpec} |
         {return, ReturnType} | CompileOpt
ReturnType = iodata | list | binary
CompileOpt = see compile/2 above
NLSpec = cr | crlf | lf | anycrlf
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

将 Subject 字符串的匹配部分替换为 Replacement 的内容。

Options 的给定方式与 re:run/3 函数相同,不同之处在于不允许使用 re:run/3 的 capture 选项。而是存在 {return, ReturnType}。默认返回类型为 iodata,以最大程度减少复制的方式构建。iodata 结果可以直接用于许多 I/O 操作。如果需要扁平的 list(),请指定 {return, list},如果首选二进制文件,请指定 {return, binary}

替换字符串可以包含特殊字符 &,该字符会在结果中插入整个匹配的表达式,以及特殊序列 \N(其中 N 是一个大于 0 的整数),这会导致在结果中插入子表达式编号 N。如果正则表达式未生成具有该编号的子表达式,则不会插入任何内容。

要在结果中插入 &\,请在其前面加上 \。请注意,Erlang 已经在文字字符串中对 \ 赋予了特殊含义,为什么单个 \ 必须写成 "\\",因此双 \ 写成 "\\\\"。例子

re:replace("abcd","c","[&]",[{return,list}]).

给出

"ab[c]d"

re:replace("abcd","c","[\\&]",[{return,list}]).

给出

"ab[&]d"

{error, ErrSpec} 返回值只能由编译引起,即当给出未预编译的格式错误的 RE 时。

split(Subject,RE) -> SplitList | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
SplitList = [ iodata() ]
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

split(Subject, RE, []) 相同。

split(Subject,RE,Options) -> SplitList | {error, ErrSpec} #

类型

Subject = iodata()
RE = mp() | iodata()
Options = [ Option ]
Option = anchored | global | notbol | noteol | notempty |
         {offset, int()} | {newline, NLSpec} | {return, ReturnType} |
         {parts, NumParts} | group | CompileOpt
NumParts = int() | infinity
ReturnType = iodata | list | binary
CompileOpt = see compile/2 above
NLSpec = cr | crlf | lf | anycrlf
SplitList = [ RetData ] | [ GroupedRetData ]
GroupedRetData = [ RetData ]
RetData = iodata() | binary() | list()
ErrSpec = {ErrString, Position}
ErrString = string()
Position = int()

此函数根据提供的正则表达式查找标记,将输入拆分为多个部分。

拆分的基本原理是运行全局正则表达式匹配,并在每次发生匹配的地方分割初始字符串。匹配的字符串部分将从输出中删除。

结果以“字符串”列表的形式给出,这是返回选项中首选的数据类型(默认为iodata)。

如果正则表达式中给出了子表达式,则匹配的子表达式也会在结果列表中返回。一个例子:

re:split("Erlang","[ln]",[{return,list}]).

将产生以下结果:

["Er","a","g"]

re:split("Erlang","([ln])",[{return,list}]).

将会产生:

["Er","l","a","n","g"]

与子表达式匹配的文本(在正则表达式中用括号标记)将插入到结果列表中找到它的位置。实际上,这意味着连接一个拆分的结果,其中整个正则表达式是一个单一的子表达式(如上面的例子中所示),将始终产生原始字符串。

由于示例中最后一部分("g")没有匹配的子表达式,因此之后不会插入任何内容。为了使字符串组和匹配子表达式的部分更加明显,可以使用group选项,它会将主题字符串的部分与拆分时匹配子表达式的部分组合在一起。

re:split("Erlang","([ln])",[{return,list},group]).

给出

[["Er","l"],["a","n"],["g"]]

这里,正则表达式首先匹配"l",导致"Er"成为结果的第一部分。当正则表达式匹配时,(唯一)子表达式绑定到"l",这就是为什么"l""Er"一起插入到组中的原因。下一个匹配是"n",使"a"成为下一个要返回的部分。由于在这种情况下子表达式绑定到子字符串"n",因此"n"被插入到这个组中。最后一组由字符串的其余部分组成,因为没有找到更多匹配项。

默认情况下,所有空字符串都会从结果列表的末尾删除,语义是我们尽可能多地分割字符串,直到到达字符串的末尾。实际上,这意味着所有空字符串都会从结果列表中删除(如果给出了group选项,则删除所有空组)。可以使用parts选项来更改此行为。让我们看一个例子:

re:split("Erlang","[lg]",[{return,list}]).

结果将是:

["Er","an"]

因为最后匹配的“g”有效地使匹配到达了字符串的末尾。但是,如果我们说我们想要更多部分:

re:split("Erlang","[lg]",[{return,list},{parts,3}]).

我们将得到最后一部分,即使在最后一次匹配(匹配"g")之后只有一个空字符串。

["Er","an",[]]

使用此输入数据不可能超过三个部分,因此:

re:split("Erlang","[lg]",[{return,list},{parts,4}]).

将给出相同的结果。要指定返回尽可能多的结果,包括末尾的任何空结果,您可以将无穷大指定为要返回的部分数量。将0指定为部分数量将给出返回除末尾的空部分之外的所有部分的默认行为。

如果捕获了子表达式,则如果未指定{parts, N},则末尾的空子表达式匹配也会从结果中删除。如果您熟悉 Perl,则默认行为与 Perl 默认行为完全对应,{parts, N} 其中 N 是一个正整数,与 Perl 使用正数第三个参数的行为完全对应,而 {parts, infinity} 行为与 Perl 例程给定负整数作为第三个参数时的行为相对应。

re:run/3 函数以前未描述的选项摘要:

  • {return, ReturnType}
    指定原始字符串的部分如何在结果列表中呈现。可能的类型有:

    • iodata
      iodata() 的变体,它在当前实现中提供最少的数据复制(通常是二进制文件,但不要依赖它)。
    • binary
      所有部分都作为二进制文件返回。
    • list
      所有部分都作为字符列表(“字符串”)返回。
  • group
    将字符串部分与匹配正则表达式子表达式的字符串部分组合在一起。

    在这种情况下,函数的返回值将是 list()list()。每个子列表都以从主题字符串中选出的字符串开头,后跟与正则表达式中按出现顺序排列的每个子表达式匹配的部分。

  • {parts, N}
    指定主题字符串要拆分成的部分数量。

    部分数量应为 0 表示默认行为“尽可能多,跳过末尾的空部分”,对于部分数量的特定最大值应为正整数,对于可能的最大部分数量应为无穷大,无论末尾的部分是否为空字符串。

支持的字符串表示形式 #

如手册摘录中所见,我建议允许将正则表达式和主题字符串都作为 iodata() 提供,这意味着可以是二进制文件、列表或二进制文件和深层列表的混合。当不涉及 Unicode 时,这基本上意味着在向 re 模块提供数据时隐式调用 iolist_to_binary()

进一步扩展 #

以下扩展尚未在原型中实现,但应包含在最终版本中:

  • Unicode 支持。Unicode 字符串应按照 EEP 10 中建议的方式表示,这意味着二进制文件中的 UTF-8、作为整数的 Unicode 字符列表或它们的混合。如果正则表达式是为 Unicode 编译的,或者在编译和一次运行中提供了 unicode 选项,则预期数据采用支持的 Unicode 格式之一,否则将抛出 badarg 异常。

  • 匹配谓词,以便在逻辑 Erlang 表达式中轻松使用正则表达式。

其中,Unicode 支持是最重要的,也是无法纯粹在 Erlang 代码中有效实现的一种。

原型实现 #

在 R12B-4 发行版中,提供了一个使用 PCRE 库的原型实现和一个参考手册页。此实现尚未完全支持 Unicode,因为在编写本文时,EEP 10 尚未被接受。原型实现还缺少“split”函数,该函数是在 R12B-4 发布后实现的。

在性能方面,使用此原型,相当简单的正则表达式匹配比当前的 regexp 模块快 75 倍。当不需要外部调度时,允许中断正则表达式执行的记账成本占性能的 1% 到 2%。在最坏的情况下,与未修改的库相比,可能会出现 5% 的性能损失,但随后会涉及实际的重新启动,因此这些数字并不完全具有可比性。

将 PCRE 编译为使用 C 堆栈进行递归调用并避免重新启动,预计在执行速度方面会获得最佳结果。但是,当不发生重新启动时,与完全可中断版本的基准测试差异仅在 1% 到 3% 的范围内,当实际发生重新启动时,差异仍然不超过 6%。

结论是,为了允许集成到 Erlang 模拟器中而无需使用异步线程而强加给 PCRE 库的额外成本,在最坏的情况下,与理论最大值相比不超过 6%。

版权 #

本文档已置于公共领域。