优化Python读取大文件的eval方法
我正在写一个Python脚本(使用Python 3.2),需要处理一个大约有80万行的文件,每个字典的键大约有15万个。文件中的每一行都是一个字典,格式如下:
{'url': 'http://address.com/document/42/1998', 'referrer': 'http://address.com/search?&q=query1', 'session': '1', 'rank': 2, 'time': 1338447254}
{'url': 'http://address.com/document/55/17', 'referrer': 'http://address.com/search&q=query2', 'session': '1', 'rank': 2, 'time': 13384462462}
对于文件中的每一行,我需要进行一些计算并存储结果。为了能够读取字典并进行操作,我使用了eval这个函数。但这样会导致1200亿次eval调用,这样会耗费很多时间。因此,我在寻找优化的方法。
欢迎大家提出各种优化建议。每一点都可能有帮助,但我主要关注的是eval和我读取文件的方式。目前,我在想有没有其他方法比eval更快,但我发现JSON无法读取这种格式,而使用split方法也没有取得好效果。还有,我读取文件的方式也可能需要优化。我尝试了以下代码中的方法,使用了“with”语句(虽然速度慢了很多,但内存消耗少)。我还尝试使用map将文件读入内存:
f_chunk = map(eval, codecs.open(chunk_file, "r", encoding="utf-8").readlines())
但这也不是很有效。
总之,脚本中的这一部分是最耗时的。它是在多个进程中运行的:
def mine(id, tmp_sessions, chunk_file, work_q, result_q, init_qsize):
#f_chunk = map(eval, codecs.open(chunk_file, "r", encoding="utf-8").readlines())
f_chunk = codecs.open(chunk_file, "r", encoding="utf-8").readlines()
while True:
try:
k = work_q.get()
if k == 'STOP':
work_q.task_done()
break # reached end of queue
except Queue.Empty:
break
#with codecs.open(chunk_file, "r", encoding="utf-8") as f_chunk:
for line in f_chunk:
#try:
jlog_nest = dict()
jlog_nest = eval(line)
#jlog_nest = json.loads(line)
#jlog_nest = line
#jlog_nest = defaultdict(line)
if jlog_nest["session"] == k: # If session is the same
query_nest = prepare_test_cases_lib.extract_query(jlog_nest["referrer"])
for q in tmp_sessions[k]:
if q[0] == query_nest:
url = jlog_nest["url"]
rank = jlog_nest["rank"]
doc_id = prepare_test_cases_lib.extract_document_id(url)
# Increase number of hits on that document, and save its rank
if doc_id in q[1]:
q[1][doc_id][0] += 1
q[1][doc_id][1].append(rank)
else:
q[1][doc_id] = [1, [rank]]
#except:
# print ("error",k)
result_q.put((k, tmp_sessions[k]))
work_q.task_done()
为了帮助理解发生了什么,tmp_session在运行上述代码之前可能是这样的:
tmp_sessions: {'39': [['q7', {}], ['q2', {}]], '40': [['q2', {}]]}
运行之后:
tmp_sessions: {'39': [['q7', {}], ['q2', {'133378': [1, [2]]}]], '40': [['q2', {'133378': [1, [2]]}]]}
在一部分真实数据上,我有562个键和2232行文件,运行了pstats,并按时间降序排序(这里只列出了前面的部分):
1284892 function calls in 76.810 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
8 0.000 0.000 77.985 9.748 {built-in method exec}
8 1.607 0.201 77.978 9.747 prepare_hard_test_cases.py:29(mine)
1254384 75.051 0.000 76.220 0.000 {built-in method eval}
562 0.008 0.000 0.050 0.000 queues.py:99(put)
8 0.000 0.000 0.029 0.004 codecs.py:685(readlines)
从中可以看出,确实是eval占用了大部分时间。
编辑:根据建议,我尝试了literal_eval。其实我之前在寻找解决方案时就发现了这个,但以为它和eval是一样的。我刚刚运行了一下,结果是一样的,但运行时间真的很糟糕:
50205868 function calls (37662028 primitive calls) in 121.494 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
8 0.001 0.000 121.494 15.187 {built-in method exec}
8 0.008 0.001 121.493 15.187 <string>:1(<module>)
8 4.935 0.617 121.485 15.186 prepare_hard_test_cases.py:29(mine)
1254384 5.088 0.000 116.425 0.000 ast.py:39(literal_eval)
1254384 1.098 0.000 71.432 0.000 ast.py:31(parse)
1254384 70.333 0.000 70.333 0.000 {built-in method compile}
13798224/1254384 22.996 0.000 39.336 0.000 ast.py:51(_convert)
7526304 8.539 0.000 23.042 0.000 ast.py:63(<genexpr>)
25087680 8.371 0.000 8.371 0.000 {built-in method isinstance}
8 0.001 0.000 0.047 0.006 codecs.py:685(readlines)
编辑2:我现在尝试了两种新方法。第一种是手动从每一行提取键和值,构建一个字典来进行操作。这在我的测试集上运行得稍微快一些:
51460252 function calls in 45.207 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
8 0.001 0.000 45.207 5.651 {built-in method exec}
8 0.003 0.000 45.207 5.651 <string>:1(<module>)
8 1.701 0.213 45.203 5.650 prepare_hard_test_cases.py:68(mine)
1254384 5.725 0.000 43.391 0.000 prepare_hard_test_cases.py:36(extractDict)
6271920 23.433 0.000 37.665 0.000 prepare_hard_test_cases.py:20(extractKeyValue)
18819074 11.308 0.000 11.308 0.000 {method 'find' of 'str' objects}
25092651 2.927 0.000 2.927 0.000 {built-in method len}
这是个好消息,但更好的方法是我第二种使用pickle的方法。现在我得到了:
30091 function calls in 5.285 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
8 0.000 0.000 5.285 0.661 {built-in method exec}
8 0.003 0.000 5.285 0.661 <string>:1(<module>)
8 0.173 0.022 5.281 0.660 prepare_hard_test_cases.py:68(mine)
570 0.001 0.000 5.057 0.009 queues.py:113(get)
2281 3.925 0.002 3.925 0.002 {method 'acquire' of '_multiprocessing.SemLock' objects}
570 1.133 0.002 1.133 0.002 {method 'recv' of '_multiprocessing.PipeConnection' objects}
8 0.029 0.004 0.029 0.004 {built-in method load}
等我有时间会尝试将这种方法应用到完整的数据集上。
有什么建议吗?
最好的祝福,
Casper
1 个回答
你可以试试ast.literal_eval()
这个方法,它是专门为这个任务设计的,可能会更快。
eval()
这个方法比较慢,而且不安全,通常不推荐使用。如果你觉得自己需要用它,建议你再看看其他方法,我可以保证99.99%的情况下你其实不需要。
另外补充一下:
f_chunk = codecs.open(chunk_file, "r", encoding="utf-8").readlines()
...
其实应该是:
with open(chunk_file, "r", encoding="utf-8") as f_chunk:
...
文件是迭代器,所以使用readlines()
会让你的程序在内存使用上不太高效。使用with
可以确保在你完成操作后文件会被正确关闭(在3.x版本中,你可以直接用open()
,因为它已经更新支持了后者的一些额外功能)。
除此之外,按照我所看到的,你的数据每一行应该都是有效的JSON格式,所以json
模块也应该可以正常工作。