优化这段Python日志解析代码

6
在我的笔记本电脑上,处理一个4.2 GB大小的输入文件的运行时间为48秒。输入文件采用制表符分隔,每个值都用引号括起来。每条记录以换行符结束,例如'"val1"\t"val2"\t"val3"\t..."valn"\n'
我尝试使用10个线程进行多进程处理:一个用于排队读取行,8个用于解析单独的行并填充输出队列,还有一个用于将输出队列减少到下面所示的defaultdict中,但代码运行时间为300秒,比以下代码慢6倍以上:
from collections import defaultdict
def get_users(log):
    users = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.
    for (i, line) in enumerate(f): 
        if i % 1000000 == 0: print "Line %d" % i # progress notification

        l = line.split('\t')
        if l[ix_profile] != '"7"': # "7" indicates a bad value
            # use list slicing to remove quotes
            users[l[ix_user][1:-1]] += 1 

    f.close()
    return users

我检查过了,通过从for循环中删除除打印语句以外的所有内容,确认我不是I/O绑定。该代码运行时间为9秒,我认为这是该代码可以运行的最低时间。

我有很多5GB的文件要处理,所以即使运行时间稍微缩短一点点(我知道,我可以删除打印语句!),也会有所帮助。我正在运行的机器有4个核心,因此我不禁想知道是否有一种方法可以使多线程/多进程代码比上面的代码运行更快。

更新:

我将多进程代码重写如下:

from multiprocessing import Pool, cpu_count
from collections import defaultdict

def parse(line, ix_profile=10, ix_user=9):
    """ix_profile and ix_user predetermined; hard-coding for expedience."""
    l = line.split('\t')
    if l[ix_profile] != '"7"':
        return l[ix_user][1:-1]

def get_users_mp():
    f = open('20110201.txt')
    h = f.readline() # remove header line
    pool = Pool(processes=cpu_count())
    result_iter = pool.imap_unordered(parse, f, 100)
    users = defaultdict(int)
    for r in result_iter:
        if r is not None:
            users[r] += 1
    return users

它在26秒内运行,速度提高了1.85倍。还可以,但使用4个核心时,并没有我期望的那么多。

利用多核心的最简单方法可能是同时运行4个副本(使用子进程),然后通过保存/加载字典的pickle来合并输出。 - user227667
1
我没有看到它比9秒慢很多的原因。你能提供一下你正在读取的文件样本吗? - Winston Ewert
@Winston,该文件有40个制表符分隔的列,字段长度从1到80个字符不等,共有825万行。很遗憾,我无法提供数据样本。 - Jason Sundram
你考虑过使用gnu的cutgrep吗? - MattH
1
这个问题可以在http://codereview.stackexchange.com/上提问。 - Acorn
如果有人感兴趣,Reid Draper编写了一些Clojure代码来完成相同的任务;它运行速度更快,但显然不是Python:https://gist.github.com/jsundram/2009477 - Jason Sundram
7个回答

4

使用正则表达式。

测试发现,进程中的昂贵部分是调用str.split()。可能每行都需要构建一个列表和一堆字符串对象,这很费时间。

首先,您需要构建一个正则表达式来匹配该行。类似于:

expression = re.compile(r'("[^"]")\t("[^"]")\t')

如果您调用 expression.match(line).groups(),您将得到前两列作为两个字符串对象提取出来,并且您可以直接使用它们进行逻辑处理。
现在假设您感兴趣的两列是前两列。如果不是,您只需调整正则表达式以匹配正确的列即可。您的代码检查标题以查看列位于何处。您可以根据此生成正则表达式,但我猜这些列实际上总是位于相同的位置。只需验证它们仍然在那里并在行上使用正则表达式即可。
编辑
从 collections 中导入 defaultdict import re
def get_users(log):
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('\'', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')

    assert ix_user < ix_profile

这段代码假设用户在个人资料之前。
    keep_field = r'"([^"]*)"'

这个正则表达式将捕获一个单列。

    skip_field = r'"[^"]*"'

这个正则表达式将匹配列,但不会捕获结果。(注意缺少括号)

    fields = [skip_field] * len(h)
    fields[ix_profile] = keep_field
    fields[ix_user] = keep_field

创建一个包含所有字段的列表,只保留我们关心的字段。
    del fields[max(ix_profile, ix_user)+1:]

删除我们关心字段后面的所有字段(这些字段需要时间匹配,而且我们不关心它们)

    regex = re.compile(r"\t".join(fields))

实际上生成正则表达式。

    users = defaultdict(int)
    for line in f:
        user, profile = regex.match(line).groups()

提取两个值,执行逻辑。
        if profile != "7": # "7" indicates a bad value
            # use list slicing to remove quotes
            users[user] += 1 

    f.close()
    return users

我的正则表达式技术显然不够强。数据位于第9列(用户)和第10列(配置文件类型)。我使用的正则表达式是re.compile('\t.*"(.+)"\t"7"\t'),但代码运行速度非常慢。有没有好的方法只提取这些列? - Jason Sundram
@Jason,我把我的代码放在那里,展示了我是如何完成它的。你的正则表达式对我来说没有意义,你似乎在检查是否有一个7,但那不会导致误报吗?它可能也更慢。你需要构建一个可以匹配整行(至少是你关心的部分)的正则表达式。 - Winston Ewert
你提到了一个很好的观点,关于我那个悲惨缺陷的正则表达式。我用你的代码处理了我的数据,单线程下只需要29秒,比我原来的48秒有了很大的改进。我会看看多线程下它的表现如何。 - Jason Sundram
@Jason,不足为奇。当你有大型任务需要分割时,线程表现更好。对于这么小的任务来说,进程间通信的开销会占据优势。然而,如果你有大量这样的文件,那么在四个核心上处理不同的文件应该能够给你带来不错的加速效果。 - Winston Ewert

2
如果你正在运行Unix或Cygwin,下面这个小脚本将为你生成用户ID的频率,其中profile != 7。应该很快。
更新后使用awk计算用户ID的数量。
#!/bin/bash

FILENAME="test.txt"

IX_PROFILE=`head -1 ${FILENAME} | sed -e 's/\t/\n/g' | nl -w 1 | grep profile.type | cut -f1`
IX_USER=`head -1 ${FILENAME} | sed -e 's/\t/\n/g' | nl -w 1 | grep profile.id | cut -f1`
# Just the userids
# sed 1d ${FILENAME} | cut -f${IX_PROFILE},${IX_USER} | grep -v \"7\" | cut -f2

# userids counted:
# sed 1d ${FILENAME} | cut -f${IX_PROFILE},${IX_USER} | grep -v \"7\" | cut -f2 | sort | uniq -c

# Count using awk..?
sed 1d ${FILENAME} | cut -f${IX_PROFILE},${IX_USER} | grep -v \"7\" | cut -f2 | awk '{ count[$1]++; } END { for (x in count) { print x "\t" count[x] } }'

我不得不稍微调整一下(sed -e 部分)才能让它正常工作。但是当我确实让它工作时,它花费了4分49秒。我认为排序(n log(n))正在影响性能。 - Jason Sundram
@Jason Sundram:好的,我刚刚在我根据你的代码的假设手写的文件上进行了测试。你有多少个唯一的用户ID,它们的格式是什么? - MattH
有260万个唯一的用户ID,它们是像这样的字母数字字符串:"6a7746195b2f3cbbebde7e8e27a50d409590d7b8"。 - Jason Sundram
使用awk,速度更快:1分钟45秒。不过仍然比纯python慢。 - Jason Sundram

1

看到你的日志文件是以制表符分隔的,你可以使用 csv 模块 - 并带有 dialect='excel-tab' 参数 - 来提高性能和可读性。当然,前提是你必须使用 Python 而不是更快的控制台命令。


感谢您的提示。代码确实更易读和清晰(包括导入语句在内共计10行),但需要88秒才能运行。 - Jason Sundram
@Jason:在多进程更新后,您还没有发布完整的代码。parse_split函数是做什么的?无论如何,似乎每个池进程都从文件中读取行。这是次优的,因为您有多个进程竞争同一资源。尝试在主进程中加载行,并使用队列将它们传递给工作进程。 - ktdrv
糟糕,parse_split 应该改成 parse。所有的代码都在那里。我以为将块大小指定给 imap_unordered 可以减少一些争用(让每个人获取 100 行然后处理它们)。将整个文件读入内存比仅滚动文件要花费更多时间,因此我不确定这是否会导致性能提高。我会尝试一下并让你知道结果。 - Jason Sundram

1

我意识到我几乎和温斯顿·尤尔特(Winston Ewert)有着完全相同的想法:构建一个正则表达式。

但是我的正则表达式:

  • 适用于ix_profile < ix_user以及ix_profile > ix_user的情况

  • 该正则表达式仅捕获用户列:使用子模式'"(?!7")[^\t\r\n"]*"'匹配配置文件列,如果该列中存在"7",则不匹配;因此我们只获取正确的用户并定义唯一组

.

此外,我测试了几种匹配和提取算法:

1)使用 re.finditer()

2)使用 re.match() 和正则表达式匹配40个字段

3)使用 re.match() 和正则表达式匹配只有max(ix_profile,ix_user) + 1个字段

4)类似于3,但是使用一个简单的字典而不是一个defaultdict实例

为了测量时间,我的代码根据您提供的信息创建一个文件。

.

我在4个代码中测试了以下4个函数:

1

def get_users_short_1(log):
    users_short = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.

    glo = 40*['[^\t]*']
    glo[ix_profile] = '"(?!7")[^\t"]+"'
    glo[ix_user] = '"([^\t"]*)"'
    glo[39] = '"[^\t\r\n]*"'
    regx = re.compile('^'+'\t'.join(glo),re.MULTILINE)

    content = f.read()
    for mat in regx.finditer(content):
        users_short[mat.group(1)] += 1

    f.close()
    return users_short

2

def get_users_short_2(log):
    users_short = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.

    glo = 40*['[^\t]*']
    glo[ix_profile] = '"(?!7")[^\t"]*"'
    glo[ix_user] = '"([^\t"]*)"'
    regx = re.compile('\t'.join(glo))


    for line in f:
        gugu = regx.match(line)
        if gugu:
            users_short[gugu.group(1)] += 1
    f.close()
    return users_short

3

def get_users_short_3(log):
    users_short = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.

    glo = (max(ix_profile,ix_user) + 1) * ['[^\t]*']
    glo[ix_profile] = '"(?!7")[^\t"]*"'
    glo[ix_user] = '"([^\t"]*)"'
    regx = re.compile('\t'.join(glo))

    for line in f:
        gugu = regx.match(line)
        if gugu:
            users_short[gugu.group(1)] += 1

    f.close()
    return users_short

4

完整的代码4,似乎是最快的:

import re
from random import choice,randint,sample
import csv
import random
from time import clock

choi = 1
if choi:
    ntot = 1000
    chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
    def ry(a=30,b=80,chars=chars,nom='abcdefghijklmnopqrstuvwxyz'):
        if a==30:
            return ''.join(choice(chars) for i in xrange(randint(30,80)))
        else:
            return ''.join(choice(nom) for i in xrange(randint(8,12)))

    num = sample(xrange(1000),200)
    num.sort()
    print 'num==',num
    several = [e//3 for e in xrange(0,800,7) if e//3 not in num]
    print
    print 'several==',several

    with open('biggy.txt','w') as f:
        head = ('aaa','bbb','ccc','ddd','profile.id','fff','ggg','hhhh','profile.type','iiii',
                'jjj','kkkk','lll','mmm','nnn','ooo','ppp','qq','rr','ss',
                'tt','uu','vv','ww','xx','yy','zz','razr','fgh','ty',
                'kfgh','zer','sdfs','fghf','dfdf','zerzre','jkljkl','vbcvb','kljlk','dhhdh')
        f.write('\t'.join(head)+'\n')
        for i in xrange(1000):
            li = [ ry(a=8).join('""') if n==4 else ry().join('""')
                   for n in xrange(40) ]
            if i in num:
                li[4] = '@#~&=*;'
                li[8] = '"7"'
            if i in several:
                li[4] = '"BRAD"'
            f.write('\t'.join(li)+'\n')



from collections import defaultdict
def get_users(log):
    users = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.
    for (i, line) in enumerate(f): 
        #if i % 1000000 == 0: print "Line %d" % i # progress notification

        l = line.split('\t')
        if l[ix_profile] != '"7"': # "7" indicates a bad value
            # use list slicing to remove quotes

            users[l[ix_user][1:-1]] += 1 
    f.close()
    return users




def get_users_short_4(log):
    users_short = {}
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.

    glo = (max(ix_profile,ix_user) + 1) * ['[^\t]*']
    glo[ix_profile] = '"(?!7")[^\t"]*"'
    glo[ix_user] = '"([^\t"]*)"'
    regx = re.compile('\t'.join(glo))

    for line in f:
        gugu = regx.match(line)
        if gugu:
            gugugroup = gugu.group(1)
            if gugugroup in users_short:
                users_short[gugugroup] += 1
            else:
                users_short[gugugroup] = 1

    f.close()
    return users_short




print '\n\n'

te = clock()
USERS = get_users('biggy.txt')
t1 = clock()-te

te = clock()
USERS_short_4 = get_users_short_4('biggy.txt')
t2 = clock()-te



if choi:
    print '\nlen(num)==',len(num),' : number of lines with ix_profile==\'"7"\''
    print "USERS['BRAD']==",USERS['BRAD']
    print 'then :'
    print str(ntot)+' lines - '+str(len(num))+' incorrect - '+str(len(several))+\
          ' identical + 1 user BRAD = '+str(ntot - len(num)-len(several)+1)    
print '\nlen(USERS)==',len(USERS)
print 'len(USERS_short_4)==',len(USERS_short_4)
print 'USERS == USERS_short_4 is',USERS == USERS_short_4

print '\n----------------------------------------'
print 'time of get_users() :\n', t1,'\n----------------------------------------'
print 'time of get_users_short_4 :\n', t2,'\n----------------------------------------'
print 'get_users_short_4() / get_users() = '+str(100*t2/t1)+ ' %'
print '----------------------------------------'

这段代码的一个例子是返回结果 4:

num== [2, 12, 16, 23, 26, 33, 38, 40, 43, 45, 51, 53, 84, 89, 93, 106, 116, 117, 123, 131, 132, 135, 136, 138, 146, 148, 152, 157, 164, 168, 173, 176, 179, 189, 191, 193, 195, 199, 200, 208, 216, 222, 224, 227, 233, 242, 244, 245, 247, 248, 251, 255, 256, 261, 262, 266, 276, 278, 291, 296, 298, 305, 307, 308, 310, 312, 314, 320, 324, 327, 335, 337, 340, 343, 350, 356, 362, 370, 375, 379, 382, 385, 387, 409, 413, 415, 419, 433, 441, 443, 444, 446, 459, 462, 474, 489, 492, 496, 505, 509, 511, 512, 518, 523, 541, 546, 548, 550, 552, 558, 565, 566, 572, 585, 586, 593, 595, 601, 609, 610, 615, 628, 632, 634, 638, 642, 645, 646, 651, 654, 657, 660, 662, 665, 670, 671, 680, 682, 687, 688, 690, 692, 695, 703, 708, 716, 717, 728, 729, 735, 739, 741, 742, 765, 769, 772, 778, 790, 792, 797, 801, 808, 815, 825, 828, 831, 839, 849, 858, 859, 862, 864, 872, 874, 890, 899, 904, 906, 913, 916, 920, 923, 928, 941, 946, 947, 953, 955, 958, 959, 961, 971, 975, 976, 979, 981, 985, 989, 990, 999]

several== [0, 4, 7, 9, 11, 14, 18, 21, 25, 28, 30, 32, 35, 37, 39, 42, 44, 46, 49, 56, 58, 60, 63, 65, 67, 70, 72, 74, 77, 79, 81, 86, 88, 91, 95, 98, 100, 102, 105, 107, 109, 112, 114, 119, 121, 126, 128, 130, 133, 137, 140, 142, 144, 147, 149, 151, 154, 156, 158, 161, 163, 165, 170, 172, 175, 177, 182, 184, 186, 196, 198, 203, 205, 207, 210, 212, 214, 217, 219, 221, 226, 228, 231, 235, 238, 240, 249, 252, 254, 259, 263]




len(num)== 200  : number of lines with ix_profile=='"7"'
USERS['BRAD']== 91
then :
1000 lines - 200 incorrect - 91 identical + 1 user BRAD = 710

len(USERS)== 710
len(USERS_short_4)== 710
USERS == USERS_short_4 is True

----------------------------------------
time of get_users() :
0.0788686830309 
----------------------------------------
time of get_users_short_4 :
0.0462885646081 
----------------------------------------
get_users_short_4() / get_users() = 58.690677756 %
----------------------------------------

但结果或多或少是不稳定的。我得到了:

get_users_short_1() / get_users() = 82.957476637 %
get_users_short_1() / get_users() = 82.3987686867 %
get_users_short_1() / get_users() = 90.2949842932 %
get_users_short_1() / get_users() = 78.8063007461 %
get_users_short_1() / get_users() = 90.4743181768 %
get_users_short_1() / get_users() = 81.9635560003 %
get_users_short_1() / get_users() = 83.9418269406 %
get_users_short_1() / get_users() = 89.4344442255 %


get_users_short_2() / get_users() = 80.4891442088 %
get_users_short_2() / get_users() = 69.921943776 %
get_users_short_2() / get_users() = 81.8006709304 %
get_users_short_2() / get_users() = 83.6270772928 %
get_users_short_2() / get_users() = 97.9821084403 %
get_users_short_2() / get_users() = 84.9307558629 %
get_users_short_2() / get_users() = 75.9384820018 %
get_users_short_2() / get_users() = 86.2964748485 %


get_users_short_3() / get_users() = 69.4332754744 %
get_users_short_3() / get_users() = 58.5814726668 %
get_users_short_3() / get_users() = 61.8011476831 %
get_users_short_3() / get_users() = 67.6925083362 %
get_users_short_3() / get_users() = 65.1208124156 %
get_users_short_3() / get_users() = 72.2621727569 %
get_users_short_3() / get_users() = 70.6957501222 %
get_users_short_3() / get_users() = 68.5310031226 %
get_users_short_3() / get_users() = 71.6529128259 %
get_users_short_3() / get_users() = 71.6153554073 %
get_users_short_3() / get_users() = 64.7899044975 %
get_users_short_3() / get_users() = 72.947531363 %
get_users_short_3() / get_users() = 65.6691965629 %
get_users_short_3() / get_users() = 61.5194374401 %
get_users_short_3() / get_users() = 61.8396133666 %
get_users_short_3() / get_users() = 71.5447862466 %
get_users_short_3() / get_users() = 74.6710538858 %
get_users_short_3() / get_users() = 72.9651233485 %



get_users_short_4() / get_users() = 65.5224210767 %
get_users_short_4() / get_users() = 65.9023813161 %
get_users_short_4() / get_users() = 62.8055210129 %
get_users_short_4() / get_users() = 64.9690049062 %
get_users_short_4() / get_users() = 61.9050866134 %
get_users_short_4() / get_users() = 65.8127125992 %
get_users_short_4() / get_users() = 66.8112344201 %
get_users_short_4() / get_users() = 57.865635278 %
get_users_short_4() / get_users() = 62.7937713964 %
get_users_short_4() / get_users() = 66.3440149528 %
get_users_short_4() / get_users() = 66.4429530201 %
get_users_short_4() / get_users() = 66.8692388625 %
get_users_short_4() / get_users() = 66.5949137537 %
get_users_short_4() / get_users() = 69.1708488794 %
get_users_short_4() / get_users() = 59.7129743801 %
get_users_short_4() / get_users() = 59.755297387 %
get_users_short_4() / get_users() = 60.6436352185 %
get_users_short_4() / get_users() = 64.5023727945 %
get_users_short_4() / get_users() = 64.0153937511 %

.

我想知道您在比我的电脑更强大的计算机上使用我的代码处理您的真实文件时会得到什么样的结果。请告诉我最新情况。

.

.

编辑 1

使用

def get_users_short_Machin(log):
    users_short = defaultdict(int)
    f = open(log)
    # Read header line
    h = f.readline().strip().replace('"', '').split('\t')
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    maxsplits = max(ix_profile, ix_user) + 1
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.
    for line in f: 
        #if i % 1000000 == 0: print "Line %d" % i # progress notification
        l = line.split('\t', maxsplits)
        if l[ix_profile] != '"7"': # "7" indicates a bad value
            # use list slicing to remove quotes
            users_short[l[ix_user][1:-1]] += 1 
    f.close()
    return users_short

我有

get_users_short_Machin() / get_users() = 60.6771821308 %
get_users_short_Machin() / get_users() = 71.9300992989 %
get_users_short_Machin() / get_users() = 85.1695214715 %
get_users_short_Machin() / get_users() = 72.7722233685 %
get_users_short_Machin() / get_users() = 73.6311173237 %
get_users_short_Machin() / get_users() = 86.0848484053 %
get_users_short_Machin() / get_users() = 75.1661981729 %
get_users_short_Machin() / get_users() = 72.8888452474 %
get_users_short_Machin() / get_users() = 76.7185685993 %
get_users_short_Machin() / get_users() = 82.7007096958 %
get_users_short_Machin() / get_users() = 71.1678957888 %
get_users_short_Machin() / get_users() = 71.9845835126 %

使用一个简单的字典:

users_short = {}
.......
for line in f: 
    #if i % 1000000 == 0: print "Line %d" % i # progress notification
    l = line.split('\t', maxsplits)
    if l[ix_profile] != '"7"': # "7" indicates a bad value
        # use list slicing to remove quotes
        us = l[ix_user][1:-1]
        if us not in users_short:
            users_short[us] = 1
        else:
            users_short[us] += 1

虽然执行时间有所改善,但仍高于我的上一个代码4。

get_users_short_Machin2() / get_users() = 71.5959919389 %
get_users_short_Machin2() / get_users() = 71.6118864535 %
get_users_short_Machin2() / get_users() = 66.3832514274 %
get_users_short_Machin2() / get_users() = 68.0026407277 %
get_users_short_Machin2() / get_users() = 67.9853921552 %
get_users_short_Machin2() / get_users() = 69.8946203037 %
get_users_short_Machin2() / get_users() = 71.8260030248 %
get_users_short_Machin2() / get_users() = 78.4243267003 %
get_users_short_Machin2() / get_users() = 65.7223734428 %
get_users_short_Machin2() / get_users() = 69.5903935612 %

.

编辑 2

最快的方法:

def get_users_short_CSV(log):
    users_short = {}
    f = open(log,'rb')
    rid = csv.reader(f,delimiter='\t')
    # Read header line
    h = rid.next()
    ix_profile = h.index('profile.type')
    ix_user = h.index('profile.id')
    # If either ix_* is the last field in h, it will include a newline. 
    # That's fine for now.

    glo = (max(ix_profile,ix_user) + 1) * ['[^\t]*']
    glo[ix_profile] = '"(?!7")[^\t\r\n"]*"'
    glo[ix_user] = '"([^\t\r\n"]*)"'
    regx = re.compile('\t'.join(glo))

    for line in f:
        gugu = regx.match(line)
        if gugu:
            gugugroup = gugu.group(1)
            if gugugroup in users_short:
                users_short[gugugroup] += 1
            else:
                users_short[gugugroup] = 1

    f.close()
    return users_short

结果

get_users_short_CSV() / get_users() = 31.6443901114 %
get_users_short_CSV() / get_users() = 44.3536176134 %
get_users_short_CSV() / get_users() = 47.2295100511 %
get_users_short_CSV() / get_users() = 45.4912200716 %
get_users_short_CSV() / get_users() = 63.7997241038 %
get_users_short_CSV() / get_users() = 43.5020255488 %
get_users_short_CSV() / get_users() = 40.9188320386 %
get_users_short_CSV() / get_users() = 43.3105062139 %
get_users_short_CSV() / get_users() = 59.9184895288 %
get_users_short_CSV() / get_users() = 40.22047881 %
get_users_short_CSV() / get_users() = 48.3615872543 %
get_users_short_CSV() / get_users() = 47.0374831251 %
get_users_short_CSV() / get_users() = 44.5268626789 %
get_users_short_CSV() / get_users() = 53.1690205938 %
get_users_short_CSV() / get_users() = 43.4022458372 %

.

编辑 3

我测试了 get_users_short_CSV(),使用的文件有10000行而不是只有1000行:

len(num)== 2000  : number of lines with ix_profile=='"7"'
USERS['BRAD']== 95
then :
10000 lines - 2000 incorrect - 95 identical + 1 user BRAD = 7906

len(USERS)== 7906
len(USERS_short_CSV)== 7906
USERS == USERS_short_CSV is True

----------------------------------------
time of get_users() :
0.794919186656 
----------------------------------------
time of get_users_short_CSV :
0.358942826532 
----------------------------------------
get_users_short_CSV() / get_users() = 41.5618307521 %

get_users_short_CSV() / get_users() = 42.2769300584 %
get_users_short_CSV() / get_users() = 45.154631132 %
get_users_short_CSV() / get_users() = 44.1596819482 %
get_users_short_CSV() / get_users() = 30.3192350266 %
get_users_short_CSV() / get_users() = 34.4856637748 %
get_users_short_CSV() / get_users() = 43.7461535628 %
get_users_short_CSV() / get_users() = 41.7577246935 %
get_users_short_CSV() / get_users() = 41.9092878608 %
get_users_short_CSV() / get_users() = 44.6772360665 %
get_users_short_CSV() / get_users() = 42.6770989413 %

1

如果使用正则表达式可以通过忽略不需要拆分的行尾来加快速度,也许更直接的方法可能会有所帮助:

[snip)
ix_profile = h.index('profile.type')
ix_user = h.index('profile.id')
maxsplits = max(ix_profile, ix_user) + 1 #### new statement ####
# If either ix_* is the last field in h, it will include a newline. 
# That's fine for now.
for (i, line) in enumerate(f): 
    if i % 1000000 == 0: print "Line %d" % i # progress notification
    l = line.split('\t', maxsplits) #### changed line ####
[snip]

请在您的数据上试一下。

0

这可能有点离题,但是Python在处理多个线程时(特别是当线程不受IO限制时)会表现出一些极其奇怪的行为。更具体地说,它有时比单线程运行要慢得多。这是由于Python中全局解释器锁(GIL)的使用方式,以确保任何给定时间内只有一个线程可以在Python解释器中执行。

由于只有一个线程实际上可以在任何给定时间使用解释器的限制,因此您拥有多个核心也无济于事。实际上,由于两个线程尝试获取GIL之间的某些病理相互作用,情况可能会变得更糟。如果您想坚持使用Python,则有两种选择:

  1. 尝试使用Python 3.2(或更高版本,3.0不可用)。它有一种处理GIL的方式,可以在许多情况下解决多线程减速问题。我假设您没有使用Python 3系列,因为您正在使用旧的print语句。
  2. 使用进程而不是线程。由于进程共享打开的文件描述符,一旦您开始读取文件(如果您确实需要),您就不需要在进程之间传递任何状态(您可以使用管道或消息)。这将略微增加初始启动时间,因为进程比线程需要更长的时间来创建,但您可以避免GIL问题。

如果您想了解更多关于Python中奇妙的晦涩部分的信息,请查看此页面上与GIL相关的演讲:http://www.dabeaz.com/talks.html


我明确地使用多进程来解决线程中的GIL问题。 - Jason Sundram

0

也许你可以做到

users[l[ix_user]] += 1 

替代

users[l[ix_user][1:-1]] += 1 

并在字典末尾删除引号,这样可以节省一些时间。

对于多线程方法:尝试每次从文件中读取几千行,并将这些千行传递给一个线程进行处理。逐行处理似乎会产生太多开销。

或者阅读this article中的解决方案,因为他似乎正在做与您尝试做的事情非常相似的事情。


@Claudio,删减分片操作并没有显著的差别(实际上会增加2秒,原因我不太清楚)。 - Jason Sundram

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