PHP:什么是解析包含很长行的文本文件的有效方法?

时间:2010-03-31 23:26:07

标签: php performance parsing file-io csv

我正在使用php中的解析器,它旨在从文本文件中提取MySQL记录。一个特定的行可能以一个字符串开头,该字符串对应于需要插入记录(行)的表,然后是记录本身。记录由反斜杠分隔,字段(列)用逗号分隔。为简单起见,我们假设我们有一个表格,代表我们数据库中的人,其中的字段是名字,姓氏和职业。因此,文件的一行可能如下

[People] =“\ Han,Solo,Smuggler \ Luke,Skywalker,Jedi ......”

省略号(...)可能是额外的人。一种简单的方法可能是使用fgets()从文件中提取一行,并使用preg_match()从该行中提取表名,记录和字段。

然而,我们假设我们有很多星球大战的角色需要跟踪。事实上,这一行很多,最终会有200,000多个字符/字节长。在这种情况下,采用上述方法提取数据库信息似乎效率低下。你必须首先将数十万个字符读入内存,然后阅读重新这些相同的字符以查找正则表达式匹配。

是否有一种方法,类似于使用文件构造的Scanner类的Java String next(String pattern)方法,允许您在扫描文件时在线匹配模式?

这个想法是你不必扫描相同的文本两次(从文件读取它到字符串,然后匹配模式)或冗余地将文本存储在内存中(在文件行字符串中)和匹配的模式)。这甚至会使性能显着提高吗?很难确切知道PHP或Java在幕后做了什么。

fgetcsv() 上 这个函数可以很容易地根据一些分隔符在文件中分割行,并且我确定它在扫描文件时逐个字符地检查分隔符。但问题是,我正在寻找基本上两个分隔符,而fgetcsv()只接受一个分隔符。例如:

我可以使用','作为分隔符。如果我将文件格式更改为也使用反斜杠的逗号,我可以将整行读入字段数组。那么问题是,我需要重复对所有字段确定记录的开始和结束位置以及准备sql。同样,如果我使用'\'作为分隔符(单个反斜杠,在此处转义),那么我需要重复覆盖所有记录以提取字段并准备sql。

我要做的是一次性检查两个逗号和反斜杠(以及其他东西,如[tablename])以获得最佳性能。如果fgetcsv()允许我指定多个分隔符(或正则表达式)或允许我更改它认为是“行尾”(从\ n或\ n \ r到只有\),那么它会很完美,但这似乎不太可能。

2 个答案:

答案 0 :(得分:3)

你可以写一个逐个字符的累积循环,它(a)在遇到逗号时将字符串推送到数组上,(b)在找到记录符号时调用函数将累积的字符串保存到mysql数据库中:

while($c = fgetc($fp)) {
  if($c == ',') {
    $fields[] = implode(null,$accumulator);
    $accumulator = array();
  } else if($c == '\\') {
    save_fields_to_mysql($fields);
    $fields = array();
    $accumulator = array();
  } else
    $accumulator[] = $c;
}

如果您确定您的字段从不包含您的字段或记录分隔符作为数据,这可能对您有用。

如果有可能,你需要提出一个转义序列来表示你的字段和记录分隔符的文字值(也可能是你的转义序列)。假设是这种情况,并假设%符号为转义字符:

define('ESCAPED',1);
define('NORMAL',0);

$readState = NORMAL;
while($c = fgetc($fp)) {
  if($readState == ESCAPED) {
    $accumulator[] = $c;
    $readState = NORMAL;
  } else if($c == '%') {
    $readState = ESCAPED;
  } else if($c == ',') {
    $fields[] = implode(null,$accumulator);
    $accumulator = array();
  } else if($c == '\\') {
    save_fields_to_mysql($fields);
    $fields = array();
    $accumulator = array();
  } else
    $accumulator[] = $c;
}

即,任何一个%的出现都会设置一个状态变量,该状态变量在下一次循环中表示,我们读取的任何字符都将被视为字面数据,而不是符号的一部分。

这应该将您的内存使用量保持在最低限度。

[更新] I / O效率怎么样?

一位评论者正确地指出,这个插图非常耗费I / O,并且由于I / O往往是时间上成本最高的操作,因此完全有可能它不是一个可接受的解决方案。

在频谱的另一端,我们可以选择将整个文件缓冲到内存中,其中包括Asker提到但想要避免的原始内存密集型解决方案。幸福的媒介可能位于中间的某个地方:我们可以使用你可以传递的读取限制作为fgets()的第二个参数来在一个I /中拉入一个稍大(但不是非常大)的字符数量/ O gulp,然后逐个字符地处理缓冲区而不是I / O流,在我们刻录缓冲区时重新填充它。

这确实使读取过程比$c = fgetc($fp)更加代码密集,因为您必须监视缓冲区中的位置以及缓冲区的填充程度以及文件中的位置。如果需要,可以在读取循环中使用一系列标志和索引变量来执行此操作,但是使用这样的抽象可能更方便:

class StrBufferedChrReader {

    private $_filename;
    private $_fp; 

    private $_bufferIdx;
    private $_bufferMax = 2048;
    private $_buffer;

    function __construct($filename=null,$bufferMax=null) {
        if($bufferMax) $this->_bufferMax = $bufferMax;
        if($filename) $this->open($filename);
    }

    function _refillBuffer() {
        if($this->_fp) {
            $this->_buffer = fgets($this->_fp,$this->_bufferMax + 1);
            $this->_bufferIdx = 0;
            return $this->_buffer;
        }
        return false;
    }

    function open($filename=null) {
        if($filename) $this->_filename = $filename;
        if($this->_fp = fopen($this->_filename)) 
            $this->_refillBuffer();
        return $this->_fp;
    }

    function getc() {
        if($this->_bufferIdx == $this->_bufferMax) 
            if(!$this->_refillBuffer())
                return false;
        return $this->_buffer[$this->_bufferIdx++];
    }

    function close() {
        $this->_buffer = null;
        $this->_bufferIdx = null;
        return fclose($this->_fp);
    }
}

你可以在上面的任何一个循环中使用它,如下所示:

$r = new StrBufferedChrReader($filename,$bufferSize);
while($c = $r->getc()) {
    ...

这样的事情允许您通过更改$ bufferSize在内存密集型解决方案和I / O密集型解决方案之间的连续体中放置许多不同的位置。更大的$ bufferSize,更多的内存使用,更少的I / O操作。更小的$ bufferSize,更少的内存使用,更多的I / O操作。

(注意:不要认为课程是生产就绪的。它可以作为一个可能的抽象的例证,可能包含一个或一个错误。可能导致视力模糊,睡眠不足,心悸,或其他副作用。使用前请咨询医生和单位测试。)

答案 1 :(得分:0)

也许使用strtok()函数?

$ string =“你好世界。今天美好的一天。”; $ token = strtok($ string,“”);

while($ token!= false)   {   echo“$ token
”;   $ token = strtok(“”);   }