Skip to main content
 首页 » 编程设计

python之从制表符分隔文件导入的 defaultdict 和 mongoDB 速度慢。谁能发现我的瓶颈

2025年05月04日16shihaiming

所以我尝试从日志文件中导入大约 8000 万的页面浏览量。我试图将它们作为 session 放入数据库中,即页面 View 组之间间隔 20 分钟。

所以最终,在我的用户数据库中,我希望每个用户都有一个像这样的字典对象列表:

{ 
    'id': 'user1' 
    'sessions':[ 
                    { 
                        "start" : ISODate("2011-04-03T23:21:59.639Z"), 
                        "end" : ISODate("2011-04-03T23:50:05.518Z"), 
                        "page_loads" : 136 
                    }, 
                    { 
                        "start" : ISODate("another date"), 
                        "end" : ISODate("later date"), 
                        "page_loads" : 20 
                    }, 
                ] 
} 

应该相当简单。所以我写了这个脚本:

howManyLinesTotal = 9999999 #i've done: wc -l in bash before to find the file size 
 
blank_dict = {'page_loads':0, 'start':0, 'end':0} 
 
latest_sessions = defaultdict(lambda: blank_dict) 
 
for line in f: #opens a gigantic gzip file called "f" 
    line = line.split('\t') #each entry is tab-delimited with: user \t datetime \t page_on_my_site 
 
    user = line[1] #grab the data from this line in the file 
    timestamp = datetime.utcfromtimestamp(float(line[2])) 
 
    latest_sessions[user]['page_loads'] += 1 #add one to this user's current session 
 
    if latest_sessions[user]['start'] == 0: #put in the start time if there isn't one 
        latest_sessions[user]['start'] = timestamp 
 
    if latest_sessions[user]['end'] == 0: #put in the end time if there isn't one 
        latest_sessions[user]['end'] = timestamp 
    else: #otherwise calculate if the new end time is 20 mins later 
        diff = (timestamp - latest_sessions[user]['end']).seconds 
        if diff > 1200: #if so, save the session to the database 
            db.update({'id':user}, {'$push':{'sessions':latest_sessions[user]}}) 
            latest_sessions[user] = blank_dict 
        else: 
            latest_sessions[user]['end'] = timestamp #otherwise just replace this endtime 
 
    count += 1 
    if count % 100000 == 0: #output some nice stats every 100,000 lines 
        print str(count) + '/' + str(howManyLinesTotal) 
 
#now put the remaining last sessions in 
for user in latest_sessions: 
    db.update({'id':user}, {'$push':{'sessions':latest_sessions[user]}}) 

对于一个 8000 万页 View 文件,我每行大约需要 0.002 秒 = 44 小时。

配备 2TB 7200rpm 希捷硬盘、32GB RAM 和 3.4Ghz 双核 i3 处理器。

这次听起来合理还是我犯了一些可怕的错误?


编辑:我们正在查看大约 90,000 多个用户,即 defaultdict 中的键


EDIT2:这是一个小得多的 106mb 文件的 cProfile 输出。我出于测试目的注释掉了实际的 mongoDB 保存:http://pastebin.com/4XGtvYWD


EDIT3:这是 cProfile 的条形图分析:http://i.imgur.com/K6pu6xx.png

请您参考如下方法:

我不能告诉你瓶颈在哪里,但我可以告诉你如何找到它。 Python 有 built in profiling tools ,它会告诉您代码的每个部分花费了多少时间。将此工具用于脚本,就像运行一样简单:

python -m cProfile my_db_import_script.py 

my_db_import_script.py 是您实际脚本的名称。该命令的作用是运行附加了分析器的脚本。脚本执行完成后,会打印出每个函数被调用了多少次,总共花费了多少时间,累积了多少时间,以及其他一些统计信息。

要将其与您的脚本一起使用,您需要处理可以在合理时间内完成的数据子集。从那里您将能够分析您发现的任何瓶颈。

优化代码的关键是永远不要假设您知道问题出在哪里。先测量并经常测量。

编辑:

在浏览了您的个人资料结果后,这些是我印象深刻的几行:

ncalls  tottime  percall  cumtime  percall filename:lineno(function) 
 
        1    0.000    0.000    0.000    0.000 gzip.py:149(_init_read) 
        1    0.000    0.000    0.000    0.000 gzip.py:153(_read_gzip_header) 
  2709407    5.543    0.000    8.898    0.000 gzip.py:200(read) 
        2    0.000    0.000    0.000    0.000 gzip.py:23(read32) 
  2242878    3.267    0.000    3.727    0.000 gzip.py:232(_unread) 
   107984    0.266    0.000    3.310    0.000 gzip.py:237(_read) 
        1    0.000    0.000    0.000    0.000 gzip.py:26(open) 
   107979    0.322    0.000    1.258    0.000 gzip.py:287(_add_read_data) 
        1    0.000    0.000    0.000    0.000 gzip.py:293(_read_eof) 
        1    0.000    0.000    0.000    0.000 gzip.py:308(close) 
        1    0.000    0.000    0.000    0.000 gzip.py:35(GzipFile) 
  2242878    8.029    0.000   23.517    0.000 gzip.py:385(readline) 
        1    0.000    0.000    0.000    0.000 gzip.py:4(<module>) 
        1    0.000    0.000    0.000    0.000 gzip.py:434(__iter__) 
  2242878    1.561    0.000   25.078    0.000 gzip.py:437(next) 
        1    0.000    0.000    0.000    0.000 gzip.py:44(__init__) 
 
  2242878    2.889    0.000    2.889    0.000 {built-in method utcfromtimestamp} 
   107979    1.627    0.000    1.627    0.000 {built-in method decompress} 
  2709408    1.451    0.000    1.451    0.000 {method 'find' of 'str' objects} 
  2242880    1.849    0.000    1.849    0.000 {method 'split' of 'str' objects} 

您会注意到我已经突出显示了所有 gzip 代码。我对 gzip 模块不是很熟悉,所以我一直在浏览 source code .看起来这个模块正在做的是向 gzip 数据公开一个像接口(interface)这样的普通文件。有几种方法可以加快速度。

  1. 如果可行,您可以事先解压缩您的文件。这将消除 gzip 的一些开销。

  2. 您可以开始优化文件的读取方式。 Here is a link举个例子,根据您一次读入的文件量不同,情况会有所不同。这个stackoverflow里面也有一些不错的建议问题。

我还强调了一个事实,即转换时间戳会占用大量时间,字符串操作函数也是如此。

归根结底,进行这种规模的任何优化的最佳方法是运行基准测试、进行更改,然后重新运行。希望这对您有所启发!