提高C++正则替换性能

11 投票
2 回答
4880 浏览
提问于 2025-04-20 21:17

我是一名初学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 个回答

3

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]约定。任何字符(除了反斜杠\)都可以作为分隔符,不仅仅是/,但如果在patternsubstituteoptions子字符串中使用分隔符,确保用反斜杠转义,例如:

  • 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识别以下修饰符:

  1. 与Perl兼容的标志
    • g:全局替换,不仅仅是第一个匹配
    • i:不区分大小写的匹配
      (PCRE_CASELESS)
    • m:多行模式:^$还会匹配换行符前后的位置
      (PCRE_MULTILINE)
    • s:让.元字符的范围包括换行符 (将换行符视为普通字符)
      (PCRE_DOTALL)
    • x:允许扩展的正则表达式语法, 在复杂模式中启用空格和注释
      (PCRE_EXTENDED)
  2. 与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)
  3. 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;
}

(注意stdpcrscpp的正则表达式在这里做的是一样的,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

2

我觉得你并没有做错什么,只是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);

撰写回答