Skip to content

Instantly share code, notes, and snippets.

@rc1021
Last active October 16, 2024 09:17
Show Gist options
  • Select an option

  • Save rc1021/4a80b4ff9c8cec51bebee2a530c99eaf to your computer and use it in GitHub Desktop.

Select an option

Save rc1021/4a80b4ff9c8cec51bebee2a530c99eaf to your computer and use it in GitHub Desktop.

Revisions

  1. rc1021 revised this gist Oct 16, 2024. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -11,12 +11,12 @@ $csv = new CsvFileEditor('file.csv');
    ## 新增資料在最後一行

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

    ## 更新指定行數的資料
  2. rc1021 revised this gist Oct 16, 2024. 2 changed files with 34 additions and 0 deletions.
    27 changes: 27 additions & 0 deletions CsvFileEditor.php
    Original file line number Diff line number Diff line change
    @@ -208,4 +208,31 @@ public function updateAtLine($data, $line = 1)
    }
    }
    }

    /**
    * 將檔案輸出陣列
    *
    * @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 [];
    }
    }
    7 changes: 7 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -81,4 +81,11 @@ $data = $csv->findPrefixFirst('User3', $findLineNumber, true);
    * 4 => "data3",
    * ]
    */
    ```

    ## 將檔案輸出陣列

    ```
    $csv->toArray();
    // [[], ...]
    ```
  3. rc1021 created this gist Oct 16, 2024.
    211 changes: 211 additions & 0 deletions CsvFileEditor.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,211 @@
    <?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);
    }
    }
    }
    }
    }
    84 changes: 84 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,84 @@
    # 使用方式

    ## 建立物件

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

    ## 新增資料在最後一行

    ```
    $csv->appendData(['Username', 'Cellphone']);
    $csv->appendData(['User1', 'Phone 0001']);
    $csv->appendData(['User2', 'Phone 0002']);
    $csv->appendData(['User3', 'Phone 0003', 'data1', 'data2']);
    $csv->appendData(['User4']);
    $csv->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",
    * ]
    */
    ```