儒雅的火腿肠 · Openlayers自定义4490坐标系 ...· 1 月前 · |
爱喝酒的皮带 · javascript ...· 2 月前 · |
侠义非凡的铅笔 · Ubuntu-MPI - 知乎· 1 年前 · |
想表白的仙人掌 · SendKeys 语句 (VBA) | ...· 1 年前 · |
逼格高的西瓜 · 基于深度残差收缩网络的电力系统暂态频率安全集 ...· 1 年前 · |
evalCpp()
转换单一计算表达式
cppFunction()
转换简单的C++函数—Fibnacci例子
sourceCpp()
转换C++程序—正负交替迭代例子
sourceCpp()
转换C++源文件中的程序—正负交替迭代例子
sourceCpp()
转换C++源程序文件—卷积例子
wrap()
把C++变量返回到R中
as()
函数把R变量转换为C++类型
as()
和
wrap()
的隐含调用
//[[Rcpp::export]]
sourceCpp()
函数中直接包含C++源程序字符串
cppFunction()
函数中直接包含C++函数源程序字符串
evalCpp()
函数中直接包含C++源程序表达式字符串
depends
指定要链接的库
invisible
要求函数结果不自动显示
clone
函数
is_na
seq_along
seq_len
pmin
和
pmax
ifelse
sapply
和
lapply
sign
diff
kable()
函数制作表格
perl=TRUE
。
stringr包提供了更方便的正则表达式功能, 其正则表达式规则是ICU正则表达式规则, 针对UTF-8编码的文本数据, 基本与perl规则兼容。
在正则表达式的模式(pattern)中,
.*+?{}\[]^$()
等字符是特殊字符,有特殊的解释。
除了
\
之外的其它12个都称为“
元字符
”(meta characters)。
在R语言中使用正则表达式时,
需要注意R字符型常量中一个
\
要写成两个。
为了避免
\
的转义,
可以使用R的原始字符串功能,
将模式写成
r"{...}"
这样的形式,
其中
...
是实际的正则表达式内容,
不需要将
\
写成两个。
如果模式中不含特殊字符,匹配为原样的子串。
也叫做字面(literal)匹配。
stringr包提供了定义正则表达式、匹配正则表达式、按正则表达式替换、抽取匹配结果、用富文本显示匹配结果等强大功能,
其中
str_view()
函数可以在RStudio软件中高亮显示匹配部分,
并在匹配部分两边用
<>
界定。
在所有输出中都用
<>
界定匹配部分。
下面的程序在字符型向量
x
的三个字符串元素中查找子字符串
"the"
并加亮显示(一般输出中用
<>
界定匹配部分):
## [1] │ New <the>me
## [3] │ In <the> present <the>me
因为某些希望原样匹配的字符串中可能有元字符(对正则表达式需要特殊解释的字符),
所以字面匹配的模式应该用
fixed()
函数保护。
在文件名中找到以
.txt
结尾的:
## [2] │ data<.txt>
regex
函数
str_view(string, pattern)
中的
pattern
应该为正则表达式类型,
如果输入了字符串,
会自动被函数
regex()
转换成正则表达式类型。
正则表达式的模式一般是区分大小写的,
## [1] │ <Dr>. Wang
通过在
regex()
函数中加选项
ignore_case=TRUE
可以进行不区分大小写的匹配:
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.
fixed()
函数也允许使用
ignore_case=TRUE
,如:
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.
在模式前面附加
(?i)
前缀式选项也可以实现不区分大小写匹配:
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.
也可以提前将输入的源字符串统一转换为小写, 这时模式中也仅使用小写, 也可以达到不区分大小写匹配的目的。 还可以将模式中大写字母和小写字母用字符类表示(见§ 49.1.4 )。
在模式中用“.”匹配任意一个字符(除了换行符
"\n"
,能否匹配此字符与选项有关)。
## [1] │ <abc>
## [2] │ c<abs>
像句点这样的字符称为
元字符
(meta characters),
在正则表达式中有特殊作用。
如果需要匹配句点本身,用“
[.]
”或者“
\.
”表示。
比如,要匹配
a.txt
这个文件名,如下做法有错误:
## [1] │ <a.txt>
## [2] │ <a0txt>
结果连
a0txt
也匹配了。用“[.]”表示句点则将句点不做特殊解释:
## [1] │ <a.txt>
## [1] │ <a.txt>
注意在R语言字符型常量中一个
\
需要写成两个。
如果仅需按照原样进行查找,
也可以将
pattern
的字符串用
fixed()
函数保护,如:
## [1] │ <a.txt>
模式中使用方括号给定一个字符类,
单个字符与字符类中任何一个字符相同都算是匹配成功。
比如,模式“
[ns]a.[.]xls
” 表示匹配的第一个字符是
n
或
s
,
第二个字符是
a
,第三个字符任意,第四个字符是句点,
然后是
xls
。
## [1] │ <sa1.xls>
## [2] │ d<na2.xls>s
## [3] │ <nat.xls>
注意匹配并不需要从开头匹配到结尾, 中间匹配是允许的,类似于搜索符合某种规律的子串。 在上例中第二个元素是从第二个字符开始匹配的,也没有匹配到末尾。
例:模式
[Rr]eg[Ee]x
可以匹配
RegEx
或
Regex
或
regex
或
regEx
。
在“
[]
”中有一些特殊字符:
-
表示一个范围,
如
[a-z]
匹配小写英文字母,
[A-Z]
匹配大写英文字母,
[a-zA-Z]
匹配大小写的英文字母,
[a-zA-Z0-9]
匹配大小写的英文字母和数字。
为了匹配一个16进制数字,
可以用
[0-9A-Fa-f]
。
^
”表示余集,比如
"[^0-9]"
表示匹配非数字。
[]
包含原样的
^
或
-
,可以用前导转义符
\
。
例:下面的模式“
[ns]a[0-9][.]xls
”要求匹配的第三个字符为数字。
## [1] │ <sa1.xls>
## [2] │ d<na2.xls>s
例:模式
[ns]a[^0-9][.]xls
要求匹配的第三个字符不能为数字:
## [3] │ <nat.xls>
要求第三个字符为
^
或
-
的例子:
## [2] │ <5 - 3>
## [3] │ <5 ^ 3>
元字符(meta characters)是在正则表达式中有特殊含义的字符。
比如句点可以匹配任意一个字符, 左方括号代表字符集合的开始。
所以元字符不能直接匹配自身,
将元字符写在方括号内可以原样匹配这些字符,
比如可以用“
[.]
”匹配一个句点,
用
[$]
匹配“
$
”符号,用
[|]
匹配“
|
”符号,
还可以用前导转义字符“
\
”来表示某个元字符需要原样匹配,
为匹配左方括号,在前面加上转义字符
\
变成
\[
,
但是在R字符串中一个
\
必须用
\\
表示,
所以模式“
\[
”在R中写成字符串常量,
必须写成
"\\["
。
其它的元字符如果要原样匹配也可以在前面加上转义字符
\
,
比如匹配
\
本身可以用
\\
,但是在R字符型常量中需要写成
"\\\\"
。
用R的原始字符串格式可以不用将
\
写成
\\
,
原始字符串是指字符串前面加上
r
字母作为前缀,
用原始字符串作为模式字符串时,
模式需要用括号包裹起来。
例,匹配
x[5]
,因为
[
是元字符,需要写成:
## [2] │ <int x[5]>
可以用原始字符串格式写成:
## [2] │ <int x[5]>
当然,因为模式中所有字符都不想作为特殊字符,
也可以用
fixed()
说明:
## [2] │ <int x[5]>
注意其中的方括号不需要用反斜杠保护。
表示空白的元字符有:
\f 换页符
\n 换行符
\r 回车符
\t 制表符
\v 垂直制表符
不同操作系统的文本文件的行分隔符不同,
为了匹配Windows格式的文本文件中的空行,
用“
\r\n\r\n
”;
为了匹配Unix格式的文本文件中的空行则用“
\r\r
”。
写成R的字符型常量时,
这些表示本身也是R的相应字符的表示,
所以在R字符型常量中这些字符不需要用两个
\
表示一个
\
。
匹配任意一个空白字符用“
\s
”,
这等价于“
[ \f\n\r\t\v]
”,
但是其中的
\
在R字符串中要写成
\\
,
所以
\s
写成
\\s
。
大写的“
\S
”则匹配任意一个非空白的字符。
用
\d
匹配一个数字,相当于
[0-9]
。
用
\D
匹配一个非数字。
## [1] │ <n1.xls>
匹配字母、数字、下划线字符用
\w
(小写),
等价于
[a-zA-Z0-9_]
。
\W
(大写)匹配这些字符以外的字符。
## [1] │ file-<s1.>xls
可以看出,模式匹配了
s1.
而没有匹配
s#.
。
在模式中可以用十六进制数和八进制数表示特殊的字符。
十六进制数用
\X
引入, 比如
\X0A
对应
\n
字符。
八进制数用
\0
引入, 比如
\011
表示
\t
字符。
例如,
str_view("abc\nefg\n", r"{\x0A}")
可以匹配两个换行符。
\d
,
\w
这样的字符类不方便用在方括号中组成字符集合,
而且也不容易记忆和认读,
按照R的双反斜杠规则写在字符串中就更难认读。
在模式中方括号内可以用
[:alpha:]
表示任意一个字母。
比如,
[[:alpha:]]
匹配任意一个字母(外层的方括号表示字符集合,
内层的方括号是POSIX字符类的固有界定符)。
这样的POSIX字符类有:
[:alpha:]
表示任意一个字母;
[:lower:]
为小写字母;
[:upper:]
为大写字母;
[:digit:]
为数字;
[:xdigit:]
为十六进制数字。
[:alnum:]
为字母数字(不包括下划线);
[:blank:]
为空格或制表符;
[:space:]
为任何一种空白字符,包括空格、制表符、换页符、换行符、回车符;
[:print:]
为可打印字符;
[:graph:]
和
[:print:]
一样但不包括空格;
[:punct:]
为
[:print:]
中除
[:alnum:]
和空白以外的所有字符;
## [1] │ <x1>
## [2] │ <_x>
## [3] │ <.x>
## [4] │ <.1>
模式匹配长度为2的字符串,
第一个字符是字母、下划线或者小数点,
第二个字符是字母、数字、下划线或者小数点。
这个模式试图匹配由两个字符组成的合法R变量名,
但是最后一个非变量名
.1
也被匹配了。
解决这样的问题可以采用后面讲到的
|
备择模式。
如果有两种模式都算正确匹配,则用
|
连接这两个模式表示两者都可以。
例如,某个人的名字用James和Jim都可以,
表示为
James|Jim
, 如
## [1] │ <James>, Bond
## [2] │ <Jim> boy
两个字符的合法R变量名的匹配:
## [1] │ <x1>
## [2] │ <_x>
## [3] │ <.x>
模式匹配相当于在字符串内部搜索某种模式,
如果必须从字符串开头匹配,
在模式中取第一个模式规定为
^
或
\A
。
如果模式中最后一个字符是
$
或
\Z
,
则需要匹配到字符串末尾。
用
\Z
匹配字符串末尾时如果末尾有一个换行符则匹配到换行符之前。
## [1] │ <n1.xls>
只匹配了第一个输入字符串。
用“
\A
”和“
\Z
”的写法:
有时候源文本的每个字符串保存了一个文本文件内容,
各行用
\n
分隔,
后面将给出匹配每行的行首与行尾的方法。
用
\b
匹配单词边界,
这样可以查找作为单词而不是单词的一部分存在的内容。
\B
匹配非单词边界。
## [1] │ a <cat> meaos
找到单独的函数
sum
而不是子串
sum
:
## [4] │ <sum>(x)
模式中在一个字符或字符集合后加后缀
+
表示一个或多个前一字符。
## [1] │ <sa1>
## [2] │ d<sa123>
例如,匹配电子邮件地址:
## [1] │ <abc123@efg.com>
匹配的电子邮件地址在
@
前面可以使用任意多个字母、数字、下划线,
在
@
后面由小数点分成两段,
每段可以使用任意多个字母、数字、下划线。
这里用了
^
和
$
表示全字符串匹配。
在一个字符或字符集合后加后缀
*
表示零个或多个前一字符,
后缀
?
表示零个或一个前一字符。
^https?://[[:alnum:]./]+$
可以匹配http或https开始的网址。
## [1] │ <http://www.163.net>
## [2] │ <https://123.456.>
(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)
x[[:digit:]]*
能匹配“x”, “x1”, “x123”这样的变量名,如:
## [1] │ <x>
## [2] │ <x1>
## [3] │ <x123>
## [1] │ <x>
## [2] │ <x1>
## [3] │ <x123>
从上面的例子可以看出, 在指定某一字符类(或集合)重复时, 不需要严格重复前一字符, 而是只要保持同一类即可。
问号可以表示零个或一个, 而加号、星号重复不能控制重复次数。 在后缀大括号中写一个整数表示精确的重复次数。
## [3] │ <123>
## [4] │ <123>4
模式匹配的是三位的数字。 因为没有要求从开头一直匹配到末尾, 所以三位以上数字也能匹配其中开始的三位。
可以在后缀大括号中指定重复的最小和最大次数,
中间用逗号分隔。
最小重复数允许指定为0。
重复数的逗号后面空置表示重复数没有上限。
例如,后缀
{3,}
表示前一模式必须至少重复3次。
例:月日年的日期格式可以用
[[:digit:]]{1,2}[-/][[:digit:]]{1,2}[-/][[:digit:]]{2,4}
比较长的正则表达式会很难认读,
我们可以用
paste0()
函数将其拆分为几个部分,
并对每一部分添加适当的注释。
如 (注意这个模式还会匹配非日期)
pat <- paste0( "[[:digit:]]{1,2}[-/]", # month "[[:digit:]]{1,2}[-/]", # day "[[:digit:]]{2,4}") # year str_view(c("2/4/1998", "13/15/198"), pat)
## [1] │ <2/4/1998>
## [2] │ <13/15/198>
也可以用
regex()
函数中加选项
comment=TRUE
加注释,
这时一个正则表达式可以写成多行,
在行尾添加注释,
所有原样空格必须转义或写在字符集中,如:
pat <- regex( r"{[[:digit:]]{1,2}[-/] # month [[:digit:]]{1,2}[-/] # day [[:digit:]]{2,4}}", # year comment = TRUE) str_view(c("2/4/1998", "13/15/198"), pat)
## [1] │ <2/4/1998>
## [2] │ <13/15/198>
无上限的重复匹配如
*
,
+
,
{3,}
等缺省是贪婪型的,
重复直到文本中能匹配的最长范围。
比如我们希望找出圆括号这样的结构,
很容易想到用
\(.+\)
这样的模式(注意圆括号是元字符,需要用反斜杠保护),
但是这不会恰好匹配一次,
模式会一直搜索到最后一个
)
为止。
## [1] │ <(1st) other (2nd)>
我们本来期望的是提取两个“(1st)”和“(2nd)”组合,
不料整个地提取了“
(1st) other (2nd)
”。
这就是因为
.+
的贪婪匹配。
如果要求尽可能短的匹配,
使用
*?
,
+?
,
{3,}?
等“懒惰型”重复模式。
在无上限重复标志后面加问号表示懒惰性重复。
比如,上例中模式修改后得到了期望的结果:
## [1] │ <(1st)> other <(2nd)>
懒惰匹配会造成搜索效率降低, 应仅在需要的时候使用。
句点通配符一般不能匹配换行,如
跨行匹配失败。
一种办法是预先用
str_replace_all()
或
gsub()
把所有换行符替换为空格。
但是这只能解决部分问题。
解决方法是在将模式用
regex()
保护并加选项
dotall=TRUE
,
使得句点通配符可以匹配换行符,
称为句点全匹配模式。
## [1] │ <(1,
## │ 2)>
也可以在在Perl格式的正则表达式开头添加
(?s)
选项,
## [1] │ <(1,
## │ 2)>
在
regex()
函数中加选项
multiline=TRUE
,
或者在正则表达式开头用
(?m)
表示把整个输入字符串看成用换行符分开的多行。
这时
^
和
$
匹配每行的开头和结尾,
“每行”是指字符串中用换行符分开的各个字符子串。
(?s)
与
(?m)
可以同时使用,写成
(?sm)
。
元数据中包含两行内容, 结果没有能够匹配, 这是因为模式要求从整个字符串开头一直匹配到末尾。
增加
multiline=TRUE
则可以对每行分别匹配,
结果找到两处匹配:
## [1] │ <(1,2)>
## │ <(3,4)>
## │
使用
(?m)
选项:
## [1] │ <(1,2)>
## │ <(3,4)>
## │
虽然正则表达式有多行和跨行选项, 但是当源数据很长时, 匹配效率会很低。
R的
readLines()
函数可以把一整个文本文件读成一个字符型向量,
每个元素为一行,
元素中不包含换行符。
R的字符型函数可以对这样的字符型向量每个元素同时处理,
也就实现了逐行处理。
如果字符串
x
中包含了一整个文本文件内容,
其中以
\n
分隔各行,
为了实现逐行处理,
可以先用
str_split()
函数拆分成不同行:
结果将是一个字符型向量, 每个元素是原来的一行,最后一个元素是空字符串。
x <- c("This is first line.\nThis is second line.\n") cl <- str_split(x, "\r?\n")[[1]]
## [1] "This is first line." "This is second line." ""
49.1.15 替换
stringr包的
str_replace_all(string, pattern, replacement)
在字符型向量string
的每个元素中查找模式pattern
, 并将所有匹配按照replacement
进行替换。 在replacement
可以用\1
,\2
中表示模式中的捕获, 除此之外元字符没有特殊作用。基本R中
gsub()
有类似功能。## [1] "123456" "011"
## [1] "456,123" "011"
注意源数据中第二个元素因为不能匹配所以就原样返回了, 没有进行替换。
str_replace()
则仅对输入字符型向量的每个元素中模式的第一次出现进行替换, 不如str_replace_all()
常用。函数
str_remove_all()
相当于在str_replace_all()
中指定替换内容为空字符串。## [1] "123456" "011"
49.1.16 分组与捕获
在正则表达式中用圆括号来分出组,
确定优先规则; 组成一个整体; 拆分出模式中的部分内容(称为捕获); 定义一段供后续引用或者替换。 圆括号中的模式称为子模式,或者捕获。
49.1.16.1 确定优先级
在使用备择模式时,
James|Jim
是在单词James和Jim之间选择。 如果希望选择的是中间的mes
和Ji
怎么办? 可以将备择模式保护起来, 如Ja(mes|Ji)m
, 就可以确定备择模式的作用范围。正则表达式有比较复杂的优先级规则, 所以在不确定那些模式组合优先采纳时, 应该使用圆括号分组, 确定运算优先级。
49.1.16.2 组成整体
元字符问号、加号、星号、大括号等表示重复, 前面的例子中都是重复一个字符或者字符类。 如果需要重复由多个字符组成的模式, 如
x[[:digit:]]{2}
怎么办? 只要将该模式写在括号中,如:## [1] │ <x01x02> ## [2] │ _<x11>x9
上例的元数据中, 第一个元素重复了两次括号中的模式, 第二个元素仅有一次括号中的模式。
用表示重复的元字符(+
,*
)重复某个模式时, 从第二次开始, 并不是要重复前面的字符或者子串, 而是重复前面的模式。 比如上例中x01x02
。49.1.16.3 捕获与向后引用
有时一个模式中部分内容仅用于定位, 而实际有用的内容是其中的一部分, 就可以将这部分有用的内容包在圆括号中作为一个捕获。
分组是自动编号的, 以左开括号的序号为准(除了作为选项、有名捕获等开扩号以外)。 在替换或者向后引用时, 可以用
\1
,\2
等表示匹配中第一个左括号对应的分组, 第二个左扩号对应的分组,……。比如,如果想严格重复前面的某个子字符串怎么办? 在模式中可以用
\1
,\2
等表示严格重复前面捕获的子字符串, 这称为“向后引用”(back reference)。例如,
([a-z]{3})\1
这样的模式可以匹配如abcabc
,uxzuxz
这样的三字母重复:## [1] │ <abcabc>
又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:
## [1] │ <2008-08-08>
49.1.16.4 捕获与替换
利用分组捕获可以对字符串进行修改。
例:希望把带有前导零的数字的前导零删除,可以用如
## [2] │ <01204>
## [1] "1204" "1204" "001204B"
上例的模式中的
\b
表示单词边界, 所以中间的0不会被当作前导零, 不是整个数字的也不会被修改。上例中的
str_view()
仅用于调试目的, 在进行替换时不是必要步骤。例:为了交换横纵坐标,可以用如下替换
x <- "1st: (5,3.6), 2nd: (2.5, 1.1)" pat <- paste0( "[(]([[:digit:].]+),", "[[:space:]]*([[:digit:].]+)[)]", sep="") repl <- "(\\2, \\1)" str_view(x, pat)
## [1] │ 1st: <(5,3.6)>, 2nd: <(2.5, 1.1)>
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"
例: 要匹配
yyyy-mm-dd
这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:x <- c("1998-05-31", "2017-01-14") pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})" repl <- r"{\2/\3/\1}" str_view(x, pat)
## [1] │ <1998-05-31> ## [2] │ <2017-01-14>
## [1] "05/31/1998" "01/14/2017"
49.1.16.5 命名捕获
可以用
(?<name>...)
这样的格式对捕获命名, 而不是仅根据开括号的序号区分。x <- c("1998-05-31", "2017-01-14") pat <- r"{(?<year>[0-9]{4})-(?<mon>[0-9]{1,2})-(?<day>[0-9]{1,2})}" str_view(x, pat)
## [1] │ <1998-05-31> ## [2] │ <2017-01-14>
但
str_replace_all()
还不支持使用命名捕获作为替换。49.1.16.6 非捕获分组
如果某个分组仅想起到分组作用但是不会提取具体的匹配内容也不会用该组内容做替换, 可以将该组变成“非捕获分组”, 办法是把表示分组开始左圆括号变成
(?:
三个字符。 这在用分组表示优先级时比较有用, 如"Jam(es|Ji)m"
可以写成"Jam(?:es|Ji)m"
。 非捕获分组在向后引用和替换时不计入\1
、\2
这样的排列中。比如,把1921-2020之间的世纪号删去,可以用
## [1] │ <1978> ## [2] │ <2017> ## [3] │ <2035>
## [1] "78" "17" "35"
其中用了非捕获分组使得备择模式
19|20
优先匹配。 注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:x <- c("1978", "2017", "2035") pat1 <- r"{\A19(2[1-9]|[3-9][0-9])\Z}" pat2 <- r"{\A20([01][0-9]|20)\Z}" repl <- r"{\1}" str_view(x, pat1)
## [1] │ <1978>
str_replace_all(pat1, repl) |> str_replace_all(pat2, repl)## [2] │ <2017>
## [1] "78" "17" "2035"
为了找到字符型向量中哪些元素能匹配模式,
可以用
str_subset()
,
str_which()
,
str_detect()
。
为了提取字符型向量中匹配模式的部分子串,
可以用
str_extract()
和
str_extract_all()
。
为了提取字符型向量中匹配模式的部分以及捕获的部分,
可以用
str_match()
和
str_match_all()
。
如果要提取的一个模式以某个后缀作为标志,
可以将这个后缀作为模式的一部分,
然后仅捕获该后缀之前的部分。
对
http://baidu.com
,
https://baidu.com
,
ftp://baidu.com
等网址,
希望提取
http
,
https
,
ftp
等协议,
可以匹配到冒号为止:
addrs <- c( "http://baidu.com", "https://baidu.com", "ftp://baidu.com") pat1 <- r"{^(.+):}" str_view(addrs, pat1)
## [1] │ <http:>//baidu.com
## [2] │ <https:>//baidu.com
## [3] │ <ftp:>//baidu.com
这样的模式多匹配了一个“
:
”号。
可以用
str_match()
函数提取其中的捕获:
## [,1] [,2]
## [1,] "http:" "http"
## [2,] "https:" "https"
## [3,] "ftp:" "ftp"
结果是字符型矩阵,第二列是所需的内容。 但这样还是在整个匹配中包含了不需要的内容。
可以用
(?=)
包含要提取的模式的标志性后缀,
但匹配结果不包含这个后缀,如:
## [1] │ <http>://baidu.com
## [2] │ <https>://baidu.com
## [3] │ <ftp>://baidu.com
可见这次没有匹配“
:
”部分。
提取只要用
str_extract()
:
## [1] "http" "https" "ftp"
如果需要提取的部分以某个前缀为标志,
可以用
(?<=)
包含这个前缀,
最后匹配的模式不不含这个前缀。
设输入的字符串向量
addrs
中都是一些网址,
希望仅匹配
//
后面的主要部分,
## [1] │ http://<baidu.com>
## [2] │ https://<baidu.com>
## [3] │ ftp://<baidu.com>
可见仅匹配了
//
后面的内容。
## [1] "baidu.com" "baidu.com" "baidu.com"
还可以用
(?!)
包含不允许的后缀,
用
(?<!)
包含不允许的前缀。
tidyr::separate_wider_regex()
可以对一个字符型列用指定的模式拆分出其中的内容。
有如下的数据:
str_view()
函数
str_view(string, pattern)
在RStudio中加亮显示
pattern
给出的正则表达式模式在
string
中的所有匹配,并将每个匹配部分用
<>
界定。
在其它输出中不加亮显示,
仅使用
<>
界定。
string
是输入的源字符型向量。
如果要匹配的是固定字符串,
写成
str_view(string, fixed(pattern))
。
如果要匹配的是单词等的边界,
模式用
boundary()
函数表示,如
str_view("a brown fox", boundary("word"))
将匹配首个单词。
regex()
函数
stringr包用到正则表达式模式的地方,
实际上应该写成
regex(pattern)
,
只写模式本身是一种简写。
regex()
函数可以指定
ignore_case=TRUE
要求不区分大小写,
指定
multi_line=TRUE
使得
^
和
$
匹配用换行符分开的每行的开头和结尾,
dotall=TRUE
使得
.
能够匹配换行符。
comment=TRUE
使得模式可以写成多行,
行尾的井号后面表示注释,
这时空格不再原样匹配,
为了匹配空格需要写在方括号内或者用反斜杠开头。
与
regex()
类似的表示模式的函数有
fixed()
,
boundary()
,
coll()
。
可以用
paste0()
将多个字符串连接在一起构成一个模式。
可以在
regex()
中用
comment = TRUE
将模式写成多行带有行尾注释的形式。
例如,年月日格式的日期:
pat_ymd <- regex(r"{ (19|20)[[:digit:]]{2} # 年份 ([-/]) (0?[1-9]|1[012]) # 月 (1[0-9]|2[0-9]|3[01]|0?[1-9]) # 日 }", comments=TRUE) str_view(c( "1978-1-03", "1911-07-31", "2023-10-18", "3000-1-1", "2023-14-01"), pat_ymd)
## [1] │ <1978-1-03>
## [2] │ <1911-07-31>
## [3] │ <2023-10-18>
可以用
str_flatten(subpats, sep)
将保存在向量
subpats
中的元素用
sep
分隔符连接起来,
组成一个长模式。
为防止输入的内容有元字符,
可以用
str_escape()
进行转义。
str_escape()
将输入内容中的正则表达式元字符转义为原样匹配,
## [1] "x\\[1\\] = myfile\\.txt"
例:要匹配保存在字符型向量中的任何一个颜色名,
将这些颜色名用
|
连接成一个长的正则表达式后匹配。
subpats <- c("red", "blue", "green") x <- c("A red fox", "The white house", "Green sleeves") pat <- regex(str_flatten(str_escape(subpats), "|"), ignore_case=TRUE) str_view(x, pat)
## [1] │ A <red> fox
## [3] │ <Green> sleeves
str_detect(string, pattern)
返回字符型向量
string
的每个元素是否匹配
pattern
中的模式的逻辑型结果。
与基本R的
grepl()
作用类似。
## [1] │ New <the>me
## [3] │ In <the> present <the>me
## [1] TRUE FALSE TRUE
上例中的
str_view()
仅用作调试目的。
当要查找的内容是tibble的一列时,
用
filter()
与
str_detct()
配合,
可以进行行子集选择。
比如,在数据框的人名中查找中间有空格的名字:
## # A tibble: 1 × 1
## name
## <chr>
## 1 李 明
可以在
dplyr::summarize()
中用
sum(str_detect(x, pat))
计算符合模式的观测个数,
用
mean
计算比例。
因为
str_detect()
返回与输入源字符串向量等长的逻辑型向量,
所以特别适用于用逻辑运算来满足复杂的条件。
在姓名中找到同时有
a
和
y
的:
x <- c("Alice", "Becka", "Gail", "Karen", "Kathy", "Mary", "Sandy") x[str_detect(x, fixed("a")) & str_detect(x, fixed("y"))]
## [1] "Kathy" "Mary" "Sandy"
有
a
或有
y
的:
x <- c("Alice", "Becka", "Gail", "Karen", "Kathy", "Mary", "Sandy") x[str_detect(x, fixed("a")) | str_detect(x, fixed("y"))]
## [1] "Becka" "Gail" "Karen" "Kathy" "Mary" "Sandy"
用这种方法可以将复杂的匹配条件拆分为多个条件, 并用与、或、非组合起来。
str_count()
则返回模式在每个元素中匹配的次数。
## [1] 6 3 0
正则表达式在匹配时, 是否允许同一源字符串中的两次匹配有重叠部分? 这是不允许的。
## [1] │ <121>2<121>
## [1] 2
如果允许重叠,
就可以有3次
121
模式。
str_subset(string, pattern)
返回字符型向量中能匹配
pattern
的那些元素组成的子集,
与基本R函数
grep(pattern, string, value=TRUE)
效果相同。
注意,返回的是整个元素而不是匹配的子串。
比如,查找人名中间有空格的:
## [2] │ [<李 明>]
## [1] "[李 明]"
注意上例中仅返回了有匹配的元素, 而且是匹配元素的整个字符串而不是匹配的部分。
str_which(string, pattern)
返回字符型向量
string
的元素当中能匹配
pattern
中的模式的元素序号。
与基本R的
grep()
作用类似。
## [1] 1 3
str_subset()
返回的是有匹配的源字符串,
而不是匹配的部分子字符串。
用
str_extract(string, pattern)
从源字符串中取出
首次
匹配的子串,
没有匹配则不返回结果。
结果是一个与
str_subset()
长度相同的字符串向量,
仅匹配元素的首个匹配子串被返回。
因为输出长度不一定等于输入长度,
所以这个函数应该慎用。
## [1] │ A f<all>ing b<all>
## [1] "all"
注意第二个输入字符串不匹配所有返回结果中不包含该元素的对应输出。
这会造成结果的对应困难,
所以应仅在确保所有输入都能匹配时才使用
str_extract()
这个函数,
一般应使用
str_extract_all()
。
str_extract_all(string, pattern)
取出所有匹配子串,
结果是一个列表,
长度与输入的字符型向量
string
的元素个数相等,
列表的每个元素对应于
string
的每个元素。
结果列表的每个元素是一个字符型数组,
存放所有匹配的子字符串,
没有匹配的输入字符串对应的输出元素是空向量(长度为0的向量)。
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
## [[1]]
## [1] "all" "all"
## [[2]]
## [1] "all"
## [[3]]
## character(0)
str_extract_all()
可以加选项
simplyfy=TRUE
,
使得返回结果变成一个字符型矩阵,
每行是原来一个元素中取出的各个子串,
列数等于最大匹配次数,
没有那么多匹配次数的填以空字符串。
如果正常匹配结果不会出现空字符就可以用这种方法简化结果的保存和访问。
## [,1] [,2]
## [1,] "all" "all"
## [2,] "all" ""
## [3,] "" ""
str_subset()
提取的是能匹配模式的元素子集,
而不是匹配的模式或者捕获;
str_extract()
和
str_extract_all()
提取的是每个元素的首次或者所有匹配的子字符串,
而不是其中的捕获。
str_match(string, pattern)
提取每个元素的首次匹配内容以及其中各个捕获分组内容,
结果是一个矩阵,
行数等于输入的字符型向量
string
的元素个数,
矩阵每行对应于向量
string
中的一个元素,
每行第一个元素是匹配内容,其它元素是各个捕获,
没有则为字符型缺失值(不是空字符串)。
比如,希望匹配中间有空格的人名并捕获空格前后部分:
## [,1] [,2] [,3]
## [1,] NA NA NA
## [2,] "李 明" "李" "明"
上例中源数据第一个元素没有匹配,
所以结果都是缺失值
NA
,
第二个元素的结果在第二行,
首先是整个匹配的子字符串,
然后是捕获的两个部分。
stringr::str_match_all(string, pattern)
匹配每个字符串中所有出现位置,
结果是一个列表,
每个列表元素对应于输入的字符型向量
string
的每个元素,
结果中每个列表元素是一个字符型矩阵,
用来保存所有各个匹配以及匹配中的捕获,
每行是一个匹配的结果,首先是匹配结果,其次是各个捕获。
结果列表中每个作为列表元素的矩阵大小不一定相同。
当某个元素完全没有匹配时,
结果列表中对应元素是行数为0的矩阵。
比如,模式为19xx或者20xx的年份, 并将其分为前两位和后两位:
x <- c("1978-2000", "2011-2020-2099", "2100-2199") pat <- r"{\b(19|20)([0-9]{2})\b}" str_view(x, pat)
## [1] │ <1978>-<2000>
## [2] │ <2011>-<2020>-<2099>
## [[1]]
## [,1] [,2] [,3]
## [1,] "1978" "19" "78"
## [2,] "2000" "20" "00"
## [[2]]
## [,1] [,2] [,3]
## [1,] "2011" "20" "11"
## [2,] "2020" "20" "20"
## [3,] "2099" "20" "99"
## [[3]]
## [,1] [,2] [,3]
下面的程序合并上面提取的年份后两位为一个字符型向量:
## [1] "78" "00" "11" "20" "99"
用基本R功能:
## [1] "78" "00" "11" "20" "99"
str_locate(string, pattern)
对输入字符型向量
string
的每个元素返回首次匹配
pattern
的开始和结束位置。
输出结果是一个两列的矩阵,每行对应于输入的一个元素,
每行的两个元素分别是首次匹配的开始和结束字符序号(按字符计算)。如
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
## start end
## [1,] 4 6
## [2,] 8 10
str_locate_all(string, pattern)
则可以返回每个元素中所有匹配的开始和结束位置,
结果是一个列表,
每个列表元素对应于输入字符型向量的每个元素,
结果中每个列表元素是一个两列的数值型矩阵,
每行为一个匹配的开始和结束字符序号。如
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
## [[1]]
## start end
## [1,] 4 6
## [2,] 12 14
## [[2]]
## start end
## [1,] 8 10
注意如果需要取出匹配的元素可以用
str_subset()
,
要取出匹配的子串可以用
str_extract()
和
str_extract_all()
,
取出匹配的子串以及分组捕获可以用
str_match()
和
str_match_all()
。
基本R函数
grep
,
sub
,
gsub
,
regexpr
,
gregexpr
,
regexec
中的
pattern
参数可以是正则表达式,
这时应设参数
fixed=FALSE
。
strsplit
函数中的参数
split
也可以是正则表达式。
regmatches
函数从
regexpr
,
gregexpr
,
regexec
的结果中提取匹配的字符串。
以原样匹配为例。
## [1] 5 -1 4
## attr(,"match.length")
## [1] 3 -1 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
这里使用了
regexpr
函数。
regexpr
函数的一般用法为:
x <- c("New theme", "Old times", "In the present theme") regexpr(pattern, text, ignore.case = FALSE, perl = FALSE, fixed = FALSE, useBytes = FALSE)
自变量为:
fixed=TRUE
选项,则当作普通原样文本来匹配;
perl=TRUE
,perl语言的正则表达式是事实上的标准,所以这样兼容性更好;
fixed=TRUE
时
pattern
作为普通原样文本解释;
regexpr()
函数返回一个整数值的向量,
长度与
text
向量长度相同,
结果的每个元素是在
text
的对应元素中
pattern
的首次匹配位置;
没有匹配时结果元素取-1。
结果会有一个
match.length
属性,表示每个匹配的长度,
无匹配时取-1。
如果仅关心源字符串向量
text
中哪些元素能匹配
pattern
,
可以用
grep
函数,如
## [1] 1 3
结果说明源字符串向量的三个元素中仅有第1、第3号元素能匹配。
如果都不匹配,返回
integer(0)
。
grep
可以使用与
regexpr
相同的自变量,
另外还可以加选项
invert=TRUE
,这时返回的是不匹配的元素的下标。
grep()
如果添加选项
value=TRUE
,
则结果不是返回有匹配的元素的下标而是返回有匹配的元素本身(不是匹配的子串),
## [1] "New theme" "In the present theme"
grepl
的作用与
grep
类似,
但是其返回值是一个长度与源字符串向量
text
等长的逻辑型向量,
每个元素的真假对应于源字符串向量中对应元素的匹配与否。如
## [1] TRUE FALSE TRUE
就像
grep()
与
grepl()
本质上给出相同的结果,只是结果的表示方式不同,
regexec()
与
regexpr()
也给出仅在表示方式上有区别的结果。
regexpr()
主要的结果是每个元素的匹配位置,
用一个统一的属性返回各个匹配长度;
regexec()
则返回一个与源字符串向量等长的列表,
列表的每个元素为匹配的位置,并且列表的每个元素有匹配长度作为属性。
所以,这两个函数只需要用其中一个就可以,下面仅使用
regexpr()
。
regexec()
的使用效果如
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## [[3]]
## [1] 4
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
grep()
,
grepl()
,
regexpr()
,
regexec()
都只能找到源字符串向量的每个元素中模式的首次匹配,
不能找到所有匹配。
gregexpr()
函数可以找到所有匹配。
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## [[3]]
## [1] 4 16
## attr(,"match.length")
## [1] 3 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
其结果是一个与源字符串向量等长的列表,
格式与
regexec()
的结果格式类似,
列表的每个元素对应于源字符串向量的相应元素,
列表元素值为匹配的位置,
并有属性
match.length
保存了匹配长度。
匹配位置和匹配长度包含了所有的匹配,
见上面例子中第三个元素的匹配结果。
函数
grep
,
grepl
结果仅给出每个元素能否匹配。
regexpr()
,
regexec()
,
gregexpr()
则包含了匹配位置与匹配长度,
这时,可以用
regmatches()
函数取出具体的匹配字符串。
regmatches()
一般格式为
其中
x
是源字符串向量,
m
是
regexpr()
、
regexec()
或
gregexpr()
的匹配结果。
x <- c("New theme", "Old times", "In the present theme") m <- regexpr("the", x, perl=TRUE) regmatches(x, m)
## [1] "the" "the"
可以看出,
regmatches()
仅取出有匹配时的匹配内容,
无匹配的内容被忽略。
取出多处匹配的例子如:
x <- c("New theme", "Old times", "In the present theme") m <- gregexpr("the", x, perl=TRUE) regmatches(x, m)
## [[1]]
## [1] "the"
## [[2]]
## character(0)
## [[3]]
## [1] "the" "the"
当
regmatches()
第二个自变量是
gregexpr()
的结果时,
其输出结果变成一个列表,
并且不再忽略无匹配的元素,
无匹配元素对应的列表元素为
character(0)
,
即长度为零的字符型向量。
对有匹配的元素,
对应的列表元素为所有的匹配字符串组成的字符型向量。
pattern
中没有正则表达式,
grep()
,
grepl()
,
regexpr()
,
gregexpr()
中都可以用
fixed=TRUE
参数取代
perl=TRUE
参数,
这时匹配总是解释为原样匹配,
即使
pattern
中包含特殊字符也是进行原样匹配。
在基本R中,
为了不区分大小写匹配,
可以在
grep
等函数调用时加选项
ignore.case=TRUE
;
## [1] 1
## [1] 1 2 3
## [1] 1 2 3
在模式中用“.”匹配任意一个字符(除了换行符
"\n"
,能否匹配此字符与选项有关)。如
## [1] 1 2 -1
## attr(,"match.length")
## [1] 3 3 -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
regexpr
仅给出每个元素中模式的首次匹配位置而不是给出匹配的内容。
regmatches
函数以原始字符型向量和匹配结果为输入,
结果返回每个元素中匹配的各个子字符串(不是整个元素),如:
## [1] "abc" "abs"
注意返回结果和输入字符型向量元素不是一一对应的,仅返回有匹配的结果。
像句点这样的字符称为元字符(meta characters),
在正则表达式中有特殊函数。
如果需要匹配句点本身,用“
[.]
”或者“
\.
”表示。
比如,要匹配
a.txt
这个文件名,如下做法有错误:
## [1] 1 2
结果连
a0txt
也匹配了。用“[.]”表示句点则将句点不做特殊解释:
## [1] 1
## [1] 1
注意在R语言字符型常量中一个
\
需要写成两个。
如果仅需按照原样进行查找,
也可以在
grep()
,
grepl()
,
regexpr()
,
gregexpr()
等函数中加选项
fixed=TRUE
,
这时不要再用
perl=TRUE
选项。
## [1] 1
模式“
[ns]a.[.]xls
” 表示匹配的第一个字符是
n
或
s
,
第二个字符是
a
,第三个字符任意,第四个字符是句点,
然后是
xls
。
## [1] 1 2 1
## attr(,"match.length")
## [1] 7 7 7
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## [1] 2
也可以用“
[[]
”表示“
[
”, 用“
[]]
”表示“
]
”,如
## [1] 2
## [1] 1
## [1] 1
## [1] 1
只匹配了第一个输入字符串。
m <- regexpr("s\\w[.]", c("file-s1.xls", "s#.xls"), perl=TRUE) regmatches(c("file-s1.xls", "s#.xls"), m)
## [1] "s1."
可以看出,模式匹配了
s1.
而没有匹配
s#.
。
## [1] 4 8
## attr(,"match.length")
## [1] 1 1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
匹配了两个换行符。
## [1] 1 2 3 4
## [1] "sa1" "sa123"
p <- "^[[:alnum:]_]+@[[:alnum:]_]+[.][[:alnum:]_]+$" x <- "abc123@efg.com" m <- regexpr(p, x, perl=TRUE) regmatches(x, m)
## [1] "abc123@efg.com"
匹配的电子邮件地址在
@
前面可以使用任意多个字母、数字、下划线,
在
@
后面由小数点分成两段,
每段可以使用任意多个字母、数字、下划线。
这里用了
^
和
$
表示全字符串匹配。
^https?://[[:alnum:]./]+$
可以匹配http或https开始的网址。
## [1] 1 2
(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)
## [1] 3 4
模式匹配的是三位的数字。
日期匹配例:
pat <- paste( c("[[:digit:]]{1,2}[-/]", "[[:digit:]]{1,2}[-/]", "[[:digit:]]{2,4}"), collapse="") grep(pat, c("2/4/1998", "13/15/198"))
## [1] 1 2
s <- "<B>1st</B> other <B>2nd</B>" p1 <- "<[Bb]>.*</[Bb]>" m1 <- regexpr(p1, s, perl=TRUE) regmatches(s, m1)[[1]]
## [1] "<B>1st</B> other <B>2nd</B>"
我们本来期望的是提取第一个“
<B>
……
</B>
”组合, 不料提取了两个“
<B>
……
</B>
”组合以及中间的部分。
比如,上例中模式修改后得到了期望的结果:
s <- "<B>1st</B> other <B>2nd</B>" p2 <- "<[Bb]>.*?</[Bb]>" m2 <- regexpr(p2, s, perl=TRUE) regmatches(s, m2)[[1]]
## [1] "<B>1st</B>"
## [1] 1
句点通配符一般不能匹配换行,如
## integer(0)
跨行匹配失败。而在HTML的规范中换行是正常的。
一种办法是预先用
gsub
把所有换行符替换为空格。
但是这只能解决部分问题。
另一方法是在Perl正则表达式开头添加
(?s)
选项,
这个选项使得句点通配符可以匹配换行符。
## [1] "<B>1st\n</B>"
多行模式例:
s <- "<B>1st\n</B>\n" mres1 <- gregexpr("^<.+?>", s, perl=TRUE) mres2 <- gregexpr("(?m)^<.+?>", s, perl=TRUE) regmatches(s, mres1)[[1]]
## [1] "<B>"
## [1] "<B>" "</B>"
字符串
s
包含两行内容,中间用
\n
分隔。
mres1
的匹配模式没有打开多行选项,
所以模式中的
^
只能匹配
s
中整个字符串开头。
mres2
的匹配模式打开了多行选项,
所以模式中的
^
可以匹配
s
中每行的开头。
例如,某个人的名字用James和Jim都可以,
表示为
James|Jim
, 如
s <- c("James, Bond", "Jim boy") pat <- "James|Jim" mres <- gregexpr(pat, s, perl=TRUE) regmatches(s, mres)
## [[1]]
## [1] "James"
## [[2]]
## [1] "Jim"
<B>
……
</B
”两边的“
<B>
”和“
</B>
”删除,
可以用如下的替换方法:
x <- "<B>1st</B> other <B>2nd</B>" pat <- "(?s)<[Bb]>(.+?)</[Bb]>" repl <- "\\1" gsub(pat, repl, x, perl=TRUE)
## [1] "1st other 2nd"
替换模式中的
\1
(写成R字符型常量时
\
要写成
\\
)表示第一个圆括号匹配的内容,
但是表示选项的圆括号(
(?s)
)不算在内。
例:希望把带有前导零的数字的前导零删除,可以用如
x <- c("123", "0123", "00123") pat <- "\\b0+([1-9][0-9]*)\\b" repl <- "\\1" gsub(pat, repl, x, perl=TRUE)
## [1] "123" "123" "123"
其中的
\b
模式表示单词边界,
这可以排除在一个没有用空格或标点分隔的字符串内部拆分出数字的情况。
例:为了交换横纵坐标,可以用如下替换
s <- "1st: (5,3.6), 2nd: (2.5, 1.1)" pat <- paste0( "[(]([[:digit:].]+),", "[[:space:]]*([[:digit:].]+)[)]") repl <- "(\\2, \\1)" gsub(pat, repl, s, perl=TRUE)
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"
例如,要匹配yyyy-mm-dd这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:
pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})" repl <- "\\2/\\3/\\1" gsub(pat, repl, c("1998-05-31", "2017-01-14"))
## [1] "05/31/1998" "01/14/2017"
分组除了可以做替换外,
还可以用来表示模式中的重复出现内容。
例如,
([a-z]{3})\1
这样的模式可以匹配如
abcabc
,
uxzuxz
这样的三字母重复。如
## [1] 1
又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:
## [1] "2008-08-08"
下面是一个非捕获分组示例。 设需要把1921-2020之间的世纪号删去,可以用
pat <- "\\A(?:19|20)([0-9]{2})\\Z" repl <- "\\1" x <- c("1978", "2017", "2035") gsub(pat, repl, x, perl=TRUE)
## [1] "78" "17" "35"
其中用了非捕获分组使得备择模式
19|20
优先匹配。
注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:
侠义非凡的铅笔 · Ubuntu-MPI - 知乎 1 年前 |