Laravel CSV 导入内存不足(允许内存耗尽)

Posted

技术标签:

【中文标题】Laravel CSV 导入内存不足(允许内存耗尽)【英文标题】:Laravel CSV Import running out of memory (allowed memory exhausted) 【发布时间】:2018-09-27 12:58:46 【问题描述】:

我有一个 members 的 CSV 文件,每月收到一次,其中包含约 6000 行。

我正在(尝试)循环遍历 CSV 文件,检查 record 是否已存在于 members 表中,如果存在,请检查它是否是相同的数据。

然后将其插入到pending 表中(在适当的地方使用exists 标志)。

我正在使用 Laravel 和 League\CSV 读取保存在我的 storage 文件夹中的文件:

class ImportController extends Controller

  public function import(Request $request) 

    $readDirectory = 'storage/csv/';
    $filename = $request->name;

    $stream = fopen($readDirectory.$filename, 'r');
    $reader = Reader::createFromStream($stream, 'r')->setHeaderOffset(0);
    $records = (new Statement())->process($reader);

    // Truncate the imported table prior to import
    Imported::truncate(); 

    foreach ($records as $record) 

        $email = $record['email'];

        $recordExists = $this->recordExists($email);

        if($recordExists) 
          // Compare the md5 of the recordArray and the memberArray and skip the record if thit's the same.
          $memberArray = $this->getmemberArray($recordExists);
          $recordArray = $this->getRecordArray($record);

          if($memberArray['hash'] === $recordArray['hash'])  continue; 

          $record['exists'] = TRUE;
          $this->write($record);

          continue;
        


        else
        
          $record['exists'] = FALSE;
          $this->write($record);
          Log::debug("missing: ".$record['URN']);

          continue;
        
      ;
    // End Foreach Loop

    return redirect()->route('upload.show');
  



  public function recordExists($urn)
    $member = Member::where('email', 'LIKE', $email)->first();
    if ($member == null)  return false; 
    return $member;
  

  public function getmemberArray($member) 
    $memberArray = [
      'email'       =>  $member->email,
      'first_name'  =>  $member->first_name,
      'last_name'   =>  $member->last_name,
      'age_years'   =>  $member->age_years,
      'gender'      =>  $member->gender,
      'address_1'   =>  $member->address_1,
      'address_2'   =>  $member->address_2,
      'address_3'   =>  $member->address_3,
      'town'        =>  $member->town,
      'county'      =>  $member->county,
      'postcode'    =>  $member->postcode,
      'sport_1'     =>  $member->sport_1,
      'sport_2'     =>  $member->sport_2,
    ];
    $memberArray['hash'] = md5(json_encode($memberArray));
    return $memberArray;
  

  public function getRecordArray($record) 
    $recordArray = [
      'email'       =>  $record['email'], 
      'first_name'  =>  $record['first_name'], 
      'last_name'   =>  $record['last_name'], 
      'age_years'   =>  $record['age_years'], 
      'gender'      =>  $record['gender'],
      'address_1'   =>  $record['address_1'], 
      'address_2'   =>  $record['address_2'], 
      'address_3'   =>  $record['address_3'], 
      'town'        =>  $record['town'], 
      'county'      =>  $record['county'], 
      'postcode'    =>  $record['postcode'], 
      'sport_1'     =>  $record['sport_1'], 
      'sport_2'     =>  $record['sport_2'], 
    ];
    $recordArray['hash'] = md5(json_encode($recordArray));
    return $recordArray;
  

  public function write($record) 

    $import = [];

    $import['email']      = $record['email'], 
    $import['first_name'] = $record['first_name'], 
    $import['last_name']  = $record['last_name'], 
    $import['age_years']  = $record['age_years'], 
    $import['gender']     = $record['gender'],
    $import['address_1']  = $record['address_1'], 
    $import['address_2']  = $record['address_2'], 
    $import['address_3']  = $record['address_3'], 
    $import['town']       = $record['town'], 
    $import['county']     = $record['county'], 
    $import['postcode']   = $record['postcode'], 
    $import['sport_1']    = $record['sport_1'], 
    $import['sport_2']    = $record['sport_2'], 
    $import['exists']     = $record['exists']

    DB::table('imported')->insert(
      $import
    );

    Log::debug($record['email']);

    return TRUE;
  

但我不断得到:

Symfony \ Component \ Debug \ Exception \ FatalErrorException (E_UNKNOWN) Allowed memory size of 134217728 bytes exhausted (tried to allocate 181321056 bytes)

如果我在 CSV 中使用更少的行,它会起作用,但这不是一个选项。

我之前使用eloquent->save() 写入数据库,但为了提高性能将其更改为DB::table()->insert

出于测试目的,我已经添加了以下内容,但它仍然无法正常工作。

set_time_limit(0);
ini_set('max_execution_time', 100000);
ini_set('memory_limit','512m');

我错过了什么吗?某种内存泄漏?

我猜它每次都将记录保存在内存中,那么有什么方法可以让它在每一行之后忘记?

另外: 有没有办法清除这块内存,以便我可以编辑代码并重试?

即使我停止并重新运行php artisan serve,它仍然会保留相同的错误消息。

【问题讨论】:

为代码道歉。我对 PHP 比较陌生,尤其是 OOP。我敢肯定,除了将它放入控制器之外,还有更好的方法可以做到这一点,但现在,我只需要它工作即可。然后我会看看 DRY、SOLID 等。 ***.com/questions/34864524 - 看看这里的信息是否有帮助。直接在 php.ini 中设置允许的内存限制,看看会发生什么。另一种选择是首先将数据直接加载到您的数据库中,然后使用 Laravel 处理该信息。另一个线程:laracasts.com/discuss/channels/laravel/… 如前所述,我已经用ini_set 提高了内存限制,但它仍然超时。不能直接上传到数据库,因为这是针对本地俱乐部的管理员的。他们每月以 CSV 格式接收有关其成员的数据,并希望更新或添加新记录。他们没有技术知识。 6000 行但数据有多大?加载 CSV 时内存使用是否合理,或者内存是否在其他地方耗尽? 【参考方案1】:

这里的问题是League\CSV 正在将整个 CSV 文件读入内存:

$records = (new Statement())->process($reader);

您应该像这样使用Readerchunk 方法一次只读取特定数量的行:

foreach($reader->chunk(50) as $row) 
    // do whatever

chunk 方法返回一个您可以迭代的Generator。你可以找到这个提到的here in the documentation。

编辑:我误读了文档并推荐了错误的方法。

您基本上只需要遍历 $reader 本身:

foreach ($reader as $row) 
    print_r($row);

此外,如果您使用的是 Mac 或您的 CSV 是在 Mac 上创建的,您需要使用以下内容才能成功读取大型 CSV 文件:

if (!ini_get('auto_detect_line_endings')) 
    ini_set('auto_detect_line_endings', '1');

参见文档的this part。

【讨论】:

您应该直接在$reader 上使用它,因为$records 已经包含整个CSV 文件内容,您只想分块阅读它。 好的,我想这样做:$stream = fopen($readDirectory.$filename, 'r'); $reader = Reader::createFromStream($stream, 'r')->setHeaderOffset(0); foreach ($reader->chunk(512) as $chunk) $records = (new Statement())->process($chunk); // Truncate the imported table prior to import Imported::truncate(); foreach ($records as $record) ... @n8udd 请检查我所做的编辑。我之前误读了文档并推荐了错误的方法。我已经用一种有效的方法更正了我的帖子,还添加了针对操作系统特定问题的提示。【参考方案2】:

我知道您正在使用php artisan serve 来运行您的服务器。您可以尝试部署某种形式的实际 Web 服务器,因为您将在生产环境中使用它。你可以试试 Apache,在 XAMPP for windows 和 Linux 上很容易。

您可以在线查看如何在您的操作系统上安装 Apache HTTP Server 或 nginx。这些比php默认服务器有更好的内存控制和使用。

【讨论】:

我试图避免只是向它投入更多的内存,而是试图找出它为什么会导致内存溢出。

以上是关于Laravel CSV 导入内存不足(允许内存耗尽)的主要内容,如果未能解决你的问题,请参考以下文章

Laravel 的 CheckForMaintenanceMode 中间件中允许的内存大小耗尽

Tensorflow 耗尽 GPU 内存:分配器 (GPU_0_bfc) 尝试分配内存不足

用户中的内存不足错误通过 grails 中的提要导入

Pandas - 导入大小为 4GB 的 CSV 文件时出现内存错误

Laravel 报告内存大小 268MB 耗尽但 php.ini 说 512MB

诊断内存泄漏 - # 字节的允许内存大小已用尽