有没有更快速的方式清理文件内的控制字符?

12

之前,我一直使用下面的代码片段清理数据。

import unicodedata, re, io

all_chars = (unichr(i) for i in xrange(0x110000))
control_chars = ''.join(c for c in all_chars if unicodedata.category(c)[0] == 'C')
cc_re = re.compile('[%s]' % re.escape(control_chars))
def rm_control_chars(s): # see http://www.unicode.org/reports/tr44/#General_Category_Values
    return cc_re.sub('', s)

cleanfile = []
with io.open('filename.txt', 'r', encoding='utf8') as fin:
    for line in fin:
        line =rm_control_chars(line)
        cleanfile.append(line)

我希望保留文件中的换行符。

下面记录了使用 cc_re.sub('', s) 替换前几行所花费的时间(第一列是时间,第二列是 len(s)):

0.275146961212 251
0.672796010971 614
0.178567171097 163
0.200030088425 180
0.236430883408 215
0.343492984772 313
0.317672967911 290
0.160616159439 142
0.0732028484344 65
0.533437013626 468
0.260229110718 236
0.231380939484 204
0.197766065598 181
0.283867120743 258
0.229172945023 208

如@ashwinichaudhary所建议的,使用s.translate(dict.fromkeys(control_chars)),同时记录所花费的时间,输出结果如下:

0.464188098907 252
0.366552114487 615
0.407374858856 164
0.322507858276 181
0.35142993927 216
0.319973945618 314
0.324357032776 291
0.371646165848 143
0.354818105698 66
0.351796150208 469
0.388131856918 237
0.374715805054 205
0.363368988037 182
0.425950050354 259
0.382766962051 209

但是我的1GB文本代码运行得非常慢。是否有其他方法来清除可控制的字符?


为什么要将整个文件保存在内存中? - Karoly Horvath
我需要稍后进行其他处理(我需要根据某些标准选择已清理的句子,然后对所选句子进行更多处理)。内存不是问题。 re.sub 是瓶颈。 - alvas
你尝试过不使用正则表达式,而只是使用标准的 replace 吗?正则表达式适用于复杂的模式,但我怀疑对于这个问题来说,replace 更有效率。另外,我建议你找到一种方法将原始的 1GB 文本分成几个部分——这应该会大大改善算法。 - jcoppens
3
你可以尝试使用str.translate函数。 - Ashwini Chaudhary
1
@alvas 对于 unicode.translate,可以这样做:s.translate(dict.fromkeys(control_chars)) - Ashwini Chaudhary
显示剩余9条评论
6个回答

7

我逐个字符地找到了一种解决方案,并使用一个100K的文件进行了基准测试:

import unicodedata, re, io
from time import time

# This is to generate randomly a file to test the script

from string import lowercase
from random import random

all_chars = (unichr(i) for i in xrange(0x110000))
control_chars = [c for c in all_chars if unicodedata.category(c)[0] == 'C']
chars = (list(u'%s' % lowercase) * 115117) + control_chars

fnam = 'filename.txt'

out=io.open(fnam, 'w')

for line in range(1000000):
    out.write(u''.join(chars[int(random()*len(chars))] for _ in range(600)) + u'\n')
out.close()


# version proposed by alvas
all_chars = (unichr(i) for i in xrange(0x110000))
control_chars = ''.join(c for c in all_chars if unicodedata.category(c)[0] == 'C')
cc_re = re.compile('[%s]' % re.escape(control_chars))
def rm_control_chars(s):
    return cc_re.sub('', s)

t0 = time()
cleanfile = []
with io.open(fnam, 'r', encoding='utf8') as fin:
    for line in fin:
        line =rm_control_chars(line)
        cleanfile.append(line)
out=io.open(fnam + '_out1.txt', 'w')
out.write(''.join(cleanfile))
out.close()
print time() - t0

# using a set and checking character by character
all_chars = (unichr(i) for i in xrange(0x110000))
control_chars = set(c for c in all_chars if unicodedata.category(c)[0] == 'C')
def rm_control_chars_1(s):
    return ''.join(c for c in s if not c in control_chars)

t0 = time()
cleanfile = []
with io.open(fnam, 'r', encoding='utf8') as fin:
    for line in fin:
        line = rm_control_chars_1(line)
        cleanfile.append(line)
out=io.open(fnam + '_out2.txt', 'w')
out.write(''.join(cleanfile))
out.close()
print time() - t0

输出结果为:
114.625444174
0.0149750709534

我试了一个1GB的文件(仅针对第二个文件),持续时间为186秒。
我也写了同一脚本的另一个版本,速度略有提高(176秒),并且更加节省内存(适用于无法放入RAM的大型文件)。
t0 = time()
out=io.open(fnam + '_out5.txt', 'w')
with io.open(fnam, 'r', encoding='utf8') as fin:
    for line in fin:
        out.write(rm_control_chars_1(line))
out.close()
print time() - t0

你能解释一下 chars = (list(u'%s' % lowercase) * 115117) + control_charsu''.join(chars[int(random()*len(chars))] for _ in range(600)) + u'\n' 吗? - alvas
是的,第一部分生成一个包含小写字母和所有控制字符的字符列表(我只是通过将小写字母乘以115117来使其与控制字符的大小相同)。我最终得到了一个巨大的字符列表。第二部分是从先前的列表中随机选择一定数量的字符,以便构建文件。所有这些只是为了生成一个文件,以测试删除控制字符的函数有多快且仍然准确...我本可以从答案中删除它,但认为它可能有助于您检查我的解决方案或帮助其他人更快地回答。 - fransua

5

与UTF-8相似,所有控制字符都编码在1个字节内(与ASCII兼容)且低于32,我建议使用以下快速代码:

#!/usr/bin/python
import sys

ctrl_chars = [x for x in range(0, 32) if x not in (ord("\r"), ord("\n"), ord("\t"))]
filename = sys.argv[1]

with open(filename, 'rb') as f1:
  with open(filename + '.txt', 'wb') as f2:
    b = f1.read(1)
    while b != '':
      if ord(b) not in ctrl_chars:
        f2.write(b)
      b = f1.read(1)

这是否足够好呢?


DEL字符是0x7F,即127,如果您将其视为控制字符,则也可以将其添加到计数中。 - Cyrille Pontvieux
这个恰当地描述了一组控制字符。 - Ross

4

这一定要用Python吗?在Python中读取文件之前,如何先清理文件。可以使用sed工具逐行处理。

请查看使用sed删除控制字符

如果将其输出到另一个文件,则可以打开该文件。不过我不知道速度会有多快。你可以在shell脚本中完成它并进行测试。根据此页面的说法,sed的速度为每秒82M个字符。

希望对你有所帮助。


3
如果你想让它运行得更快?将输入分成多个块,将数据处理代码封装为一个方法,并使用Python的multiprocessing包进行并行处理,写入一些共同的文本文件。逐个字符地处理这些内容是最简单的方法,但总是需要一些时间。 https://docs.python.org/3/library/multiprocessing.html

1
我很惊讶没有人提到 mmap,这可能是合适的选择。
注意:我将其作为答案放在这里,以防有用,并且很抱歉我现在没有时间实际测试和比较它。
您可以将文件加载到内存中(有点),然后可以实际上对对象运行 re.sub()。 这有助于消除IO瓶颈,并允许您在一次性写回之前就地更改字节。
之后,您可以尝试使用 str.translate() vs re.sub() 并包括任何进一步的优化,例如双缓冲CPU和IO或使用多个CPU核心/线程。
但是它看起来会像这样;
import mmap

f = open('test.out', 'r')
m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)

从mmap文档中摘取的一段很好的内容是;

..您可以在大多数需要字符串的地方使用mmap对象;例如,您可以使用re模块搜索内存映射文件。由于它们是可变的,因此可以通过执行obj[index] ='a'来更改单个字符,..


0

我会尝试几件事情。

首先,使用全局替换的正则表达式进行替换。

其次,设置一个已知控制字符范围的正则表达式字符类,而不是单个控制字符的类。
(这是为了防止引擎无法将其优化为范围。
范围在汇编级别需要两个条件语句,
而不是在类中的每个字符上都需要条件语句)

第三,由于您要删除字符,请在类后面添加一个贪婪量词
这样就不需要在每个单个字符匹配后进入替换子程序,而是一次性获取所有相邻字符
按需处理。

我不知道Python中关于正则表达式构造的语法,
也不知道Unicode中的所有控制码,但结果可能如下所示:

[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F]+

最耗时的部分是将结果复制到另一个字符串中。
最省时间的部分是查找所有控制代码,这个时间非常短。

所有条件相同的情况下,正则表达式(如上所述)是最快的方法。


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