Python,并行处理大型文本文件

时间:2015-02-21 01:38:32

标签: python parallel-processing

数据文件(SAM文件)中的样本记录:

M01383  0  chr4  66439384  255  31M  *  0  0  AAGAGGA GFAFHGD  MD:Z:31 NM:i:0
M01382  0  chr1  241995435  255 31M  *  0  0  ATCCAAG AFHTTAG  MD:Z:31 NM:i:0
......
  • 数据文件是逐行的
  • 数据文件的大小从1G​​到5G不等。

我需要逐行浏览数据文件中的记录,从每一行获取一个特定值(例如第4个值,66439384),并将此值传递给另一个函数进行处理。然后会更新一些结果计数器。

基本工作流程如下:

# global variable, counters will be updated in search function according to the value passed. 
counter_a = 0    
counter_b = 0
counter_c = 0

open textfile:
    for line in textfile:
        value = line.split()[3]
        search_function(value)    # this function takes abit long time to process

def search_function (value):
    some conditions checking:
        update the counter_a or counter_b or counter_c

使用单个进程代码和大约1.5G数据文件,在一个数据文件中运行所有记录大约需要20个小时。我需要更快的代码,因为有超过30种这种数据文件。

我正在考虑并行处理N个块中的数据文件,并且每个块将执行上述工作流并同时更新全局变量(counter_a,counter_b,counter_c)。但我不知道如何在代码中实现这一点,或者这将是有效的。

我可以访问一台服务器机器:24个处理器和大约40G RAM。

任何人都可以帮忙吗?非常感谢。

4 个答案:

答案 0 :(得分:3)

最简单的方法可能是使用现有代码一次完成所有30个文件 - 仍然需要一整天,但您可以一次完成所有文件。 (即“9个月内9个婴儿”很容易,“1个月内1个婴儿”很难)

如果您真的希望更快地完成单个文件,则取决于您的计数器实际更新的方式。如果几乎所有工作都在分析价值,您可以使用多处理模块卸载它:

import time
import multiprocessing

def slowfunc(value):
    time.sleep(0.01)
    return value**2 + 0.3*value + 1

counter_a = counter_b = counter_c = 0
def add_to_counter(res):
    global counter_a, counter_b, counter_c
    counter_a += res
    counter_b -= (res - 10)**2
    counter_c += (int(res) % 2)

pool = multiprocessing.Pool(50)
results = []

for value in range(100000):
    r = pool.apply_async(slowfunc, [value])
    results.append(r)

    # don't let the queue grow too long
    if len(results) == 1000:
        results[0].wait()

    while results and results[0].ready():
        r = results.pop(0)
        add_to_counter(r.get())

for r in results:
    r.wait()
    add_to_counter(r.get())

print counter_a, counter_b, counter_c

这将允许50个slowfuncs并行运行,因此不需要花费1000s(= 100k * 0.01s),完成需要20s(100k / 50)* 0.01s。如果您可以像上面那样将函数重构为“slowfunc”和“add_to_counter”,那么您应该可以获得24倍的加速。

答案 1 :(得分:1)

一次读取一个文件,使用所有CPU运行search_function()

#!/usr/bin/env python
from multiprocessing import Array, Pool

def init(counters_): # called for each child process
    global counters
    counters = counters_

def search_function (value): # assume it is CPU-intensive task
    some conditions checking:
        update the counter_a or counter_b or counter_c
        counter[0] += 1 # counter 'a'
        counter[1] += 1 # counter 'b'
    return value, result, error

if __name__ == '__main__':
    counters = Array('i', [0]*3)
    pool = Pool(initializer=init, initargs=[counters])
    values = (line.split()[3] for line in textfile)
    for value, result, error in pool.imap_unordered(search_function, values,
                                                    chunksize=1000):
        if error is not None:
            print('value: {value}, error: {error}'.format(**vars()))
    pool.close()
    pool.join()
    print(list(counters))

确保(例如,通过编写包装器)异常不会逃脱next(values)search_function()

答案 2 :(得分:0)

您不想做的是将文件传递给虚拟CPU。如果是这种情况,文件打开/读取可能会导致磁头在整个磁盘上随机弹跳,因为文件可能遍布整个磁盘。

相反,将每个文件分成块并处理块。

使用一个CPU打开文件。将整个事物读入数组文本。你想这样做是一个大规模的读取,以防止磁头在磁盘周围颠簸,假设你的文件以相对较大的顺序块放在磁盘上。

将其大小(以字节为单位)除以N,得到(全局)值K,即每个CPU应处理的近似字节数。分叉N个线程,并将每个线程传递给它的索引i,并为每个文件复制一个句柄。

每个线程我将一个线程局部扫描指针p作为offset i * K启动到Text中。它会扫描文本,递增p并忽略文本,直到找到换行符。此时,它开始处理线(扫描线时增加p)。当处理一行时,Tt在文本文件中的索引大于(i + 1)* K时停止。

如果每行的工作量大致相等,那么你的N个核心将在大约相同的时间内完成。

(如果您有多个文件,则可以启动下一个文件。)

如果您知道文件大小小于内存,则可以安排将文件读取为流水线,例如,在处理当前文件时,文件读取线程正在读取下一个文件。

答案 3 :(得分:0)

此解决方案适用于一组文件。

对于每个文件,它将它划分为指定数量的行对齐块,并行解决每个块,然后组合结果。

它从磁盘流传输每个块;这有点慢,但不会消耗太多的内存。我们依靠磁盘缓存和缓冲读取来防止头部抖动。

用法就像

python script.py -n 16 sam1.txt sam2.txt sam3.txt

script.py

import argparse
from io import SEEK_END 
import multiprocessing as mp

#
# Worker process
#
def summarize(fname, start, stop):
    """
    Process file[start:stop]

    start and stop both point to first char of a line (or EOF)
    """
    a = 0
    b = 0
    c = 0

    with open(fname, newline='') as inf:
        # jump to start position
        pos = start
        inf.seek(pos)

        for line in inf:
            value = int(line.split(4)[3])

            # *** START EDIT HERE ***
            #

            # update a, b, c based on value

            #
            # *** END EDIT HERE ***

            pos += len(line)
            if pos >= stop:
                break

    return a, b, c

def main(num_workers, sam_files):
    print("{} workers".format(num_workers))
    pool = mp.Pool(processes=num_workers)

    # for each input file
    for fname in sam_files:
        print("Dividing {}".format(fname))
        # decide how to divide up the file
        with open(fname) as inf:
            # get file length
            inf.seek(0, SEEK_END)
            f_len = inf.tell()
            # find break-points
            starts = [0]
            for n in range(1, num_workers):
                # jump to approximate break-point
                inf.seek(n * f_len // num_workers)
                # find start of next full line
                inf.readline()
                # store offset
                starts.append(inf.tell())

        # do it!
        stops = starts[1:] + [f_len]
        start_stops =  zip(starts, stops)
        print("Solving {}".format(fname))
        results = [pool.apply(summarize, args=(fname, start, stop)) for start,stop in start_stops]

        # collect results
        results = [sum(col) for col in zip(*results)]
        print(results)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Parallel text processor')
    parser.add_argument('--num_workers', '-n', default=8, type=int)
    parser.add_argument('sam_files', nargs='+')
    args = parser.parse_args()
    main(args.num_workers, args.sam_files)
    main(args.num_workers, args.sam_files)