使用PHP从1.3GB的文本文件中提取文本的最佳方法是什么?

5
我有一个1.3GB的文本文件,需要在PHP中提取一些信息。我已经研究过了,有几种不同的方法可以做到我所需要的,但总是需要一些澄清,哪种方法最好,或者是否存在其他更好的方法,我不知道呢?
文本文件中我需要的信息仅为每行的前40个字符,文件中大约有1700万行。每行的前40个字符将被插入到数据库中。
我拥有的方法如下;
// REMOVE TIME LIMIT
set_time_limit(0);
// REMOVE MEMORY LIMIT
ini_set('memory_limit', '-1');
// OPEN FILE
$handle = @fopen('C:\Users\Carl\Downloads\test.txt', 'r');
if($handle) {
    while(($buffer = fgets($handle)) !== false) {
        $insert[] = substr($buffer, 0, 40);
    }
    if(!feof($handle)) {
        // END OF FILE
    }
    fclose($handle);
}

上面的方法是逐行读取并获取数据,我已经将所有的数据库插入命令排序,每次以事务方式执行50个插入命令,总共执行10次。
下一个方法基本相同,但使用file()将所有行存储到数组中,然后使用foreach获取数据?不过我对这种方法并不确定,因为该数组实际上会有超过1700万个值。
另一种方法是仅提取文件的一部分,重新编写未使用的数据,并在执行该部分后调用header来重新启动脚本?
在获取最快和最有效的结果方面,哪种方法是最好的?还是有更好的方法来处理这个问题,我没有想到?
此外,我计划在wamp中使用此脚本,但在浏览器中测试运行时已经出现了超时的问题,即使将脚本超时时间设置为0。有没有办法让脚本在不通过浏览器访问页面的情况下运行?

对于最后一点,php path/to/script.php 将执行该脚本。 - sarnold
@sarnold 我只需要在命令行中执行吗?谢谢。 - Griff
1
是的,直接从命令行开始。如果您打算经常执行它,您还可以将其制作为可执行脚本文件,方法是在脚本的第一行添加“#!/path/to/php”,然后运行“chmod 755 path/to/script”或“chmod 500”或任何适当的权限。 - sarnold
使用一些高级编程语言(如Java)来压缩整个文件,准备每行所需的数据,然后使用PHP将数据添加到数据库中,这个方案怎么样? - TeaCupApp
打开文件,使用 fgetl 读取每一行,进行所需操作,并像这样循环。您不会在任何时候存储超过一行的内存。那么文件有多大也无关紧要。 - Ansari
1
如果在PHP中进行此操作,您应该使用预处理语句并同时插入数据。您可以进行基准测试以查看是否有益于使用扩展插入语法一次插入(例如)1000个。 - Matthew
3个回答

5

到目前为止,你的表现不错,请不要使用"file()"函数,因为它很可能会达到RAM使用限制并终止你的脚本。

我甚至不会将东西累积到“insert []”数组中,因为这也会浪费RAM。如果可以的话,立即插入数据库。

顺便说一句,有一个很好的工具叫做“cut”,你可以用它来处理文件。

cut -c1-40 file.txt

你甚至可以将cut的标准输出重定向到一些插入数据库的PHP脚本中。
cut -c1-40 file.txt | php -f inserter.php

inserter.php可以从php://stdin读取行并插入到数据库中。

"cut"是所有Linux都可用的标准工具,如果你使用Windows可以通过MinGW shell获取它,或作为msystools的一部分(如果您使用git),或者使用gnuWin32安装本机win32应用程序。


但是,这对于MySQL来说不会太过分吗?一次性插入1700万次或每次50次?在插入50次后,数组将被重置。 - Griff
@Griff,这是PHP数组访问与MySQL插入速度的比较。它可能会更快,但也可能会更慢。唯一检查的方法是进行基准测试。此外,使用仅参数更改的预处理INSERT语句可能有所帮助。 - Milan Babuškov
感谢您提供这些信息。我会立即开始工作,并告诉您我的进展情况 :) - Griff
1
谢谢,这个完美地运行了。脚本已经运行了5分钟,并插入了超过100万行数据。我所做的不同之处就是使用了插入数组,在达到600时进行多次插入。谢谢伙计! - Griff

2

为什么要在PHP中这样做,而你的关系型数据库几乎肯定已经内置了批量导入功能呢?例如,MySQL就有 LOAD DATA INFILE

LOAD DATA INFILE 'data.txt'
INTO TABLE `some_table`
  FIELDS TERMINATED BY ''
  LINES TERMINATED BY '\n';
  ( @line )
SET `some_column` = LEFT( @line, 40 );

一个查询。

MySQL还拥有mysqlimport实用程序,可以从命令行包装此功能。


我的共享主机不允许我使用INFILE,这是我的首选。 - Griff

1

以上都不是问题所在。使用fgets()的问题在于它不能按照你的期望工作。当达到最大字符数时,下一次调用fgets()将继续在同一行上。你已经正确地识别了使用file()的问题。第三种方法是一个有趣的想法,你也可以用其他解决方案来实现它。

话虽如此,你使用fgets()的第一个想法非常接近,但我们需要稍微修改它的行为。这里是一个定制版本,它将按照你的期望工作:

function fgetl($fp, $len) {
    $l = 0;
    $buffer = '';
    while (false !== ($c = fgetc($fp)) && PHP_EOL !== $c) {
        if ($l < $len)
            $buffer .= $c;
        ++$l;
    }
    if (0 === $l && false === $c) {
        return false;
    }
    return $buffer;
}

立即执行插入操作,否则会浪费内存。确保使用{{link1:预处理语句}}来插入这么多行数据;这将大大减少执行时间。当您只能提交数据时,您不希望在每次插入时提交完整查询。


自从PHP 4.3.0以来,这仍然是一个问题吗?另外,如果有76倍的函数调用,速度会如何? - Wiseguy
我相信这是fgets()函数的预期行为。如果您正在使用预处理语句(http://php.net/manual/en/pdo.prepared-statements.php),则速度不应该成为问题。 - ksiimson
@KSiimson 我正在使用PDO预处理语句,@Wiseguy 这就是我认为省略length属性会产生我想要的效果的原因? - Griff
@Griff 对的,省略 length 属性并使用 substr() 将得到相同的结果。只有当行数超过几千个字符时,使用我的示例中的 fgetc() 才会有益处。因此,除非你的行很长,否则你可以放心使用! - ksiimson
@KSiimson 是的,这些行不超过500个字符,但我已经保存了这个函数,它肯定会派上用场!谢谢。 - Griff

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