这是获取并删除文件第一行的最有效方法吗?

19
我有一个脚本,每次被调用时它会获取一个文件的第一行。每一行都是固定长度为32个字母或数字,并以"\r\n"结尾。 在获取了第一行后,脚本会将其删除。
这是通过以下方式实现的:
$contents = file_get_contents($file));
$first_line = substr($contents, 0, 32);
file_put_contents($file, substr($contents, 32 + 2)); //+2 because we remove also the \r\n

显然这个方法可行,但我想知道是否有更加聪明(或者更高效)的方法?

在我的简单解决方案中,我基本上读取并重新编写整个文件,只为了获取并删除第一行


你可以在内存中更高效地处理它(循环读取一行,逐行写出除第一行外的所有行),但这看起来会很复杂,并且容易出错。我会和你做一样的事情。无论如何,文件都是按顺序存储的,从第一个字节开始。 - Seva Alekseyev
1
如果您可以将文件存储为索引,并通过索引执行所有读写操作,那么这个操作可能会更快,因为您只需从索引中简单地删除该行,这样做比在完整文件上执行此操作要便宜得多。但是,如果文件很小,则I/O成本将小于维护索引的开销。 - anijhaw
3
我能想到的唯一高度优化的解决类似问题的方案,涉及文件系统驱动程序中的IOCTL,可以从文件中剪切掉第一个逻辑块(大小取决于硬件和实现),而不会触及其余部分。但这只是在解决不存在的问题上进行学术性练习,并且肯定不是你需要的。 :) - Seva Alekseyev
为什么不在第三行将“32 + 2”替换为“34”呢?这可能会节省几毫秒的时间,如果您一直这样做,可能会更多。很抱歉这不能作为评论(我没有足够的声望)。 - Arye Eidelman
12个回答

28

昨天我想到了这个想法:

function read_and_delete_first_line($filename) {
  $file = file($filename);
  $output = $file[0];
  unset($file[0]);
  file_put_contents($filename, $file);
  return $output;
}

你仍然读取并重写整个文件,但我承认这样稍微好一点。+1 - Marco Demaio
谢谢,这正是我在寻找的。 - snapplex
我们在谈论多大的规模? - Raf A.

14

除了重写文件之外,没有更有效的方法来完成这个任务。


13

不需要创建第二个临时文件,也不需要将整个文件存入内存:

if ($handle = fopen("file", "c+")) {             // open the file in reading and editing mode
    if (flock($handle, LOCK_EX)) {               // lock the file, so no one can read or edit this file 
        while (($line = fgets($handle, 4096)) !== FALSE) { 
            if (!isset($write_position)) {        // move the line to previous position, except the first line
                $write_position = 0;
            } else {
                $read_position = ftell($handle); // get actual line
                fseek($handle, $write_position); // move to previous position
                fputs($handle, $line);           // put actual line in previous position
                fseek($handle, $read_position);  // return to actual position
                $write_position += strlen($line);    // set write position to the next loop
            }
        }
        fflush($handle);                         // write any pending change to file
        ftruncate($handle, $write_position);     // drop the repeated last line
        flock($handle, LOCK_UN);                 // unlock the file
    }
    fclose($handle);
}

1
你可以在代码旁边加上一些简短的注释,解释你正在做什么吗? - Marco Demaio
这段代码根本不起作用,它只是简单地覆盖了原来的行。请参考Marcos Fernandez Ramo的答案查看基于这个代码的工作版本。答案链接 - user

6
这将移动文件的第一行,你不需要像使用“file”函数时那样将整个文件加载到内存中。也许对于小文件而言比'file'慢一些(也许是这样,但我敢打赌不会),但它能够轻松处理更大的文件。
$firstline = false;
if($handle = fopen($logFile,'c+')){
    if(!flock($handle,LOCK_EX)){fclose($handle);}
    $offset = 0;
    $len = filesize($logFile);
    while(($line = fgets($handle,4096)) !== false){
        if(!$firstline){$firstline = $line;$offset = strlen($firstline);continue;}
        $pos = ftell($handle);
        fseek($handle,$pos-strlen($line)-$offset);
        fputs($handle,$line);
        fseek($handle,$pos);
    }
    fflush($handle);
    ftruncate($handle,($len-$offset));
    flock($handle,LOCK_UN);
    fclose($handle);
}

它是否比Edakos的答案更好? - user
我之所以制作它,是因为我无法使Edakos的解决方案正常工作。不过,在第3行有一个错误的“continue”。 - Marcos Fernandez Ramos
确实,那个答案中的代码不起作用。给你加一分。 - user
1
@MarcosFernandezRamos,为什么不编辑并删除错误的“continue”?而是用注释来提到它... - vdegenne
@발렌텐 懒惰吧,我猜。 - Marcos Fernandez Ramos
如果您尝试在单个服务器调用中运行此代码片段多次,请记住filesize()可能会返回缓存值。如果是这种情况,请在每次调用之前运行clearstatcache() - sfscs

5

您可以迭代文件,而不是将它们全部存储在内存中。

$handle = fopen("file", "r");
$first = fgets($handle,2048); #get first line.
$outfile="temp";
$o = fopen($outfile,"w");
while (!feof($handle)) {
    $buffer = fgets($handle,2048);
    fwrite($o,$buffer);
}
fclose($handle);
fclose($o);
rename($outfile,$file);

1
+1:我认为这更节省内存,但不一定更快。当然,如果文件太大无法放入内存中,它也不会崩溃。 - Marco Demaio

4

通常我不建议为此打开shell,但如果您不经常处理大文件,可能有些话需要说:

$lines = `wc -l myfile` - 1;
`tail -n $lines myfile > newfile`;

这很简单,不需要将整个文件读入内存。

但我不建议在处理小型文件或非常频繁的使用中使用此方法。开销太高。


2
这种方式并不高效,而且代码也不具备可移植性。 - anijhaw
5
对于一个3GB的文件来说,这种方法比大多数在这里发布的答案更有效率。大多数发布的答案会因为文件过大而出现内存溢出错误。你说得没错,不过这种方法并不具备可移植性。只有在非常特定的情况下,这种解决方案才是有用/可接受的。 - Frank Farmer
希望能在这个问题上得到一些评论,我精通Linux,但仍然遇到了麻烦。谢谢。 - iGNEOS

2

这里有一种方法:

$contents = file($file, FILE_IGNORE_NEW_LINES);
$first_line = array_shift($contents);
file_put_contents($file, implode("\r\n", $contents));

还有无数其他方法也可以做到这一点,但所有方法都涉及以某种方式分离第一行并保存其余部分。您无法避免重写整个文件。另一种替代方法:

list($first_line, $contents) = explode("\r\n", file_get_contents($file), 2);
file_put_contents($file, implode("\r\n", $contents));

你的第一个例子会生成冗余的换行符。如果在file()中没有使用FILE_IGNORE_NEW_LINES标志,你就不需要再次使用新行将这些行连接起来。 - Decent Dabbler
Ulman: +1 很有趣的代码,谢谢!我以前从未使用过文件函数。 - Marco Demaio

2
你可以将位置信息存储到文件本身中。例如,文件的前8个字节可以存储一个整数。该整数是文件中第一行真正字节偏移量。
因此,您不再删除行。相反,删除一行意味着更改起始位置。使用fseek()定位到它,然后像往常一样读取行。
文件最终会变得很大。您可以定期清理孤立的行以减小文件大小。
但是,说真的,只需使用数据库,不要做这样的事情。

1
我的问题是大文件。我只需要编辑或删除第一行。这是我使用的解决方案。不需要在变量中加载完整的文件。目前回显,但您始终可以保存内容。
$fh = fopen($local_file, 'rb');
echo "add\tfirst\tline\n";  // add your new first line.
fgets($fh); // moves the file pointer to the next line.
echo stream_get_contents($fh); // flushes the remaining file.
fclose($fh);

0

我认为这对于任何文件大小都是最好的

$myfile = fopen("yourfile.txt", "r") or die("Unable to open file!");
$ch=1;

while(!feof($myfile)) {
  $dataline= fgets($myfile) . "<br>";
  if($ch == 2){
  echo str_replace(' ', '&nbsp;', $dataline)."\n";
  }
  $ch = 2;
} 
fclose($myfile);

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