
# 加载文件

本课程的目的是学习如何从笔记本电脑磁盘上的文件中提取数据。 我们将加载文本文件中的单词和数据文件中的数字。 在此过程中，我们将了解文件名和文件路径的更多信息。 我们的通用分析程序模板的前两个元素，表示获取数据然后将其加载到数据结构中：

1. 获取数据，这意味着找到合适的文件，或从 Web 收集数据并存储在文件中
2. 从磁盘加载数据并放入组织成数据结构的内存中

目前，我们只需手动从网上下载现成的数据文件，即可满足第一步。在[ MSAN692 - 数据采集](https://github.com/parrt/msan692)中，我们将学习如何以编程方式从 Web 提取数据。 本讲座重点介绍分析程序模板的第二步。

随着我们继续，我将反复要求你输入一些这样的例子。 了解从文件加载数据的代码模式至关重要。 请输入您的代码，不要剪切和粘贴。

## 什么是文件

正如我们之前所讨论的，磁盘和 RAM 都是内存的形式。 RAM 比磁盘快得多（但更小）但是当电源耗尽时 RAM 全部消失。 另一方面，磁盘是持久的。 文件只是由文件名标识的，磁盘上的一大块数据。 您一直使用文件。例如，我们可以双击文本文件或 Excel 文件，这将打开一个应用程序来显示这些文件。

我们需要能够编写从文件中读取数据的 Python 程序，就像 Excel 那样。 在 Python 程序中访问 RAM 中的数据非常容易，我们只需使用索引来引用列表中的各种元素，例如`names [i]`。 访问文件数据不太方便，因为我们必须首先将文件显式加载到工作内存中。 例如，我们可能希望将文件中的名称列表加载到`names`列表中。

**如果文件太大而无法同时放入内存，我们必须以块的形式处理数据。**现在，让我们假设所有文件都适合内存。

即便如此，访问文件也有点麻烦，因为我们必须明确地让 Python 打开一个文件，然后（通常）在我们完成后关闭它。 我们还必须区分对文件的读取和写入，这决定了我们打开文件的模式。 我们可以在读，写或追加模式下打开文件。 对于本实验，我们只关注“打开文件来读取”的默认情况。 以下是在读取模式（默认）下打开一个名为`foo.txt`的文件，然后立即关闭该文件的方式：

```
f = open('foo.txt')  # open for read mode
f.close()            # ok, we're done
```

嗯......从`open()`返回什么样的对象并存储在`f`中？ 为什么我们要关闭文件？

## 文件描述符

当我们打开文件时，Python 为我们提供了一个“文件对象”，它实际上只是操作系统为我们提供的句柄或描述符。 它是一个唯一的标识符，以及操作系统识别我们使用的文件的方式。 **文件对象不是文件名，也不是磁盘上的文件本身。**它实际上只是一个描述符和文件的引用。

我们将使用文件名来使用`open()`获取文件对象，并使用文件对象来获取文件内容。

```python
f = open("data/prices.txt") # or just "prices.txt"
print(type(f))
print(f)
f.close()
print(f.closed)

'''
<class '_io.TextIOWrapper'>
<_io.TextIOWrapper name='data/prices.txt' mode='r' encoding='UTF-8'>
True
'''
```

（将`TextIOWrapper`看作*文件*。）

关闭操作通知操作系统您不再需要该资源。 操作系统一次只能打开这么多文件，因此您应该在使用完毕后关闭文件。

稍后，当您学习将数据写入文件时，关闭操作也很重要。 关闭文件会刷新内存缓冲区中的，需要写入的所有数据。 从Python文档：

> “编写程序的一个常见的错误是，你使用代码将所需的所有数据添加到文件中，但程序最终不会创建文件。通常这意味着你忘了关闭文件。”

<img src="img/redbang.png" width="30" align="left"> 

为了避免混淆，请记住这个比喻。 您的房屋内容（文件）与您的地址（文件名）不同，并且与写有地址的文件（文件描述符）不同。进一步来说：

1. 文件名是一个标识磁盘上文件的字符串。 它可以是完全限定的或相对于当前工作目录。

2. 文件对象不是文件名，也不是磁盘上的文件本身。 它实际上只是一个描述符和文件的引用。

3. 文件的内容不同于 Python 给我们的文件名和文件对象（描述符）。

### Python WITH 语句

更新版本的 Python 提供了一种很好的机制来避免忘记文件关闭操作。 `with`语句更通用，但我们只是用它来自动关闭文件。 即使`with`语句中有强制程序终止的异常，关闭操作也会发生。 这是使用方式：

```python
with open("data/prices.txt") as f:
    contents = f.read()
    print(type(contents))
print(contents[0:10])
print(f.closed)

'''
<class 'str'>
0.605
0.60
True
'''
```

## 文件名称和路径

你知道文件名是什么，因为之前你已经创建了很多文件。 （顺便说一下，另一个提醒是，不要在文件或目录名中使用空格。）*路径*是目录或文件的唯一限定符或定位符。一个*完全限定的*文件名给出了来自文件系统根目录的描述，用`/`分隔。文件系统的根在路径名的开头用`/`（正斜杠）标识。您可能习惯将其视为“Macintosh HD”，但从编程的角度来看，它只是`/`。在 Windows 上，我们不会在这里考虑，根目录包括盘符和反斜杠，如`C:\`。 这是一个有用的图表，显示了名为`view.py`的文件的完全限定路径名的成分：

<img src="img/path-names.png" width="750">

作为简写，你可以用`~`开始一个路径，这意味着“我的主目录”。 在 Mac 上为`/Users/parrt`或您的任何用户 ID。 在 Linux 上，它可能是`/home/parrt`。

路径中的最后一个元素是文件名或目录。 例如，要引用上图中保存`view.py`的目录，请使用路径`/Users/parrt/classes/msan501/images-parrt`。或者，使用简写，完全限定的路径是`~/classes/msan501/images-parrt`。这是一个使用一些完全限定路径的 bash 会话示例：

```bash
$ ls /Users/parrt/classes/msan501/images-parrt/view.py
/Users/parrt/classes/msan501/images-parrt/view.py
$ cd /Users/parrt/classes/msan501/images-parrt
$ pwd
/Users/parrt/classes/msan501/images-parrt
$ cd ~/classes/msan501/images-parrt
$ pwd
/Users/parrt/classes/msan501/images-parrt
```

### 当前工作目录

所有程序都以*当前工作目录*的概念运行。 因此，如果一个程序在`~/classes/msan501/images-parrt`目录中运行，那么程序可以仅仅使用文件名，引用那个目录中的任何数据文件 - 不需要路径。例如，让我们使用`ls`程序来演示不同类型的路径。

```bash
$ cd ~/classes/msan501/images-parrt
$ ls
view.py
$ ls /Users/parrt/classes/msan501
images-parrt/
$ ls /Users/parrt/classes
msan501/
```

任何不以`~`或`/`开头的路径都称为*相对路径名*。为了完整起见，请注意`..`表示当前工作目录上级的目录：

```bash
$ cd ~/classes/msan501/images-parrt
$ ls ..
images-parrt/
$ ls ../..
msan501/
```

有时您会看到我使用`/ tmp`，这是一个临时目录或垃圾场。重新启动时，该目录中的所有文件通常都会被删除。

## 加载文本文件

正如我们在课程早期讨论的那样，文件只是一些位。这就是我们解释有意义的位的方式。 这些位可以代表图像，电影，文章，数据，Python 程序文本等等。 让我们将任何包含字符的文件成为文本文件，任何其他文件成为二进制文件。

文本文件通常是每个字符 1 个字节（8 位），并具有行的概念。 一行只是以`\r\n`（Windows）或`\n`（UNIX，Mac）终止的字符序列。 文本文件通常是一系列行。 下载此示例文本文件[`IntroIstanbul.txt`](https://raw.githubusercontent.com/parrt/msan501/master/notes/data/IntroIstanbul.txt)，以便我们可以使用。 您可以将它保存在`/tmp`或您在课堂作业中使用的任何目录中。 出于本讨论的目的，我将数据文件放在此`notes`目录的名为`data`的子目录中。

该文件的前 10 行如下所示：

```python
! head -10 data/IntroIstanbul.txt

'''
The City and ITS People
Istanbul is one of the worlds most venerable cities. Part
of the citys allure is its setting, where Europe faces Asia acr­oss
the winding turquoise waters of the Bosphorus, making it the only city
in the world to bridge two continents.
'''
```

你可以忽略前面的`!`，因为它只是告诉这个 Jupyter 笔记本运行后面的终端命令。 如果你想要的话，在这种情况下你可以把`!`想象成`$`终端提示符。

现在，让我们以原始方式而不是文本编辑器检查文件的内容。 `od`命令（八进制转储）对于查看文件的字节很有用。 使用选项`-c`将内容看作 1 字节字符：

```python
! od -c data/IntroIstanbul.txt | head -5

'''
0000000   \n          \n          \n                  \n                
0000020           \n                                   T   h   e       C
0000040    i   t   y       a   n   d       I   T   S       P   e   o   p
0000060    l   e  \n                                   I   s   t   a   n
0000100    b   u   l       i   s       o   n   e       o   f       t   h
'''
```


那个`| head -5`（竖线`|`看起来像一个管道）将`od`命令通过管道连接到`head`程序，它给出了前五行的输出。 当我们有很多输出时，我们也可以将输出传递给`more`程序来对长输出进行分页。

```bash
$ od -c data/IntroIstanbul.txt | more
...
```

你看到的`\n`字符代表我们知道的回车符。左边的数字是文件中的字符偏移量（看起来它们是八进制而不是十进制，顺便说一句；使用`-A d`来获取十进制地址）。

让我们看一些处理文本文件的常见编程模式。

### 模式：将所有文件内容加载到字符串中

操作序列是打开，加载，关闭。

```python
with open('data/IntroIstanbul.txt') as f:
    contents = f.read() # read all content of the file
print(contents[0:200]) # print just the first 200 characters

'''
The City and ITS People
Istanbul is one of the worlds most venerable cities. Part
of the citys allure is its setting, where Europe faces Asia acr­oss
'''
```


### 练习

*不要剪切和粘贴*，键入该序列并确保您可以从 Python 打印文件的内容。 使用您保存`IntroIstanbul.txt`的任何目录，而不是`data`。

### 模式：将文件的所有单词加载到列表中

这个模式只是前面我们`split()`空格字符来获取列表的扩展：

```python
with open('data/IntroIstanbul.txt') as f:
contents = f.read() # read all content of the file
words = contents.split(' ')
print(words[0:100]) # print first 100 words

# ['\n', '', '\n', '', '\n', '', '', '', '\n', '', '', '', '', '', '\n', '', '', '', '', '', '', '', 'The', 'City', 'and', 'ITS', 'People\n', '', '', '', '', '', '', '', 'Istanbul', 'is', 'one', 'of', 'the', 'worlds', 'most', 'venerable', 'cities.', 'Part\n', '', '', '', '', '', '', '', 'of', 'the', 'citys', 'allure', 'is', 'its', 'setting,', 'where', 'Europe', 'faces', 'Asia', 'acr\xadoss\n', '', '', '', '', '', '', '', 'the', 'winding', 'turquoise', 'waters', 'of', 'the', 'Bosphorus,', 'making', 'it', 'the', 'only', 'city\n', '', '', '', '', '', '', '', 'in', 'the', 'world', 'to', 'bridge', 'two', 'continents.\n', '', '', '', '']
```

因为我们在空格字符上进行拆分，所以一行中的换行符和多个空格字符会产生无效的“单词”。我们需要在使用之前将该列表转换为新列表。

### 练习

使用*过滤*编程模式，过滤`words`中的大于 1 个字符的单词；放入另一个名为`words2`的列表中。提示，`len(s)`获取字符串`s`的长度。[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

### 练习

通过编写一个名为`getwords`的函数，将所有这些放在一起，该函数将`filename`作为参数并返回长于一个字符的单词列表。这是“将文件的所有单词加载到列表中”模式和上一个练习的组合。[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

## 加载文件的所有行

将文件内容读入字符串并不总是那么有用。我们通常希望处理我们单词，或文本文件的行，像刚才看到的那样。自然语言处理（NLP）将专注于使用单词，但让我们看一些数据文件，这些文件通常将文件结构化为数据行。每行代表观测，数据点或记录。

我们可以用`\n`来分割文本内容来获取行，但是 Python 为我们提供了这样的功能。 为了给我们一些数据，请下载[`price.txt`](https://raw.githubusercontent.com/parrt/msan501/master/notes/data/prices.txt)，其中包含价格清单，每个价格一行。 这是另一种非常常见的编程模式：

### 模式：将文件的所有行读入列表。

序列是打开，读取行，关闭：

```python
with open('data/prices.txt') as f:
    prices = f.readlines() # get lines of file into a list
prices[0:10]

'''
['0.605\n',
 '0.600\n',
 '0.594\n',
 '0.592\n',
 '0.600\n',
 '0.616\n',
 '0.623\n',
 '0.628\n',
 '0.630\n',
 '0.629\n']
'''
```


### 练习

*不要剪切和粘贴*，输入该代码并确保您可以将文件的行读入列表。

### 练习

在列表的每个元素上使用`strip()`函数，以便得到：`['0.605', '0.600', '0.594', ...]`。 使用列表推导式将价格*映射*到价格列表的新版本。[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

### 将字符串列表转换为 numpy 数组

数字末尾有`\n`字符，但这不是问题，因为我们可以使用 [NumPy](http://www.numpy.org/) 轻松转换它：

```python
import numpy as np
prices2 = np.array(prices, dtype=float) # convert to array of numbers
print(type(prices2))
print(prices2[0:10])

from lolviz import *
objviz(prices2)

'''
<class 'numpy.ndarray'>
[0.605 0.6   0.594 0.592 0.6   0.616 0.623 0.628 0.63  0.629]
'''
```

![svg](img/3.1_files_26_1.svg)


### 练习

将此转换添加到上一个练习中，并确保获得了`array`输出。 （我试图给你键入代码的重复经验，从文件中读取数据并以某种方式处理它。）[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

## 加载 CSV 文件

我们来看一个更复杂的数据文件。 下载[`heights.csv`](https://raw.githubusercontent.com/parrt/msan501/master/notes/data/player-heights.csv)，其开头如下：

```python
! head -4 data/player-heights.csv

'''
Football height, Basketball height
6.329999924, 6.079999924
6.5, 6.579999924
6.5, 6.25
'''
```

它仍然是一个文本文件，但现在我们开始认为文本文件可能遵循特定的格式。 在这种情况下，我们将其识别为*逗号分隔值*（CSV）文件。 它还有给列命名的标题行，这意味着我们需要区分对待第一行和文件其余部分。

### 模式：将 CSV 文件加载到列表的列表中

我们已经知道如何打开文件并获取行，所以让我们这样做，并将行分为标题和数据成分：

```python
import numpy as np

with open('data/player-heights.csv') as f:
    lines = f.readlines()

lines = [line.strip() for line in lines] # remove \n on end
lines[0:5]

'''
['Football height, Basketball height',
 '6.329999924, 6.079999924',
 '6.5, 6.579999924',
 '6.5, 6.25',
 '6.25, 6.579999924']
'''
```


```python
header = lines[0]
data = lines[1:] # slice

# print it back out

print(header)
for d in data[0:5]:
    print(d)
    
'''
Football height, Basketball height
6.329999924, 6.079999924
6.5, 6.579999924
6.5, 6.25
6.25, 6.579999924
6.5, 6.25
'''
```


### 练习

作为练习，输入该代码并确保打印出标题和前五行数据。

### 练习

数据的每一行都是一个包含两个数字的字符串。 我们需要使用`split(',')`将该字符串转换为带有两个浮点数的列表。 将所有两个元素的列表组合成一个整体列表，为我们提供了我们需要的二维表，这是我们的下一个练习。

编写一个名为`getcsv(filename)`的函数，它返回一个行列表的列表，其中第一行是标题行。 去掉行末尾的任何`\n`字符。 输出应如下所示：

```python
[['6.329999924', ' 6.079999924'], ['6.5', ' 6.579999924'], ['6.5', ' 6.25']]
```

尽可能使用列表推导。[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

### 练习

`import pandas as pd`，然后将上一个练习中的数据转换为数据帧。 Pandas 不会自动理解第一行是标题，因此将切片`data[1:]`用作`pd.DataFrame()`数据帧构造函数的第一个参数，然后传递`data[0]`作为 `columns`参数。 打印出来，你应该看到类似的东西：

```
   Football height  Basketball height
0      6.329999924        6.079999924
1              6.5        6.579999924
2              6.5               6.25
...
```

[【答案】](https://github.com/parrt/msan501/blob/master/notes/code/files.py)

## 使用 Pandas 加载 CSV 文件

当然，加载 CSV 是数据科学家需要一直做的事情，所以你可以在 [Pandas](http://pandas.pydata.org) 中使用一个简单的函数，你可能会变得非常熟悉：

```python
import pandas as pd
prices = pd.read_csv('data/prices.txt', header=None)
prices.head(5)
```

| | 0 |
| --- | --- |
| 0 | 0.605 |
| 1 | 0.600 |
| 2 | 0.594 |
| 3 | 0.592 |
| 4 | 0.600 |

(`header=None`表示文件的第一行中没有列名。)

这甚至适用于带有标题行的 CSV 文件：

```python
data = pd.read_csv('data/player-heights.csv')
data.head(5)
```


|  | Football height | Basketball height |
| --- | --- | --- |
| 0 | 6.33 | 6.08 |
| 1 | 6.50 | 6.58 |
| 2 | 6.50 | 6.25 |
| 3 | 6.25 | 6.58 |
| 4 | 6.50 |  |

我们将在[数据帧](dataframes.ipynb)中再次看到这些内容。

## 逐行处理文件

将文本行放入内存的先前机制很有效，只是它需要我们将所有内容一次性加载到内存中。这是非常低效的，并且将我们可以处理的数据的大小限制为我们拥有的内存量。

### 模式：多次地逐行读取数据

我们可以使用`for`循环，其中数据序列是文件描述符：

```python
with open('data/prices.txt') as f:
    for line in f: # for each line in the file
        print(float(line)) # process the line in some way
```


```python
n = 5
with open('data/prices.txt') as f:
    for line in f: # for each line in the file
        if n>0:
            print(float(line)) # process the line in some way
        n -= 1
        
'''
0.605
0.6
0.594
0.592
0.6
'''
```

### 练习

键入处理文件的行的新版本。不要剪切和粘贴！

### 练习

创建一个名为`getsum`的函数，它将文件名字符串作为参数，并返回该文件中行值之和。 不要使用`readlines()`函数。 在列表推导式中使用`for line in f`循环手动获取行的列表。 使用文件名`data/prices.txt`调用`getsum`并打印出价格总和。 使用`sum(...)`来求和列表推导创建的列表。

## 关闭文件后的操作

请记住，关闭文件后，您无法再从中读取数据：

```python
f = open('data/prices.txt')
f.close()
f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file
```

## 总结

本课程的主要编程模式是：

* **模式**：将所有文件内容加载到字符串中。
* **模式**：将文件的所有单词加载到列表中。
* **模式**：将文件的所有行读入列表。
* **模式**：将数字列表加载到 numpy 数组中。
* **模式**：将 CSV 文件加载到 2D numpy 数组中。

您应该能够快速，轻松地编写这些模式，而无需从 stackoverflow 中剪切和粘贴。