提高C++正则替换性能
我是一名初学C++的程序员,正在做一个小项目,需要处理一些比较大的XML文件,并把里面的XML标签去掉。我用C++0x的正则表达式库成功实现了这个功能。不过,我遇到了一些性能问题。光是读取文件和执行正则替换这个操作,在我的电脑上就要花大约6秒钟。如果我加上一些编译器的优化选项,这个时间可以缩短到2秒。但用Python的话,我可以在不到100毫秒的时间内完成。显然,我的C++代码效率很低。我该怎么做才能加快这个速度呢?
我的C++代码:
std::regex xml_tags_regex("<[^>]*>");
for (std::vector<std::string>::iterator it = _files.begin(); it !=
_files.end(); it++) {
std::ifstream file(*it);
file.seekg(0, std::ios::end);
size_t size = file.tellg();
std::string buffer(size, ' ');
file.seekg(0);
file.read(&buffer[0], size);
buffer = regex_replace(buffer, xml_tags_regex, "");
file.close();
}
我的Python代码:
regex = re.compile('<[^>]*>')
for filename in filenames:
with open(filename) as f:
content = f.read()
content = regex.sub('', content)
补充一下,我并不在乎一次性处理完整个文件。我发现逐行、逐词或逐字符读取文件会显著降低速度。
2 个回答
C++11的正则表达式替换确实比较慢,至少目前是这样。不过,PCRE在模式匹配速度上表现得要好得多,但PCRECPP在基于正则表达式的替换方面提供的功能非常有限,正如手册中所说:
你可以用“rewrite”替换“str”中“pattern”的第一个匹配项。在“rewrite”中,可以使用反斜杠转义的数字(\1到\9)来插入与模式中对应的括号组匹配的文本。 \0在“rewrite”中指的是整个匹配的文本。
这和Perl的's'命令相比,实在是太差了。这就是我为什么为PCRE写了一个C++的封装器,它能以接近Perl的's'命令的方式处理基于正则表达式的替换,并且支持16位和32位的字符字符串:PCRSCPP:
命令字符串语法
命令语法遵循Perl的
s/pattern/substitute/[options]
约定。任何字符(除了反斜杠\
)都可以作为分隔符,不仅仅是/
,但如果在pattern
、substitute
或options
子字符串中使用分隔符,确保用反斜杠转义,例如:
s/\\/\//g
用来把所有反斜杠替换成正斜杠记得在C++代码中双写反斜杠,除非使用原始字符串字面量(见字符串字面量):
pcrscpp::replace rx("s/\\\\/\\//g");
模式字符串语法
模式字符串直接传递给
pcre*_compile
,因此必须遵循PCRE语法,具体可以参考PCRE文档。替换字符串语法
替换字符串的反向引用语法类似于Perl:
$1
...$n
:第n个捕获子模式匹配。$&
和$0
:整个匹配${label}
:匹配的带标签的子模式。label
最多可以是32个字母数字加下划线字符('A'-'Z'
,'a'-'z'
,'0'-'9'
,'_'
),第一个字符必须是字母$`
和$'
(反引号和撇号)分别指的是匹配前后文本的区域。和Perl一样,使用的是未修改的文本,即使之前进行了全局替换。此外,以下转义序列也被识别:
\n
:换行\r
:回车\t
:水平制表符\f
:换页符\b
:退格\a
:警报,铃声\e
:转义\0
:二进制零任何其他转义序列
\<char>
,都被解释为<char>
,这意味着你也需要转义反斜杠。选项字符串语法
像Perl一样,选项字符串是一系列允许的修饰符字母。PCRSCPP识别以下修饰符:
- 与Perl兼容的标志
g
:全局替换,不仅仅是第一个匹配i
:不区分大小写的匹配
(PCRE_CASELESS)m
:多行模式:^
和$
还会匹配换行符前后的位置
(PCRE_MULTILINE)s
:让.
元字符的范围包括换行符 (将换行符视为普通字符)
(PCRE_DOTALL)x
:允许扩展的正则表达式语法, 在复杂模式中启用空格和注释
(PCRE_EXTENDED)- 与PHP兼容的标志
A
:“锚定”模式:只查找“锚定”的匹配项:从零偏移开始的匹配。在单行模式下,与在所有模式的替代分支前加^
是一样的
(PCRE_ANCHORED)D
:将美元符号$
视为主题结束断言,仅覆盖默认的结束,或在结束时换行前立即结束。 在多行模式下被忽略
(PCRE_DOLLAR_ENDONLY)U
:反转*
和+
的贪婪逻辑:默认情况下不贪婪,?
切换回贪婪。(?U)
和(?-U)
在模式中的切换保持不变
(PCRE_UNGREEDY)u
:Unicode模式。将模式和主题视为UTF8/UTF16/UTF32字符串。 与PHP不同,也影响换行符,\R
,\d
,\w
等的匹配
((PCRE_UTF8/PCRE_UTF16/PCRE_UTF32) | PCRE_NEWLINE_ANY | PCRE_BSR_UNICODE | PCRE_UCP)- PCRSCPP自己的标志:
N
:跳过空匹配
(PCRE_NOTEMPTY)T
:将替换视为一个简单字符串,即不进行反向引用 和转义序列解释n
:丢弃不匹配的字符串部分以进行替换
注意:PCRSCPP并不会自动添加换行符, 替换结果是匹配的简单连接, 在多行模式下要特别注意这一点
我写了一段简单的速度测试代码,存储了一个“move.sh”文件的10倍副本,并测试了正则表达式在结果字符串上的性能:
#include <pcrscpp.h>
#include <string>
#include <iostream>
#include <fstream>
#include <regex>
#include <chrono>
int main (int argc, char *argv[]) {
const std::string file_name("move.sh");
pcrscpp::replace pcrscpp_rx(R"del(s/(?:^|\n)mv[ \t]+(?:-f)?[ \t]+"([^\n]+)"[ \t]+"([^\n]+)"(?:$|\n)/$1\n$2\n/Dgn)del");
std::regex std_rx (R"del((?:^|\n)mv[ \t]+(?:-f)?[ \t]+"([^\n]+)"[ \t]+"([^\n]+)"(?:$|\n))del");
std::ifstream file (file_name);
if (!file.is_open ()) {
std::cerr << "Unable to open file " << file_name << std::endl;
return 1;
}
std::string buffer;
{
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0);
if (size > 0) {
buffer.resize(size);
file.read(&buffer[0], size);
buffer.resize(size - 1); // strip '\0'
}
}
file.close();
std::string bigstring;
bigstring.reserve(10*buffer.size());
for (std::string::size_type i = 0; i < 10; i++)
bigstring.append(buffer);
int n = 10;
std::cout << "Running tests " << n << " times: be patient..." << std::endl;
std::chrono::high_resolution_clock::duration std_regex_duration, pcrscpp_duration;
std::chrono::high_resolution_clock::time_point t1, t2;
std::string result1, result2;
for (int i = 0; i < n; i++) {
// clear result
std::string().swap(result1);
t1 = std::chrono::high_resolution_clock::now();
result1 = std::regex_replace (bigstring, std_rx, "$1\\n$2", std::regex_constants::format_no_copy);
t2 = std::chrono::high_resolution_clock::now();
std_regex_duration = (std_regex_duration*i + (t2 - t1)) / (i + 1);
// clear result
std::string().swap(result2);
t1 = std::chrono::high_resolution_clock::now();
result2 = pcrscpp_rx.replace_copy (bigstring);
t2 = std::chrono::high_resolution_clock::now();
pcrscpp_duration = (pcrscpp_duration*i + (t2 - t1)) / (i + 1);
}
std::cout << "Time taken by std::regex_replace: "
<< std_regex_duration.count()
<< " ms" << std::endl
<< "Result size: " << result1.size() << std::endl;
std::cout << "Time taken by pcrscpp::replace: "
<< pcrscpp_duration.count()
<< " ms" << std::endl
<< "Result size: " << result2.size() << std::endl;
return 0;
}
(注意std
和pcrscpp
的正则表达式在这里做的是一样的,pcrscpp
表达式中的尾随换行是因为std::regex_replace
没有去掉换行,尽管使用了std::regex_constants::format_no_copy
)
并在一个大型(20.9 MB)的shell移动脚本上运行:
Running tests 10 times: be patient...
Time taken by std::regex_replace: 12090771487 ms
Result size: 101087330
Time taken by pcrscpp::replace: 5910315642 ms
Result size: 101087330
如你所见,PCRSCPP的速度超过了2倍。我预计随着模式复杂度的增加,这个差距会更大,因为PCRE在处理复杂模式时表现得更好。我最初是为自己写了一个封装器,但我觉得它对其他人也会有用。
祝好,
Alex
我觉得你并没有做错什么,只是C++的正则表达式库在这个用例下速度没有Python的快。这也不奇怪,因为Python的正则表达式底层其实也是用C/C++写的,而且经过多年的优化,速度变得相当快,这在Python中是个很重要的特性,所以自然会很快。
不过,如果你需要更快的速度,C++还有其他选择。我以前用过PCRE(http://pcre.org/),效果很好,当然现在也有其他不错的库。
不过在这个特定的情况下,你其实可以不使用正则表达式就能实现你的目标,我快速测试后发现这样可以提高10倍的性能。例如,下面的代码会扫描你的输入字符串,把所有内容复制到一个新的缓冲区,当遇到<
时,它会开始跳过字符,直到看到结束的>
。
std::string buffer(size, ' ');
std::string outbuffer(size, ' ');
... read in buffer from your file
size_t outbuffer_len = 0;
for (size_t i=0; i < buffer.size(); ++i) {
if (buffer[i] == '<') {
while (buffer[i] != '>' && i < buffer.size()) {
++i;
}
} else {
outbuffer[outbuffer_len] = buffer[i];
++outbuffer_len;
}
}
outbuffer.resize(outbuffer_len);