关于优化:在不同字符类型之间插入空格的Python脚本:为什么这个*如此*慢?

Python script to insert space between different character types: Why is this *so* slow?

我正在处理一些混合语言的文本,我已经对这些文本进行了一些处理,并且这些文本的形式是单个字符列表(称为"字母")。我可以通过简单地测试每个字符是否有大小写(使用一个称为"test_lang"的小函数)来判断它是哪种语言。然后我想在不同类型的字符之间插入一个空格,所以我没有任何混合字符类型的单词。同时,我想在单词和标点之间插入一个空格(我在一个名为"punc"的列表中定义了这个空格)。我写了一个脚本,它以非常直截了当的方式完成了这项工作,这对我来说是有意义的(见下文),但显然是错误的方式,因为它的速度非常慢。

有人能告诉我做这个更好的方法是什么吗?

1
2
3
4
5
6
7
8
9
10
11
12
# Add a space between Arabic/foreign mixes, and between words and punc
cleaned =""
i = 0
while i <= len(letters)-2: #range excludes last letter to avoid Out of Range error for i+1
    cleaned += letters[i]
    # words that have case are Latin; otherwise Arabic
    if test_lang(letters[i]) != test_lang(letters[i+1]):
        cleaned +=""
    if letters[i] in punc or letters[i+1] in punc:
        cleaned +=""
    i += 1
cleaned += letters[len(letters)-1] # add in last letter


这里发生了一些事情:

  • 对字符串中的每个字母调用test_lang()两次,这可能是速度慢的主要原因。
  • 在python中连接字符串不是很有效,您应该使用一个列表或生成器,然后使用str.join()(很可能是''.join())。

下面是我将采用的方法,使用itertools.groupby()

1
2
3
4
5
from itertools import groupby
def keyfunc(letter):
    return (test_lang(letter), letter in punc)

cleaned = ' '.join(''.join(g) for k, g in groupby(letters, keyfunc))

这将把字母组合成相同语言的连续字母,不管它们是否标点符号,然后''.join(g)将每个组转换回一个字符串,然后' '.join()组合这些字符串,在每个字符串之间添加一个空格。

此外,正如DSM的评论所指出的,确保punc是一套。


每次执行字符串串联时,都会创建一个新的字符串。字符串得到的时间越长,每次连接所花费的时间就越长。

http://en.wikipedia.org/wiki/schlemiel-the-painter's-u算法

您最好声明一个足够大的列表来存储输出的字符,并在末尾加入它们。


我建议一个完全不同的解决方案,应该非常快:

1
2
import re
cleaned = re.sub(r"(?<!\s)\b(?!\s)","", letters, flags=re.LOCALE)

这会在每个单词边界处插入一个空格(将单词定义为"字母数字字符序列,包括当前区域设置中的重音字符",这在大多数情况下都适用),除非它是靠近空白的单词边界。

这应该分为拉丁和阿拉伯字符以及拉丁和标点符号。


保持操作的原始代码的基本逻辑,我们不做所有的[i]和[i+1]索引来加速它。我们使用prev和next引用扫描字符串,保持prev在next后面一个字符:

1
2
3
4
5
6
7
8
9
10
11
# Add a space between Arabic/foreign mixes, and between words and punc
cleaned = ''
prev = letters[0]
for next in letters[1:]:
    cleaned += prev
    if test_lang(prev) != test_lang(next):
        cleaned += ' '
    if prev in punc or next in punc:
        cleaned += ' '
    prev = next
cleaned += next

对1000万个字符的字符串进行测试表明,这大约是操作代码速度的两倍。正如其他人指出的那样,"字符串串联很慢"的抱怨已经过时了。使用""隐喻再次运行测试。与使用字符串串联相比,join(…)隐喻的执行速度稍慢。

进一步的加速可能不是通过调用test_lang()函数,而是通过嵌入一些简单的代码来实现。无法评论,因为我不知道test_lang()的作用是什么。

编辑:删除了不应该存在的"返回"语句(测试剩余部分!).

编辑:也可以通过不在同一个字符上两次调用test_lang()来加快速度(在一个循环中的下一个循环中,然后在下面的循环中调用prev)。缓存测试语言(下一个)结果。


这里有一个使用yield的解决方案。我想知道这是否比你原来的解决方案快。

这样可以避免原始文件中的所有索引。它只是迭代输入,保留前一个字符。

如果您的需求在将来发生变化,这应该很容易修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ch_sep = ' '

def _sep_chars_by_lang(s_input):
    itr = iter(s_input)
    ch_prev = next(itr)

    yield ch_prev

    while True:
        ch = next(itr)
        if test_lang(ch_prev) != test_lang(ch) or ch_prev in punc:
            yield ch_sep
        yield ch
        ch_prev = ch

def sep_chars_by_lang(s_input):
    return ''.join(_sep_chars_by_lang(s_input))

假设test_lang不是瓶颈,我会尝试:

1
2
3
4
5
6
''.join(
    x + ' '
    if x in punc or y in punc or test_lang(x) != test_lang(y)
    else x
    for x, y in zip(letters[:-1], letters[1:])
)