文件、csv 与 json
4 道练习题·预计 60 分钟·做对一题解锁下一段
前言
前面学了模块,知道了怎么用 import ,把别人写好的工具拿来用。有了这个本事,就可以回头解决记账本的一个老问题了。
记账本一路写到 v3,能记、能算、能汇总,输错了也不怕。可它一直有个老毛病:程序一关,记的账就全没了。每次打开,都得从头记起。
这一章,咱们就来治好它:把账存进文件,下次打开还在。
要做到这一点,得用上两个 Python 自带的模块:csv 和 json 。那要不要先把这两个模块从头到尾背一遍呢?不用。第 9 章说过,模块不用一个一个地背,知道它能干什么,用的时候再查就行。这一章就是这么做的:带着「把账存起来」这个需求,边用边查。
先给各位童鞋打个预防针:文件读写一开始会有点绕,打开方式、编码、换行,每一样都可能出岔子。出了错也别着急,按实战 3 的办法,先看报错的最后一行,再找行号,大多数问题都能自己解决。
学这一章之前,需要先学完 实战 4 的记账本 v3,以及 第 9 章 的 import 和「几个常用的标准库」里的 json。
目录
这一章会学到:
- 一、程序一关,账就没了——变量只活在程序运行的时候
- 二、先学会读写一个文件——
open()、with和encoding - 三、账本存成 csv——一行一笔账,表格软件也能打开
- 四、换成 json 试试——存进去是什么类型,读出来还是什么类型
- 五、常见坑
- 六、自己查一次文档——csv 文件为什么要写
newline='' - 七、记账本 v4——给
Ledger加上save()和load() - 八、小结与回看
学完这一章,你的记账本会把每一笔账存进文件,下次打开程序,接着上次的账往下记。
一、程序一关,账就没了
程序一关,账为什么就没了呢?先回顾一下实战 4 的记账本 v3:记账的时候,账都存在 book.entries 这个列表里。
问题就出在这儿。列表、字典这些变量,都放在电脑的内存里。内存有个特点:速度快,可程序一结束,里面的东西就全清空了。所以每次运行记账本,book.entries 都是从一个空列表开始。
那怎样才能让账留下来呢?得把它存进文件。文件放在硬盘上,程序关了、电脑关了,它都还在。下次打开程序,再把文件里的账读回来就行。
所以这一章要做两件事:
- 存:程序结束之前,把账写进文件。
- 读:程序开始的时候,把文件里的账读回来。
不过,在网页里运行代码,有一点要先说清楚:网页里的 Python 碰不到你电脑上的硬盘。这一章的代码在网页里写出的文件,存在浏览器临时准备的一块地方,刷新页面就没了。所以,想亲眼看到「关掉程序,下次打开账还在」,要把代码存成 .py 文件,在自己电脑上运行。运行以后,文件会出现在你运行程序时所在的文件夹里。
存也好,读也好,都得先学会怎么读写文件。咱们就从最简单的文本文件开始。
二、先学会读写一个文件
咱们先写一个文件,再把它读出来,看看都要用到哪些写法:
with open('ledger.txt', 'w', encoding='utf-8') as f:
f.write('奶茶 12.5\n')
f.write('午饭 25\n')
with open('ledger.txt', encoding='utf-8') as f:
print(f.read())输出的结果:
奶茶 12.5
午饭 25
这几行代码里,新东西不少,咱们一样一样来看:
open('ledger.txt', 'w', encoding='utf-8'):打开一个叫ledger.txt的文件。第二个参数是打开方式,'w'是写(write):文件不存在就新建一个,已经存在就清空了重写。不写这个参数,就是'r'读(read)。encoding='utf-8':用哪种编码来存文字。有中文的时候一定要写上,不然换一台电脑读,可能就是一堆乱码,甚至直接报错。第五节「常见坑」会细说。with ... as f::打开文件,给它起个名字叫f。缩进里的代码执行完,Python 会自动把文件关掉。with是怎么做到的,第 21 章会讲;现在先记住这个写法:读写文件,都用with。f.write():把一段文字写进文件。它不会自动换行,要换行得自己加\n。f.read():把整个文件读成一个字符串。
细心的童鞋会发现,输出的最后多了一个空行。这是因为文件最后有一个 \n ,print() 打印完,自己又换了一次行。
f.read() 是一下子读出整个文件。要是想一行一行地处理呢?文件也可以一行一行地读,用 for 遍历它就行:
with open('ledger.txt', 'w', encoding='utf-8') as f:
f.write('奶茶 12.5\n')
f.write('午饭 25\n')
with open('ledger.txt', encoding='utf-8') as f:
for line in f:
print(line.strip())输出的结果:
奶茶 12.5
午饭 25这里为什么要先 strip() 一下呢?因为读出来的每一行,末尾都带着换行符 \n ,所以先用 strip() 把它去掉。
前面说过,用 'w' 打开,已经存在的文件会被清空了重写。那要是想在文件后面接着写呢?这时候就用 'a' ,也就是追加(append):
with open('ledger.txt', 'w', encoding='utf-8') as f:
f.write('奶茶 12.5\n')
with open('ledger.txt', 'a', encoding='utf-8') as f:
f.write('午饭 25\n')
with open('ledger.txt', encoding='utf-8') as f:
print(f.read())输出的结果:
奶茶 12.5
午饭 25
可以看到,第二次打开用的是 'a' ,午饭接在了奶茶后面,先写的那一笔没有被冲掉。
这样一来,文本文件就能存账了。可是读回来以后,奶茶 12.5 只是一整行字符串,名称和金额还得自己拆开。要是再加上类别,就更麻烦了。
那么,有没有现成的格式,专门用来存这种一条一条的数据呢?有,csv 和 json 就是最常用的两种。下一节先来看 csv。
三、账本存成 csv
先说 csv。它是一种很常见的表格格式:一行一条记录,每一项用逗号隔开。Excel、WPS 这些表格软件都能直接打开它。
那怎么把账写成 csv 呢?Python 自带一个 csv 模块。咱们的每一笔账正好是一个字典,所以用 csv.DictWriter 来写最方便:
import csv
entries = [
{'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
{'item': '地铁', 'category': '交通', 'cost': 4},
]
with open('ledger.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['item', 'category', 'cost'])
writer.writeheader()
writer.writerows(entries)
with open('ledger.csv', encoding='utf-8') as f:
print(f.read())输出的结果:
item,category,cost
奶茶,餐饮,12.5
地铁,交通,4
对照输出,看看写文件的那几行各干了什么:
fieldnames告诉它有哪几列,按什么顺序排。writeheader()先写第一行,也就是表头。writerows(entries)把每一笔账写成一行。
另外,这回 open() 里多了一个 newline='' 。它是干什么的,先不急,照着写就行,第六节查文档的时候再细说。
写进去了,还得能读回来。读用的是 csv.DictReader ,每一行会变回一个字典:
import csv
entries = [
{'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
{'item': '地铁', 'category': '交通', 'cost': 4},
]
with open('ledger.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['item', 'category', 'cost'])
writer.writeheader()
writer.writerows(entries)
with open('ledger.csv', encoding='utf-8', newline='') as f:
for row in csv.DictReader(f):
print(row)输出的结果:
{'item': '奶茶', 'category': '餐饮', 'cost': '12.5'}
{'item': '地铁', 'category': '交通', 'cost': '4'}是不是发现什么不对?存进去的时候,cost 是数字 12.5 ,读回来却变成了字符串 '12.5' 。
这是为什么呢?因为 csv 文件里存的全是文字,它可不记得哪一列原来是数字。这和实战 1 的 input() 是一回事:读进来的都是字符串,要当数字用,就得自己转换。所以下面读的时候,顺手把每一笔的金额转成数字:
import csv
entries = [
{'item': '奶茶', 'category': '餐饮', 'cost': 12.5},
{'item': '地铁', 'category': '交通', 'cost': 4},
]
with open('ledger.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['item', 'category', 'cost'])
writer.writeheader()
writer.writerows(entries)
loaded = []
with open('ledger.csv', encoding='utf-8', newline='') as f:
for row in csv.DictReader(f):
row['cost'] = float(row['cost'])
loaded.append(row)
print(loaded)
print(sum([entry['cost'] for entry in loaded]))输出的结果:
[{'item': '奶茶', 'category': '餐饮', 'cost': 12.5}, {'item': '地铁', 'category': '交通', 'cost': 4.0}]
16.5转换以后,金额又变回了数字,合计也能算出来了。
csv 还有一点要注意:要把 csv 文件拿给 Excel 打开的话,把 encoding='utf-8' 换成 encoding='utf-8-sig' 。不然 Excel 可能认不出这是 UTF-8 编码,中文就显示成了乱码。
到这里,账已经能存成 csv,再读回来了。只是读回来的数字还得自己转换,多少有点麻烦。下一节换成 json,看看能不能省掉这一步。在那之前,先做一道练习:用 csv.DictReader 读一个文件,把金额加起来。
编辑器上面已经把两天的账分别写进了 day1.csv 和 day2.csv 。请写一个函数 csv_total(path) :用 csv.DictReader 读这个文件,返回所有金额的合计。
注意:从 csv 读回来的金额是字符串,要先转换成数字再加。
运行后的输出应该是:
第一天:20.50 元
第二天:79.50 元推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai