本节书摘来自异步社区《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文件可能会缺少某些值,或者数据损坏了,以及出现其他一些可能会使类型转换操作失败的情况,这都是很常见的。因此,除非可以保证数据不会出错,否则就需要考虑这些情况(也许需要加上适当的异常处理代码)。