# 5.1-文件存储

文件存储形式多种多样，比如可以保存成 TXT 纯文本形式，也可以保存为 JSON 格式、CSV 格式等，本节就来了解一下文本文件的存储方式。

## 5.1.1　TXT 文本存储

将数据保存到 TXT 文本的操作非常简单，而且 TXT 文本几乎兼容任何平台，但是这有个缺点，那就是不利于检索。所以如果对检索和数据结构要求不高，追求方便第一的话，可以采用 TXT 文本存储。本节中，我们就来看下如何利用 Python 保存 TXT 文本文件。

### 1. 本节目标

本节中，我们要保存知乎上 “发现” 页面的 “热门话题” 部分，将其问题和答案统一保存成文本形式。

### 2. 基本实例

首先，可以用 requests 将网页源代码获取下来，然后使用 pyquery 解析库解析，接下来将提取的标题、回答者、回答保存到文本，代码如下：

```python
import requests
from pyquery import PyQuery as pq

url = 'https://www.zhihu.com/explore'
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}
html = requests.get(url, headers=headers).text
doc = pq(html)
items = doc('.explore-tab .feed-item').items()
for item in items:
    question = item.find('h2').text()
    author = item.find('.author-link-line').text()
    answer = pq(item.find('.content').html()).text()
    file = open('explore.txt', 'a', encoding='utf-8')
    file.write('\n'.join([question, author, answer]))
    file.write('\n' + '=' * 50 + '\n')
    file.close()
```

这里主要是为了演示文件保存的方式，因此 requests 异常处理部分在此省去。首先，用 requests 提取知乎的 “发现” 页面，然后将热门话题的问题、回答者、答案全文提取出来，然后利用 Python 提供的 open 方法打开一个文本文件，获取一个文件操作对象，这里赋值为 file，接着利用 file 对象的 write 方法将提取的内容写入文件，最后调用 close 方法将其关闭，这样抓取的内容即可成功写入文本中了。

运行程序，可以发现在本地生成了一个 explore.txt 文件，其内容如图 5-1 所示。

![](/files/-Ll_JyUUBbFv8Optufrb) 图 5-1 文件内容

这样热门问答的内容就被保存成文本形式了。

这里 open 方法的第一个参数即要保存的目标文件名称，第二个参数为 a，代表以追加方式写入到文本。另外，我们还指定了文件的编码为 utf-8。最后，写入完成后，还需要调用 close 方法来关闭文件对象。

### 3. 打开方式

在刚才的实例中，open 方法的第二个参数设置成了 a，这样在每次写入文本时不会清空源文件，而是在文件末尾写入新的内容，这是一种文件打开方式。关于文件的打开方式，其实还有其他几种，这里简要介绍一下。

* r：以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
* rb：以二进制只读方式打开一个文件。文件指针将会放在文件的开头。
* r+：以读写方式打开一个文件。文件指针将会放在文件的开头。
* rb+：以二进制读写方式打开一个文件。文件指针将会放在文件的开头。
* w：以写入方式打开一个文件。如果该文件已存在，则将其覆盖。如果该文件不存在，则创建新文件。
* wb：以二进制写入方式打开一个文件。如果该文件已存在，则将其覆盖。如果该文件不存在，则创建新文件。
* w+：以读写方式打开一个文件。如果该文件已存在，则将其覆盖。如果该文件不存在，则创建新文件。
* wb+：以二进制读写格式打开一个文件。如果该文件已存在，则将其覆盖。如果该文件不存在，则创建新文件。
* a：以追加方式打开一个文件。如果该文件已存在，文件指针将会放在文件结尾。也就是说，新的内容将会被写入到已有内容之后。如果该文件不存在，则创建新文件来写入。
* ab：以二进制追加方式打开一个文件。如果该文件已存在，则文件指针将会放在文件结尾。也就是说，新的内容将会被写入到已有内容之后。如果该文件不存在，则创建新文件来写入。
* a+：以读写方式打开一个文件。如果该文件已存在，文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在，则创建新文件来读写。
* ab+：以二进制追加方式打开一个文件。如果该文件已存在，则文件指针将会放在文件结尾。如果该文件不存在，则创建新文件用于读写。

### 4. 简化写法

另外，文件写入还有一种简写方法，那就是使用 with as 语法。在 with 控制块结束时，文件会自动关闭，所以就不需要再调用 close 方法了。这种保存方式可以简写如下：

```python
with open('explore.txt', 'a', encoding='utf-8') as file:
    file.write('\n'.join([question, author, answer]))
    file.write('\n' + '=' * 50 + '\n')
```

如果想保存时将原文清空，那么可以将第二个参数改写为 w，代码如下：

```python
with open('explore.txt', 'w', encoding='utf-8') as file:
    file.write('\n'.join([question, author, answer]))
    file.write('\n' + '=' * 50 + '\n')
```

上面便是利用 Python 将结果保存为 TXT 文件的方法，这种方法简单易用，操作高效，是一种最基本的保存数据的方法。

## 5.1.2　JSON 文件存储

JSON，全称为 JavaScript Object Notation, 也就是 JavaScript 对象标记，它通过对象和数组的组合来表示数据，构造简洁但是结构化程度非常高，是一种轻量级的数据交换格式。本节中，我们就来了解如何利用 Python 保存数据到 JSON 文件。

### 1. 对象和数组

在 JavaScript 语言中，一切都是对象。因此，任何支持的类型都可以通过 JSON 来表示，例如字符串、数字、对象、数组等，但是对象和数组是比较特殊且常用的两种类型，下面简要介绍一下它们。

对象：它在 JavaScript 中是使用花括号 {} 包裹起来的内容，数据结构为 {key1：value1, key2：value2, ...} 的键值对结构。在面向对象的语言中，key 为对象的属性，value 为对应的值。键名可以使用整数和字符串来表示。值的类型可以是任意类型。

数组：数组在 JavaScript 中是方括号 \[] 包裹起来的内容，数据结构为 \["java", "javascript", "vb", ...] 的索引结构。在 JavaScript 中，数组是一种比较特殊的数据类型，它也可以像对象那样使用键值对，但还是索引用得多。同样，值的类型可以是任意类型。

所以，一个 JSON 对象可以写为如下形式：

```javascript
[{
    "name": "Bob",
    "gender": "male",
    "birthday": "1992-10-18"
}, {
     "name": "Selina",
    "gender": "female",
    "birthday": "1995-10-18"
}]
```

由中括号包围的就相当于列表类型，列表中的每个元素可以是任意类型，这个示例中它是字典类型，由大括号包围。

JSON 可以由以上两种形式自由组合而成，可以无限次嵌套，结构清晰，是数据交换的极佳方式。

### 2. 读取 JSON

Python 为我们提供了简单易用的 JSON 库来实现 JSON 文件的读写操作，我们可以调用 JSON 库的 loads 方法将 JSON 文本字符串转为 JSON 对象，可以通过 dumps() 方法将 JSON 对象转为文本字符串。

例如，这里有一段 JSON 形式的字符串，它是 str 类型，我们用 Python 将其转换为可操作的数据结构，如列表或字典：

```python
import json

str = '''
[{
    "name": "Bob",
    "gender": "male",
    "birthday": "1992-10-18"
}, {
    "name": "Selina",
    "gender": "female",
    "birthday": "1995-10-18"
}]
'''
print(type(str))
data = json.loads(str)
print(data)
print(type(data))
```

运行结果如下：

```python
<class'str'>
[{'name': 'Bob', 'gender': 'male', 'birthday': '1992-10-18'}, {'name': 'Selina', 'gender': 'female', 'birthday': '1995-10-18'}]
<class 'list'>
```

这里使用 loads 方法将字符串转为 JSON 对象。由于最外层是中括号，所以最终的类型是列表类型。

这样一来，我们就可以用索引来获取对应的内容了。例如，如果想取第一个元素里的 name 属性，就可以使用如下方式：

```python
data[0]['name']
data[0].get('name')
```

得到的结果都是：

```
Bob
```

通过中括号加 0 索引，可以得到第一个字典元素，然后再调用其键名即可得到相应的键值。获取键值时有两种方式，一种是中括号加键名，另一种是通过 get 方法传入键名。这里推荐使用 get 方法，这样如果键名不存在，则不会报错，会返回 None。另外，get 方法还可以传入第二个参数（即默认值），示例如下：

```python
data[0].get('age')
data[0].get('age', 25)
```

运行结果如下：

```python
None
25
```

这里我们尝试获取年龄 age，其实在原字典中该键名不存在，此时默认会返回 None。如果传入第二个参数（即默认值），那么在不存在的情况下返回该默认值。

值得注意的是，JSON 的数据需要用双引号来包围，不能使用单引号。例如，若使用如下形式表示，则会出现错误：

```python
import json

str = '''
[{
    'name': 'Bob',
    'gender': 'male',
    'birthday': '1992-10-18'
}]
'''
data = json.loads(str)
```

运行结果如下：

```python
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 3 column 5 (char 8)
```

这里会出现 JSON 解析错误的提示。这是因为这里数据用单引号来包围，请千万注意 JSON 字符串的表示需要用双引号，否则 loads 方法会解析失败。

如果从 JSON 文本中读取内容，例如这里有一个 data.json 文本文件，其内容是刚才定义的 JSON 字符串，我们可以先将文本文件内容读出，然后再利用 loads 方法转化：

```python
import json

with open('data.json', 'r') as file:
    str = file.read()
    data = json.loads(str)
    print(data)
```

运行结果如下：

```python
[{'name': 'Bob', 'gender': 'male', 'birthday': '1992-10-18'}, {'name': 'Selina', 'gender': 'female', 'birthday': '1995-10-18'}]
```

### 3. 输出 JSON

另外，我们还可以调用 dumps 方法将 JSON 对象转化为字符串。例如，将上例中的列表重新写入文本：

```python
import json

data = [{
    'name': 'Bob',
    'gender': 'male',
    'birthday': '1992-10-18'
}]
with open('data.json', 'w') as file:
    file.write(json.dumps(data))
```

利用 dumps 方法，我们可以将 JSON 对象转为字符串，然后再调用文件的 write 方法写入文本，结果如图 5-2 所示。

![](/files/-Ll_JyUjPgpA0zZ3kZi4) 图 5-2 写入结果

另外，如果想保存 JSON 的格式，可以再加一个参数 indent，代表缩进字符个数。示例如下：

```python
with open('data.json', 'w') as file:
    file.write(json.dumps(data, indent=2))
```

此时写入结果如图 5-3 所示。

![](/files/-Ll_JyUlIauQCGh5qwGL)

图 5-3 写入结果

这样得到的内容会自动带缩进，格式会更加清晰。

另外，如果 JSON 中包含中文字符，会怎么样呢？例如，我们将之前的 JSON 的部分值改为中文，再用之前的方法写入到文本：

```python
import json

data = [{
    'name': ' 王伟 ',
    'gender': ' 男 ',
    'birthday': '1992-10-18'
}]
with open('data.json', 'w') as file:
    file.write(json.dumps(data, indent=2))
```

写入结果如图 5-4 所示。

![](/files/-Ll_JyUn_V4GhhJgZI4f)

图 5-4 写入结果

可以看到，中文字符都变成了 Unicode 字符，这并不是我们想要的结果。

为了输出中文，还需要指定参数 ensure\_ascii 为 False，另外还要规定文件输出的编码：

```python
with open('data.json', 'w', encoding='utf-8') as file:
    file.write(json.dumps(data, indent=2, ensure_ascii=False))
```

写入结果如图 5-5 所示。 ![](/files/-Ll_JyUpVgIvqmL5PM13)

图 5-5 写入结果

可以发现，这样就可以输出 JSON 为中文了。

本节中，我们了解了用 Python 进行 JSON 文件读写的方法，后面做数据解析时经常会用到，建议熟练掌握。

## 5.1.3　CSV 文件存储

CSV，全称为 Comma-Separated Values，中文可以叫作逗号分隔值或字符分隔值，其文件以纯文本形式存储表格数据。该文件是一个字符序列，可以由任意数目的记录组成，记录间以某种换行符分隔。每条记录由字段组成，字段间的分隔符是其他字符或字符串，最常见的是逗号或制表符。不过所有记录都有完全相同的字段序列，相当于一个结构化表的纯文本形式。它比 Excel 文件更加简洁，XLS 文本是电子表格，它包含了文本、数值、公式和格式等内容，而 CSV 中不包含这些内容，就是特定字符分隔的纯文本，结构简单清晰。所以，有时候用 CSV 来保存数据是比较方便的。本节中，我们来讲解 Python 读取和写入 CSV 文件的过程。

### 1. 写入

这里先看一个最简单的例子：

```python
import csv

with open('data.csv', 'w') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(['id', 'name', 'age'])
    writer.writerow(['10001', 'Mike', 20])
    writer.writerow(['10002', 'Bob', 22])
    writer.writerow(['10003', 'Jordan', 21])
```

首先，打开 data.csv 文件，然后指定打开的模式为 w（即写入），获得文件句柄，随后调用 csv 库的 writer 方法初始化写入对象，传入该句柄，然后调用 writerow 方法传入每行的数据即可完成写入。

运行结束后，会生成一个名为 data.csv 的文件，此时数据就成功写入了。直接以文本形式打开的话，其内容如下：

```
id,name,age
10001,Mike,20
10002,Bob,22
10003,Jordan,21
```

可以看到，写入的文本默认以逗号分隔，调用一次 writerow 方法即可写入一行数据。用 Excel 打开的结果如图 5-6 所示。

![](/files/-Ll_JyUrLJIwDoEXOwQs)

图 5-6 打开结果

如果想修改列与列之间的分隔符，可以传入 delimiter 参数，其代码如下：

```python
import csv

with open('data.csv', 'w') as csvfile:
    writer = csv.writer(csvfile, delimiter=' ')
    writer.writerow(['id', 'name', 'age'])
    writer.writerow(['10001', 'Mike', 20])
    writer.writerow(['10002', 'Bob', 22])
    writer.writerow(['10003', 'Jordan', 21])
```

这里在初始化写入对象时传入 delimiter 为空格，此时输出结果的每一列就是以空格分隔了，内容如下：

```
id name age
10001 Mike 20
10002 Bob 22
10003 Jordan 21
```

另外，我们也可以调用 writerows 方法同时写入多行，此时参数就需要为二维列表，例如：

```python
import csv

with open('data.csv', 'w') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(['id', 'name', 'age'])
    writer.writerows([['10001', 'Mike', 20], ['10002', 'Bob', 22], ['10003', 'Jordan', 21]])
```

输出效果是相同的，内容如下：

```
id,name,age
10001,Mike,20
10002,Bob,22
10003,Jordan,21
```

但是一般情况下，爬虫爬取的都是结构化数据，我们一般会用字典来表示。在 csv 库中也提供了字典的写入方式，示例如下：

```python
import csv

with open('data.csv', 'w') as csvfile:
    fieldnames = ['id', 'name', 'age']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerow({'id': '10001', 'name': 'Mike', 'age': 20})
    writer.writerow({'id': '10002', 'name': 'Bob', 'age': 22})
    writer.writerow({'id': '10003', 'name': 'Jordan', 'age': 21})
```

这里先定义 3 个字段，用 fieldnames 表示，然后将其传给 DictWriter 来初始化一个字典写入对象，接着可以调用 writeheader 方法先写入头信息，然后再调用 writerow 方法传入相应字典即可。最终写入的结果是完全相同的，内容如下：

```
id,name,age  
10001,Mike,20  
10002,Bob,22  
10003,Jordan,21
```

这样就可以完成字典到 CSV 文件的写入了。

另外，如果想追加写入的话，可以修改文件的打开模式，即将 open 函数的第二个参数改成 a，代码如下：

```python
import csv  

with open('data.csv', 'a') as csvfile:  
    fieldnames = ['id', 'name', 'age']  
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)  
    writer.writerow({'id': '10004', 'name': 'Durant', 'age': 22})
```

这样在上面的基础上再执行这段代码，文件内容便会变成：

```
id,name,age  
10001,Mike,20  
10002,Bob,22  
10003,Jordan,21  
10004,Durant,22
```

可见，数据被追加写入到文件中。

如果要写入中文内容的话，可能会遇到字符编码的问题，此时需要给 open 参数指定编码格式。比如，这里再写入一行包含中文的数据，代码需要改写如下：

```python
import csv

with open('data.csv', 'a') as csvfile:
    fieldnames = ['id', 'name', 'age']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    writer.writerow({'id': '10004', 'name': 'Durant', 'age': 22})
```

这里需要给 open 函数指定编码，否则可能发生编码错误。

另外，如果接触过 pandas 等库的话，可以调用 DataFrame 对象的 to\_csv 方法来将数据写入 CSV 文件中。

### 2. 读取

我们同样可以使用 csv 库来读取 CSV 文件。例如，将刚才写入的文件内容读取出来，相关代码如下：

```python
import csv  

with open('data.csv', 'r', encoding='utf-8') as csvfile:  
    reader = csv.reader(csvfile)  
    for row in reader:  
        print(row)
```

运行结果如下：

```
['id', 'name', 'age']  
['10001', 'Mike', '20']  
['10002', 'Bob', '22']  
['10003', 'Jordan', '21']  
['10004', 'Durant', '22']  
['10005', ' 王伟 ', '22']
```

这里我们构造的是 Reader 对象，通过遍历输出了每行的内容，每一行都是一个列表形式。注意，如果 CSV 文件中包含中文的话，还需要指定文件编码。

另外，如果接触过 pandas 的话，可以利用 read\_csv 方法将数据从 CSV 中读取出来，例如：

```
import pandas as pd  

df = pd.read_csv('data.csv')  
print(df)
```

运行结果如下：

```
      id    name  age  
0  10001    Mike   20  
1  10002     Bob   22  
2  10003  Jordan   21  
3  10004  Durant   22  
4  10005    王伟   22
```

在做数据分析的时候，此种方法用得比较多，也是一种比较方便地读取 CSV 文件的方法。

本节中，我们了解了 CSV 文件的写入和读取方式。这也是一种常用的数据存储方式，需要熟练掌握。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://python3webspider.cuiqingcai.com/5.1-wen-jian-cun-chu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
