Skip to content

Instantly share code, notes, and snippets.

@rc1021
Last active October 16, 2024 09:17
Show Gist options
  • Save rc1021/4a80b4ff9c8cec51bebee2a530c99eaf to your computer and use it in GitHub Desktop.
Save rc1021/4a80b4ff9c8cec51bebee2a530c99eaf to your computer and use it in GitHub Desktop.
Laravel高效處理CSV文件,不需要使用套件

使用方式

建立物件

use App\Commons\CsvFileEditor;
$csv = new CsvFileEditor('file.csv');
// file path: /var/www/storage/app/csv_file_editor/test.csv

新增資料在最後一行

$csv->appendData(['Username', 'Cellphone'])
    ->appendData(['User1', 'Phone 0001'])
    ->appendData(['User2', 'Phone 0002'])
    ->appendData(['User3', 'Phone 0003', 'data1', 'data2'])
    ->appendData(['User4'])
    ->appendData(['User5', '']);

更新指定行數的資料

// 更新第 4 行資料
$csv->updateAtLine(['User3-1', 'Phone 0003', 'data1', 'data2', 'data3'], 4);

取得指定行數的資料

$data = $csv->fetchAtLine(4);
/**
 * $data: [
 *   "User3-1",
 *   "Phone 0003",
 *   "data1",
 *   "data2",
 *   "data3",
 * ]
 */
 
$data = $csv->fetchAtLine(4, true);
/**
 * $data: [
 *   "Username" => "User3-1",
 *   "Cellphone" => ""Phone 0003",
 *   2 => "data1",
 *   3 => "data2",
 *   4 => "data3",
 * ]
 */

取得第一行以指定文字開頭的資料

$data = $csv->findPrefixFirst('User3', $findLineNumber);
/**
 * $findLineNumber; // 4
 * 
 * $data: [
 *   "User3-1",
 *   "Phone 0003",
 *   "data1",
 *   "data2",
 *   "data3",
 * ]
 */
 
$data = $csv->findPrefixFirst('User3', $findLineNumber, true);
/**
 * $findLineNumber; // 4
 *
 * $data: [
 *   "Username" => "User3-1",
 *   "Cellphone" => ""Phone 0003",
 *   2 => "data1",
 *   3 => "data2",
 *   4 => "data3",
 * ]
 */

將檔案輸出陣列

$csv->toArray();
// [[], ...]
<?php
namespace App\Commons;
use Illuminate\Support\Facades\Storage;
class CsvFileEditor
{
/** @var string csv檔案路徑 */
public $filepath;
function __construct($filepath, $disk = null)
{
// 確定 $filepath 是否為完整路徑
if (!realpath($filepath))
$filepath = Storage::disk($disk)->path('csv_file_editor/' . ltrim($filepath, '/'));
$this->filepath = $filepath;
}
/**
* 將陣列轉為 csv 字串
*
* @param mixed $data
* @return string
*/
private function convertArrayToCsvString(array $data)
{
$fp = fopen('php://temp', 'r+');
fputcsv($fp, array_values($data));
rewind($fp);
$csvData = stream_get_contents($fp);
fclose($fp);
return $csvData;
}
/**
* 將 csv 字串轉為陣列
*
* @param mixed $csvString
* @return array
*/
private function convertCsvStringToArray($csvString)
{
// 將字串轉換為類似文件的資源
$handle = fopen('php://memory', 'r+');
fwrite($handle, $csvString);
// 將文件指標指回到文件開頭
rewind($handle);
// 取得第一行 csv
$csvData = fgetcsv($handle);
fclose($handle);
return $csvData;
}
/**
* combineArraysWithPadding
*
* @param mixed $keys
* @param mixed $values
* @return array
*/
private function combineArrayWithPaddingOrTrimming($keys, $values)
{
$numKeys = count($keys);
$numValues = count($values);
// 如果 keys 的數量少於 values,則補足
if ($numKeys < $numValues) {
// 使用一個範圍來生成補足的鍵名
$keys = array_merge($keys, range($numKeys, $numValues - 1));
}
// 只保留前面的鍵名
$keys = array_slice($keys, 0, $numValues);
// 使用 array_combine 來合併
return array_combine($keys, $values);
}
/**
* 檔案是否存在
*
* @return string|bool
*/
public function exists()
{
return realpath($this->filepath) ? $this->filepath : false;
}
/**
* 檔案路徑
*
* @return string
*/
public function filePath()
{
return $this->filepath;
}
/**
* 取得指定行數的資料
*
* @param mixed $line 指定行數,預設第1行
* @param mixed $withColumnName 是否加入欄位名稱(第一行)
* @return array
*/
public function fetchAtLine($line = 1, $withColumnName = false)
{
if($filePath = $this->exists()) {
if ($handle = fopen($filePath, 'r')) {
// 標題欄
$header = [];
$currentLine = 0;
while (($data = fgetcsv($handle)) !== false) {
if (empty($header))
$header = $data;
if (++$currentLine === $line) {
fclose($handle);
if ($withColumnName)
return $this->combineArrayWithPaddingOrTrimming($header, $data);
return $data;
}
}
fclose($handle);
}
}
return [];
}
/**
* 找到第一個 $prefix 文字開頭的行內容
*
* @param mixed $prefix
* @param mixed $findAtLine 在第 n 行找到的資料,找不到資料時值為最後一行
* @param mixed $withColumnName 是否加入欄位名稱(第一行)
* @return array
*/
public function findPrefixFirst ($prefix, &$findAtLine = 0, $withColumnName = false)
{
if($filePath = $this->exists()) {
if ($handle = fopen($filePath, 'r')) {
// 標題欄
$header = [];
// 逐行讀取
while (($line = fgets($handle)) !== false) {
if (empty($header))
$header = $this->convertCsvStringToArray($line);
++$findAtLine;
// 檢查該行是否以指定字串開頭
if (strpos($line, $prefix) === 0) {
fclose($handle);
if ($withColumnName)
return $this->combineArrayWithPaddingOrTrimming($header, $this->convertCsvStringToArray($line));
return $this->convertCsvStringToArray($line);
}
}
fclose($handle);
}
}
return [];
}
/**
* 將 $data 寫入最後一行
*
* @param mixed $data
* @return self
*/
public function appendData(array $data)
{
$dirPath = dirname($this->filepath);
if (!is_dir($dirPath)) {
mkdir($dirPath, 0755, true); // 創建目錄及其父目錄
}
$csvData = $this->convertArrayToCsvString($data);
file_put_contents($this->filepath, $csvData, FILE_APPEND | LOCK_EX);
return $this;
}
/**
* 更新指定行數的資料
*
* @param mixed $data
* @param mixed $line
* @return void
*/
public function updateAtLine($data, $line = 1)
{
if($filePath = $this->exists()) {
// 檢查是否存在 sed 命令
$hasSed = shell_exec('command -v sed');
if ($hasSed) {
// 如果有 sed,使用 sed -i 來修改第一行
$escapedLine = escapeshellarg($this->convertArrayToCsvString($data)); // 避免命令注入風險
exec("sed -i $line . 's/.*/$escapedLine/' $filePath");
}
// 否則使用 PHP 方法來修改第一行
else {
// 打開文件,讀取內容
$fileContents = file($filePath);
// 行數存在才更新
if (!empty($fileContents) && isset($fileContents[$line - 1])) {
$fileContents[$line - 1] = $this->convertArrayToCsvString($data);
file_put_contents($filePath, implode("", $fileContents), LOCK_EX);
}
}
}
}
/**
* 將檔案輸出陣列
*
* @return array
*/
public function toArray()
{
if($filePath = $this->exists()) {
if ($fileContents = file($filePath)) {
// 將字串轉換為類似文件的資源
$handle = fopen('php://temp', 'r+');
foreach($fileContents as $csvString) {
fwrite($handle, $csvString);
}
// 將文件指標指回到文件開頭
rewind($handle);
$array = [];
while (($data = fgetcsv($handle)) !== false) {
array_push($array, $data);
}
fclose($handle);
return $array;
}
}
return [];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment