我们有一个包含大约4600万条CSV格式记录的文件。每条记录有大约18个字段,其中一个是64字节ID。我们有另一个文件,其中包含大约167K个唯一ID。需要将与ID对应的记录拉出。因此,我们编写了一个python程序,它将167K ID读入一个数组,并处理4600万条记录文件,检查每条记录中是否存在ID。以下是代码片段:
import csv
...
csvReadHandler = csv.reader(inputFile, delimiter=chr(1))
csvWriteHandler = csv.writer(outputFile, delimiter=chr(1), lineterminator='\n')
for fieldAry in csvReadHandler:
lineCounts['orig'] += 1
if fieldAry[CUSTOMER_ID] not in idArray:
csvWriteHandler.writerow(fieldAry)
lineCounts['mod'] += 1
在一小组数据上测试程序,以下是处理时间:
lines: 117929 process time: 236.388447046 sec
lines: 145390 process time: 277.075321913 sec
我们已经开始在美国东部时间晚上3点到凌晨3点开始在4600万个记录文件(大小约为13 GB)上运行程序,现在它是美国东部时间早上10点左右,它仍在处理中!
问题:
if fieldAry[CUSTOMER_ID] not in idArray:
有更好的选择吗?
谢谢!
更新:这是在带有EBS附加卷的EC2实例上处理的。
答案 0 :(得分:7)
你应该必须使用set
而不是list
; 之前 for
循环执行:
idArray = set(idArray)
csvReadHandler = csv.reader(inputFile, delimiter=chr(1))
csvWriteHandler = csv.writer(outputFile, delimiter=chr(1), lineterminator='\n')
for fieldAry in csvReadHandler:
lineCounts['orig'] += 1
if fieldAry[CUSTOMER_ID] not in idArray:
csvWriteHandler.writerow(fieldAry)
lineCounts['mod'] += 1
看到令人难以置信的加速;您正在使用 天 值得花费不必要的处理时间,因为您选择了错误的数据结构。
具有in
的 set
运算符具有 O(1)时间复杂度,而 O(n)时间复杂度为list
。这可能听起来像“不是什么大不了”,但实际上它是你脚本中的瓶颈。即使set
的 O 的常量稍高一些。因此,您的代码在单个in
操作上使用的时间超过了30000次。如果在最佳版本中它需要30秒,那么现在你只需要在这一行上花费10天。
请参阅以下测试:我生成100万个ID并将10000个放入另一个列表 - to_remove
。然后我像你一样做for
循环,为每条记录执行in
操作:
import random
import timeit
all_ids = [random.randint(1, 2**63) for i in range(1000000)]
to_remove = all_ids[:10000]
random.shuffle(to_remove)
random.shuffle(all_ids)
def test_set():
to_remove_set = set(to_remove)
for i in all_ids:
if i in to_remove_set:
pass
def test_list():
for i in all_ids:
if i in to_remove:
pass
print('starting')
print('testing list', timeit.timeit(test_list, number=1))
print('testing set', timeit.timeit(test_set, number=1))
结果:
testing list 227.91903045598883
testing set 0.14897623099386692
set
版需要149毫秒; list
版本需要228秒。现在这些都是小数字:在你的情况下,你有100万输入记录,而我的100万;因此,您需要将testing set
时间乘以50:您的数据集需要大约7.5秒。
列表版本,另一方面,您需要将该时间乘以50 * 17 - 不仅是输入记录的50倍,而且是匹配的记录的17倍。因此我们得到227 * 50 * 17 = 192950。
因此,您的算法花费了2.2天的时间,通过使用正确的数据结构可以在7.5秒内完成。当然,这并不意味着您可以在7.5秒内扫描整个50 GB文档,但也可能不超过2.2天。所以我们改变了:
2 days 2.2 days
|reading and writing the files||------- doing id in list ------|
类似
2 days 7.5 seconds (doing id in set)
|reading and writing the files||
答案 1 :(得分:1)
免责声明: 不解释原因而不解释原因,因为OP并未包含其整个代码库或硬件/基础架构设计。但如果我在我的代码或逻辑中犯了严重错误,请解释它们并相应地投票。
让我们开始定义你遇到的瓶颈(有些是显而易见的,有些不是)。
对于CSV阅读器,它们很好但可能效率不高。
既然你要阅读这两个文件,我会考虑以下几点:
逐行读取13GB文件并将ID
保存在字典中,而不检查键/值是否存在。 (为什么?因为检查值是否存在比仅覆盖它更慢,字典还有一个额外的好处,即键是唯一的,因此重复将被淘汰) 或添加它到许多其他人所描述的set()
。
然后逐行阅读较小的文件,并检查dict
或set
是否包含ID
。
dict()
vs set()
vs list()
以下是set()
,list()
和dict()
三种数据类型的比较:
使用的代码:test.py
(11.719028949737549, 999950, 'Using dict() for inserts')
(1.8462610244750977, 'dict() time spent looking through the data')
(11.793760061264038, 999961, 'Using set() for inserts')
(7.019757986068726, 'set() time spent looking through the data')
(15min+, 'Using list()') # Honestly, I never let it run to it's finish.. It's to slow.
正如您所看到的,dict
比set
略快,而list()
完全落后(请参阅Antti的说明)。我应该指出我的数据有点偏斜,因为它是速度差异的快速而肮脏的演示,但是仍然会遇到一般的想法。
因此,如果您无法访问源数据的数据库版本,并且您需要使用Python,那么ID会使用以下内容:
delimiter = b'\x01'
big_source = {}
with open('13_gig_source.log', 'rb') as fh:
for line in fh:
csv = line.split(delimiter)
big_source[csv[4]] = None # 5:th column, change to match CUSTOMER_ID
output = open('result.log', 'wb')
with open('smaller_file.log', 'rb') as fh:
for line in fh:
csv = line.split(delimiter)
if csv[4] in big_source:
output.write(csv[4] + b'\n')
output.close()
由于我不知道数据存在哪一列,因此我没有优化split()
。
例如,如果它是您尝试提取的最后一列,请执行line.rsplit(delimiter, 1)[-1]
。或者,如果它是3:d列,请执行line.split(delimiter, 3)[2]
,因为它会中止在delimiter
函数中查找split()
的下一个位置的过程。
是的,有些工具可能更适合这种工具,例如awk
,因为它是用C编写的特定工具来执行非常具体的任务。即使Python基于C,它仍然在C代码之上有很多抽象层,并且在很大程度上比为特定任务编写的对应C工具要慢。
现在我没有数据来测试它,也不是 PRO With Nix-Commands 或 PWN-C 。所以我会留下其他人给你举例,但我发现了这个:
它可能会有所帮助。
答案 2 :(得分:0)
加快速度的最简单方法是将线路处理与某些分布式解决方案并行化。最简单的就是使用multiprocessing.Pool。你应该这样做(语法没有检查):
from multiprocessing import Pool
p = Pool(processes=4)
p.map(process_row, csvReadHandler)
尽管如此,python并不是进行这种批处理的最佳语言(主要是因为写入磁盘非常慢)。最好将所有磁盘写入管理(缓冲,排队等)留给linux内核,这样使用bash解决方案会更好。最有效的方法是将输入文件分成块,然后简单地执行反grep来过滤id。
for file in $list_of_splitted_files; then
cat $file | grep -v (id1|id2|id3|...) > $file.out
done;
如果您之后需要合并:
for file in $(ls *.out); then
cat $file >> final_results.csv
done
考虑:
答案 3 :(得分:0)
我认为最好使用数据库来解决这个问题 首先创建一个像MySql或其他任何东西的数据库 然后将您的数据从文件写入2表 最后使用简单的SQL查询来选择行 像这样的东西: select * from table1 where id in(select table from table2)