字符串

字符串是字符的有限序列。当然,当人们问什么是字符时,真正的麻烦就来了。英语使用者熟悉的字符是字母 ABC 等,以及数字和常见的标点符号。这些字符与 0 到 127 之间的整数值映射一起,由 ASCII 标准标准化。当然,非英语语言中使用了许多其他字符,包括带重音和其他修改的 ASCII 字符的变体,相关的脚本(如西里尔字母和希腊字母),以及与 ASCII 和英语完全无关的脚本,包括阿拉伯语、汉语、希伯来语、印地语、日语和韩语。Unicode 标准解决了什么是字符的确切复杂性,并普遍被认为是解决此问题的权威标准。根据您的需要,您可以完全忽略这些复杂性,而只是假装只有 ASCII 字符存在,或者您可以编写可以处理处理非 ASCII 文本时可能遇到的任何字符或编码的代码。Julia 使处理纯 ASCII 文本变得简单高效,并且处理 Unicode 尽可能简单高效。特别是,您可以编写 C 风格的字符串代码来处理 ASCII 字符串,它们将按预期工作,无论是在性能方面还是在语义方面。如果此类代码遇到非 ASCII 文本,它将以清晰的错误消息优雅地失败,而不是静默地引入损坏的结果。发生这种情况时,修改代码以处理非 ASCII 数据非常简单。

Julia 的字符串有一些值得注意的高级特性

  • Julia 中用于字符串(和字符串字面量)的内置具体类型是 String。它通过 UTF-8 编码支持完整的 Unicode 字符范围。(提供了 transcode 函数用于在其他 Unicode 编码之间进行转换。)
  • 所有字符串类型都是抽象类型 AbstractString 的子类型,外部包定义了额外的 AbstractString 子类型(例如,用于其他编码)。如果您定义了一个期望字符串参数的函数,则应将类型声明为 AbstractString 以接受任何字符串类型。
  • 与 C 和 Java 一样,但与大多数动态语言不同,Julia 具有一个用于表示单个字符的一流类型,称为 AbstractCharAbstractChar 的内置 Char 子类型是一个 32 位原始类型,可以表示任何 Unicode 字符(并且基于 UTF-8 编码)。
  • 与 Java 一样,字符串是不可变的:AbstractString 对象的值不能更改。要构造不同的字符串值,您可以从其他字符串的部分构造一个新字符串。
  • 从概念上讲,字符串是从索引到字符的部分函数:对于某些索引值,不返回字符值,而是抛出异常。这允许通过编码表示的字节索引而不是字符索引高效地索引字符串,对于 Unicode 字符串的可变宽度编码,这无法同时高效且简单地实现。

字符

Char 值表示单个字符:它只是一个 32 位原始类型,具有特殊的字面量表示和适当的算术行为,并且可以转换为表示 Unicode 代码点 的数值。(Julia 包可能会定义 AbstractChar 的其他子类型,例如,为了优化其他 文本编码 的操作。)以下是 Char 值的输入和显示方式(请注意,字符字面量用单引号分隔,而不是双引号)

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> typeof(c)
Char

您可以轻松地将 Char 转换为其整数值,即代码点

julia> c = Int('x')
120

julia> typeof(c)
Int64

在 32 位架构上,typeof(c) 将是 Int32。您可以像这样轻松地将整数值转换回 Char

julia> Char(120)
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

并非所有整数值都是有效的 Unicode 代码点,但为了性能,Char 转换不会检查每个字符值是否有效。如果您想检查每个转换的值是否为有效的代码点,请使用 isvalid 函数

julia> Char(0x110000)
'\U110000': Unicode U+110000 (category In: Invalid, too high)

julia> isvalid(Char, 0x110000)
false

在撰写本文时,有效的 Unicode 代码点是 U+0000U+D7FFU+E000U+10FFFF。这些代码点尚未全部分配有可理解的含义,也不一定能够被应用程序解释,但所有这些值都被认为是有效的 Unicode 字符。

您可以使用 \u 后跟最多四个十六进制数字或 \U 后跟最多八个十六进制数字(最长的有效值只需要六个)在单引号中输入任何 Unicode 字符

julia> '\u0'
'\0': ASCII/Unicode U+0000 (category Cc: Other, control)

julia> '\u78'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> '\u2200'
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> '\U10ffff'
'\U10ffff': Unicode U+10FFFF (category Cn: Other, not assigned)

Julia 使用您的系统的区域设置和语言设置来确定哪些字符可以按原样打印,哪些字符必须使用通用的转义 \u\U 输入形式输出。除了这些 Unicode 转义形式之外,还可以使用所有 C 的传统转义输入形式

julia> Int('\0')
0

julia> Int('\t')
9

julia> Int('\n')
10

julia> Int('\e')
27

julia> Int('\x7f')
127

julia> Int('\177')
127

您可以对 Char 值进行比较和有限的算术运算

julia> 'A' < 'a'
true

julia> 'A' <= 'a' <= 'Z'
false

julia> 'A' <= 'X' <= 'Z'
true

julia> 'x' - 'a'
23

julia> 'A' + 1
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)

字符串基础

字符串字面量用双引号或三双引号分隔(而不是单引号)

julia> str = "Hello, world.\n"
"Hello, world.\n"

julia> """Contains "quote" characters"""
"Contains \"quote\" characters"

字符串中的长行可以通过在换行符前加上反斜杠 (\) 来分解

julia> "This is a long \
       line"
"This is a long line"

如果要从字符串中提取字符,则对其进行索引

julia> str[begin]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[1]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[end]
'\n': ASCII/Unicode U+000A (category Cc: Other, control)

许多 Julia 对象,包括字符串,都可以用整数索引。第一个元素(字符串的第一个字符)的索引由 firstindex(str) 返回,最后一个元素(字符)的索引由 lastindex(str) 返回。关键字 beginend 可在索引操作中用作给定维度上的第一个和最后一个索引的简写。字符串索引(如 Julia 中的大多数索引)是基于 1 的:对于任何 AbstractStringfirstindex 始终返回 1。但是,正如我们将在下面看到的,对于字符串,lastindex(str) 通常等于 length(str),因为某些 Unicode 字符可能占据多个“代码单元”。

您可以像普通值一样对 end 执行算术和其他操作

julia> str[end-1]
'.': ASCII/Unicode U+002E (category Po: Punctuation, other)

julia> str[end÷2]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

使用小于 begin (1) 或大于 end 的索引将引发错误

julia> str[begin-1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [0]
[...]

julia> str[end+1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [15]
[...]

您还可以使用范围索引提取子字符串

julia> str[4:9]
"lo, wo"

请注意,表达式 str[k]str[k:k] 不会给出相同的结果

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[6:6]
","

前者是类型为Char的单字符值,而后者是恰好只包含一个字符的字符串值。在 Julia 中,它们是截然不同的东西。

范围索引会复制原始字符串中所选部分的副本。或者,可以使用类型SubString创建字符串的视图。更简单地说,在代码块上使用@views宏会将所有字符串切片转换为子字符串。例如

julia> str = "long string"
"long string"

julia> substr = SubString(str, 1, 4)
"long"

julia> typeof(substr)
SubString{String}

julia> @views typeof(str[1:4]) # @views converts slices to SubStrings
SubString{String}

一些标准函数,如chopchompstrip,会返回SubString

Unicode 和 UTF-8

Julia 完全支持 Unicode 字符和字符串。如上文所述,在字符字面量中,Unicode 代码点可以使用 Unicode \u\U 转义序列以及所有标准 C 转义序列来表示。这些同样可以用于编写字符串字面量

julia> s = "\u2200 x \u2203 y"
"∀ x ∃ y"

这些 Unicode 字符是显示为转义序列还是显示为特殊字符,取决于终端的区域设置及其对 Unicode 的支持。字符串字面量使用 UTF-8 编码。UTF-8 是一种可变宽度编码,这意味着并非所有字符都以相同数量的字节(“代码单元”)进行编码。在 UTF-8 中,ASCII 字符——即代码点小于 0x80(128)的字符——按 ASCII 方式编码,使用单个字节,而代码点 0x80 及以上的字符则使用多个字节编码——每个字符最多四个字节。

Julia 中的字符串索引指的是代码单元(= UTF-8 的字节),它是用于编码任意字符(代码点)的固定宽度构建块。这意味着并非每个字符串索引都一定是字符的有效索引。如果在这样的无效字节索引处索引字符串,则会抛出错误

julia> s[1]
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> s[2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[3]
ERROR: StringIndexError: invalid index [3], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[4]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

在这种情况下,字符是一个三字节字符,因此索引 2 和 3 无效,下一个字符的索引是 4;这个下一个有效索引可以通过nextind(s,1)计算,之后的下一个索引可以通过nextind(s,4)依此类推。

由于end始终是集合中的最后一个有效索引,因此如果倒数第二个字符是多字节字符,则end-1引用无效的字节索引。

julia> s[end-1]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

julia> s[end-2]
ERROR: StringIndexError: invalid index [9], valid nearby indices [7]=>'∃', [10]=>' '
Stacktrace:
[...]

julia> s[prevind(s, end, 2)]
'∃': Unicode U+2203 (category Sm: Symbol, math)

第一种情况有效,因为最后一个字符y和空格都是一个字节字符,而end-2索引到多字节表示的中间。此情况的正确方法是使用prevind(s, lastindex(s), 2),或者,如果使用该值索引到s,则可以编写s[prevind(s, end, 2)],并且end扩展为lastindex(s)

使用范围索引提取子字符串也需要有效的字节索引,否则会抛出错误

julia> s[1:1]
"∀"

julia> s[1:2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[1:4]
"∀ "

由于可变长度编码,字符串中的字符数(由length(s)给出)并不总是与最后一个索引相同。如果您遍历索引 1 到lastindex(s)并索引到s,则在没有抛出错误的情况下返回的字符序列是构成字符串s的字符序列。因此length(s) <= lastindex(s),因为字符串中的每个字符都必须有自己的索引。以下是在字符串s中遍历字符的一种低效且冗长的方式

julia> for i = firstindex(s):lastindex(s)
           try
               println(s[i])
           catch
               # ignore the index error
           end
       end
∀

x

∃

y

空行实际上包含空格。幸运的是,对于遍历字符串中的字符,上述笨拙的习惯用法是不必要的,因为您可以将字符串用作可迭代对象,无需异常处理

julia> for c in s
           println(c)
       end
∀

x

∃

y

如果您需要获取字符串的有效索引,可以使用nextindprevind函数递增/递减到下一个/上一个有效索引,如上所述。您还可以使用eachindex函数迭代有效字符索引

julia> collect(eachindex(s))
7-element Vector{Int64}:
  1
  4
  5
  6
  7
 10
 11

要访问编码的原始代码单元(UTF-8 的字节),可以使用codeunit(s,i)函数,其中索引i1连续运行到ncodeunits(s)codeunits(s)函数返回一个AbstractVector{UInt8}包装器,允许您像访问数组一样访问这些原始代码单元(字节)。

Julia 中的字符串可以包含无效的 UTF-8 代码单元序列。此约定允许将任何字节序列视为String。在这种情况下,规则是从左到右解析代码单元序列时,字符由与以下位模式之一的开头匹配的最长 8 位代码单元序列形成(每个x可以是01

  • 0xxxxxxx;
  • 110xxxxx 10xxxxxx;
  • 1110xxxx 10xxxxxx 10xxxxxx;
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx;
  • 10xxxxxx;
  • 11111xxx.

特别是,这意味着过长和过高的代码单元序列及其前缀被视为单个无效字符,而不是多个无效字符。此规则可以通过示例更好地解释

julia> s = "\xc0\xa0\xe2\x88\xe2|"
"\xc0\xa0\xe2\x88\xe2|"

julia> foreach(display, s)
'\xc0\xa0': [overlong] ASCII/Unicode U+0020 (category Zs: Separator, space)
'\xe2\x88': Malformed UTF-8 (category Ma: Malformed, bad data)
'\xe2': Malformed UTF-8 (category Ma: Malformed, bad data)
'|': ASCII/Unicode U+007C (category Sm: Symbol, math)

julia> isvalid.(collect(s))
4-element BitArray{1}:
 0
 0
 0
 1

julia> s2 = "\xf7\xbf\xbf\xbf"
"\U1fffff"

julia> foreach(display, s2)
'\U1fffff': Unicode U+1FFFFF (category In: Invalid, too high)

我们可以看到字符串s中的前两个代码单元形成了空格字符的过长编码。它无效,但在字符串中被接受为单个字符。接下来的两个代码单元形成了一个有效的三个字节 UTF-8 序列的开头。但是,第五个代码单元\xe2不是其有效的延续。因此,代码单元 3 和 4 也被解释为此字符串中的错误格式的字符。类似地,代码单元 5 形成一个错误格式的字符,因为|不是它的有效延续。最后,字符串s2包含一个过高的代码点。

Julia 默认使用 UTF-8 编码,可以通过包添加对新编码的支持。例如,LegacyStrings.jl包实现了UTF16StringUTF32String类型。目前,关于其他编码以及如何实现对其支持的更多讨论超出了本文档的范围。有关 UTF-8 编码问题的进一步讨论,请参见下面关于字节数组字面量的部分。transcode函数用于在各种 UTF-xx 编码之间转换数据,主要用于处理外部数据和库。

连接

最常见和最有用的字符串操作之一是连接

julia> greet = "Hello"
"Hello"

julia> whom = "world"
"world"

julia> string(greet, ", ", whom, ".\n")
"Hello, world.\n"

必须注意潜在的危险情况,例如无效 UTF-8 字符串的连接。生成的字符串可能包含与输入字符串不同的字符,其字符数可能低于连接字符串的字符数之和,例如

julia> a, b = "\xe2\x88", "\x80"
("\xe2\x88", "\x80")

julia> c = string(a, b)
"∀"

julia> collect.([a, b, c])
3-element Vector{Vector{Char}}:
 ['\xe2\x88']
 ['\x80']
 ['∀']

julia> length.([a, b, c])
3-element Vector{Int64}:
 1
 1
 1

这种情况仅可能发生在无效的 UTF-8 字符串中。对于有效的 UTF-8 字符串,连接会保留字符串中的所有字符以及字符串长度的可加性。

Julia 还提供*用于字符串连接

julia> greet * ", " * whom * ".\n"
"Hello, world.\n"

虽然*对于使用+进行字符串连接的语言的用户来说可能是一个令人惊讶的选择,但*的这种用法在数学中,特别是在抽象代数中,是有先例的。

在数学中,+通常表示交换运算,其中操作数的顺序无关紧要。矩阵加法就是一个例子,其中对于任何形状相同的矩阵ABA + B == B + A。相反,*通常表示非交换运算,其中操作数的顺序很重要。矩阵乘法就是一个例子,其中通常A * B != B * A。与矩阵乘法一样,字符串连接是非交换的:greet * whom != whom * greet。因此,*是作为中缀字符串连接运算符的更自然的选择,与常见的数学用法一致。

更准确地说,所有有限长度字符串S以及字符串连接运算符*一起构成一个自由幺半群S*)。此集合的单位元是空字符串""。每当自由幺半群不是交换的时,运算通常表示为\cdot*或类似的符号,而不是+,如前所述,+通常表示交换性。

插值

但是,使用连接构造字符串可能会变得有点麻烦。为了减少对这些冗长的string调用或重复乘法的需要,Julia 允许使用$将表达式插入字符串字面量中,就像在 Perl 中一样

julia> greet = "Hello"; whom = "world";

julia> "$greet, $whom.\n"
"Hello, world.\n"

这更易读且方便,并且等效于上述字符串连接——系统会将此明显的单个字符串字面量重写为调用string(greet, ", ", whom, ".\n")

$后面的最短完整表达式被视为其值要插入字符串的表达式。因此,您可以使用括号将任何表达式插入字符串

julia> "1 + 2 = $(1 + 2)"
"1 + 2 = 3"

连接和字符串插值都会调用string将对象转换为字符串形式。但是,string实际上只是返回print的输出,因此新类型应该向printshow添加方法,而不是string

大多数非AbstractString对象都被转换为与它们作为字面量表达式输入的方式紧密对应的字符串

julia> v = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> "v: $v"
"v: [1, 2, 3]"

stringAbstractStringAbstractChar值的恒等式,因此它们被插入字符串中,本身不带引号也不带转义

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> "hi, $c"
"hi, x"

要在字符串字面量中包含文字$,请使用反斜杠对其进行转义

julia> print("I have \$100 in my account.\n")
I have $100 in my account.

三引号字符串字面量

当使用三引号("""...""")创建字符串时,它们具有一些特殊的行为,对于创建较长的文本块很有用。

首先,三引号字符串也会缩进到缩进最少的行的级别。这对于在缩进的代码中定义字符串很有用。例如

julia> str = """
           Hello,
           world.
         """
"  Hello,\n  world.\n"

在这种情况下,"""之前的最后一个(空)行设置缩进级别。

缩进级别被确定为所有行中最长公共起始空格或制表符序列,不包括"""后面的行以及仅包含空格或制表符的行(包含"""的行始终包含在内)。然后,对于所有行,不包括"""后面的文本,删除公共起始序列(包括仅包含空格和制表符的行,如果它们以此序列开头),例如

julia> """    This
         is
           a test"""
"    This\nis\n  a test"

接下来,如果"""后面跟着一个换行符,则从结果字符串中删除该换行符。

"""hello"""

等效于

"""
hello"""

"""

hello"""

开头将包含一个字面换行符。

缩进去除后会执行换行符的去除。例如

julia> """
         Hello,
         world."""
"Hello,\nworld."

如果使用反斜杠去除换行符,缩进去除也将得到尊重

julia> """
         Averylong\
         word"""
"Averylongword"

尾随空格保持不变。

三引号字符串字面量可以包含"字符而无需转义。

请注意,字面字符串中的换行符,无论是单引号还是三引号,都会在字符串中生成一个换行符 (LF) 字符 \n,即使您的编辑器使用回车符 (CR) 或 CRLF 组合来结束行。要在字符串中包含 CR,请使用显式转义 \r;例如,您可以输入字面字符串 "a CRLF line ending\r\n"

常用操作

您可以使用标准比较运算符对字符串进行字典序比较

julia> "abracadabra" < "xylophone"
true

julia> "abracadabra" == "xylophone"
false

julia> "Hello, world." != "Goodbye, world."
true

julia> "1 + 2 = 3" == "1 + 2 = $(1 + 2)"
true

您可以使用 findfirstfindlast 函数搜索特定字符的索引

julia> findfirst('o', "xylophone")
4

julia> findlast('o', "xylophone")
7

julia> findfirst('z', "xylophone")

您可以通过使用 findnextfindprev 函数在给定偏移量处开始搜索字符

julia> findnext('o', "xylophone", 1)
4

julia> findnext('o', "xylophone", 5)
7

julia> findprev('o', "xylophone", 5)
4

julia> findnext('o', "xylophone", 8)

您可以使用 occursin 函数检查字符串中是否包含子字符串

julia> occursin("world", "Hello, world.")
true

julia> occursin("o", "Xylophon")
true

julia> occursin("a", "Xylophon")
false

julia> occursin('o', "Xylophon")
true

最后一个示例显示 occursin 也可以查找字符字面量。

另外两个方便的字符串函数是 repeatjoin

julia> repeat(".:Z:.", 10)
".:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:."

julia> join(["apples", "bananas", "pineapples"], ", ", " and ")
"apples, bananas and pineapples"

其他一些有用的函数包括

非标准字符串字面量

在某些情况下,您希望构造一个字符串或使用字符串语义,但标准字符串构造的行为并非完全符合需要。对于这些情况,Julia 提供了非标准字符串字面量。非标准字符串字面量看起来像一个常规的双引号字符串字面量,但前面立即带有标识符,并且其行为可能与普通字符串字面量不同。

正则表达式字节数组字面量版本号字面量(如下所述)是一些非标准字符串字面量的示例。用户和包也可以定义新的非标准字符串字面量。在 元编程 部分提供了更多文档。

正则表达式

有时您不是在查找完全相同的字符串,而是在查找特定的模式。例如,假设您正在尝试从一个大型文本文件中提取一个日期。您不知道该日期是什么(这就是您搜索它的原因),但您知道它看起来类似于 YYYY-MM-DD。正则表达式允许您指定这些模式并搜索它们。

Julia 使用 Perl 兼容正则表达式 (regexes) 的版本 2,由 PCRE 库提供(有关更多详细信息,请参阅 PCRE2 语法说明)。正则表达式与字符串以两种方式相关:明显的联系是正则表达式用于在字符串中查找正则模式;另一个联系是正则表达式本身作为字符串输入,这些字符串被解析成一个状态机,该状态机可用于有效地搜索字符串中的模式。在 Julia 中,正则表达式使用以各种以 r 开头的标识符为前缀的非标准字符串字面量输入。最基本的正则表达式字面量没有任何选项打开,只需使用 r"..."

julia> re = r"^\s*(?:#|$)"
r"^\s*(?:#|$)"

julia> typeof(re)
Regex

要检查正则表达式是否与字符串匹配,请使用 occursin

julia> occursin(r"^\s*(?:#|$)", "not a comment")
false

julia> occursin(r"^\s*(?:#|$)", "# a comment")
true

可以在这里看到,occursin 只返回 true 或 false,指示给定正则表达式在字符串中是否出现匹配。但是,通常情况下,人们不仅想知道字符串是否匹配,还想知道它是如何匹配的。要捕获有关匹配的信息,请改用 match 函数

julia> match(r"^\s*(?:#|$)", "not a comment")

julia> match(r"^\s*(?:#|$)", "# a comment")
RegexMatch("#")

如果正则表达式与给定字符串不匹配,match 将返回 nothing - 一个特殊值,在交互式提示符下不会打印任何内容。除了不打印之外,它是一个完全正常的数值,您可以通过编程方式对其进行测试

m = match(r"^\s*(?:#|$)", line)
if m === nothing
    println("not a comment")
else
    println("blank or comment")
end

如果正则表达式匹配,则 match 返回的值为 RegexMatch 对象。这些对象记录表达式如何匹配,包括模式匹配的子字符串以及任何捕获的子字符串(如果有)。此示例仅捕获与之匹配的子字符串的部分,但也许我们希望捕获注释字符后任何非空白文本。我们可以执行以下操作

julia> m = match(r"^\s*(?:#\s*(.*?)\s*$|$)", "# a comment ")
RegexMatch("# a comment ", 1="a comment")

调用 match 时,您可以选择指定要开始搜索的索引。例如

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",1)
RegexMatch("1")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",6)
RegexMatch("2")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",11)
RegexMatch("3")

您可以从 RegexMatch 对象中提取以下信息

  • 匹配的整个子字符串:m.match
  • 捕获的子字符串作为字符串数组:m.captures
  • 整个匹配开始的偏移量:m.offset
  • 捕获的子字符串的偏移量作为向量:m.offsets

对于不匹配的捕获,m.captures 在该位置包含 nothing 而不是子字符串,而 m.offsets 具有零偏移量(回想一下,Julia 中的索引为 1 为基数,因此字符串中的零偏移量无效)。这是一个有点牵强的例子对

julia> m = match(r"(a|b)(c)?(d)", "acd")
RegexMatch("acd", 1="a", 2="c", 3="d")

julia> m.match
"acd"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 "c"
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 2
 3

julia> m = match(r"(a|b)(c)?(d)", "ad")
RegexMatch("ad", 1="a", 2=nothing, 3="d")

julia> m.match
"ad"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 nothing
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 0
 2

将捕获作为数组返回很方便,这样您就可以使用解构语法将它们绑定到局部变量。为方便起见,RegexMatch 对象实现了迭代器方法,这些方法传递到 captures 字段,因此您可以直接解构匹配对象

julia> first, second, third = m; first
"a"

还可以通过使用捕获组的编号或名称来索引 RegexMatch 对象来访问捕获。

julia> m=match(r"(?<hour>\d+):(?<minute>\d+)","12:45")
RegexMatch("12:45", hour="12", minute="45")

julia> m[:minute]
"45"

julia> m[2]
"45"

使用 replace 时,可以在替换字符串中引用捕获,方法是使用 \n 来引用第 n 个捕获组,并在替换字符串前加上 s。捕获组 0 指的是整个匹配对象。命名捕获组可以在替换中使用 \g<groupname> 引用。例如

julia> replace("first second", r"(\w+) (?<agroup>\w+)" => s"\g<agroup> \1")
"second first"

编号捕获组也可以像 \g<n> 一样引用,以消除歧义,例如

julia> replace("a", r"." => s"\g<0>1")
"a1"

您可以通过在结束双引号标记后使用 imsx 标志的某种组合来修改正则表达式的行为。这些标志与 Perl 中的含义相同,如 perlre 手册页 中的摘录所述

i   Do case-insensitive pattern matching.

    If locale matching rules are in effect, the case map is taken
    from the current locale for code points less than 255, and
    from Unicode rules for larger code points. However, matches
    that would cross the Unicode rules/non-Unicode rules boundary
    (ords 255/256) will not succeed.

m   Treat string as multiple lines.  That is, change "^" and "$"
    from matching the start or end of the string to matching the
    start or end of any line anywhere within the string.

s   Treat string as single line.  That is, change "." to match any
    character whatsoever, even a newline, which normally it would
    not match.

    Used together, as r""ms, they let the "." match any character
    whatsoever, while still allowing "^" and "$" to match,
    respectively, just after and just before newlines within the
    string.

x   Tells the regular expression parser to ignore most whitespace
    that is neither backslashed nor within a character class. You
    can use this to break up your regular expression into
    (slightly) more readable parts. The '#' character is also
    treated as a metacharacter introducing a comment, just as in
    ordinary code.

例如,以下正则表达式启用了所有三个标志

julia> r"a+.*b+.*?d$"ism
r"a+.*b+.*?d$"ims

julia> match(r"a+.*b+.*?d$"ism, "Goodbye,\nOh, angry,\nBad world\n")
RegexMatch("angry,\nBad world")

r"..." 字面量是在没有插值和转义的情况下构造的(除了引号 " 仍然需要转义)。以下是一个显示与标准字符串字面量区别的示例

julia> x = 10
10

julia> r"$x"
r"$x"

julia> "$x"
"10"

julia> r"\x"
r"\x"

julia> "\x"
ERROR: syntax: invalid escape sequence

也支持形式为 r"""...""" 的三引号正则表达式字符串(对于包含引号或换行的正则表达式可能很方便)。

Regex() 构造函数可用于以编程方式创建有效的正则表达式字符串。这允许在构造正则表达式字符串时使用字符串变量的内容和其他字符串操作。上述任何正则表达式代码都可以在 Regex() 的单个字符串参数中使用。以下是一些示例

julia> using Dates

julia> d = Date(1962,7,10)
1962-07-10

julia> regex_d = Regex("Day " * string(day(d)))
r"Day 10"

julia> match(regex_d, "It happened on Day 10")
RegexMatch("Day 10")

julia> name = "Jon"
"Jon"

julia> regex_name = Regex("[\"( ]\\Q$name\\E[\") ]")  # interpolate value of name
r"[\"( ]\QJon\E[\") ]"

julia> match(regex_name, " Jon ")
RegexMatch(" Jon ")

julia> match(regex_name, "[Jon]") === nothing
true

请注意 \Q...\E 转义序列的使用。\Q\E 之间的所有字符都被解释为字面字符。这对于匹配否则将是正则表达式元字符的字符很方便。但是,在将此功能与字符串插值一起使用时需要注意,因为插值的字符串本身可能包含 \E 序列,从而意外终止字面匹配。在包含在正则表达式中之前,需要对用户输入进行清理。

字节数组字面量

另一个有用的非标准字符串字面量是字节数组字符串字面量:b"..."。此形式允许您使用字符串表示法来表达只读字面量字节数组 - 即 UInt8 值的数组。这些对象的类型是 CodeUnits{UInt8, String}。字节数组字面量的规则如下

  • ASCII 字符和 ASCII 转义生成单个字节。
  • \x 和八进制转义序列生成与转义值对应的字节
  • Unicode 转义序列生成以 UTF-8 编码该代码点的字节序列。

这些规则之间存在一些重叠,因为小于 0x80 (128) 的 \x 和八进制转义的行为同时由前两个规则涵盖,但此处这些规则一致。总之,这些规则允许您轻松地使用 ASCII 字符、任意字节值和 UTF-8 序列来生成字节数组。这是一个使用所有三个的示例

julia> b"DATA\xff\u2200"
8-element Base.CodeUnits{UInt8, String}:
 0x44
 0x41
 0x54
 0x41
 0xff
 0xe2
 0x88
 0x80

ASCII 字符串“DATA”对应于字节 68、65、84、65。\xff 生成单个字节 255。Unicode 转义 \u2200 以 UTF-8 编码为三个字节 226、136、128。请注意,生成的字节数组不对应于有效的 UTF-8 字符串

julia> isvalid("DATA\xff\u2200")
false

如前所述,CodeUnits{UInt8, String} 类型表现为 UInt8 的只读数组,如果您需要标准向量,则可以使用 Vector{UInt8} 进行转换

julia> x = b"123"
3-element Base.CodeUnits{UInt8, String}:
 0x31
 0x32
 0x33

julia> x[1]
0x31

julia> x[1] = 0x32
ERROR: CanonicalIndexError: setindex! not defined for Base.CodeUnits{UInt8, String}
[...]

julia> Vector{UInt8}(x)
3-element Vector{UInt8}:
 0x31
 0x32
 0x33

还要观察 \xff\uff 之间的显着区别:前一个转义序列编码字节 255,而后一个转义序列表示代码点 255,它在 UTF-8 中编码为两个字节

julia> b"\xff"
1-element Base.CodeUnits{UInt8, String}:
 0xff

julia> b"\uff"
2-element Base.CodeUnits{UInt8, String}:
 0xc3
 0xbf

字符字面量使用相同的行为。

对于小于 \u80 的代码点,每个代码点的 UTF-8 编码恰好是对应 \x 转义生成的单个字节,因此可以安全地忽略这种区别。但是,对于 \x80\xff\u80\uff 的转义,存在主要差异:前者转义都编码单个字节,除非后面跟着非常特定的延续字节,否则不构成有效的 UTF-8 数据,而后者转义都表示具有两个字节编码的 Unicode 代码点。

如果以上内容过于复杂,请尝试阅读"每个软件开发人员绝对、肯定必须了解的关于 Unicode 和字符集的最低限度知识"。这是一篇关于 Unicode 和 UTF-8 的优秀入门文章,可以帮助您缓解一些相关困惑。

版本号字面量

版本号可以通过以下形式的非标准字符串字面量轻松表示:v"..."。版本号字面量创建VersionNumber对象,这些对象遵循语义版本控制的规范,因此由主版本、次版本和修订版本数字组成,后跟预发布和构建字母数字注释。例如,v"0.2.1-rc1+win64" 分解为主版本 0、次版本 2、修订版本 1、预发布 rc1 和构建 win64。在输入版本字面量时,除了主版本号之外,所有其他内容都是可选的,因此例如 v"0.2" 等价于 v"0.2.0"(带有空的预发布/构建注释),v"2" 等价于 v"2.0.0",依此类推。

VersionNumber 对象主要用于轻松且正确地比较两个(或多个)版本。例如,常量VERSION保存 Julia 版本号作为 VersionNumber 对象,因此可以使用简单的语句定义一些特定于版本的行为,例如

if v"0.2" <= VERSION < v"0.3-"
    # do something specific to 0.2 release series
end

请注意,在上面的示例中,使用了非标准版本号 v"0.3-",后面带有一个 -:此表示法是 Julia 对标准的扩展,用于指示低于任何 0.3 版本的版本,包括其所有预发布版本。因此,在上述示例中,代码仅在稳定的 0.2 版本下运行,并排除诸如 v"0.3.0-rc1" 之类的版本。为了也允许不稳定(即预发布)的 0.2 版本,应如下修改下界检查:v"0.2-" <= VERSION

另一个非标准版本规范扩展允许使用尾随 + 来表示构建版本的上限,例如 VERSION > v"0.2-rc1+" 可以用来表示高于 0.2-rc1 及其任何构建的任何版本:它将对版本 v"0.2-rc1+win64" 返回 false,对 v"0.2-rc2" 返回 true

在比较中使用此类特殊版本是一种良好的做法(特别是,除非有充分理由不这样做,否则应该始终在上界使用尾随 -),但它们不能用作任何内容的实际版本号,因为它们在语义版本控制方案中是无效的。

除了用于VERSION常量之外,VersionNumber 对象还广泛用于 Pkg 模块中,用于指定包版本及其依赖项。

原始字符串字面量

不进行插值或取消转义的原始字符串可以通过以下形式的非标准字符串字面量表示:raw"..."。原始字符串字面量创建普通的 String 对象,其中包含完全按照输入的封闭内容,不进行插值或取消转义。这对于包含其他语言中代码或标记的字符串很有用,这些语言使用 $\ 作为特殊字符。

例外情况是引号仍然必须转义,例如 raw"\"" 等价于 "\""。为了能够表达所有字符串,反斜杠也必须转义,但仅当出现在引号字符之前时。

julia> println(raw"\\ \\\"")
\\ \"

请注意,前两个反斜杠在输出中逐字出现,因为它们没有出现在引号字符之前。但是,下一个反斜杠字符转义了其后的反斜杠,最后一个反斜杠转义了一个引号,因为这些反斜杠出现在引号之前。