合并2个非常大的文本文件,更新每一行,不使用内存


Merging 2 very large text files, update each line, without using memory

假设我有两个文本文件,每个文件大约有200万行(每个文件大小约为50-80MB)。两个文件的结构是相同的:

Column1 Column2 Column3
...

列1永远不会改变,列2:相同的值可能不在两个文件中,并且在两个文件中的顺序也不会相同,Column3是一个数字,在每个文件中都会不同。

我需要能够将它们合并到一个文件中,由列2匹配。如果两个文件中都存在Column2,则通过将两个文件中Column3的值相加来更新Column3。

如果文件不是那么大,我可以很容易地在PHP中做到这一点,通过将两个文件的每一行读入数组并从那里开始,但是这样做很容易使可用内存过载。

是否有一种方法可以做到这一点,而无需将每一行加载到内存中?我主要熟悉PHP,但如果Python、Java或Shell脚本不是太复杂而难以理解,我也可以使用它们。

我会使用命令行sort(1)来合并和排序文件。之后,它应该是一个简单的脚本来计算总和。我不懂PHP,所以我用python来举例:

sort -k2 <file1> <file2> | python -c "
  import itertools,sys
  allLines = (x.strip().split(' ') for x in sys.stdin)
  groups = itertools.groupby(allLines, lambda x:x[1])
  for k,lines in groups:
      firstLine = iter(g).next()
      print firstLine[0], firstline[1], sum(int(x[2]) for x in lines)
"

好的,如果我没看错的话,你会有:

file1:

abc 12 34
abc 56 78
abc 90 12

file2:

abc 90 87  <-- common column 2
abc 12 67  <---common column 2
abc 23 1   <-- unique column 2

输出应该是:

abc 12 101
abc 90 99

如果是这种情况,那么像这样(假设它们是。csv格式):

$f1 = fopen('file1.txt', 'rb');
$f2 = fopen('file2.txt', 'rb');
$fout = fopen('outputxt.');
$data = array();
while(1) {
    if (feof($line1) || feof($line2)) {
        break; // quit if we hit the end of either file
    }
    $line1 = fgetcsv($f1);
    if (isset($data[$line1[1]])) {
       // saw the col2 value earlier, so do the math for the output file:
       $col3 = $line1[2] + $data[$line1[1]];
       $output = array($line[0], $line1[1], $col3);
       fputcsv($fout, $output);
       unset($data[$line1[1]]);
    } else {
       $data[$line1[1]] = $line1; // cache the line, if the col2 value wasn't seen already
    }
    $line2 = fgetcsv($f2);
    if (isset($data[$line2[1]])) {
       $col3 = $data[$line2[1]] + $line2[2];
       $newdata = array($line2[0], $line2[1], $col3);
       fputcsv($fout, $newdata);
       unset($data[$line2[1]]); // remove line from cache
    } else {
       $data[$line2[1]] = $line2;
    }
}
fclose($f1);
fclose($f2);
fclose($fout);

这是我的头,没有测试,可能不会工作,YMMV,等等…

如果预先对两个输入文件进行排序,那么使用column2作为排序键,将极大地简化事情。这样可以减小缓存大小,因为您可以知道是否已经看到了匹配的值,以及何时转储早期缓存的数据。

让您感到困惑的是,您正在查看两个文件。没必要这么做。用马克的例子:file1:

abc 12 34
abc 56 78
abc 90 12

file2:

abc 90 87  
abc 12 67  
abc 23 1  
然后

sort file1 file2 > file3

收益率file3:

abc 12 34
abc 12 67  
abc 23 1
abc 56 78
abc 90 12
abc 90 87  

CS-101的第二周,将其简化为最终形式

您可以使用Python sqlite3包含模块轻松解决此问题,而无需使用太多内存(大约13 Mb, 100万行):

import sqlite3
files = ("f1.txt", "f2.txt")    # Files to compare
# # Create test data
# for file_ in files:
#   f = open(file_, "w")
#   fld2 = 0
#   for fld1 in "abc def ghi jkl".split():
#       for fld3 in range(1000000 / 4):
#           fld2 += 1
#           f.write("%s %s %s'n" % (fld1, fld2, 1))
# 
#   f.close()
sqlite_file = "./join.tmp"      # or :memory: if you don't want to create a file
cnx = sqlite3.connect(sqlite_file)
for file_ in range(len(files)):     # Create & load tables
    table = "file%d" % (file_+1)
    cnx.execute("drop table if exists %s" % table)
    cnx.execute("create table %s (fld1 text, fld2 int primary key, fld3 int)" % table)
    for line in open(files[file_], "r"):
        cnx.execute("insert into %s values (?,?,?)" % table, line.split())
# Join & result
cur = cnx.execute("select f1.fld1, f1.fld2, (f1.fld3+f2.fld3) from file1 f1 join file2 f2 on f1.fld2==f2.fld2")
while True:
    row = cur.fetchone()
    if not row:
        break
    print row[0], row[1], row[2]
cnx.close()

PHP的memory_limit适用于web服务器脚本的主要任务。它非常不适合批处理数据,比如您正在尝试做的工作。问题是PHP配置的memory_limit,而不是您试图做一些需要"太多"内存的事情。我的手机有足够的内存,只需加载2 80Mb的文件到内存中,并且以快速/简单的方式完成此操作,更不用说任何类型的真正的计算机应该能够轻松加载千兆字节(或至少1gb)的数据。

显然,您可以在运行时使用ini_set设置PHP的memory_limit(按照今天的标准,这是任意的,非常小),仅用于此脚本。您知道服务器上实际有多少可用内存吗?我知道按照今天的标准,很多共享主机提供商会给你非常少的内存,因为他们不希望你做比处理网页请求更多的事情。但是,您可以直接在PHP中按照您想要的方式执行此操作,而无需跳过各种步骤(并大大减慢进程),以避免一次将所有文件加载到内存中。