ABSP第14章:处理CSV和JSON文件(1)
在 第13章 ,我们讲了怎么从pdf和word文档里面提取文字。这些文件是二进制文件,需要特殊的python模块来读取信息。CSV和JSON文件,则属于纯文本文件。直接使用文本编辑器就可以打开这些文件了,像IDLE文件编辑器就可以。不过python还是有专门处理csv和json的模块,为处理这些文件的信息提供额外的支持。
CSV的全称是逗号分隔值(comma-seperated values)。CSV文件是简化的以纯文本形式存储的电子表格。Python的csv模块让CSV文件的读取更加简单。
JSON则是javascript用来存储信息的主要格式,全称是Javascript Object Notation。你不用管Javascript是怎么使用JSON的,不过学习一下JSON文件的结构以及处理还是很有用的。因为很多的网络信息都采用JSON格式传输。
CSV模块
CSV文件的每一行代表电子表格的一行,每个","作为列和列之间的分隔。随书附件里的example.xlsx如果存成CSV文件的话会像下面这样:
4/5/2015 13:34,Apples,73
4/5/2015 3:41,Cherries,85
4/6/2015 12:46,Pears,14
4/8/2015 8:59,Oranges,52
4/10/2015 2:07,Apples,152
4/10/2015 18:10,Bananas,23
4/10/2015 2:40,Strawberries,98
上面的文件会作为接下来编程的基础文件。你可以自己把这些文件保存成example.csv,或者在随书附件里找同名文件。
CSV作为纯文本文件结构式非常简单的,所以很多Excel具有的特性CSV都没有,比如:
- 值是没有类型区分的,所有的值都被当做字符串
- 不能设置字号和颜色
- 不能具有多个工作簿
- 不能设置单元格的宽和高
- 不能合并单元格
- 不能插入图片或图表
CSV最大的优势就是简单,很多的程序都支持CSV文件,也可以用大多数文本编辑器打开,是电子表格非常直观的表达方式。CSV就和它的名字一样,由逗号分隔的值组成的文本文件。
既然CSV文件是纯文本,你可能会想要用第八章提到的各种方法去读取。比如,既然每个单元格都是由逗号分隔的,那不就可以用split()把每一行都分割成单元格啦?不过要注意,不是每个逗号都代表单元格之间的间隔。CSV文件具有自己的转义字符系统(escape character)让单元格可以包含逗号或者其他的符号。简单的使用split()是没办法识别这些转义字符的。因为有这些潜在的问题,你应该坚持使用csv模块来读取CSV文档。
读取器对象
要从一个CSV文件里面读取数据,你需要新建一个读取器(Reader object)。Reader对象可以遍历csv文件中的每一行。在互动编程环境里面输入下面的代码(首先把example.csv文件拷贝到当前工作目录中):
❶ >>> import csv
❷ >>> exampleFile = open('example.csv')
❸ >>> exampleReader = csv.reader(exampleFile)
❹ >>> exampleData = list(exampleReader)
❹ >>> exampleData
[['4/5/2015 13:34', 'Apples', '73'], ['4/5/2015 3:41', 'Cherries', '85'],
['4/6/2015 12:46', 'Pears', '14'], ['4/8/2015 8:59', 'Oranges', '52'],
['4/10/2015 2:07', 'Apples', '152'], ['4/10/2015 18:10', 'Bananas', '23'],
['4/10/2015 2:40', 'Strawberries', '98']]
python自带了csv模块,所以不用pip install ...就可以直接导入[1]。
要使用csv模块打开CSV文件,首先用open()打开[2],到这里都和之前读取文件差不多。但是接下来不是用read()或者readlines()来读取open()返回的文件对象,而是用csv.reader()方法打开[3]。这样你就可以获得一个Reader对象作进一步使用。再次提醒,不要把文件名字符串直接交给csv.reader(),打不开的。
获取Reader中数据最直接的办法就是使用list()函数把Reader对象转换成Python列表[4]。对Reader使用list()以后会返回一个由列表组成的列表,在程序中这个2维列表存储在了exampleData变量里面。[5]返回的是这个2维列表的样子。
现在你把CSV文件转化成了2维列表,你可以通过exampleData[行][列]访问特定行和列的单元格。注意这里的行和列是index,从0开始计数。在互动编程环境下面输入下面的代码:
>>> exampleData[0][0]
'4/5/2015 13:34'
>>> exampleData[0][1]
'Apples'
>>> exampleData[0][2]
>>> exampleData[1][1]
'Cherries'
>>> exampleData[6][1]
'Strawberries'
exampleData[0][0]返回的是CSV文件第一行第一列的内容,exampleData[0][2]返回的是第一行第三列的内容。如前所述, CSV里面所有单元格的内容都是用字符串进行存储 。
从Reader对象使用For循环读取数据
对于大型CSV文件,你可能需要使用for循环去读取Reader中的数据。这样可以避免一次将整个CSV文件读入内存占用过多系统资源。在互动编程环境中输入下面的代码:
>>> import csv
>>> exampleFile = open('example.csv')
>>> exampleReader = csv.reader(exampleFile)
>>> for row in exampleReader:
print('Row #' + str(exampleReader.line_num) + ' ' + str(row))
Row #1 ['4/5/2015 13:34', 'Apples', '73']
Row #2 ['4/5/2015 3:41', 'Cherries', '85']
Row #3 ['4/6/2015 12:46', 'Pears', '14']
Row #4 ['4/8/2015 8:59', 'Oranges', '52']
Row #5 ['4/10/2015 2:07', 'Apples', '152']
Row #6 ['4/10/2015 18:10', 'Bananas', '23']
Row #7 ['4/10/2015 2:40', 'Strawberries', '98']
上面的程序首先是导入csv模块,用open()打开特定CSV文件,使用csv.reader()读取文件对象的内容。和上一个例子直接转化为list()不同,这里应用for循环遍历Reader对象的每一行。每一行都是值的列表,每个值都表示某个单元格的内容。
for row in exampleReader: 里面,没有转换成list,Reader对象直接就可以被遍历
print()函数会按照Row #行号 该行内容的形式输出。其中行号是通过exampleReader.line_num属性获得。该属性以1开始计数显示行号。
Reader对象只能够遍历 一次 。如果要重新遍历一遍的话,必须重新用csv.reader建一个Reader对象才行。
写入(Writer)对象
用写入(Writer)对象可以将数据写进CSV文档。你可以用csv.writer()方法生成一个Writer对象。在互动编程环境里面输入下面的代码:
>>> import csv
❶ >>> outputFile = open('output.csv', 'w', newline='')
❷ >>> outputWriter = csv.writer(outputFile)
>>> outputWriter.writerow(['spam', 'eggs', 'bacon', 'ham'])
>>> outputWriter.writerow(['Hello, world!', 'eggs', 'bacon', 'ham'])
>>> outputWriter.writerow([1, 2, 3.141592, 4])
>>> outputFile.close()
首先仍旧是用open()方法打开一个文件对象,注意是w模式[1]。这个文件对象接下来可以传给csv.writer()生成一个Writer对象[2]。
在Windows环境下面,你需要在newline关键词参数中传入""这个空字符。为什么要这么做就超出这本书的讨论范围了。总之如果你不这么做的话,生成的csv各行之间会多了一个空行,也就是像下图这样:
Writer对象的writerow()方法接收一个list作为参数。list中的每一个值都会作为一个独立的单元格写入输出csv文件里。writerow()会返回该行写入的所有字符数(包含换行符)
上面的代码生成的output.csv文档是下面这样的:
spam,eggs,bacon,ham
"Hello, world!",eggs,bacon,ham
1,2,3.141592,4
注意Writer对象是怎样自动将单元格中的逗号进行转义的。这就是csv模块带来的好处,你不用自己操心转义这些事情了。
分隔符与换行符关键字参数
如果你想使用tab符号替代逗号作为分隔符,或者就是希望每一行数据占用两行的空间,你可以参照下面的代码自己试试:
>>> import csv
>>> csvFile = open('example.tsv', 'w', newline='')
❶ >>> csvWriter = csv.writer(csvFile, delimiter='\t', lineterminator='\n\n')
>>> csvWriter.writerow(['apples', 'oranges', 'grapes'])
>>> csvWriter.writerow(['eggs', 'bacon', 'ham'])
>>> csvWriter.writerow(['spam', 'spam', 'spam', 'spam', 'spam', 'spam'])
>>> csvFile.close()
在上面的代码里面,我们把间隔符delimiter设置成了'\t',也就是tab符号,而lineterminator则是两个新行符号“\n”。delimiter间隔符指的是一行里面在单元格之间出现的字符,默认是使用逗号。line terminator行终止符是指一行里面最后的一个字符,默认是新行符号(\n)。我们通过在csv.writer()里面设置关键字参数把这两个符号都作了自定义。
在[1]里面我们做了间隔符以及换行符的自定义,然后作了3次writerow加入三行内容。通过上面程序生成的example.tsv是下面这个样子的:
apple oranges grapes
eggs bacon ham
spam spam spam spam spam spam spam
练手项目:去除CSV文件中的表头
比如你现在有几百个CSV文件,你准备把这些CSV文件交给自动化程序处理,但是那个程序并不需要表头(第一行),虽然使用Excel软件手动逐个删除也可以,但是显然是非常慢的一件事情。所以我们来写一个程序去处理这个工作。
这个程序会打开当前工作目录下面的每一个拓展名为.csv的文件,读取CSV文件的内容,将第一行去掉以后再把内容新的文件里。新的文件内容和原来相同,只是没有表头。
注意:
和往常一样,只要写涉及到修改文件的程序,一定要注意把文件做好备份。万一你的程序出了什么问题至少还能用备份恢复。谁也不想一不小心把自己的文件给抹掉了。
在顶层设计上,程序需要实现下面的功能:
- 找到当前工作目录下的所有CSV文件
- 读取每个CSV文件的内容
- 把去除了首行的内容写入一个新的CSV文件中
而在代码层面上,程序需要从以下几个方面工作:
- 利用os.listdir()获取文件列表,并遍历里面的所有文件,去除非CSV文件
- 建立一个CSV Reader对象,读取文件内容,基于line_num属性判断是不是第一行,如果是第一行就需要跳过
- 建立一个Writer对象,将制度数据写入新的文件中
现在用文本编辑器新建一个removeCsvHeader.py并开始书写程序。
第1步:遍历所有CSV文件
程序里面需要做的第一件事情就是把当前工作目录下的所有CSV文件都找出来。你的 removeCsvHeader.py应该是下面这样:
#! python3
# removeCsvHeader.py - Removes the header from all CSV files in the current
# working directory.
import csv, os
os.makedirs('headerRemoved', exist_ok=True)
# Loop through every file in the current working directory.
for csvFilename in os.listdir('.'):
if not csvFilename.endswith('.csv'):
❶ continue # skip non-csv files
print('Removing header from ' + csvFilename + '...')
# TODO: Read the CSV file in (skipping first row).
# TODO: Write out the CSV file.
上面的代码中,os.makedirs()会生成一个headerRemoved文件夹,接下来生成的无表头CSV文件会全部存储进这个文件夹里面。对os.listdir('.')用for遍历只能实现一半的要求:遍历当前工作目录下所有文件;但是你还需要把当前工作目录里面的CSV文件找出来,跳过非.csv文件。[1]的continue语句让for循环在遇到非CSV文件的时候直接跳过当次循环,进入下一个文件。
为了让上面的代码多少有点输出,我们写了一个print()根据找到的csv文件名输出一些信息。随后添加一些TODO注释提醒接下来该写什么东西。
第2步:读取CSV文件
我们的程序并不是把原来文件的第一行去掉,而是给当前CSV文件做一个拷贝,但是里面不包含第一行的内容。如果新的文件的路径以及文件名与原来的文件一模一样就会覆盖掉源文件。
我们的程序要想办法能确认当前行的内容到底是不是表头(在我们这个例子里,问题被简化为:当前行是不是第一行)。把下面的内容添加到removeCsvHeader.py里面
#! python3
# removeCsvHeader.py - Removes the header from all CSV files in the current
# working directory.
--snip--
# Read the CSV file in (skipping first row).
csvRows = []
csvFileObj = open(csvFilename)
readerObj = csv.reader(csvFileObj)
for row in readerObj:
if readerObj.line_num == 1:
continue # skip first row
csvRows.append(row)
csvFileObj.close()
# TODO: Write out the CSV file.
Reader对象的line_num属性表示当前读取的是CSV文件的第几行。在前面遍历所有文件的for循环内部我们要建立第2个for循环,遍历Reader对象的所有行,并且保留除了第一行以外的所有内容。
在for循环遍历Reader对象的所有行的时候,代码通过line_num属性来确定当前行是不是1.如果是1 的话就跳过(if readerObj.line_num == 1)。在第一行往后所有的行都会被保留,加入csvRows列表中。
第3步:将去掉了第一行的数据写入CSV文件
现在csvRows包含了除第一行以外的所有数据,接下来就把列表写入headerRemoved文件夹中的CSV文件就可以了。像代码添加如下内容:
#! python3
# removeCsvHeader.py - Removes the header from all CSV files in the current
# working directory.
--snip--
# Loop through every file in the current working directory.
❶ for csvFilename in os.listdir('.'):
if not csvFilename.endswith('.csv'):
continue # skip non-CSV files
--snip--
# Write out the CSV file.
csvFileObj = open(os.path.join('headerRemoved', csvFilename), 'w',
newline='')
csvWriter = csv.writer(csvFileObj)
for row in csvRows: