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

Sha*_*aun 8 php csv performance file-io parsing

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

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

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

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

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

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

Onfgetcsv()
此功能可以很容易地根据某些分隔符在文件中拆分行,并且我确定它在扫描文件时逐个字符地检查分隔符.然而,问题是我正在寻找基本上两个分隔符,并且fgetcsv()只接受一个分隔符.例如:

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

我所试图做的是检查在最大性能一举逗号和反斜杠(也许还有其他的东西,如[表名]).如果fgetcsv()允许我指定多个分隔符(或正则表达式)或允许我更改它认为是"行尾"(从\n或\n\r到只有\),那么它将完美地工作,但是这似乎不可能.

Wes*_*n C 4

您可以编写一个逐字符累加循环,(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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

另一方面,我们可以选择将整个文件缓冲到内存中,其中包括 Asker 提到但想要避免的原始内存密集型解决方案。快乐的媒介可能位于中间的某个地方:我们可以使用可以作为第二个参数传递的读取限制,fgets()在单个 I/O 吞吐中引入稍大(但不是大得离谱)的字符数,然后逐个字符地处理该缓冲区而不是 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

$r = new StrBufferedChrReader($filename,$bufferSize);
while($c = $r->getc()) {
    ...
Run Code Online (Sandbox Code Playgroud)

像这样的事情允许您通过更改 $bufferSize 来在内存密集型解决方案和 I/O 密集型解决方案之间的连续体上标出许多不同的点。$bufferSize 越大,内存使用量越大,I/O 操作越少。$bufferSize 更小,内存使用量更少,I/O 操作更多。

(注意:不要假设该类已准备好用于生产。它只是作为可能的抽象的说明,可能包含一对一或其他错误。可能会导致视力模糊、睡眠不足、心悸或其他方面效果。使用前请咨询医生并进行单元测试。)