如何使用php从文件中删除一行?

23

我有一个名为 $dir 的文件和一个名为 $line 的字符串,我知道这个字符串是该文件的一行,但我不知道它的行号,我想将其从文件中删除,该怎么办?

可以使用awk吗?


awk是一个外部程序,您需要使用exec或类似的函数来调用它。 - Raisen
相关链接:https://stackoverflow.com/q/49909147/2943403 - mickmackusa
10个回答

20
$contents = file_get_contents($dir);
$contents = str_replace($line, '', $contents);
file_put_contents($dir, $contents);

@ibrahim - 你说的“没用”是什么意思?你能通过file_get_contents获取到所有内容吗?告诉我具体哪个部分没有起作用。 - Naveed Ahmad
"file_put_contents" 不会将内容写入文件。 - ibrahim
也许您没有文件的写入权限。请查看:http://php.net/manual/zh/function.file-put-contents.php - Naveed Ahmad
4
完全删除该行: $contents = str_replace($line."\n", '', $contents); - Andrés Chandía
3
有时换行符会有所不同,我刚刚尝试了一下我的解决方案,它按预期工作了。无论如何,请尝试使用 $contents = str_replace($line.PHP_EOL, '', $contents); 替换。 - Andrés Chandía
显示剩余3条评论

20

逐行阅读文件内容,将所有不匹配的行写入到另一个文件中。然后用新文件替换原文件。


awk怎么样?能在PHP中使用吗? - ibrahim
4
没必要。PHP自己就能很好地处理它。 - Ignacio Vazquez-Abrams
1
这个文件可能有数十万行,所以我希望用最有效率的方法来完成它 :) - ibrahim
当您处理大文件(超过RAM大小)时,这是更有效的方法。 - HILARUDEEN S ALLAUDEEN

12

这只是查看每一行,如果不是要删除的内容,则将其推送到一个数组中,该数组将被写回文件。参见此处。

 $DELETE = "the_line_you_want_to_delete";

 $data = file("./foo.txt");

 $out = array();

 foreach($data as $line) {
     if(trim($line) != $DELETE) {
         $out[] = $line;
     }
 }

 $fp = fopen("./foo.txt", "w+");
 flock($fp, LOCK_EX);
 foreach($out as $line) {
     fwrite($fp, $line);
 }
 flock($fp, LOCK_UN);
 fclose($fp);  

为了简单起见,为什么不将$out构建为字符串而不是数组,然后使用file_put_contents和LOCK_EX呢?采用这种方法的原因是什么? - Cyrille

6

不需要使用awk也可以解决:

function remove_line($file, $remove) {
    $lines = file($file, FILE_IGNORE_NEW_LINES);
    foreach($lines as $key => $line) {
        if($line === $remove) unset($lines[$key]);
    }
    $data = implode(PHP_EOL, $lines);
    file_put_contents($file, $data);
}

SO正在询问有关awk的问题。你的答案可能解决了问题,但请尽量解释一下你是如何解决这个问题的。 - Michael
1
@Michael,SO提出了两个问题!1)我应该怎么做?2)是否可以使用awk?所以我回答了第一个问题。并且看到最佳答案标记,它没有使用awk - Nabi K.A.Z.

5
另一种方法是逐行读取文件,直到找到匹配项,然后将文件截断到该点,然后附加其余的行。

1
你怎么得到那些行?你刚刚截断了文件。 - Ignacio Vazquez-Abrams
将它们存入内存,然后进行截断处理,只要文件不太大。并不是说这是更好的解决方案,但应该会减少磁盘写入次数。 - mpen

3
所有回答都有一个共同点,就是它们将完整的文件加载到内存中。这里提供了一种在不将文件内容复制到变量中的情况下删除一个或多个行的实现方法。
这个想法是迭代文件中的每一行。如果要移除一行,则将该行长度添加到$byte_offset中。然后下一行向上移动$byte_offset字节。这样做直到所有后续行都被处理完毕。如果所有行都被处理完毕,则会删除文件的最后$byte_offset字节。
我猜这对于更大的文件来说速度更快,因为不需要复制任何东西。而且我猜在某些文件尺寸上,其他答案可能根本不起作用,而这个方法应该可以。但是我没有测试过。
使用方法:
$file = fopen("path/to/file", "a+");
// remove lines 1 and 2 and the line containing only "line"
fremove_line($file, 1, 2, "line");
fclose($file);

fremove_line()函数的代码:

/**
 * Remove the `$lines` by either their line number (as an int) or their content
 * (without trailing new-lines).
 * 
 * Example:
 * ```php
 * $file = fopen("path/to/file", "a+"); // must be opened writable
 * // remove lines 1 and 2 and the line containing only "line"
 * fremove_line($file, 1, 2, "line");
 * fclose($file);
 * ```
 * 
 * @param resource $file The file resource opened by `fopen()`
 * @param int|string ...$lines The one-based line number(s) or the full line 
 *     string(s) to remove, if the line does not exist, it is ignored
 * 
 * @return boolean True on success, false on failure
 */
function fremove_line($file, ..$lines): bool{
    // set the pointer to the start of the file
    if(!rewind($file)){
        return false;
    }

    // get the stat for the full size to truncate the file later on
    $stat = fstat($file);
    if(!$stat){
        return false;
    }

    $current_line = 1; // change to 0 for zero-based $lines
    $byte_offset = 0;
    while(($line = fgets($file)) !== false){
        // the bytes of the lines ("number of ASCII chars")
        $line_bytes = strlen($line);

        if($byte_offset > 0){
            // move lines upwards
            // go back the `$byte_offset`
            fseek($file, -1 * ($byte_offset + $line_bytes), SEEK_CUR);
            // move the line upwards, until the `$byte_offset` is reached
            if(!fwrite($file, $line)){
                return false;
            }
            // set the file pointer to the current line again, `fwrite()` added `$line_bytes`
            // already
            fseek($file, $byte_offset, SEEK_CUR);
        }

        // remove trailing line endings for comparing
        $line_content = preg_replace("~[\n\r]+$~", "", $line);

        if(in_array($current_line, $lines, true) || in_array($line_content, $lines, true)){
            // the `$current_line` should be removed so save to skip the number of bytes 
            $byte_offset += $line_bytes;
        }

        // keep track of the current line
        $current_line++;
    }

    // remove the end of the file
    return ftruncate($file, $stat["size"] - $byte_offset);
}

3
这也适用于当你想在一行中查找子字符串(ID)并用新的一行替换旧行时。 代码:
$contents = file_get_contents($dir);
$new_contents = "";
if (strpos($contents, $id) !== false) { // if file contains ID
    $contents_array = explode(PHP_EOL, $contents);
    foreach ($contents_array as &$record) {    // for each line
        if (strpos($record, $id) !== false) { // if we have found the correct line
            continue; // we've found the line to delete - so don't add it to the new contents.
        } else {
            $new_contents .= $record . "\r"; // not the correct line, so we keep it
        }
    }
    file_put_contents($dir, $new_contents); // save the records to the file
    echo json_encode("Successfully updated record!");
}
else {
    echo json_encode("failed - user ID ". $id ." doesn't exist!");
}

例子:

输入:"123,学生"

旧文件:

ID,职业

123,学生

124,砖工

运行代码将更改文件为:

新文件:

ID,职业

124,砖工


如果“if”分支什么也不做,为什么还要有“else”分支呢? - mickmackusa
除了可读性之外,没有其他理由。这是我五年前还是初级开发人员时写的。 - ChickenFeet
很好。当你有时间的时候,请更新这篇文章(人们正在学习编码)以反映你当前开发知识的最佳实践。此外,通过引用修改$record似乎也是不必要的。 - mickmackusa
1
@mickmackusa 我不再使用PHP,所以我不会费心去重写和测试。它有很好的注释,所以学习的人可以跟着走。如果人们想要更好的答案,他们可以使用Naveed Ahmad的答案。 - ChickenFeet

2
我认为处理文件最好的方式是像处理字符串一样进行操作:
/**
 * Removes the first found line inside the given file.
 *
 * @param string $line The line content to be searched.
 * @param string $filePath Path of the file to be edited.
 * @param bool $removeOnlyFirstMatch Whether to remove only the first match or
 * the whole matches.
 * @return bool If any matches found (and removed) or not.
 *
 * @throw \RuntimeException If the file is empty.
 * @throw \RuntimeException When the file cannot be updated.
 */
function removeLineFromFile(
    string $line,
    string $filePath,
    bool $removeOnlyFirstMatch = true
): bool {
    // You can wrap it inside a try-catch block
    $file = new \SplFileObject($filePath, "r");

    // Checks whether the file size is not zero
    $fileSize = $file->getSize();
    if ($fileSize !== 0) {
        // Read the whole file 
        $fileContent = $file->fread($fileSize);
    } else {
        // File is empty
        throw new \RuntimeException("File '$filePath' is empty");
    }

    // Free file resources
    $file = null;

    // Divide file content into its lines
    $fileLineByLine = explode(PHP_EOL, $fileContent);

    $found = false;
    foreach ($fileLineByLine as $lineNumber => $thisLine) {
        if ($thisLine === $line) {
            $found = true;
            unset($fileLineByLine[$lineNumber]);

            if ($removeOnlyFirstMatch) {
                break;
            }
        }
    }

    // We don't need to update file either if the line not found
    if (!$found) {
        return false;
    }

    // Join lines together
    $newFileContent = implode(PHP_EOL, $fileLineByLine);

    // Finally, update the file
    $file = new \SplFileObject($filePath, "w");
    if ($file->fwrite($newFileContent) !== strlen($newFileContent)) {
        throw new \RuntimeException("Could not update the file '$filePath'");
    }

    return true;
}

以下是需要翻译的内容:

这里简要介绍一下正在进行的操作:获取整个文件内容,将内容分成行(即作为一个数组),查找匹配项并删除它们,将所有行连接在一起,如果有任何更改,则将结果保存回文件。

现在让我们使用它:

// $dir is your filename, as you mentioned
removeLineFromFile($line, $dir);

注意事项:

  • You can use fopen() family functions instead of SplFileObject, but I do recommend the object form, as it's exception-based, more robust and more efficient (in this case at least).

  • It's safe to unset() an element of an array being iterated using foreach (There's a comment here showing it can lead unexpected results, but it's totally wrong: As you can see in the example code, $value is copied (i.e. it's not a reference), and removing an array element does not affect it).

  • $line should not have new line characters like \n, otherwise, you may perform lots of redundant searches.

  • Don't use

    $fileLineByLine[$lineNumber] = "";
    // Or even
    $fileLineByLine[$lineNumber] = null;
    

    instead of

    unset($fileLineByLine[$key]);
    

    The reason is, the first case doesn't remove the line, it just clears the line (and an unwanted empty line will remain).

希望这有所帮助。

2

将文本转换为数组,删除第一行并重新将其转换为文本

$line=explode("\r\n",$text);
unset($line[0]);
$text=implode("\r\n",$line);

提问者说:“但我不知道它的行号”。 - mickmackusa

1

像这样:

file_put_contents($filename, str_replace($line . "\r\n", "", file_get_contents($filename)));

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