base R的gsub与stringr::str_replace_all行为差异原因探究
为什么
gsub()和stringr::str_replace_all()在这个正则场景下结果不同? 先来看你的测试场景:
txt <- ".72 2.51\n2015** 2.45 2.30 2.00 1.44 1.20 1.54 1.84 1.56 1.94 1.47 0.86 1.01\n2016** 1.53 1.75 2.40 2.62 2.35 2.03 1.25 0.52 0.45 0.56 1.88 1.17\n2017** 0.77 0.70 0.74 1.12 0.88 0.79 0.10 0.09 0.32 0.05 0.15 0.50\n2018** 0.70 0"
你调用gsub(".*2017|2018.*", "", txt)得到了预期结果:
[1] "** 0.77 0.70 0.74 1.12 0.88 0.79 0.10 0.09 0.32 0.05 0.15 0.50\n"
但stringr::str_replace_all(txt, ".*2017|2018.*", "")却只替换了2017和2018相关的片段,保留了其他内容,这背后的核心原因是两个函数使用的正则引擎对.的默认行为不同:
核心差异分析
1. base R的gsub()(POSIX正则引擎)
默认情况下,POSIX正则中的.会匹配所有字符,包括换行符。这意味着:
- 你的模式
.*2017会贪婪地匹配从文本开头到最后一个2017的全部内容(包括中间的换行符),也就是把".72 2.51\n2015** ...\n2016** ...\n2017"整个片段选中; - 模式的另一个分支
2018.*会匹配从第一个2018到文本结尾的所有内容; gsub()会全局替换所有匹配到的这些长片段,最终就剩下2017之后、2018之前的目标内容。
2. stringr::str_replace_all()(ICU正则引擎)
ICU正则引擎中,.默认不匹配换行符,.*只能匹配同一行内的内容:
.*2017只能匹配2017所在行的开头到2017(也就是字符串"2017"),前面的多行内容因为包含换行符,无法被这个分支匹配;2018.*只能匹配2018所在行的全部内容;- 所以
str_replace_all()只会替换这两个小片段,留下了前面的所有行和2017之后的"** ... \n"部分。
让str_replace_all()得到预期结果的方法
只需要在正则模式前加上(?s)前缀,开启ICU的dotall模式,让.匹配包括换行符在内的所有字符即可:
stringr::str_replace_all(txt, "(?s).*2017|2018.*", "")
执行这个命令后,你会得到和gsub()完全一致的输出。
另外,你也可以反过来验证:在base R中使用perl=TRUE(启用PCRE正则,和ICU的默认行为一致),此时gsub的结果会和str_replace_all()默认结果相同:
gsub(".*2017|2018.*", "", txt, perl=TRUE)
内容的提问来源于stack exchange,提问作者qdread
相关产品推荐
相关产品推荐

