在PHP中迭代字符串中的每一行

165

我有一个表单,允许用户上传文本文件或将文件内容复制/粘贴到文本区域。我可以轻松区分两者,并将他们输入的任何一个放入字符串变量中,但我接下来该怎么做呢?

我需要迭代字符串中的每一行(最好不用担心不同机器上的换行符),确保它只有一个标记(没有空格、制表符、逗号等),并对数据进行消毒,然后根据所有行生成一个SQL查询。

我是个相当不错的程序员,所以我知道大致的实现方法,但自从我和PHP打交道以来已经这么久了,我感觉我在寻找错误的东西,因此得到的信息都是没用的。我遇到的主要问题是我想逐行读取字符串的内容。如果它是一个文件,那就很容易解决。

我主要在寻找有用的PHP函数,而不是如何实现它的算法。有什么建议吗?


你可能需要先规范化换行符。方法s($myString)->normalizeLineEndings()可在https://github.com/delight-im/PHP-Str(遵循MIT许可证的库)中使用,该库还有许多其他有用的字符串帮助程序。你可能想要查看源代码。 - caw
8个回答

225

使用preg_split对包含文本的变量进行拆分,并迭代返回的数组:

foreach(preg_split("/((\r?\n)|(\r\n?))/", $subject) as $line){
    // do stuff with $line
} 

12
更好的正则表达式是 /((\r?\n)|(\r\n?))/ - Félix Saparelli
3
为了匹配Unix的LF(\n),MacOS<9的CR(\r),Windows的CR+LF(\r\n)和罕见的LF+CR(\n\r),应该使用正则表达式:/((\r?\n)|(\n?\r))/ - Waiting for Dev...
2
这很可能会对多字节数据造成灾难性的影响。 - pguardiario
@CrisG /(\r?\n?)/ 可以匹配 ''(空字符串),这将把字符串分割成字符。 - Félix Saparelli
如果你想要一个简短的答案,CodeAngry展示 /[\r\n]+/ 可以工作。但是需要注意的是,_这将不加区分地匹配单个和多个换行符_,这可能是不想要的。当然,出于性能原因,应该避免使用正则表达式进行适当的换行符拆分。 - Félix Saparelli
显示剩余5条评论

189

我希望提出一个 显著 更快速(且内存占用更小)的替代方案:strtok,而非preg_split

$separator = "\r\n";
$line = strtok($subject, $separator);

while ($line !== false) {
    # do something with $line
    $line = strtok( $separator );
}

测试性能,我在一个有1.7万行的测试文件上进行了100次迭代:preg_split花费了27.7秒,而strtok只花费了1.4秒。

请注意,尽管$separator被定义为"\r\n",但strtok将分隔符设置为任意字符,并且自PHP4.1.0起,跳过空行/标记。

有关strtok的详细信息,请参阅手册条目:http://php.net/strtok


28
处理大量行集时,为了性能考虑,给这个做个加一。 - CodeAngry
5
尽管这个函数API很混乱(需要使用不同的参数进行调用),但这是最佳解决方案。既不应该使用prey_split也不应该使用explode来生成结构化的字符串片段。这就像用火箭筒打苍蝇一样。 - Maciej Sz
3
如果您在应用程序运行时检查内存使用情况,那么您会看到魔法。它实际上将您正在读取的文件拉入内存,以便在循环遍历每一行时使用,并且它会保留您的令牌位置。您需要刷新它才能真正实现内存效率。http://php.net/strtok#103051 - AbsoluteƵERØ
2
快速记录,使用strtok()在那个while循环中处理其他内容会导致错误。我之前也用它来获取字符串中第一个空格之前的所有内容(https://dev59.com/wnE95IYBdhLWcg3wDpmg#2477411),花了我一分钟才意识到为什么事情没有按计划进行。 - But those new buttons though..
2
这应该是被接受的答案,可能是所有选项中最快的解决方案。 - John
显示剩余5条评论

107

如果你需要处理不同系统中的换行符,你可以简单地使用PHP预定义常量PHP_EOL(http://php.net/manual/en/reserved.constants.php),并且只需使用explode函数来避免正则表达式引擎的开销。

$lines = explode(PHP_EOL, $subject);

36
注意:它可以在不同系统上运行,但不能很好地处理来自其他系统的字符串。 PHP手册 显示 PHP_EOL (string) 是“此平台的正确换行符号”。 - wadim
1
@wadim是正确的!如果您在Unix服务器上处理Windows文本文件,它将会失败。 - javsmo
1
请注意,根据您的行长度,这可能会消耗大量内存来处理大字符串。 - Synchro
1
请注意,如果最后一行包含行终止符,则在此之后还会返回另一个空字符串。 - user1804599

25

虽然有点复杂且不美观,但我认为这是正确的方法:

$fp = fopen("php://memory", 'r+');
fputs($fp, $data);
rewind($fp);
while($line = fgets($fp)){
  // deal with $line
}
fclose($fp);

2
+1,你也可以使用php://temp将更大的数据存储到临时磁盘文件中。 - CodeAngry
6
需要注意的是,这种方法可以检测到空行,而 strtok() 方法则不行。具体的文档请参见 http://php.net/manual/en/wrappers.php.php#refsect2-wrappers.php-unknown-unknown-unknown-unknown-unknown-descriptios。 - Josip Rodin

9

strtok可能存在的内存问题:

由于其中一个建议的解决方案使用了strtok,不幸的是它没有指出潜在的内存问题(尽管它声称具有内存效率)。根据手册使用strtok时:

请注意,只有第一次调用strtok使用字符串参数。 每个后续调用strtok只需要使用令牌,因为它会跟踪当前字符串的位置。

它通过将文件加载到内存中来实现这一点。如果您正在使用大文件,则需要在循环完文件后刷新它们。

<?php
function process($str) {
    $line = strtok($str, PHP_EOL);

    /*do something with the first line here...*/

    while ($line !== FALSE) {
        // get the next line
        $line = strtok(PHP_EOL);

        /*do something with the rest of the lines here...*/

    }
    //the bit that frees up memory
    strtok('', '');
}

如果您只关心物理文件(例如数据挖掘):

根据手册,对于文件上传部分,您可以使用file命令:

 //Create the array
 $lines = file( $some_file );

 foreach ( $lines as $line ) {
   //do something here.
 }

6
foreach(preg_split('~[\r\n]+~', $text) as $line){
    if(empty($line) or ctype_space($line)) continue; // skip only spaces
    // if(!strlen($line = trim($line))) continue; // or trim by force and skip empty
    // $line is trimmed and nice here so use it
}

^ 这是正确换行的方法,与Regexp跨平台兼容 :)


5

考虑到您需要能够处理不同机器上的换行符,Kyril的答案是最好的。

"我主要寻找有用的PHP函数,而不是如何执行的算法。 有什么建议吗?"

我经常使用以下函数:

  • explode() 可以将字符串按照给定的分隔符拆分为数组。
  • implode() 是explode的对应函数,可以将数组转换回字符串。

4

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