返回

Laravel 导入导出 TSV 文件:完整指南及常见问题解答

php

Laravel 中导入导出 TSV 文件:搞定它!

咱平时用 Excel 处理数据挺方便的,但有时候也需要处理 TSV (Tab-Separated Values) 这种用制表符分隔数据的文件。 在 Laravel 项目里导入导出 TSV,该怎么弄呢?别急,这篇博客给你讲明白。

一、TSV 格式有啥特别?

TSV,顾名思义,就是用制表符 (\t) 来分隔每一列数据。相比于 CSV (Comma-Separated Values) 用逗号分隔,TSV 在处理包含逗号的数据时更方便,不容易出错。 简单来说,TSV 就是纯文本文件,每行代表一条记录,每列数据之间用 Tab 键隔开。

二、为啥导入导出老是碰壁?

你提供的代码片段是导出部分的, 已经接近正确, 问题可能是出在这几个地方:

  1. fputcsv 的默认分隔符不是制表符: fputcsv 默认使用逗号作为分隔符,所以需要明确指定。
  2. 导入部分代码缺失: 你没有给出导入相关的代码, 无从判断问题。
  3. 数据格式问题: 导入的数据可能格式不对, 例如: 包含了换行符等。
  4. $result格式有问题。 array_unshift($result, array_keys($result[0])); 这段, 如果 $result[0]不存在, 可能会报错. 且array_keys($result[0])提取的只是$result[0]这个子数组的键, 如果你期待获取所有的键,这样是不对的。

三、一步步解决:TSV 导入导出方案

1. 导出 TSV 文件

核心思路是:设置正确的 HTTP Headers,使用 fputcsv 并指定制表符作为分隔符,将数据流式输出到浏览器。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; // 如果你要从数据库获取数据

class TsvController extends Controller
{
    public function export()
    {
        // 假设你从数据库获取数据,这里用一个示例数组代替
        // $data = DB::table('your_table')->get()->toArray();
         $data = [
            ['id' => 1, 'name' => '产品A', 'description' => "很好\t用的产品"],
            ['id' => 2, 'name' => '产品B', 'description' => '另一个产品'],
        ];
        // 提取表头(也就是数据的键)
       $headers = array_keys(current($data));

        // 组装数据:将表头作为第一行
       $output = [];
       $output[] =$headers;

        // 循环数据部分, 组装为二维数组
       foreach ($data as $row) {
           // 注意对象转数组的类型转换. 如果是从数据直接取出,这一步不需要。
           $output[] = (array) $row;
        }
        

        $headers = [
            'Content-Type' => 'text/tab-separated-values; charset=utf-8', //明确指出字符集
            'Content-Disposition' => 'attachment; filename="data.tsv"',
        ];

        $callback = function () use ($output) {
            $file = fopen('php://output', 'w');
            //设置BOM头,防止中文乱码(尤其是在Windows Excel)
             fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));

            foreach ($output as $row) {
                fputcsv($file, $row, "\t"); // 指定制表符作为分隔符
            }
            fclose($file);
        };

        return response()->stream($callback, 200, $headers);
    }
}

代码解释:

  • current($data):获取数组的第一个元素,用于提取表头。
  • array_keys():获取数组的键名(即字段名)作为表头。
  • Content-Type: 设置为 text/tab-separated-values; charset=utf-8,明确告诉浏览器这是一个 TSV 文件,并且指定编码为UTF-8, 避免中文乱码。
  • Content-Disposition: 设置为 attachment; filename="data.tsv",让浏览器下载文件,并指定文件名为 data.tsv
  • fputcsv($file, $row, "\t"): 使用 fputcsv 函数,并将分隔符设置为 \t (制表符)。
  • fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); : 设置 BOM 头.

安全建议:

  • 如果数据来自用户输入,务必对数据进行验证和过滤,防止恶意代码注入。
  • 对于大量数据的导出,可以考虑使用队列 (Queues) 进行异步处理,避免阻塞主线程。

2. 导入 TSV 文件

导入的思路是:读取上传的文件,逐行解析数据,使用制表符分割每行数据,并将数据保存到数据库或进行其他处理。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; // 如果你需要保存到数据库
use Illuminate\Support\Facades\Validator; // 用于数据验证

class TsvController extends Controller
{
   public function import(Request $request)
    {
        // 验证上传的文件
        $validator = Validator::make($request->all(), [
            'file' => 'required|file|mimes:tsv,txt', // 允许 tsv 和 txt 扩展名
        ]);

        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator); // 返回错误信息
        }

        $file = $request->file('file');
        $path = $file->getRealPath();

        $data = [];
        $handle = fopen($path, 'r');

        if ($handle !== FALSE) {
            // 获取表头(第一行)
             $header = fgetcsv($handle, 0, "\t"); //使用\t作为分隔符.  0 作为长度代表不限制长度.
           
            while (($row = fgetcsv($handle, 0, "\t")) !== FALSE) {
                  //数据与表头进行关联. 前提是tsv文件的列与header数量完全一致。
                if(count($header) === count($row)){
                   $data[] = array_combine($header, $row);
                }
                else{
                   //处理错误,记录日志等等,按需进行。这里只做简单的输出
                   echo "数据格式错误,行数据:".implode("\t",$row);
                }

            }
            fclose($handle);
        }
        //数据处理。 插入数据库, 打印, 或做其它业务操作.
      //   dd($data); //可以打印 $data看看导入的数据
      foreach($data as $item){
          //假设你要插入一个叫做'products'的数据表
           DB::table('products')->insert($item);
      }

        return "导入成功!"; // 或者重定向到其他页面
    }
}

代码解释:

  • 文件验证: 使用 Validator 验证上传的文件是否为 TSV 或 TXT 文件,并且存在。
  • $file->getRealPath(): 获取上传文件的临时路径。
  • fopen($path, 'r'): 以只读模式打开文件。
  • fgetcsv($handle, 0, "\t"): 逐行读取文件内容,使用 \t 作为分隔符。0 代表不限制行的长度。
  • array_combine($header, $row): 将表头数组和数据行数组合并为一个关联数组。
  • 错误处理: 检查$header$row的列数, 如果数量不一致,证明文件内容格式可能有误.

安全建议:

  • 文件类型验证: 严格限制上传文件的类型,不要仅仅依赖文件扩展名。可以结合 MIME 类型进行更严格的验证。
  • 文件大小限制: 限制上传文件的大小,防止恶意上传超大文件导致服务器崩溃。 你可以在 php.ini 里面设置 upload_max_filesizepost_max_size。 也可以在 Laravel 的 config/filesystems.php里设置.
  • 数据验证: 对导入的每一列数据进行类型、长度、格式等方面的验证,确保数据符合业务要求。
  • 数据清理: 对导入的数据进行清理,移除不必要的空格、特殊字符等。
  • 异常捕获: 对文件读取、数据解析等过程进行异常捕获,防止程序意外终止,并记录详细的错误日志。
  • 防止重复导入: 可以根据业务需求增加逻辑防止重复导入。 比如导入前查询数据库里是否已经存在相同数据。

3. 进阶用法: 批量插入和处理

对于大量数据的导入,逐行插入数据库效率较低。 可以考虑使用批量插入或 chunk 方式处理数据。

使用批量插入
   // 在 import 方法内部...
        $chunkSize = 500; // 每次插入的记录数
        $chunks = array_chunk($data, $chunkSize);

        foreach ($chunks as $chunk) {
             // 假设插入到products表
            DB::table('products')->insert($chunk);
        }
使用 chunk 方法 (如果从数据库读取)

如果你要处理已经存在于数据库中的数据(例如对现有数据进行某种转换并导出 TSV),可以使用 chunk 方法:

public function exportLargeData()
{
  $headers = [
    'Content-Type' => 'text/tab-separated-values; charset=utf-8',
    'Content-Disposition' => 'attachment; filename="large_data.tsv"',
  ];
  $callback = function () {
    $file = fopen('php://output', 'w');
      fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); // BOM 头
      //先写表头
      $header =['id','name','description'];//手动指定, 或者想办法获取.
       fputcsv($file, $header, "\t");

    DB::table('your_table')
      ->orderBy('id') // 可以按 ID 排序
      ->chunk(500, function ($rows) use ($file) {
        foreach ($rows as $row) {
            //这里假设数据库里取出来是对象
             $rowArray = (array)$row;

          fputcsv($file, $rowArray, "\t");
        }
      });
      fclose($file);
  };

  return response()->stream($callback,200, $headers);
}

chunk 方法会每次从数据库中取出指定数量的记录(这里是 500),并对这些记录进行处理。 这样可以有效降低内存占用,避免一次性加载过多数据导致内存溢出。

这样,导入导出 TSV 文件的问题就都搞定了。 记住,写代码时多注意细节,安全问题也要考虑到。