【问题标题】:How to process CSV with 100k+ lines in PHP?如何在 PHP 中处理 100k+ 行的 CSV?
【发布时间】:2015-06-29 22:16:35
【问题描述】:

我有一个超过 100.000 行的 CSV 文件,每行有 3 个用分号分隔的值。总文件大小约为。 5MB。

CSV 文件格式如下:

stock_id;product_id;amount
==========================
1;1234;0
1;1235;1
1;1236;0
...
2;1234;3
2;1235;2
2;1236;13
...
3;1234;0
3;1235;2
3;1236;0
...

我们有 10 只股票,它们在 CSV 中的索引为 1-10。在数据库中,我们将它们保存为 22-31。

CSV 是按 stock_id、product_id 排序的,但我觉得没关系。

我有什么

<?php

session_start();

require_once ('db.php');

echo '<meta charset="iso-8859-2">';

// convert table: `CSV stock id => DB stock id`
$stocks = array(
    1  => 22,
    2  => 23,
    3  => 24,
    4  => 25,
    5  => 26,
    6  => 27,
    7  => 28,
    8  => 29,
    9  => 30,
    10 => 31
);

$sql = $mysqli->query("SELECT product_id FROM table WHERE fielddef_id = 1");

while ($row = $sql->fetch_assoc()) {
    $products[$row['product_id']] = 1;
}

$csv = file('export.csv');

// go thru CSV file and prepare SQL UPDATE query
foreach ($csv as $row) {
    $data = explode(';', $row);
    // $data[0] - stock_id
    // $data[1] - product_id
    // $data[2] - amount

    if (isset($products[$data[1]])) {
        // in CSV are products which aren't in database
        // there is echo which should show me queries
        echo "  UPDATE t 
                SET value = " . (int)$data[2] . " 
                WHERE   fielddef_id = " . (int)$stocks[$data[0]] . " AND 
                        product_id = '" . $data[1] . "' -- product_id isn't just numeric
                LIMIT 1<br>";
    }
}

问题是echo 写下 100k 行太慢了,需要很长时间。我不确定 MySQL 会做什么,它是否会更快,或者需要多少时间。我这里没有测试机,所以我担心在产品服务器上进行测试。

我的想法是将 CSV 文件加载到更多变量(更好的数组)中,如下所示,但我不知道为什么。

$csv[0] = lines 0      - 10.000;
$csv[1] = lines 10.001 - 20.000;
$csv[2] = lines 20.001 - 30.000;
$csv[3] = lines 30.001 - 40.000;
etc. 

我发现例如。 Efficiently counting the number of lines of a text file. (200mb+),但我不确定它对我有什么帮助。

当我将 foreach 替换为 print_r 时,我会在

知道如何更新数据库中的这么多记录吗?
谢谢。

【问题讨论】:

  • 作为替代方案,您可以创建一个文件,然后使用 load data local infile 这将比在 PHP 中完成所有加载要快得多
  • 你为什么使用会话?在循环之外使用数组存储而不是每次都重置,听起来你在 1-10 和 21-31 之间有一个固定的映射,所以你可以用简单的 case 语句替换所有数组查找(或者如果有的话)然后使用 fgetcsv() 循环读取您的 csv 并根据需要插入要快得多。你似乎已经为一些你可以在不到 5 行的时间内完成的事情写了很多代码 oO
  • @Dave:真的有 5 行吗?也许可能,我不是程序员,尽我所能。您能在 asnwer 中告诉我更多信息吗?我用于加载 csv 文件的会话仅一次,但确实该脚本只会启动一次。即将删除会话。
  • 作为旁注:$_SESSION['products'][$row['value']] = 1;将不起作用,因为 $row['value'] 始终为空。您应该改用 $row['product_id']。
  • @EdsonMedina:只是错字,抱歉。会话由 product_ids 正确填充。

标签: php mysql csv


【解决方案1】:

使查询更大,即使用循环编译更大的查询。您可能需要将其拆分为多个块(例如,一次处理 100 个),但当然不要一次执行一个查询(适用于任何类型,插入,更新,如果可能的话,甚至选择)。这应该会大大提高性能。

一般建议不要循环查询。

【讨论】:

  • 同样适用,但我会更新答案说更新:)
  • 他没有循环查询。他只是在附和 SQL。
  • @EdsonMedina:echo 只是为了测试,然后我只想用mysql_query 替换echo
  • @panther 这个想法是不是每次都替换每条记录?
  • @EdsonMedina:是的,每个库存中的每个产品都会更新(我预计只有 ± 1% 的产品会保持相同的数量,其余的会改变)。
【解决方案2】:

类似这样的东西(请注意,这是 100% 未经测试的,我可能需要一些调整才能实际工作:))

//define array may (probably better ways of doing this
$stocks = array(
    1  => 22,
    2  => 23,
    3  => 24,
    4  => 25,
    5  => 26,
    6  => 27,
    7  => 28,
    8  => 29,
    9  => 30,
    10 => 31
);

$handle = fopen("file.csv", "r")); //open file
while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) {
    //loop through csv

    $updatesql = "UPDATE t SET `value` = ".$data[2]." WHERE   fielddef_id = ".$stocks[$data[0]]." AND product_id = ".$data[1];
   echo "$updatesql<br>";//for debug only comment out on live
}

无需进行初始选择,因为您只是在代码中将产品数据设置为 1,并且从您的描述中可以看出,您的产品 ID 始终是正确的,它只是您的 fielddef 列,其中包含地图.

也只是为了实时不要忘记将你的实际 mysqli 执行命令放在你的 $updatesql 中;

为您提供与实际使用代码的比较(我可以进行基准测试!) 这是我用于上传文件的导入器的一些代码(它并不完美,但它可以完成它的工作)

if (isset($_POST['action']) && $_POST['action']=="beginimport") {
            echo "<h4>Starting Import</h4><br />";
            // Ignore user abort and expand time limit 
            //ignore_user_abort(true);
            set_time_limit(60);
                if (($handle = fopen($_FILES['clientimport']['tmp_name'], "r")) !== FALSE) {
                    $row = 0;
                    //defaults 
                    $sitetype = 3;
                    $sitestatus = 1;
                    $startdate = "2013-01-01 00:00:00";
                    $enddate = "2013-12-31 23:59:59";
                    $createdby = 1;
                    //loop and insert
                    while (($data = fgetcsv($handle, 10000, ",")) !== FALSE) {  // loop through each line of CSV. Returns array of that line each time so we can hard reference it if we want.
                        if ($row>0) {
                            if (strlen($data[1])>0) {
                                $clientshortcode = mysqli_real_escape_string($db->mysqli,trim(stripslashes($data[0])));
                                $sitename = mysqli_real_escape_string($db->mysqli,trim(stripslashes($data[0]))." ".trim(stripslashes($data[1])));
                                $address = mysqli_real_escape_string($db->mysqli,trim(stripslashes($data[1])).",".trim(stripslashes($data[2])).",".trim(stripslashes($data[3])));
                                $postcode = mysqli_real_escape_string($db->mysqli,trim(stripslashes($data[4])));
                                //look up client ID
                                $client = $db->queryUniqueObject("SELECT ID FROM tblclients WHERE ShortCode='$clientshortcode'",ENABLE_DEBUG);

                                if ($client->ID>0 && is_numeric($client->ID)) {
                                    //got client ID so now check if site already exists we can trust the site name here since we only care about double matching against already imported sites.
                                    $sitecount = $db->countOf("tblsites","SiteName='$sitename'");
                                    if ($sitecount>0) {
                                        //site exists
                                        echo "<strong style=\"color:orange;\">SITE $sitename ALREADY EXISTS SKIPPING</strong><br />";
                                    } else {
                                        //site doesn't exist so do import
                                        $db->execute("INSERT INTO tblsites (SiteName,SiteAddress,SitePostcode,SiteType,SiteStatus,CreatedBy,StartDate,EndDate,CompanyID) VALUES 
                                        ('$sitename','$address','$postcode',$sitetype,$sitestatus,$createdby,'$startdate','$enddate',".$client->ID.")",ENABLE_DEBUG);
                                        echo "IMPORTED - ".$data[0]." - ".$data[1]."<br />";
                                    }
                                } else {
                                    echo "<strong style=\"color:red;\">CLIENT $clientshortcode NOT FOUND PLEASE ENTER AND RE-IMPORT</strong><br />";
                                }
                                fcflush();
                                set_time_limit(60); // reset timer on loop
                            }
                        } else {
                            $row++;
                        }
                    } 
                    echo "<br />COMPLETED<br />";
                }
                fclose($handle);
                unlink($_FILES['clientimport']['tmp_name']);
            echo "All Imports finished do not reload this page";
        }

在大约 10 秒内导入 150k 行

【讨论】:

  • 我需要从数据库中取出所有产品;在 CSV 中是不在数据库中的产品。
  • 然后将查询更改为插入并使用重复键更新,以便插入新产品或更新现有产品。假设您的产品 ID 是唯一的键列
  • 产品 ID 当然是唯一的。但我只想更新现有的,不插入其他的(在数据库中只有电子商店中可用的产品,在 CSV 中,所有产品都在“普通”商店中)。
  • 在这种情况下,标准更新可以正常工作,如果存在则更新,如果不存在则失败,只是捕获错误并继续,而不是在失败时死掉。或者您可以使用存在,这样查询就不会严重失败我不确定mysqli准备好的语句,但即使where子句不匹配,只要语法正确,它们仍然可以完成。
【解决方案3】:

每次更新每条记录的成本太高(主要是由于寻找,也来自写入)。

您应该先TRUNCATE 表,然后再次插入所有记录(假设您没有链接到该表的外部外键)。

为了让它更快,你应该在插入之前锁定表,然后再解锁。这将防止每次插入时都发生索引。

【讨论】:

    【解决方案4】:

    由于问题的答案和问题,我有解决方案。其基础来自@Dave,我只是更新它以更好地传递问题。

    <?php
    
    require_once 'include.php';
    
    // stock convert table (key is ID in CSV, value ID in database)
    $stocks = array(
        1  => 22,
        2  => 23,
        3  => 24,
        4  => 25,
        5  => 26,
        6  => 27,
        7  => 28,
        8  => 29,
        9  => 30,
        10 => 31
    );
    
    // product IDs in CSV (value) and Database (product_id) are different. We need to take both IDs from database and create an array of e-shop products
    $sql = mysql_query("SELECT product_id, value FROM cms_module_products_fieldvals WHERE fielddef_id = 1") or die(mysql_error());
    
    while ($row = mysql_fetch_assoc($sql)) {
        $products[$row['value']] = $row['product_id'];
    }
    
    $handle = fopen('import.csv', 'r');
    $i = 1;
    
    while (($data = fgetcsv($handle, 1000, ';')) !== FALSE) {
        $p_id = (int)$products[$data[1]];
    
        if ($p_id > 0) {
            // if product exists in database, continue. Without this condition it works but we do many invalid queries to database (... WHERE product_id = 0 updates nothing, but take a time)
            if ($i % 300 === 0) {
                // optional, we'll see what it do with the real traffic
                sleep(1);
            }
    
            $updatesql = "UPDATE table SET value = " . (int)$data[2] . " WHERE fielddef_id = " . $stocks[$data[0]] . " AND product_id = " . (int)$p_id . " LIMIT 1";
            echo "$updatesql<br>";//for debug only comment out on live
            $i++;
        }
    }
    
    // cca 1.5sec to import 100.000k+ records
    fclose($handle);
    

    【讨论】:

      【解决方案5】:

      就像我在评论中所说,使用 SPLFileObject 迭代 CSV 文件。使用准备好的语句来减少在每个循环中调用 UPDATE 的性能开销。此外,将您的两个查询合并在一起,没有任何理由先提取所有产品行并根据 CSV 检查它们。您可以使用 JOIN 来确保只有第二个表中与第一个表中的产品相关且即当前 CSV 行的那些股票会得到更新:

      /* First the CSV is pulled in */
      $export_csv = new SplFileObject('export.csv');
      $export_csv->setFlags(SplFileObject::READ_CSV | SplFileObject::DROP_NEW_LINE | SplFileObject::READ_AHEAD);
      $export_csv->setCsvControl(';');
      
      /* Next you prepare your statement object */
      $stmt = $mysqli->prepare("
      UPDATE stocks, products 
      SET value = ?
      WHERE
      stocks.fielddef_id = ? AND 
      product_id = ? AND
      products.fielddef_id = 1
      LIMIT 1
      ");
      
      $stmt->bind_param('iis', $amount, $fielddef_id, $product_id);
      
      /* Now you can loop through the CSV and set the fields to match the integers bound to the prepared statement and execute the update on each loop. */
      
      foreach ($export_csv as $csv_row) {
          list($stock_id, $product_id, $amount) = $csv_row;
          $fielddef_id = $stock_id + 21;
      
          if(!empty($stock_id)) {
              $stmt->execute();
          }
      }
      
      $stmt->close();
      

      【讨论】:

        猜你喜欢
        • 2014-05-02
        • 2016-04-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多