"csv" --- CSV 檔案讀取及寫入
****************************

**原始碼：**Lib/csv.py

======================================================================

所謂的 CSV (Comma Separated Values) 檔案格式是試算表及資料庫中最常見的
匯入、匯出檔案格式。在嘗試以 **RFC 4180** 中的標準化方式來描述格式之前
，CSV 格式已經使用了許多年。由於缺少一個完善定義的標準，意味著各個不同
的應用程式會在資料產生及銷毀時有微妙的差別。這些不同之處使得從不同資料
來源處理 CSV 檔案時會非常擾人。儘管如此，雖然分隔符號和引號字元有所不
同，整體的格式非常相似，可以寫個單一模組來高效率的操作這樣的資料，讓程
式設計師可以隱藏讀取及寫入資料的細節。

"csv" 模組實作透過 class 去讀取、寫入 CSV 格式的表格資料。它讓程式設計
師可以說出：「以 Excel 為首選並寫入該種格式的資料」或是「從 Excel 產生
的檔案來讀取資料」，且無需知道這是 Excel 所使用的 CSV 格式等精確的細節
。程式設計師也可以描述其他應用程式所理解的 CSV 格式或他們自行定義具有
特殊意義的 CSV 格式。

"csv" 模組的 "reader" 及 "writer" 物件可以讀取及寫入序列。程式設計師也
可以透過 "DictReader" 及 "DictWriter" class（類別）使用 dictionary （
字典）讀取及寫入資料。

也參考:

  **PEP 305** - CSV 檔案 API
     Python Enhancement Proposal (PEP) 所提出的 Python 附加功能。


模組內容
========

"csv" 模組定義了以下函式：

csv.reader(csvfile, /, dialect='excel', **fmtparams)

   回傳一個讀取器物件 (reader object) 並處理在指定的 *csvfile* 中的每
   一行，csvfile 必須是字串的可疊代物件 (iterable of strings)，其中每
   個字串都要是讀取器所定義的 csv 格式，csvfile 通常是個類檔案物件或者
   list。如果 *csvfile* 是個檔案物件，則需開啟時使用 "newline=''"。
   [1] *dialect* 為一個可選填的參數，可以用為特定的 CSV dialect（方言
   ） 定義一組參數。它可能為 "Dialect" 的一個子類別 (subclass) 的實例
   或是由 "list_dialects()" 函式回傳的多個字串中的其中之一。另一個可選
   填的關鍵字引數 *fmtparams* 可以在這個 dialect 中覆寫 (override) 個
   別的格式化參數 (formatting parameter)。關於 dialect 及格式化參數的
   完整說明，請見段落 Dialect 與格式參數。

   從 CSV 檔案讀取的每一列會回傳為一個字串列表。除非格式選項
   "QUOTE_NONNUMERIC" 有被指定（在這個情況之下，沒有引號的欄位都會被轉
   換成浮點數），否則不會進行自動資料型別轉換。

   一個簡短的用法範例：

      >>> import csv
      >>> with open('eggs.csv', newline='') as csvfile:
      ...     spamreader = csv.reader(csvfile, delimiter=' ', quotechar='|')
      ...     for row in spamreader:
      ...         print(', '.join(row))
      Spam, Spam, Spam, Spam, Spam, Baked Beans
      Spam, Lovely Spam, Wonderful Spam

csv.writer(csvfile, /, dialect='excel', **fmtparams)

   回傳一個寫入器物件 (writer object)，其負責在給定的類檔案物件 (file-
   like object) 上將使用者的資料轉換為分隔字串 (delimited string)。
   *csvfile* 可以為具有 "write()" method 的任何物件。若 *csvfile* 為一
   個檔案物件，它應該使用 "newline=''" 開啟 [1] 。*dialect* 為一個可選
   填的參數，可以用為特定的 CSV dialect 定義一組參數。它可能為
   "Dialect" 的一個子類別的實例或是由 "list_dialects()" 函式回傳的多個
   字串中的其中之一。另一個可選填的關鍵字引數 *fmtparams* 可以在這個
   dialect 中覆寫個別的格式化參數。關於 dialect 及格式化參數的完整說明
   ，請見段落 Dialect 與格式參數。為了更容易與有實作 DB API 的模組互相
   接合，"None" 值會被寫成空字串。雖然這不是一個可逆的變換，這使得dump
   （傾印） SQL NULL 資料值到 CSV 檔案上就無需讓 "cursor.fetch*" 呼叫
   回傳的資料進行預處理 (preprocessing)。其餘非字串的資料則會在寫入之
   前用 "str()" 函式進行字串化 (stringify)。

   一個簡短的用法範例：

      import csv
      with open('eggs.csv', 'w', newline='') as csvfile:
          spamwriter = csv.writer(csvfile, delimiter=' ',
                                  quotechar='|', quoting=csv.QUOTE_MINIMAL)
          spamwriter.writerow(['Spam'] * 5 + ['Baked Beans'])
          spamwriter.writerow(['Spam', 'Lovely Spam', 'Wonderful Spam'])

csv.register_dialect(name, /, dialect='excel', **fmtparams)

   將 *dialect* 與 *name* 進行關聯 (associate)。*name* 必須為字串。這
   個 dialect 可以透過傳遞 "Dialect" 的子類別進行指定；或是關鍵字引數
   *fmtparams*；或是以上兩者皆是，並透過關鍵字引數來覆寫 dialect 的參
   數。關於 dialect 及格式化參數的完整說明，請見段落 Dialect 與格式參
   數。

csv.unregister_dialect(name)

   從 dialect 註冊表 (registry) 中，刪除與 *name* 關聯的 dialect。若
   *name* 如果不是註冊的 dialect 名稱，則會產生一個 "Error"。

csv.get_dialect(name)

   回傳一個與 *name* 關聯的 dialect。若 *name* 如果不是註冊的 dialect
   名稱，則會產生一個 "Error"。這個函式會回傳一個 immutable （不可變物
   件） "Dialect"。

csv.list_dialects()

   回傳所有已註冊的 dialect 名稱。

csv.field_size_limit()
csv.field_size_limit(new_limit)

   回傳目前的剖析器 (parser) 允許的最大字串大小。如果 *new_limit* 被給
   定，則會變成新的最大字串大小。

"csv" 模組定義了下列的類別：

class csv.DictReader(f, fieldnames=None, restkey=None, restval=None, dialect='excel', *args, **kwds)

   建立一個物件，其運作上就像一般的讀取器，但可以將每一列資訊 map （對
   映） 到 "dict" 中，可以透過選填的參數 *fieldnames* 設定 key。

   參數 *fieldnames* 是一個 *sequence*。如果 *fieldnames* 被省略了，檔
   案 *f* 中第一列的值會被當作欄位標題，且於結果中會被省略。如果
   *fieldname* 有提供，它們就會被使用，且第一列會被包含在結果中。不管
   欄位標題是如何決定的，dictionary 都會保留原始的排序。

   如果一列資料中的欄位比欄位標題還多，其餘的資料及以 *restkey* （預設
   為 "None"）特指的欄位標題會放入列表當中並儲存。如果一個非空的 (non-
   blank) 列中的欄位比欄位標題還少，缺少的值則會填入 *restval* （預設
   為 "None"）的值。

   所有其他選填的引數或關鍵字引數皆會傳遞至下層的 "reader" 實例。

   如果傳遞至 *fieldnames* 的引數是個疊代器，則會被迫成為一個 "list"。

   在 3.6 版的變更: 回傳的列已成為型別 "OrderedDict"。

   在 3.8 版的變更: 回傳的列已成為型別 "dict"。

   一個簡短的用法範例：

      >>> import csv
      >>> with open('names.csv', newline='') as csvfile:
      ...     reader = csv.DictReader(csvfile)
      ...     for row in reader:
      ...         print(row['first_name'], row['last_name'])
      ...
      Eric Idle
      John Cleese

      >>> print(row)
      {'first_name': 'John', 'last_name': 'Cleese'}

class csv.DictWriter(f, fieldnames, restval='', extrasaction='raise', dialect='excel', *args, **kwds)

   建立一個物件，其運作上就像一般的寫入器，但可以將 dictionary map 到
   輸出的列上。參數 *fieldnames* 是一個鍵值的 "sequence" 且可以辨識
   dictionary 中傳遞至 "writerow()" method 寫入至檔案 *f* 中的值。如果
   dictionary 中缺少了 *fieldnames* 的鍵值，則會寫入選填的參數
   *restval* 的值。如果傳遞至 "writerow()" method 的 dictionary 包含了
   一個 *fieldnames* 中不存在的鍵值，選填的參數 *extrasaction* 可以指
   出該執行的動作。如果它被設定為 "'raise'"，預設會觸發 "ValueError"。
   如果它被設定為 "'ignore'"，dictionary 中額外的值會被忽略。其他選填
   的引數或關鍵字引數皆會傳遞至下層的 "writer" 實例。

   請記得這不像類別 "DictReader"，在類別 "DictWriter" 中，參數
   *fieldnames* 並不是選填的。

   如果傳遞至 *fieldnames* 的引數是個疊代器，則會被迫成為一個 "list"。

   一個簡短的用法範例：

      import csv

      with open('names.csv', 'w', newline='') as csvfile:
          fieldnames = ['first_name', 'last_name']
          writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

          writer.writeheader()
          writer.writerow({'first_name': 'Baked', 'last_name': 'Beans'})
          writer.writerow({'first_name': 'Lovely', 'last_name': 'Spam'})
          writer.writerow({'first_name': 'Wonderful', 'last_name': 'Spam'})

class csv.Dialect

   類別 "Dialect" 是一個容器類別，其屬性 (attribute) 包含如何處理雙引
   號、空白、分隔符號等資訊。由於缺少一個嚴謹的 CSV 技術規範，不同的應
   用程式會產出有巧妙不同的 CSV 資料。"Dialect" 實例定義了 "reader" 以
   及 "writer" 的實例該如何表示。

   所有可用的 "Dialect" 名稱會透過 "list_dialects()" 回傳，且它們可以
   透過特定 "reader" 及 "writer" 類別的初始器 (initializer,
   "__init__") 函式進行註冊，就像這樣：

      import csv

      with open('students.csv', 'w', newline='') as csvfile:
          writer = csv.writer(csvfile, dialect='unix')

class csv.excel

   類別 "excel" 定義了透過 Excel 產生的 CSV 檔案的慣用屬性。它被註冊的
   dialect 名稱為 "'excel'"。

class csv.excel_tab

   類別 "excel_tab" 定義了透過 Excel 產生並以 Tab 作為分隔的 CSV 檔案
   的慣用屬性。它被註冊的 dialect 名稱為 "'excel-tab'"。

class csv.unix_dialect

   類別 "unix_dialect" 定義了透過 UNIX 系統產生的 CSV 檔案的慣用屬性，
   換句話說，使用 "'\n'" 作為換行符號且所有欄位都被引號包覆起來。它被
   註冊的 dialect 名稱為 "'unix'"。

   在 3.2 版被加入.

class csv.Sniffer

   類別 "Sniffer" 被用來推斷 CSV 檔案的格式。

   類別 "Sniffer" 提供了兩個 method：

   sniff(sample, delimiters=None)

      分析給定的 *sample* 且回傳一個 "Dialect" 子類別，反應出找到的格
      式參數。如果給定選填的參數 *delimiters*，它會被解釋為一個字串且
      含有可能、有效的分隔字元。

   has_header(sample)

      如果第一列的文字顯示將作為一系列的欄位標題，會分析 sample 文字（
      假定他是 CSV 格式）並回傳 "True"。檢查每一欄時，會考慮是否滿足兩
      個關鍵標準其中之一，判斷 sample 是否包含標題：

      * 第二列至第 n 列包含數字

      * 第二列到第 n 列包含的字串中至少有一個值的長度與該行的假定標題
        的長度不同。

      Twenty-one rows after the header are sampled; if more than half
      of the columns + rows meet the criteria, "True" is returned.

   備註:

     此方法是一個粗略的啟發，可能會產生偽陽性及偽陰性 (false positives
     and negatives)。

一個 "Sniffer" 的使用範例：

   with open('example.csv', newline='') as csvfile:
       dialect = csv.Sniffer().sniff(csvfile.read(1024))
       csvfile.seek(0)
       reader = csv.reader(csvfile, dialect)
       # ... 在這邊處理 CSV 檔案 ...

"csv" 模組定義了以下常數：

csv.QUOTE_ALL

   引導 "writer" 物件引用所有欄位。

csv.QUOTE_MINIMAL

   引導 "writer" 物件只引用包含特殊字元的欄位，例如：*delimiter*、
   *quotechar*、"'\r'"、"'\n'" 或是 *lineterminator* 的其他字元。

csv.QUOTE_NONNUMERIC

   引導 "writer" 物件引用所有非數字的欄位。

   引導 "reader" 物件轉換所有非引用的欄位為 "float"。

   備註:

     Some numeric types, such as "bool", "Fraction", or "IntEnum",
     have a string representation that cannot be converted to "float".
     They cannot be read in the "QUOTE_NONNUMERIC" and "QUOTE_STRINGS"
     modes.

csv.QUOTE_NONE

   引導 "writer" 物件不得引用欄位。當目前的 *delimiter*、*quotechar*、
   "'\r'"、"'\n'" 或是 *lineterminator* 的其他字元出現在輸出資料時，在
   他之前的字元是目前的 *escapechar*。如果沒有設定 *escapechar*，若遇
   到任何字元需要逸出，寫入器則會引發 "Error"。設定 *quotechar* 為
   "None" 以防止逸出。

   引導 "reader" 物件不對引號進行特別處理。

csv.QUOTE_NOTNULL

   引導 "writer" 物件引用所有非 "None" 的欄位。這與 "QUOTE_ALL" 相似，
   除非如果欄位值為 "None"，該欄位則被寫成空（沒有引號）字串。

   引導 "reader" 物件將空（沒有引號）欄位直譯 (interpret) 為 "None"，
   否則會和 "QUOTE_ALL" 有相同的表現方式。

   在 3.12 版被加入.

csv.QUOTE_STRINGS

   引導 "writer" 物件永遠在字串的欄位前後放置引號。這與
   "QUOTE_NONNUMERIC" 相似，除非如果欄位值為 "None"，該欄位則被寫成空
   （沒有引號）字串。

   引導 "reader" 物件將空（沒有引號）字串直譯為 "None"，否則會和
   "QUOTE_ALL" 有相同的表現方式。

   在 3.12 版被加入.

"csv" 模組定義下列例外：

exception csv.Error

   當偵測到錯誤時，任何函式都可以引發。


Dialect 與格式參數
==================

為了讓指定輸入及輸出紀錄的格式更方便，特定的格式化參數會被組成 dialect
。一個 dialect 是 "Dialect" class 的子類別，其包含多個描述 CSV 檔案格
式的多個屬性。當建立 "reader" 或 "writer" 物件時，程式設計師可以指定一
個字串或是一個 "Dialect" 的子類別作為 dialect 參數。此外，或是作為替代
，在*dialect*參數中，程式設計師可以指定個別的格式化參數，其與
"Dialect" 類別定義的屬性具有相同的名字。

Dialect 支援下列屬性：

Dialect.delimiter

   一個單一字元 (one-character) 的字串可已用來分割欄位。預設為 "','"。

Dialect.doublequote

   控制 *quotechar* 的實例何時出現在欄位之中，並讓它們自己被放在引號之
   內。當屬性為 "True"，字元會是雙引號。若為 "False"，在 *quotechar*
   之前會先使用 *escapechar* 作為前綴字。預設為 "True"。

   在輸出時，若 *doublequote* 是 "False" 且*逸出字元*沒有被設定，當一
   個*引號*在欄位中被發現時，"Error" 會被引發。

Dialect.escapechar

   一個單一字元的字串，會被寫入器用來逸出需要逸出的字元：

      * the *delimiter*, the *quotechar*, "'\r'", "'\n'" and any of
        the characters in *lineterminator* are escaped if *quoting* is
        set to "QUOTE_NONE";

      * the *quotechar* is escaped if *doublequote* is "False";

      * *escapechar* 本身。

   在讀取時，*escapechar* 會移除後面字元的任何特殊意義。預設為 "None"
   ，表示禁止逸出。

   在 3.11 版的變更: *escapechar* 為空是不被接受的。

Dialect.lineterminator

   由 "writer" 產生被用來分行的字串。預設為 "'\r\n'"。

   備註:

     "reader" 是 hard-coded 辨別 "'\r'" or "'\n'" 作為行尾 (end-of-
     line)，並忽略*lineterminator*。未來可能會改變這個行為。

Dialect.quotechar

   一個單一字元的字串被用於引用包含特殊字元的欄位，像是 *delimiter*、
   *quotechar* 或是換行字元（"'\r'"、"'\n'" 或是 *lineterminator* 的任
   一字元）。預設為 "'"'"。如果 *quoting* 設定為 "QUOTE_NONE"，可以設
   定為 "None" 以防止逸出 "'"'"。

   在 3.11 版的變更: *quotechar* 為空是不被允許的。

Dialect.quoting

   控制 writer 何時產生引號，以及 reader 如何辨識引號。如 *quotechar*
   不為 "None"，則可以使用任何 QUOTE_* 常數且預設為 "QUOTE_MINIMAL"。
   否則預設為 "QUOTE_NONE"。

Dialect.skipinitialspace

   When "True", spaces immediately following the *delimiter* are
   ignored. The default is "False".  When combining "delimiter=' '"
   with "skipinitialspace=True", unquoted empty fields are not
   allowed.

Dialect.strict

   若為 "True"，若有錯誤的 CSV 輸入則會引發 "Error"。預設為 "False"。


讀取器物件
==========

讀取器物件（"reader()" 函式回傳的 "DictReader" 實例與物件）有下列公開
方法 (public method)：

csvreader.__next__()

   回傳一個列表為讀入器的可疊代物件的下一列內容（若該物件是由
   "reader()" 回傳）或是一個 dict（若為 "DictReader" 實例），會依據目
   前的 "Dialect" 進行剖析。通常會用 "next(reader)" 來進行呼叫。

讀取器物件有下列公開屬性 (public attributes)：

csvreader.dialect

   dialect 的唯讀敘述，會被剖析器使用。

csvreader.line_num

   來源疊代器所讀取的行數。這與回傳的紀錄數不同，因為可以進行跨行紀錄
   。

DictReader 物件有下列公開屬性：

DictReader.fieldnames

   若在建立物件時沒有作為參數傳遞，這個屬性會在第一次存取之前或是第一
   筆資料被讀取之前進行初始化 (initialize)。


寫入器物件
==========

"writer" 物件（"writer()" 函式回傳的 "DictWriter" 實例與物件）有下列公
開方法。對於 "writer" 物件而言，一個*列*中必須為一個可疊代的字串或是數
字；對於 "DictWriter" 物件而言，則必須為一個 dictionary ，且可以對應欄
位標題至字串或數字（會先透過 "str()" 進行傳遞）。請注意，在寫入複數
(complex number) 時會用小括號 (parens) 包起來。這可能在其他程式讀取
CSV 檔案時導致某些問題（假設他們完全支援複雜數字）。

csvwriter.writerow(row, /)

   將參數 *row* 寫入至寫入器的檔案物件中，並依照目前的 "Dialect" 進行
   格式化。回傳下層檔案物件 *write* 方法的回傳值。

   在 3.5 版的變更: 新增對任意可疊代物件 (arbitrary iterables) 的支援
   。

csvwriter.writerows(rows, /)

   將 *rows* 中所有元素（為上述的一個可疊代的 *row* 物件）寫入至寫入器
   的檔案物件中，並依照目前的 dialect 進行格式化。

寫入器物件有下列公開屬性：

csvwriter.dialect

   dialect 的唯讀敘述，會被寫入器使用。

DictWriter 物件有下列公開方法：

DictWriter.writeheader()

   將具欄位標題的一列（於建構函式 (constructor) 中指定的）寫入至寫入器
   的檔案物件中，並依照目前的 dialect 進行格式化。回傳內部呼叫
   "csvwriter.writerow()" 的回傳值。

   在 3.2 版被加入.

   在 3.8 版的變更: "writeheader()" 現在也會回傳內部呼叫
   "csvwriter.writerow()" 的回傳值。


範例
====

最簡單的讀取 CSV 檔案範例：

   import csv
   with open('some.csv', newline='') as f:
       reader = csv.reader(f)
       for row in reader:
           print(row)

讀取一個其他格式的檔案：

   import csv
   with open('passwd', newline='') as f:
       reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
       for row in reader:
           print(row)

相對最簡單、可行的寫入範例為：

   import csv
   with open('some.csv', 'w', newline='') as f:
       writer = csv.writer(f)
       writer.writerows(someiterable)

當 "open()" 被使用於開啟並讀取一個 CSV 檔案，該檔案會預設使用系統預設
的編碼格式（請見 "locale.getencoding()"），並解碼為 unicode。若要使用
不同編碼格式進行檔案解碼，請使用 open 函式的 "encoding" 引數：

   import csv
   with open('some.csv', newline='', encoding='utf-8') as f:
       reader = csv.reader(f)
       for row in reader:
           print(row)

同理可以應用到使用不同編碼格式進行寫入：當開啟輸出檔案時，指定
"encoding" 引數。

註冊一個新的 dialect ：

   import csv
   csv.register_dialect('unixpwd', delimiter=':', quoting=csv.QUOTE_NONE)
   with open('passwd', newline='') as f:
       reader = csv.reader(f, 'unixpwd')

稍微進階的讀取器用法 -- 擷取及回報錯誤：

   import csv, sys
   filename = 'some.csv'
   with open(filename, newline='') as f:
       reader = csv.reader(f)
       try:
           for row in reader:
               print(row)
       except csv.Error as e:
           sys.exit(f'file {filename}, line {reader.line_num}: {e}')

而當模組無法直接支援剖析字串時，仍可以輕鬆的解決：

   import csv
   for row in csv.reader(['one,two,three']):
       print(row)

-[ 註解 ]-

[1] If "newline=''" is not specified, newlines embedded inside quoted
    fields will not be interpreted correctly, and on platforms that
    use "\r\n" line endings on write an extra "\r" will be added.  It
    should always be safe to specify "newline=''", since the csv
    module does its own (*universal*) newline handling.
