《Python Cookbook(第3版)中文版》——第6章 数据编码与处理 6.1 读写CSV数据

本节书摘来自异步社区《Python Cookbook(第3版)中文版》一书中的第6章,第6.1节,作者[美]David Beazley , Brian K.Jones,陈舸 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第6章 数据编码与处理

本章主要关注的重点是利用Python来处理以各种常见编码形式所呈现出的数据,比如CSV文件、JSON、XML以及二进制形式的打包记录。与数据结构那章不同,本章不会把重点放在特定的算法之上,而是着重处理数据在程序中的输入和输出问题上。

6.1 读写CSV数据

6.1.1 问题

我们想要读写CSV文件中的数据。

6.1.2 解决方案

对于大部分类型的CSV数据,我们都可以用csv库来处理。比如,假设在名为stocks.csv的文件中包含有如下的股票市场数据:

    Symbol,Price,Date,Time,Change,Volume
    "AA",39.48,"6/11/2007","9:36am",-0.18,181800
    "AIG",71.38,"6/11/2007","9:36am",-0.15,195500
    "AXP",62.58,"6/11/2007","9:36am",-0.46,935000
    "BA",98.31,"6/11/2007","9:36am",+0.12,104800
    "C",53.08,"6/11/2007","9:36am",-0.25,360900
    "CAT",78.29,"6/11/2007","9:36am",-0.23,225400

下面的代码示例告诉我们如何将这些数据读取为元组序列:

import csv
with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
        # Process row
        ...

在上面的代码中,row将会是一个元组。因此,要访问特定的字段就需要用到索引,比如row[0](表示Symbol)和row[4](表示Change)。

由于这样的索引常常容易混淆,因此这里可以考虑使用命名元组。示例如下:

from collections import namedtuple
with open('stock.csv') as f:
    f_csv = csv.reader(f)
    headings = next(f_csv)
    Row = namedtuple('Row', headings)
    for r in f_csv:
        row = Row(*r)
        # Process row
        ...

这样就可以使用每一列的标头比如row.Symbol和row.Change来取代之前的索引了。应该要指出的是,这个方法只有在每一列的标头都是合法的Python标识符时才起作用。如果不是的话,就必须调整原始的标头(比如,把非标识符字符用下划线或其他类似的符号取代)。

另一种可行的方式是将数据读取为字典序列。可以用下面的代码实现:

import csv
with open('stocks.csv') as f:
    f_csv = csv.DictReader(f)
    for row in f_csv:
        # process row
        ...

在这个版本中,可以通过行标头来访问每行中的元素。比如,row['Symbol']或者row['Change']。

要写入CSV数据,也可以使用csv模块来完成,但是要创建一个写入对象。示例如下:

headers = ['Symbol','Price','Date','Time','Change','Volume']
rows = [('AA', 39.48, '6/11/2007', '9:36am', -0.18, 181800),
        ('AIG', 71.38, '6/11/2007', '9:36am', -0.15, 195500),
        ('AXP', 62.58, '6/11/2007', '9:36am', -0.46, 935000),
       ]
with open('stocks.csv','w') as f:
    f_csv = csv.writer(f)
    f_csv.writerow(headers)
    f_csv.writerows(rows)

如果数据是字典序列,那么可以这样处理:

headers = ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume']
rows = [{'Symbol':'AA', 'Price':39.48, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.18, 'Volume':181800},
        {'Symbol':'AIG', 'Price': 71.38, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.15, 'Volume': 195500},
        {'Symbol':'AXP', 'Price': 62.58, 'Date':'6/11/2007',
          'Time':'9:36am', 'Change':-0.46, 'Volume': 935000},
        ]
with open('stocks.csv','w') as f:
    f_csv = csv.DictWriter(f, headers)
    f_csv.writeheader()
    f_csv.writerows(rows)

6.1.3 讨论

应该总是选择使用csv模块来处理,而不是自己手动分解和解析CSV数据。比如,我们可能会倾向于写出这样的代码:

with open('stocks.csv') as f:
    for line in f:
        row = line.split(',')
        # process row
        ...

这种方式的问题在于仍然需要自己处理一些令人厌烦的细节问题。比如说,如果有任何字段是被引号括起来的,那么就要自己去除引号。此外,如果被引用的字段中恰好包含有一个逗号,那么产生出的那一行会因为大小错误而使得代码崩溃(因为原始数据也是用逗号分隔的)。

默认情况下,csv库被实现为能够识别微软Excel所采用的CSV编码规则。这也许是最为常见的CSV编码规则了,能够带来最佳的兼容性。但是,如果查阅csv的文档,就会发现有几种方法可以将编码微调为其他的格式(例如,修改分隔字符等)。比方说,如果想读取以tab键分隔的数据,可以使用下面的代码:

# Example of reading tab-separated values
with open('stock.tsv') as f:
    f_tsv = csv.reader(f, delimiter='\t')
    for row in f_tsv:
        # Process row
        ...

如果正在读取CSV数据并将其转换为命名元组,那么在验证列标题时要小心。比如,某个CSV文件中可能在标题行中包含有非法的标识符字符,就像下面的示例这样:

Street Address,Num-Premises,Latitude,Longitude
5412 N CLARK,10,41.980262,-87.668452

这会使得创建命名元组的代码出现ValueError异常。要解决这个问题,应该首先整理标题。例如,可以对非法的标识符字符进行正则替换,示例如下:

import re
with open('stock.csv') as f:
    f_csv = csv.reader(f)
    headers = [ re.sub('[^a-zA-Z_]', '_', h) for h in next(f_csv) ]
    Row = namedtuple('Row', headers)
    for r in f_csv:
        row = Row(*r)
        # Process row
        ...

此外,还需要重点强调的是,csv模块不会尝试去解释数据或者将数据转换为除字符串之外的类型。如果这样的转换很重要,那么这就是我们需要自行处理的问题。下面这个例子演示了对CSV数据进行额外的类型转换:

col_types = [str, float, str, str, float, int]
with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
        # Apply conversions to the row items
        row = tuple(convert(value) for convert, value in zip(col_types, row))
        ...

作为另外一种选择,下面这个例子演示了将选中的字段转换为字典:

print('Reading as dicts with type conversion')
field_types = [ ('Price', float),
                ('Change', float),
                ('Volume', int) ]
with open('stocks.csv') as f:
    for row in csv.DictReader(f):
        row.update((key, conversion(row[key]))
                    for key, conversion in field_types)
        print(row)

一般来说,对于这样的转换都应该小心为上。在现实世界中,CSV文件可能会缺少某些值,或者数据损坏了,以及出现其他一些可能会使类型转换操作失败的情况,这都是很常见的。因此,除非可以保证数据不会出错,否则就需要考虑这些情况(也许需要加上适当的异常处理代码)。

上一篇:关于浮动的一些简单知识


下一篇:《Python Cookbook(第3版)中文版》——6.2 读写JSON数据