PHP:验证“文本”文件的行,同时提取一些统计信息?

PHP: validate the lines of a "text" file while extracting some stats at the same time?

提问人:Fravadona 提问时间:7/3/2023 最后编辑:Fravadona 更新时间:7/3/2023 访问量:52

问:

我有一个文件(来自 POST 请求),我想根据一些约束进行验证:

  • 所有行只能由 ASCII 可打印字符组成。
  • 必须至少有一个 XYZ 记录(以 开头的行)。@XYZ
  • 最多只能有 999999 个 XYZ 记录

为此,我创建了一个通用函数,该函数按块读取文件,并将每一行传递给回调进行验证:

/*
 * Iterates over each line of the file, passing them to the callback function for validation.
 * When the callback function returns false, or when there is an error,
 * the validation process ends.
 * 
 * @param string   $filename       The name of the file to validate.
 * @param callable $callback       The callback function to use for validating each line.
 * @param string   $line_delimiter The line-ending delimiter (default is "\n").
 * @param integer  $buffer_size    The maximum number of bytes to read from the file at a time (default is 8192).
 *
 * @return Returns true when $callback returned true for each line, false if not, and NULL on error.
 *
 * @warning When $buffer_size is not large enough to contain a whole line, $callback will validate chunks of lines.
 */
function validate_file_lines($filename, $callback, $line_delimiter = "\n", $buffer_size = 8192)
{
    $handle = fopen($filename, 'rb');
    $is_valid = (false === $handle ? null : true);

    $remainder = '';

    while ( $is_valid && !feof($handle) )
    {
        $buffer = fread($handle, $buffer_size);

        if ( false === $buffer )
        {
            $is_valid = null;
        }
        else
        {
            $lines_array = explode($line_delimiter, $buffer);
            $lines_array_key_last = count($lines_array) - 1;

            $lines_array[0] = $remainder . $lines_array[0];

            if ( $lines_array_key_last !== 0 )
            {
                $remainder = $lines_array[$lines_array_key_last];
                unset($lines_array[$lines_array_key_last]);
            }

            foreach ( $lines_array as $line )
            {
                $is_valid = $callback($line);
                if ( ! $is_valid )
                    break;
            }
        }
    }
    @fclose($handle);
    return $is_valid;
}

现在,我正在使用它来验证文件,例如:

HEAD good
@XYZ 1
@XYZ 1
%END

HEAD better
@XYZ 2 2
%END
$xyz_count = 0;
$xyz_min = 1;
$xyz_max = 999999;

$is_valid_line = function($line) use(&$xyz_count, $xyz_max) {
    $is_valid = true;
    if ( ctype_print($line) )
    {
        if ( substr($line, 0, 6) === '@XYZ ' )
        {
            ++$xyz_count;
            $is_valid = $xyz_count <= $xyz_max;
        }
    }
    else if ( '' !== @$line[0] )
    {
        $is_valid = false;
    }
    return $is_valid;
};

var_dump(
    validate_file_lines('file.txt', $is_valid_line) && $xyz_count >= $xyz_min
);

电流输出为:

bool(false)

虽然我期待:

bool(true)

我做错了什么?


旁白

SPL 是否提供任何用于遍历文件行的类?

PHP 验证 文本 回调

评论

1赞 Reed 7/3/2023
我建议添加一些调试输出并创建一些测试用例。调试输出可能会显示您正在测试的每个事物的行和 true/false。你的第一个测试用例应该非常简单,并且变得越来越大,越来越复杂,直到你找到错误。<strike>就遍历文件行而言,我会说是函数</strike> - 我虽然是一行一行的,但我错了。我宁愿将所有行加载到内存中并循环数组,特别是因为您期望潜在的非常大的文件。fread
0赞 Reed 7/3/2023
另外,我认为如果没有更多的行,则返回 false,所以你应该能够fread()while (($buffer = fread($handle)) !== false)
0赞 Reed 7/3/2023
fgets()它的fgets!不是 fread()。php.net/manual/en/function.fgets.php
1赞 Reed 7/3/2023
而你的......长度应为 5,而不是 6。 是 5 个字符。substr($line, 0, 6) === '@XYZ '@XYZ

答:

1赞 Reed 7/3/2023 #1

您需要是 5 个字符,而不是 6 个字符。您可以使用按行读取。这是一个可能有效的准系统解决方案。你的模式应该只是substr()fgets()r

此外,还可以添加调试打印以显示发生错误的位置。

<?php
$fh = fopen($filename, 'r');
$valid = true;
$xyz_count = 0;
while ($valid && $line = fgets($fh)){
    if (!ctype_print($line))$valid = false;
    if (substr($line, 0, 5) == '@XYZ ')$xyz_count++;
    if ($xyz_count >= $xyz_max)$valid = false;

    // if (!$valid)echo "LINE (fail): {$line}";

}
if ($xyz_count === 0)$valid = false;
fclose($fh);

评论

1赞 Fravadona 7/3/2023
谢谢@Reed,在代码中查找问题时,子字符串大小超出了我的眼睛。
0赞 Fravadona 7/3/2023
我一直在用 和 做一些基准测试,后者是最快的。我主要担心的是该文件可能是二进制文件;在这种情况下安全吗?fgetsstream_get_linefread+explodefgetsfgets
1赞 Reed 7/3/2023
@Fravadona,我没有考虑过二进制文件。我不知道,但我认为你的担忧是有道理的,而且胎化/爆炸可能更安全。在你的爆炸中,一定要考虑,因为如果没记错的话,某些系统/文件用来表示换行符。您可以为 fgets() 提供长度,但我觉得这违背了您的目的。explode(["\r\n", "\n"])\r\n