提高Python代码性能

3

如何提高这段简单的Python代码的性能? 使用re.search查找匹配行难道不是最好的方法吗?因为它比Perl慢近6倍,或者我做错了什么吗?

#!/usr/bin/env python

import re
import time
import sys

i=0
j=0
time1=time.time()
base_register =r'DramBaseAddress\d+'
for line in  open('rndcfg.cfg'):
    i+=1
    if(re.search(base_register, line)):
        j+=1
time2=time.time()

print (i,j)
print (time2-time1)    
print (sys.version)

这段代码完成的时间大约为0.96秒(10次运行的平均值)
输出结果:

168197 2688
0.8597519397735596
3.3.2 (default, Sep 24 2013, 15:14:17)
[GCC 4.1.1]

以下 Perl 代码可以在 0.15 秒内完成此操作。
#!/usr/bin/env perl
use strict;
use warnings;

use Time::HiRes qw(time);

my $i=0;my $j=0;
my $time1=time;
open(my $fp, 'rndcfg.cfg');
while(<$fp>)
{
    $i++;
    if(/DramBaseAddress\d+/)
    {
        $j++;
    }
}
close($fp);
my $time2=time;

printf("%d,%d\n",$i,$j);
printf("%f\n",$time2-$time1);
printf("%s\n",$]);


输出:

168197,2688
0.135579
5.012001

编辑:修正了正则表达式——这会稍微降低性能


1
如果您需要多次使用正则表达式,或者在这种情况下,请使用“in”而不是“if base_register in line:”,您应该预编译正则表达式。 - Klaus D.
1
如果您阅读re.compile的注释,您会看到:“最近传递给re.compile()和模块级匹配函数的模式的编译版本被缓存,因此一次只使用少量正则表达式的程序无需担心编译正则表达式。” - Matthias
3个回答

5
实际上,在Python中,正则表达式比字符串方法效率低。从https://docs.python.org/2/howto/regex.html#use-string-methods得知:
“字符串有几种用于执行固定字符串操作的方法,它们通常更快,因为实现是一个被优化过的单个小C循环,而不是大型、更通用的正则表达式引擎。”
如果用str.find替换re.search将会使运行时间更短。否则,使用其他人建议的in操作符也将被优化。
至于Python和Perl版本之间的速度差异,我只能归功于每种语言本身的内在质量:text processing - python vs perl performance

我如何在正则表达式中使用 str.find - Jean
哦,str.find根本不是正则表达式;它只是一个简单的子字符串匹配。我刚刚注意到你的正则表达式中有\d+部分,因此这可能并不完全适用,但如果您只想计算带有“DramBaseAddress”的行数(也就是说,如果尾随数字没有区别),那么可以将if(re.search(base_register, line)):替换为if "DramBaseAddress" in line:if line.find("DramBaseAddress") > -1:。然而,如果尾随数字很重要,那么最好的选择是下面@Veedrac的答案。 - oxymor0n

1
在这种情况下,您使用的是固定字符串,而不是正则表达式。
对于普通字符串,有更快的方法:
>>> timeit.timeit('re.search(regexp, "banana")', setup = "import re;     regexp=r'nan'")
1.2156920433044434
>>> timeit.timeit('"banana".index("nan")')
0.23752403259277344
>>> timeit.timeit('"banana".find("nan")')
0.2411658763885498

现在这种文本处理是Perl的拿手好戏(也称作实用提取和报告语言)(也称作病态折中垃圾列表生成器),并且多年来已经进行了广泛的优化。所有这些集体的关注都会累积起来。

1

即使有缓存,调用re.compile的开销也非常大。建议使用

is_wanted_line = re.compile(r"DramBaseAddress\d+").search

for i, line in enumerate(open('rndcfg.cfg')):
    if is_wanted_line(line):
        j += 1

代替。

此外,您可以执行

key = "DramBaseAddress"
is_wanted_line = re.compile(r"DramBaseAddress\d+").search

for i, line in enumerate(open('rndcfg.cfg')):
    if key in line and is_wanted_line(line):
        j += 1

为了进一步降低开销。

您还可以考虑自己进行缓冲:

key = b"DramBaseAddress"
is_wanted_line = re.compile(rb"DramBaseAddress\d+").search

with open("rndcfg.cfg", "rb") as file:
    rest = b""

    for chunk in iter(lambda: file.read(32768), b""):
        i += chunk.count(b"\n")
        chunk, _, rest = (rest + chunk).rpartition(b"\n")

        if key in rest and is_wanted_line(chunk):
            j += 1

    if key in rest and is_wanted_line(rest):
        j += 1

该方法可以去除换行和编码开销。(这并不完全相同,因为它没有考虑每个块中的多个实例。这种行为相对简单,但在您的情况下可能并不严格需要。)

这有点重量级,但如果删除 i += chunk.count(b"\n"),则速度是 Perl 的三倍 - 如果删除则为 8 倍!


你有没有任何测试来验证“尽管有缓存,调用re.compile的开销仍然很大”的说法?我经常使用re.compile lol - oxymor0n
1
@oxymor0n 我计时了,它花费了将近三分之二的时间。请注意,对于一次性使用或搜索长字符串,开销很可能会得到更好的摊销。 - Veedrac
有趣,谢谢。我一直以为缓存性能会比那更好:/ - oxymor0n

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接