【问题标题】:PHP script keeps timing out while importing data from CSV to MySQL将数据从 CSV 导入 MySQL 时 PHP 脚本一直超时
【发布时间】:2014-01-02 11:43:34
【问题描述】:

我被这个难住了。我对 PHP/MySQL 的初级水平略高于初级水平,并且在这个网站上发帖时非常新。 GoDaddy 将我切换到网格服务器以提高性能并阐明我编写脚本的方式存在的问题。它从 CSV 中抓取数据并尝试插入到规范化数据库中。

CSV 未标准化,因此需要进行大量检查以查看是否存在某些内容。我最初有它打开/关闭结果集,但后来有人建议我使用准备好的语句,不幸的是我遇到了同样的问题。在得到广泛的“内部服务器错误”之前,我可以通过大约 1200 条 14k 记录。日志中的错误引用了一项安全功能,该功能可防止在短时间内过多地访问 FastCGI 服务器。

我想要找出和学习的是完成我想要做的事情的正确方法——检查是否存在某些东西;如果是,则获取记录 ID。如果没有,则插入数据并获取新 ID。我的代码如下。它从一个简单的 php 文件上传表单中获取文件名和隐藏属性,然后从那里开始。这只会由我使用,并且我插入的数据是公共记录,因此安全性不是主要问题。

<?php

if ($_POST["upload"] == "1") {

    //Connect to the database
    $hostname = xxx;
    $username = xxx;
    $dbname = xxx;
    $password = xxx;

    $dbh = mysqli_connect($hostname,$username,$password,$dbname) or die("Problem connecting: ".mysqli_error());

    $stmt = mysqli_stmt_init($dbh);


    //check for file errors
  if ($_FILES["file"]["error"] > 0)
    { echo "Return Code: " . $_FILES["file"]["error"] . "<br>"; }
  //No file errors
  else
    {

    //If file already exists
    if (file_exists($_FILES["file"]["name"]))
    { 
            echo $_FILES["file"]["name"] . " already exists.";
            exit; 
    }

    //If it doesn't exist
    else
      {
      move_uploaded_file($_FILES["file"]["tmp_name"],
      $_FILES["file"]["name"]);
      echo "Stored in: " . $_FILES["file"]["name"] . "<br><br>";
      $strFileName = $_FILES["file"]["name"];
      }
    }

    //File reporting
    echo "Upload: " . $_FILES["file"]["name"] . "<br>";
    echo "Type: " . $_FILES["file"]["type"] . "<br>";
    echo "Size: " . ($_FILES["file"]["size"] / 1024) . " kB<br>";
    echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br>";



$row = 0;
if (($handle = fopen($strFileName, "r")) !== FALSE) {
    while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
        $num = count($data);
        $row++;
        $strPermitNo = trim($data[0]);

        //Check to see if the permit is already in the database
        $sql = "SELECT LocID FROM tbl_TABC_Locations WHERE LocPermitNo = ?";

        if (mysqli_stmt_prepare($stmt, $sql))
        {
            mysqli_stmt_bind_param($stmt, "s", $strPermitNo);
            mysqli_stmt_bind_result($stmt, $intLocID);
            mysqli_stmt_execute($stmt);

            $strPermitResult = 0;

            while (mysqli_stmt_fetch($stmt))
            {
                $strPermitResult = $intLocID;
            }
        }

        //If no permits, insert it
        if ($strPermitResult == "0")
        {       
            //Clean Location name
            $strLocName = trim($data[1]);
            $strLocName = str_replace('"', "", $strLocName);
            $strLocName = str_replace(";","-", $strLocName);
            $strLocName = addslashes($strLocName);

            $strInsertQuery = "INSERT INTO tbl_TABC_Locations (LocName,LocAddress,LocCity,LocState,LocZip,LocCounty,LocPhone,LocPermitNo) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

            if (mysqli_stmt_prepare($stmt, $strInsertQuery)) 
            {
              mysqli_stmt_bind_param($stmt, 'ssssiiis', $field1, $field2, $field3, $field4, $field5, $field6, $field7, $field8);

              $field1 = $strLocName;
              $field2 = trim(addslashes($data[2]));
              $field3 = trim(addslashes($data[3]));
              $field4 = trim($data[4]);
              $field5 = trim($data[5]);
              $field6 = trim($data[6]);
              $field7 = trim($data[7]);
              $field8 = $strPermitNo;
              mysqli_stmt_execute($stmt);

              $intLocID = mysqli_insert_id($dbh);
            }
        }

        else 
        {
            $intLocID = $strPermitResult;
        }


        //Report dates
        $strReportDate = trim($data[8]);
        $aryNewDate = explode("/", $strReportDate);
        $strNewYear = $aryNewDate[0];
        $strNewMonth = $aryNewDate[1];

        //Check to see if the report date is already in there
        $sql = "SELECT ReportDateID FROM tbl_TABC_ReportDates WHERE ReportYear = ? AND ReportMonth = ?";

        if (mysqli_stmt_prepare($stmt, $sql))
        {
            mysqli_stmt_bind_param($stmt, "ii", $strNewYear, $strNewMonth);
            mysqli_stmt_bind_result($stmt, $intReportDateID);
            mysqli_stmt_execute($stmt);

            $strReportDateResult = 0;

            while (mysqli_stmt_fetch($stmt)) 
            {
                $strReportDateResult = $intReportDateID;
            }
        }   

        if ($strReportDateResult == "0")
        {
            $strInsertQuery = "INSERT INTO tbl_TABC_ReportDates (ReportMonth,ReportYear) VALUES (?, ?)";

            if (mysqli_stmt_prepare($stmt, $strInsertQuery)) 
            {
              mysqli_stmt_bind_param($stmt, "ii", $field1, $field2);

              $field1 = $strNewMonth;
              $field2 = $strNewYear;

              mysqli_stmt_execute($stmt);

              $intDateID = mysqli_insert_id($dbh);
            }
        }
        else
        {
            $intReportDateID = $strReportDateResult;
        }


        //Check to see if they have reported for the month already, and if not, add the report      
        $sql = "SELECT ReportID FROM tbl_TABC_Reports WHERE ReportDateID = ? AND LocID = ?";    

        if (mysqli_stmt_prepare($stmt, $sql))
        {
            mysqli_stmt_bind_param($stmt, "ii", $intReportDateID, $intLocID);
            mysqli_stmt_bind_result($stmt, $intReportID);
            mysqli_stmt_execute($stmt);

            $strReportIDResult = 0;

            while (mysqli_stmt_fetch($stmt)) 
            {
                $strReportIDResult = $intReportID;
            }
        }   


        if ($strReportIDResult == "0")
        {

            $strInsertQuery = "INSERT INTO tbl_TABC_Reports (LocID,ReportDateID,TaxReceipts) VALUES (?, ?, ?)";

            if (mysqli_stmt_prepare($stmt, $strInsertQuery)) 
            {
              mysqli_stmt_bind_param($stmt, "iid", $field1, $field2, $field3);

              $field1 = $intLocID;
              $field2 = $intReportDateID;
              $field3 = trim($data[9]);

              mysqli_stmt_execute($stmt);

              echo "New report<br>\n";
            }
        }
        else { echo "<b>Already reported</b><br>"; }

    }
    echo "Closing file now";
    fclose($handle);
}

mysqli_close($dbh);
}

日志中的错误是这样的:

[2436594] [fcgid:warn] (104)Connection reset by peer: [client xxx] mod_fcgid: 从 FastCGI 服务器读取数据时出错,referer(我的网页地址)

[fcgid:warn] (104)Connection reset by peer: [client xxx] mod_fcgid: ap_pass_brigade failed in handle_request_ipc function, referer(我的网页地址)

编辑 12/15(将准备好的语句拉出循环)。现在我仍然收到“准备好的语句中的变量数不匹配”错误:

$sql1 = "SELECT LocID FROM tbl_TABC_Locations WHERE LocPermitNo = ?";
$ps_ChkPermit = mysqli_stmt_prepare($stmt, $sql1);

if ($ps_ChkPermit)
{
mysqli_stmt_bind_param($stmt, "s", $strPermitNo);
mysqli_stmt_bind_result($stmt, $intLocID);
mysqli_stmt_execute($stmt);
...
}

【问题讨论】:

  • 我有一个类似的脚本,可以毫无问题地解析成千上万条记录。你用什么运行php?阿帕奇? Nginx?检查服务器日志并查看错误
  • 马上做两件事。首先:您应该在进入处理 CSV 数据的循环之前准备一次您的语句。在循环内只是绑定和执行。其次,不要检查和插入。在架构中将要检查的关键字段设置为 UNIQUE,然后只需插入即可。如果您有重复的 MySQL 将标记您可以检查的错误。如果你没有,你可以从last_insert_id()检索id
  • 你得到什么错误?如果您使用的是 linux,只需键入 tail /var/www/apache2/error.log
  • “日志中的错误引用了安全功能” - 请将该详细信息添加到您的问题中。 “我可以通过大约 1200 条 14k 记录” - 听起来像是超时,而不是 DoS 保护。这需要多长时间?在线过程中需要超过几秒钟的任何内容都应优化或移至离线系统。
  • 好的,超时了。您的脚本需要很长时间才能完成。同时,如果您急需完成,您可以将 CSV 拆分为一口大小……

标签: php mysql csv


【解决方案1】:

一般来说,您希望从循环内到循环外移除尽可能多的计算。因为在循环的每次迭代中,每一行代码都会一遍又一遍地执行。

@MikeW 建议您尽量减少脚本的资源需求。考虑以下示例(未经测试的代码!):

while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {

    $sql = "SELECT LocID FROM tbl_TABC_Locations WHERE LocPermitNo = ?";

    if (mysqli_stmt_prepare($stmt, $sql)) // <-- this keeps running every time.
    {
        mysqli_stmt_bind_param($stmt, "s", $strPermitNo);
        mysqli_stmt_bind_result($stmt, $intLocID);
        mysqli_stmt_execute($stmt);
        ...
    }
}

如果每次都一样,为什么还要一遍又一遍地准备 SQL 语句?

$sql = "SELECT LocID FROM tbl_TABC_Locations WHERE LocPermitNo = ?";

// this statement gets prepped outside the loop and runs only once.
$prepared_statement = mysqli_stmt_prepare($stmt, $sql);

// loop starts...
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {

    if ($prepared_statement)
    {
        // and then you simply bind the params and execute from within the loop.
        mysqli_stmt_bind_param($stmt, "s", $strPermitNo);
        mysqli_stmt_bind_result($stmt, $intLocID);
        mysqli_stmt_execute($stmt);
        ...
    }
}

这样,您可以节省资源,尤其是当您必须处理 CSV 中的多行时。

当然,这意味着您需要为不同的查询使用不同的变量名称,以便您可以识别每一个。在您当前声明 $stmt 的地方执行此操作

他建议的第二部分涉及更多工作。为了减少查询次数,您可以在数据库中创建一个新字段,其中包括 YEAR 和 MONTH,并将其设置为 mysql 中的UNIQUE 索引。这样如果你尝试插入一条已有的记录,mysql就会抛出错误。

如果插入时出现错误,您可以假设您有该日期的报告。如果您没有错误,则报告是新的。

那么您就没有额外的步骤来准备另一个查询来检查报告是否存在!

正如我在上面所指出的,您还可以减小 CSV 文件的大小,这样就不会花费太长时间来完成。

根据@halfer 的建议,使用 PHP-CLI 运行此脚本可能更简单。没有内存限制超时 - 但这意味着您需要将上传的文件保存在某处并使用 cron 任务稍后处理它们......

需要熟悉命令行:)

希望这能解决一些问题......祝你好运!

【讨论】:

  • 我从来没有听说过 PHP-CLI...但是这听起来很有趣,因为输入数据并不一定需要通过 HTTP。不幸的是,GoDaddy 不支持它!当脚本遇到已经存在的记录时,我如何捕捉错误?
  • 请查看php.net/mysql_error。尝试谷歌示例。如果您仍然需要帮助,请发布一个新问题并标记我。我的回答是否足以帮助您解决您的问题?
  • mysql_error 看起来很简单!我将回到它并进行上面提出的更改,看看会发生什么。我相信会有更多的问题......
  • 如果 ReportDate 已经存在并且我在插入时出错,我如何才能有效地获取它的 ID 以用于下一个查询?那不是另一个查询,我又回到了同样的情况吗?
  • @lindenmj,你的 cmets 真的在问一个不同的问题。您应该问另一个问题或尝试在谷歌上搜索您的选项。这是为了避免通过 SO cmets 进行冗长的讨论。无论如何,请查看dev.mysql.com/doc/refman/5.5/en/insert-on-duplicate.html
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-07
相关资源
最近更新 更多