【问题标题】:PHP refresh page after outputting some HTML (breaking along operation into chunks)输出一些 HTML 后的 PHP 刷新页面(将操作分成块)
【发布时间】:2021-05-03 10:43:08
【问题描述】:

这些天我主要在客户端编写代码,所以我的 PHP 生锈了,在这种特殊情况下我更喜欢纯 PHP 设计。

我不需要任何客户端代码。我想读取大量文本文件,提取一些数据并将其插入 MySql 数据库。

  1. 这可能需要相当长的时间,所以
  2. 我想echo() 一些进展的迹象。另外,
  3. 我担心它可能需要比默认的 30 秒更长的时间,但我不喜欢打破这个限制,以防我的脚本挂起。

为了执行#2,我通常会使用header('Refresh:1');但是如果刷新发生在 MySql INSERT 操作的中间怎么办?

我想在INSERT 之前和页面重新加载时,我可以只用COUNT MySql 表条目来确定从哪里开始处理下一个文本文件。

在完成INSERT 之后,我可以在循环的几次迭代中重新加载页面,但是如何,因为我无法使用header('Location:X');,因为我已经echo()ed 进度。也许我可以以某种方式伪造表单提交?但是,这感觉很老套。

这似乎是一种通用的设计模式,我相信很久以前就有比我更聪明的人想出一个优雅的解决方案。有人能告诉我那个解决方案是什么吗?最好是纯PHP。我已经搜索过,但找不到任何东西。


目前,我正在使用 JS 解决方案:echo '<script language="javascript">window.location.href ="sciprt.php"</script>';,但更喜欢纯 PHP

【问题讨论】:

  • 这是一次性的,还是需要反复做? A PHP CLI (command line) script has no execution time limit,这是一个选项吗? PHP 处于阻塞状态,因此重新加载/重定向不会中断其他发生的事情(除非它是超时)。移动/删除您已经处理过的文件是一个选项吗?
  • 这是用户相当有规律地(每天、每周、每月)做的事情。我还想向非技术用户显示进度,这意味着浏览器中的某些内容。我不可能是唯一的。只是想知道什么是“最佳实践”(叹气!还记得我们什么时候有一个“最佳实践”标签吗?)

标签: php


【解决方案1】:

您可能不认为这是一个 PHP 解决方案,因为为<BODY> 标记指定了onload 参数。话又说回来,纯洁并不像人们想象的那样。但是我把它作为一种技术扔掉了,你可以用它来给用户一个正在运行的伪“进度条”,你的脚本只会在你想要的时候被打断,你可以将任何重启参数传回给脚本。在这个演示中,唯一传回的“重启参数”是驱动进度条的连续整数,但你可以理解:

<?php
if (!isset($_REQUEST['progress'])) {
    // initially no parameters specified, so use starting value of 0:
    $progress = 0;
}
else {
    $progress = (int)$_REQUEST['progress'];
}
// increment progress:
$progress++;
// draw progress bar:
$progress_bar = str_repeat ('+' , $progress);

// simulate doing some work:
sleep(5);

// simulate being done or not:
$done = $progress == 5;
?>
<html>
    <head>
        <title>Test</title>
    </head>
<?php if (!$done) { ?>
    <body onload="document.f.submit();">
<?php } else { ?>
    <body>
<?php } ?>
    Progress: <?= $progress_bar ?><br>
<?php if (!$done) { ?>
        <form name="f" method="post">
            <input type="hidden" name="progress" value="<?= $progress ?>">
            <!-- add any other hidden variables you need to resume where you left off -->
        </form>
<?php } else { ?>
        Done!
<?php } ?>
    </body>
</html>

【讨论】:

  • 我做了一点更新:显然,当我们“完成”时,我们应该从 标签中省略 onload 参数,因为我们不会输出表单重新提交。
【解决方案2】:

为此,我会使用 import.lock 文件。这是我建议的工作流程:

  1. 当用户开始处理一些数据时,我创建了 import.lock 文件,在这个文件中我们可以写入我们已经处理了多少条记录以及最后处理的批次中最重要的索引。如何创建批量创建策略取决于您,您可以使用临时 .json 文件来实现它,例如,将 3 个大文件合并为小块。
  2. 我假设您已经实现了文件读取和分块,因此例如我们可以进行 foreach 并且每次迭代都将一些数据放入 MySQL。
  3. 正如我在每次迭代之前所说的,我们使用已处理的块编号更新 import.lock。在 foreach 结束的那一刻,我们删除 import.lock 文件或清除它。
  4. 每次迭代我们都会通过简单的 PHP 刷新来刷新页面,刷新后读取 import.lock 文件,获取最后导入的批处理索引,然后从下一个重新开始。

为了实现进度,你计算所有创建的垃圾(chunk1.json,chunk2.json ...),假设我们有 100 个块,我们已经处理了 15 个,import.lock 的值为 15,所以这意味着 15 % 完毕。刷新后我们取了 16 个块 (chunk16.json)。

在每个垃圾完成后,我们将其从文件系统中删除。

实现示例

这是我的实现示例,经过测试和工作: 存储库链接:https://github.com/RomkaLTU/php-progress-case 您仍然需要实现 JSON 文件生成。 正如用户 Mawg 所说的恢复 Monica 建议的那样,我在此示例中使用 session 来跟踪进度。

您可能需要调整 header("refresh: 2") 和块大小。

session_start();

$dataDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
$chunkFiles = array_values(array_diff(scandir($dataDir), ['..', '.']));
$chunksCount = count($chunkFiles);
$currentChunk = $_SESSION['currentChunk'] ?? 1;

// need to identify current index of proccessing file
// need to follow naming convention here or make something smarter
$currentChunkIndex = array_search('chunk_' . $currentChunk . '.json', $chunkFiles);

if (isset($chunkFiles[$currentChunkIndex])) {
    $chunkFilePath = $dataDir . DIRECTORY_SEPARATOR . $chunkFiles[$currentChunkIndex];
    $dataJson = file_get_contents($chunkFilePath);
    $data = json_decode($dataJson, true);

    foreach ($data as $item) {
        // @TODO do something with a $item
    }

    unlink($chunkFilePath);

    $_SESSION['currentChunk'] = $currentChunk + 1;

    if (!empty($chunksCount)) {
        header("refresh: 2");
    } else {
        $_SESSION['currentChunk'] = 1;
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <?php echo $chunksCount * 3 . ' records left...'; ?>
</body>
</html>

【讨论】:

  • 一个文件?为什么不是session
  • "Every iteration we refresh the page with a simple PHP refresh" - 你能告诉我你会怎么做吗?因为,正如我在问题中所说,header('Refresh 在我输出 HTML 后将无法工作,我需要这样做以显示进度
  • 添加了实现示例。
  • 我会在这里使用 Laravel 并排队等待
  • 会话可能会遇到内存问题,具体取决于组合块的大小。 IMO 使用文件是一个非常明智的选择。
【解决方案3】:

理想情况下,不能通过 HTTP 即时执行“可能需要相当长的时间”的脚本。最好的做法是对作业进行排队,http请求只是将作业添加到队列中,并不直接运行,所以这是一个快速且可预测的操作,不会超时,然后作业独立于HTTP服务器运行,该作业由具有较大资源超时或没有资源超时的队列管理器执行。您可以从客户端通过 HTTP 定期轮询(使用 HTTP 刷新或 Javascript)以向用户显示一些进度状态。

一个极简但完全可行的队列管理器可以是一个 cronjob,它启动一个简单的 PHP Cli 脚本,该脚本读取和写入一些关于作业(文件、数据库等)的持久锁定数据。对于更复杂的用例(确定优先级或分配负载),使用专用队列管理器软件可能会很有用。

如果 cron 不是一个选项,您也可以运行 PHP CLI 脚本作为 100% 纯 PHP 队列管理器的守护进程。

这里的主要概念是将运行不可预测或长时间的作业与通过无状态 HTTP 以可预测且快速的方式提供 HTML 页面分离。

【讨论】:

    猜你喜欢
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    • 2011-04-23
    • 1970-01-01
    • 2012-04-12
    • 2021-10-08
    • 2015-11-14
    • 2012-02-08
    相关资源
    最近更新 更多