在字符串文本中使用 \x
几乎总是一个糟糕的主意,但在正则表达式中使用它尤其危险。
考虑 R 4.2.1 或更早版本中的此“不要做”示例
text <- "Hello\u00a0R"
gsub("\xa0", "", text)
a0
是 Unicode “不换行空格”的代码点,该示例在 UTF-8 区域设置中运行。目的是删除空格;大约半年前,R-devel 邮件列表中讨论了一个稍微复杂的变体。
结果是 "Hello R"
,空格未删除。虽然有点牵强,但此示例给出了原因
text <- "Only ASCII <,>,a and digits: <a0><a1><a2>"
gsub("\xa0", "", text)
结果是 ""Only ASCII <,>,a and digits: <a1><a2>"
,因此删除了字符串的“<a0>
”部分。问题在于 R 在将 \xa0
传递给正则表达式引擎之前,会将其在正则表达式中的 \xa0
转换为 ASCII 字符串 "<a0>"
。
R 这样做是因为字符串无效。首先,解析器将 \xa0
扩展为字节 a0
,当 gsub
需要将字符串转换为 UTF16-LE 以供 TRE 使用时,它无法转换,因为 a0
是无效的 UTF-8(并且我们在 UTF-8 区域设置中运行)。代码点 a0
在 UTF-8 中被编码为 c2
a0
。因此,R 将无效字节转义为 "<a0>"
并生成一个有效的 UTF16-LE 字符串,但这并不是预期的字符串。现在 R-devel 中已进行其他检查,以便 R 实际报告错误(稍后会详细介绍),而不是转义无效字节。
然而,问题的根源在于用户错误。模式本身是一个无效的字符串。当 R 在 Latin-1 区域设置中使用时,这可能曾经有效,在该区域设置中,a0
字节表示不换行空格,并且可能仅在那里测试过,而不在其他区域设置中测试过。最近的 R 中很少使用 Latin-1,因此此问题现在会对用户造成更大的影响。
为了在一定程度上缓解此问题,可以将 \x
传递给正则表达式引擎,因此在正则表达式中加倍反斜杠。\\x
是一个 ASCII 字符串,因此始终有效。但是,请参见下文。
默认情况下(perl=FALSE
,fixed=FALSE
),使用 ?regex
中描述的 POSIX 扩展正则表达式,并且未记录这些正则表达式支持 \x
转义。虽然当前使用的实现 TRE 支持它们,但因此不应该使用此功能(例如,作为防止 R 中的实现切换到不同引擎的情况的预防措施)。因此,为此应该使用 Perl 正则表达式(perl=TRUE
),它有其他优点,所以这并不限制模块必须记住这一点。
Perl 正则表达式的显著优势包括通常可以节省编码转换到 UTF-16LE(以及返回)并且可以访问 Unicode 属性。因此,当重新访问现有代码以修复此类问题时,无论如何,切换到 Perl 正则表达式可能是有益的(但这需要小心,因为表达式并不完全相同,请参见?regex
)。
但是,更糟糕的是使用 \\x
转义,它有这样的风险:解释取决于正则表达式引擎的模式,因此仍然可以是特定于区域设置的。此示例在 ISO-8859-2 中有效(结果为 "cesky"
),但在 UTF-8 区域设置中无效
text <- "\u010desky"
text <- iconv(text, from="UTF-8", to="")
gsub("\\xe8", "c", text, perl=TRUE)
这在 UTF-8 区域设置中有效,但在 ISO-8859-2 中无效
text <- "\u010desky"
text <- iconv(text, from="UTF-8", to="")
gsub("\\x{010d}", "c", text, perl=TRUE)
原因是第一次在区域设置模式下运行 Perl 正则表达式(e8
是“带抑扬符的拉丁小写字母 C”的代码),第二次在 UTF 模式下运行(其中 010d
是代码,代码点号)。
使用的模式取决于区域设置和输入字符串。可以通过确保其中一个输入为 UTF-8(不包括 ASCII)来强制使用 UTF 模式。但是,如果文本参数是一个具有多个元素的向量,我们仍然不能选择任何元素转换为 UTF-8,我们必须选择一个不是 ASCII 的元素。或者,所有元素都可能是 ASCII,那么我们必须转换模式或替换。所以也许将所有输入显式转换为 UTF-8?从技术上讲,这样做可行,但值得麻烦吗?
有一种更简单的方法来确保结果与区域设置无关(并且其中一个输入是 UTF-8,特别是模式)
text <- "Hello\u00a0R"
gsub("\u00a0", "", text)
和
text <- "\u010desky"
text <- iconv(text, from="UTF-8", to="")
gsub("\u010d", "c", text)
这适用于默认(POSIX 扩展)正则表达式、Perl 正则表达式和“固定”表达式,因为 Unicode(UTF-8)字符是由解析器创建的。(\\x
不适用于固定表达式,并且仅记录为适用于 Perl 正则表达式)。
原则上,删除不换行空格的第一个示例可能通常可以推广到引用其他类型的空格,例如通过 Perl 正则表达式支持的 Unicode 属性。
检测问题
使用 R-devel 的最新版本,传递给正则表达式的无效字符串现在也会在以前未检测到的情况下被检测到。
> text <- "Hello\u00a0R"
gsub("\xa0", "", text)
Error in gsub("\xa0", "", text) : 'pattern' is invalid
In addition: Warning message:
In gsub("\xa0", "", text) : unable to translate '<a0>' to a wide string
18 个 CRAN 和 5 个 Bioconductor 软件包检查现在由于新检查而明显失败,允许软件包作者修复问题。但是,所有在其正则表达式中使用 \x
(或 \\x
)的软件包作者都应该修复它们。
不建议通过 useBytes
禁用新添加的检查,因为这还可能导致创建无效字符串,除了可能通过更改正则表达式函数的操作模式来破坏代码外,基本上只是隐藏了问题。而且,即使 R 现在没有检测到,它也可能会很快被检测到。