如何将包含 200,00 行的巨大 CSV 文件导入 MySQL(异步且快速)?

Posted

技术标签:

【中文标题】如何将包含 200,00 行的巨大 CSV 文件导入 MySQL(异步且快速)?【英文标题】:How to import huge CSV file with 200,00 rows to MySQL (asynchronous and fast)? 【发布时间】:2015-12-06 21:52:56 【问题描述】:

我必须编写一个 php 脚本,将给定 CSV 文件中的数据导入 mysql 数据库。给定的 CSV 文件最多可以包含 200,000 行。 我尝试了以下方法,但出现了问题:

    LOAD DATA LOCAL INFILE:我不能使用 LOAD DATA LOCAL INFILE 语句,因为我想在上传行之前先进行一些验证,而且我们的数据库管理员不希望我使用该语句,我不知道为什么. FOR 循环:在 FOR 循环中逐行插入会花费太多时间,从而导致连接超时。

现在,我正在考虑一种解决方案,将 CSV 文件拆分成更小的块,然后异步插入它们。我已经完成了 CSV 的拆分,但我目前不知道如何以快速安全的方式异步插入到我的数据库中。但我听说我会在这里使用 Ajax。

您可以推荐任何解决方案吗?提前非常感谢!

【问题讨论】:

您可以使用LOAD DATA LOCAL INFILE 进行验证,只需在您执行导入的表上创建一个触发器并让触发器进行验证。 200k 行对于加载数据本地 infile 来说是很小的,听起来像是要走的路。您还可以在导入之前进行验证并重新编写文件。 我真的明白 LOAD DATA LOCAL INFILE 是最好的方法。如果这只是我的标准,我会那样做。但我的任务是异步进行。另外,我无权修改我们的数据库。真的,我的老板只是想让我编写一个 PHP 脚本,将这些行加载到 MySQL 数据库中。 LOAD DATA LOCAL INFILE 旁边的最佳方法是什么? 为什么你的老板要求通过 PHP 来做这件事?似乎它应该是像 MySQL Workbench 这样的工作,有这么多的记录。 【参考方案1】:

感谢所有回答这个问题的人。我发现了一个解决方案! 只是想分享一下,以防有人需要创建一个 PHP 脚本,将一个巨大的 CSV 文件导入 MySQL 数据库(异步且快速!)我已经用 400,000 行测试了我的代码,并且导入在几秒钟内完成。 我相信它适用于更大的文件,您只需修改最大上传文件大小。

在本例中,我将把一个包含两列(name、contact_number)的 CSV 文件导入到一个包含相同列的 MySQL 数据库中。

您的 CSV 文件应如下所示:

安娜,0906123489

约翰,0908989199

彼得,0908298392

...

...

所以,这就是解决方案。

首先,创建你的表

CREATE TABLE `testdb`.`table_test`
( `id` INT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(100) NOT NULL ,
`contact_number` VARCHAR(100) NOT NULL ,
PRIMARY KEY (`id`)) ENGINE = InnoDB;

其次,我有 4 个 PHP 文件。您所要做的就是将其放入一个文件夹中。 PHP文件如下:

index.php

<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="csv" value="" />
<input type="submit" name="submit" value="Save" /></form>

connect.php

<?php
//modify your connections here
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testDB";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) 
    die("Connection failed: " . $conn->connect_error);
 
?>

senddata.php

<?php
include('connect.php');
$data = $_POST['file'];
$handle = fopen($data, "r");
$test = file_get_contents($data);
if ($handle) 
    $counter = 0;
    //instead of executing query one by one,
    //let us prepare 1 SQL query that will insert all values from the batch
    $sql ="INSERT INTO table_test(name,contact_number) VALUES ";
    while (($line = fgets($handle)) !== false) 
      $sql .= "($line),";
      $counter++;
    
    $sql = substr($sql, 0, strlen($sql) - 1);
     if ($conn->query($sql) === TRUE) 
     else 
     
    fclose($handle);
 else   
 
//unlink CSV file once already imported to DB to clear directory
unlink($data);
?>

上传.php

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.js"></script>
<script>
//Declaration of function that will insert data into database
 function senddata(filename)
        var file = filename;
        $.ajax(
            type: "POST",
            url: "senddata.php",
            data: file,
            async: true,
            success: function(html)
                $("#result").html(html);
            
        )
        
 </script>
<?php
$csv = array();
$batchsize = 1000; //split huge CSV file by 1,000, you can modify this based on your needs
if($_FILES['csv']['error'] == 0)
    $name = $_FILES['csv']['name'];
    $ext = strtolower(end(explode('.', $_FILES['csv']['name'])));
    $tmpName = $_FILES['csv']['tmp_name'];
    if($ext === 'csv') //check if uploaded file is of CSV format
        if(($handle = fopen($tmpName, 'r')) !== FALSE) 
            set_time_limit(0);
            $row = 0;
            while(($data = fgetcsv($handle)) !== FALSE) 
                $col_count = count($data);
                //splitting of CSV file :
                if ($row % $batchsize == 0):
                    $file = fopen("minpoints$row.csv","w");
                endif;
                $csv[$row]['col1'] = $data[0];
                $csv[$row]['col2'] = $data[1];
                $min = $data[0];
                $points = $data[1];
                $json = "'$min', '$points'";
                fwrite($file,$json.PHP_EOL);
                //sending the splitted CSV files, batch by batch...
                if ($row % $batchsize == 0):
                    echo "<script> senddata('minpoints$row.csv'); </script>";
                endif;
                $row++; 
            
            fclose($file);
            fclose($handle);
        
    
    else
    
        echo "Only CSV files are allowed.";
    
    //alert once done.
    echo "<script> alert('CSV imported!') </script>";

?>

就是这样!您已经有了一个可以在几秒钟内导入多行的纯 PHP 脚本! :) (感谢我的伙伴教我如何使用 ajax 并给了我一个想法)

【讨论】:

你真的拯救了我的一天!它工作得非常好,而且速度快得令人难以置信。我只想问一件事:在哪里更改 CSV-Files 的分隔符? @RoyRobsen 看看 fgetcsv($handle, $batchsize, ';') 它对我有用 任何人都知道在每个 minpoints$row.csv 中添加标题并在导入中忽略? 完美运行!谢谢 !但是当行看起来像时我如何导入CSV:“Ana”,“0906123489” 没有人回答罗希特?我也需要这样做。如果有标题行,我知道如何忽略标题行,但问题是如果有多个拆分文件,每个文件的第一行将被忽略,因此导入的记录数量将是错误的。这就是为什么最好为每个拆分文件添加相同的标题。【参考方案2】:

主要的缓慢来自于发送每一行作为它自己的请求。我建议以mysqldump --opt 使用的相同格式每 1000 或 500 行发送一次查询,所以以这种方式构建一个长字符串

 insert into datatable (name, prename, commen) 
   values ('wurst', 'hans', 'someone')
   , ('bush', 'george', 'otherone')
   , ...
   ;

您应该检查您的行允许多长,或者如果 MySQL-Server 在您的控制范围内,您可以延长最大查询长度。

如果这仍然太长(我的意思是 200K 根本不算多),那么您可以尝试改进 csv-reading。

拆分成这些块有点麻烦,但您可以为此编写一个小块类,这样添加行会更容易一些。

这个类的用法是这样的

$chunk->prepare("insert into datatable (name, prename, comment) values");
$chunk->setSize(1000);

foreach ($row...)
   if($query = $chunk->addRow(...))
       callUpdate($query);
   

if($query = $chunk->clear())
  callUpdate($query);

【讨论】:

【参考方案3】:

我仍然会在临时表中使用 LOAD DATA LOCAL INFILE,并使用 MySQL 对数据库中的所有数据进行验证、过滤、清理等操作,然后使用准备就绪的记录填充目标表。

【讨论】:

老兄,他的老板拒绝LOAD DATA LOCAL INFILE 据我记得,LOAD DATA INFILE 仅适用于没有复制的设置,因此大多数大型应用程序中的安装都会死在它上面(复制对于生成备份、故障转移和其他一些事情很有用)。无论如何,临时表是个好主意,因此您可以在将数据推送到真实表之前非常轻松地进行更复杂的检查。【参考方案4】:

您可以在 PHP 中使用 fgetcsv()。

这是一个例子:

// Open the file with PHP
$oFile = fopen('PATH_TO_FILE', 'w');

// Get the csv content
$aCsvContent = fgetcsv($oFile);

// Browse your csv line per line
foreach($aCsvContent as $aRow)

    $sReqInsertData = ' INSERT
                        INTO
                            TABLENAME
                        SET
                            FIELD1 = "'.$aRow[0].'",
                            FIELD2 = "'.$aRow[1].'",
                            FIELD3 = "'.$aRow[2].'",
                            FIELD4 = "'.$aRow[3].'",
                            FIELD5 = "'.$aRow[4].'",
                            FIELD6 = "'.$aRow[5].'",
                            FIELD7 = "'.$aRow[6].'",
                            FIELD8 = "'.$aRow[7].'"';

    // Execute your sql with mysqli_query or something like this
    mysqli_query($sReqInsertData);


// Close you file
fclose($oFile);

【讨论】:

感谢您的回答。但我认为这与我在问题(第 2 项)中提到的类似。当应用于 200,00 行时,这会导致连接超时。 [增加] (php.net/manual/en/function.set-time-limit.php) 超时至 300 秒 fopen() 中的“w”不应该是“r”吗?我刚刚使用您的代码将输入文件截断为零长度......还是我遗漏了什么? 您应该使用 R,因为您不需要写入文件。如果它只适用于 W,那么就使用它。

以上是关于如何将包含 200,00 行的巨大 CSV 文件导入 MySQL(异步且快速)?的主要内容,如果未能解决你的问题,请参考以下文章

如何按行条件将巨大的 csv 文件读入 R?

如何操作一个巨大的 csv 文件(> 12GB)?

如何将每个给定长度的行的 Bigquery 表提取到 Google Storage 中的 csv 文件?

如何在 python 中遍历大型 CSV 文件时轻松使用内存?

如何根据日期列在不同的文本/csv文件中转储一个巨大的mysql表?

比较 2 个 CSV 巨大的 CSV 文件并使用 perl 将差异打印到另一个 csv 文件