第 2 章 CSV 文件
CSV(comma-separated value,逗号分隔值)文件格式是一种非常简单的数据存储与分享方式。CSV 文件将数据表格存储为纯文本,表格(或电子表格)中的每个单元格都是一个数值或字符串。与 Excel 文件相比,CSV 文件的一个主要优点是有很多程序可以存储、转换和处理纯文本文件;相比之下,能够处理 Excel 文件的程序却不多。所有电子表格程序、文字处理程序或简单的文本编辑器都可以处理纯文本文件,但不是所有的程序都能处理 Excel 文件。尽管 Excel 是一个功能非常强大的工具,但是当你使用 Excel 文件时,还是会被局限在 Excel 提供的功能范围内。CSV 文件则为你提供了非常大的自由,使你在完成任务的时候可以选择合适的工具来处理数据——如果没有现成的工具,那就使用 Python 自己开发一个!
当你使用 CSV 文件时,确实会失去某些 Excel 功能:在 Excel 电子表格中,每个单元格都有一个定义好的“类型”(数值、文本、货币、日期等),CSV 文件中的单元格则只是原始数据。幸好,Python 在识别不同数据类型方面相当聪明,第 1 章中已经展示了这一点。使用 CSV 文件的另一个问题是它只能保存数据,不能保存公式。但是,通过将数据存储(CSV 文件)和数据处理(Python 脚本)分离,你可以很容易地在不同数据集上进行加工处理。当数据存储和数据处理过程分开进行时,错误(不管是数据处理中的错误,还是数据存储中的错误)不但更容易被发现,而且更难扩散。
要使用 CSV 文件开始工作,需要先创建一个 CSV 文件,你可以从以下地址(https://github.com/cbrownley/foundations-for-analytics-with-python/blob/master/csv/supplier_data.csv)下载这个文件,步骤如下。
(1) 打开一个新的电子表格,向其中加入数据,如图 2-1 所示。
图 2-1:向 supplier_data.csv 文件中添加数据
(2) 将文件保存在桌面上,文件名为 supplier_data.csv。
要确认 supplier_data.csv 确实是纯文本文件。
(1) 将所有打开的窗口最小化,在桌面上找到 supplier_data.csv。
(2) 在文件上点击鼠标右键。
(3) 选择“Open with”,然后选择一个文本编辑器,如 Notepad、Notepad++ 或 Sublime Text。
当你在文本编辑器中打开这个文件时,它看上去应该如图 2-2 所示。
图 2-2:Notepad 中的 supplier_data.csv 文件
正如你所看到的,这个文件是一个简单的纯文本文件。每行包含 5 个由逗号分隔的值。对这种文件的另一种理解是由逗号划定了 Excel 电子表格中的 5 列。现在你可以关闭这个文件了。
2.1 基础Python与pandas
前言中曾提到过,本章的每一小节都提供两种版本的代码来完成具体的数据处理任务。每小节中的第一种代码版本展示了如何使用基础 Python 来完成任务。第二种版本展示了如何使用 pandas 来完成任务。你会看到,使用 pandas 完成任务相对来说更容易,需要的代码更少。所以,如果你已经理解了 pandas 简化了的编程概念和操作,只是要简单完成任务的话,pandas 版的代码就非常有用。但是,每个小节都会先介绍基础 Python 版本的代码,以使你学会如何使用通用的编程概念和操作来完成任务。通过介绍两种代码版本,希望可以给你如下选择:一是使用 pandas 快速完成任务;二是学习通用的编程技能,并在提高编码能力的基础上获得解决问题的能力。对于 pandas 版本的代码,本章的解释不会像基础 Python 版那样详细,你可以将这里的示例代码当作使用 pandas 完成任务的指导手册来使用。当你完成了本书中的练习之后,如果想成为 pandas 专家,建议你继续学习 Wes McKinney 的著作《利用 Python 进行数据分析》。
2.1.1 读写CSV文件(第1部分)
基础Python,不使用
csv
模块现在开始学习如何使用基础 Python 代码来读写和处理 CSV 文件(不使用内置的
csv
模块)。先看看下面的示例代码,然后当你使用csv
模块时,就会知道代码在幕后都做了些什么。要处理 CSV 文件,先新建一个 Python 脚本,名为 1csv_read_with_simple_parsing_and_write.py。
在 Spyder 或一个文本编辑器中输入下列代码:
1 #!/usr/bin/env python3 2 import sys 3 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 7 with open(input_file, 'r', newline='') as filereader: 8 with open(output_file, 'w', newline='') as filewriter: 9 header = filereader.readline() 10 header = header.strip() 11 header_list = header.split(',') 12 print(header_list) 13 filewriter.write(','.join(map(str,header_list))+'\n') 14 for row in filereader: 15 row = row.strip() 16 row_list = row.split(',') 17 print(row_list) 18 filewriter.write(','.join(map(str,row_list))+'\n')
在桌面上,将程序保存为 1csv_read_with_simple_parsing_and_write.py。
图 2-3、图 2-4 和图 2-5 分别展示了使用 Anaconda Spyder、Notepad++(Windows)和 TextWrangler(macOS)编写脚本的界面。
图 2-3:Anaconda Spyder 中的 Python 脚本 1csv_read_with_simple_parsing_and_write.py
图 2-4:Notepad++(Windows)中的 Python 脚本 1csv_read_with_simple_parsing_and_write.py
图 2-5:TextWrangler(macOS)中的 Python 脚本 1csv_read_with_simple_parsing_and_write.py
在运行脚本并查看输出之前,先研究一下脚本中的代码想做些什么。这里将按照顺序依次讨论每个代码块(下面提到的行编号指的是屏幕截图中的行编号)。
#!/usr/bin/env python3 import sys
正如第 1 章中讨论过的,第 1 行是注释行,可以使脚本在不同的操作系统之间具有可移植性。第 3 行代码导入 Python 内置的
sys
模块,可以使你在命令行窗口中向脚本发送附加的输入。input_file = sys.argv[1] output_file = sys.argv[2]
第 5 和 6 行代码使用
sys
模块的argv
参数,它是一个传递给 Python 脚本的命令行参数列表,也就是当你运行脚本时在命令行中输入的内容。下面给出了一个在 Windows 系统中使用命令行参数读取 CSV 格式的输入文件和写入 CSV 格式的输出文件的例子:python script_name.py "C:\path\to\input_file.csv" "C:\path\to\output_file.csv"
第一个词
python
告诉计算机使用 Python 程序来处理其余的命令行参数。Python 收集其余的参数,放入argv
这个特殊的列表中。列表中的第一个元素argv[0]
用作脚本名称,所以argv[0]
表示 script_name.py。下一个命令行参数是"C:\path\to\input_file.csv"
,即 CSV 输入文件的路径和文件名。Python 将这个参数保存在argv[1]
中,所以脚本第 5 行代码将这个值赋给变量input_file
。最后一个命令行参数是"C:\path\to\output_file.csv"
,即 CSV 输出文件的路径和文件名。Python 将这个参数保存在argv[2]
中,第 6 行代码把这个值赋给了变量output_file
。with open(input_file, 'r', newline='') as filereader: with open(output_file, 'w', newline='') as filewriter:
第 8 行代码是一个
with
语句,将input_file
打开为一个文件对象filereader
。'r'
表示只读模式,说明打开input_file
是为了读取数据。第 9 行代码是另一个with
语句,将output_file
打开为一个文件对象filewriter
。'w'
表示可写模式,说明打开output_file
是为了写入数据。正如 1.5.3 节中介绍的那样,with
语句非常有用,因为它可以在语句结束时自动关闭文件对象。header = filereader.readline() header = header.strip() header_list = header.split(',')
第 10 行代码使用文件对象的
readline
方法读取输入文件中的第一行数据,在本例中,第一行是标题行,读入后将其作为字符串并赋给名为header
的变量。第 11 行代码使用string
模块中的strip
函数去掉header
中字符串两端的空格、制表符和换行符,并将处理过的字符串重新赋给header
。第 12 行代码使用string
模块的split
函数将字符串用逗号拆分成列表,列表中的每个值都是一个列标题,最后将列表赋给变量header_list
。print(header_list) filewriter.write(','.join(map(str,header_list))+'\n')
第 13 行代码是一个
print
语句,将header_list
中的值(也就是列标题)打印到屏幕上。第 14 行代码使用
filewriter
对象的write
方法将header_list
中的每个值写入输出文件。因为这行代码比较复杂,所以需要仔细说明一下。map
函数将str
函数应用于header_list
中的每个元素,确保每个元素都是字符串。然后,join
函数在header_list
中的每个值之间插入一个逗号,将这个列表转换为一个字符串。在此之后,在这个字符串最后添加一个换行符。最后,filewriter
对象将这个字符串写入输出文件,作为输出文件的第一行。for row in filereader: row = row.strip() row_list = row.split(',') print(row_list) filewriter.write(','.join(map(str,row_list))+'\n')
第 15 行代码创建了一个
for
循环,在输入文件剩余的各行中迭代。第 16 行代码使用strip
函数除去每行字符串两端的空格、制表符和换行符,然后将处理过的字符串重新赋给变量row
。第 17 行使代码用split
函数用逗号将字符串拆分成一个列表,列表中的每个值都是这行中某一列的值,然后,将列表赋给变量row_list
。第 18 行代码将row_list
中的值打印到屏幕上。第 19 行代码将这些值写入输出文件。脚本对输入文件中的每一行数据都执行第 16~19 行代码,因为这 4 行代码在第 15 行代码中的
for
循环下面是缩进的。你可以在命令行窗口或终端窗口中通过运行脚本做一下测试。如下所示。
命令行窗口(Windows)
(1) 打开一个命令行窗口。
(2) 切换到桌面(你存放 Python 脚本的地方)。
要完成这个操作,输入以下命令,然后按回车键:
cd "C:\Users\[Your Name]\Desktop"
(3) 运行 Python 脚本。
要完成这个操作,输入以下命令,然后按回车键:
python 1csv_simple_parsing_and_write.py supplier_data.csv\ output_files\1output.csv
终端窗口(macOS)
(1) 打开一个终端窗口。
(2) 切换到桌面(你存放 Python 脚本的地方)。
要完成这个操作,输入以下命令,然后按回车键:
cd /Users/[Your Name]/Desktop
(3) 为 Python 脚本添加可执行权限。
要完成这个操作,输入以下命令,然后按回车键:
chmod +x 1csv_simple_parsing_and_write.py
(4) 运行 Python 脚本。
要完成这个操作,输入以下命令,然后按回车键:
./1csv_simple_parsing_and_write.py supplier_data.csv\ output_files/1output.csv
如图 2-6 所示,你会看到输出被打印到命令行窗口或终端窗口中。
图 2-6:运行 Python 脚本 1csv_read_with_simple_parsing_and_write.py 的输出结果
输入文件中的所有行都被打印到了屏幕上,也被写入了输出文件。在多数情况下,你不需要将输入文件中的所有数据重新写到输出文件中,因为输入文件中就有所有的数据。但是这个例子仍然是非常有用的,因为你可以参考例子中的代码,将
filewriter.write
语句嵌入到带有判断条件的业务逻辑中,确保你只将需要的某些行写入输出文件。pandas
要使用 pandas 处理 CSV 文件,在文本编辑器中输入下列代码,并将文件保存为 pandas_parsing_and_write.py(这个脚本读取 CSV 文件,在屏幕上打印文件内容,并将内容写入一个输出文件):
#!/usr/bin/env python3 import sys import pandas as pd input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) print(data_frame) data_frame.to_csv(output_file, index=False)
要运行这个脚本,在命令行中输入以下命令,命令在不同的操作系统中会有些差别。
Windows 操作系统
python pandas_parsing_and_write.py supplier_data.csv\ output_files\pandas_output.csv
macOS 操作系统
chmod +x pandas_parsing_and_write.py ./pandas_parsing_and_write .py supplier_data.csv\ output_files/pandas_output.csv
你会注意到在 pandas 版的脚本中,创建了一个变量
data_frame
。同列表、字典与元组相似,数据框也是存储数据的一种方式。数据框中保留了“表格”这种数据组织方式,不需要使用列表套列表的方式来分析数据。数据框包含在 pandas 包中,如果你不在脚本中导入pandas
,就不能使用数据框。将变量命名为data_frame
,就像使用变量名list
一样,在学习阶段,这样做是可以的,但是以后,你应该使用更有描述性的变量名。脏数据
现实世界中,数据通常是“脏”的。有些值会因为某些原因而缺失,手工输入或传感器出错都可以造成数据错误。某些情况下,人们会故意记下错误的数据,因为只能这样做。我曾经见过在餐厅收据中,将乐啤露记为“可乐(加奶酪)”,因为结账系统中没有“乐啤露”这个选项,所以使用系统的店员就加入了这个订单选项,并告知了订餐员和打饮料的服务员。但是这样一来,负责跟踪库存和订货的管理人员就有一大堆奇怪的数据需要核实了。
在电子表格数据中,你也会遇到这样的问题,并想出解决的办法。在练习各章中的示例代码时,也要注意这种情况。请记住每个人都会遇到“脏”数据的问题,这是数据分析工作中最令人头疼也是最令人兴奋的部分,通常也是工作量最大的部分,这是必须要做的工作!
2.1.2 基本字符串分析是如何失败的
基本的 CSV 分析失败的一个原因是列中包含额外的逗号。打开 supplier_data.csv,将 Cost 列中的最后两个成本数量分别改为 $6,015.00 和 $1,006,015.00。做完这两个修改之后,输入文件应如图 2-7 所示。
图 2-7:修改后的输入文件(supplier_data.csv)
修改了输入文件之后,要看看你的简单的分析脚本如何失败,需要在修改后的新输入文件上重新运行脚本。保存修改后的文件,然后按向上箭头键,找到之前运行过的命令,或者重新输入以下命令,然后按回车键:
python 1csv_simple_parsing_and_write.py supplier_data.csv\
output_files\1output.csv
你会看到输出被打印到屏幕上,如图 2-8 所示。
图 2-8:在修改后的 supplier_data.csv 上运行脚本
你可以看到,这里的脚本是按照行中的逗号分析每行数据的。此脚本对标题行和前 10 个数据行的处理都是正确的,因为它们没有嵌入到数据中的逗号。但是,脚本错误地拆分了最后两行,因为数据中有逗号。
有许多方法可以改进这个脚本中的代码,处理包含逗号的数值。例如,可以使用正则表达式来搜索带有嵌入逗号的模式,就像 $6,015.00 和 $1,006,015.00,然后删除这些值中的逗号,再使用余下的逗号来拆分行。但是,为了不使脚本复杂化,可以使用 Python 内置的 csv
模块,设计这个模块的目的就是为了方便灵活地处理复杂的 CSV 文件。
2.1.3 读写CSV文件(第2部分)
基础Python,使用csv
模块
使用 Python 内置的 csv
模块处理 CSV 文件的一个优点是,这个模块就是被设计用于正确处理数据值中的嵌入逗号和其他复杂模式的。它可以识别出这些模式并正确地分析数据,所以你不需要仅仅为了正确处理数据而花费时间来设计正则表达式和条件逻辑,可以将节省的时间用来管理数据、执行计算和写入输出。
接下来导入 Python 内置的 csv
模块并用它来处理包含数值 $6,015.00 和 $1,006,015.00 的输入文件。你将学会如何使用 csv
模块,并理解它是如何处理数据中的逗号的。
在文本编辑器中输入以下代码,并将文件保存为 2csv_reader_parsing_and_write.py:
1 #!/usr/bin/env python3
2 import csv
3 import sys
4 input_file = sys.argv[1]
5 output_file = sys.argv[2]
6 with open(input_file, 'r', newline='') as csv_in_file:
7 with open(output_file, 'w', newline='') as csv_out_file:
8 filereader = csv.reader(csv_in_file, delimiter=',')
9 filewriter = csv.writer(csv_out_file, delimiter=',')
10 for row_list in filereader:
11 print(row_list)
12 filewriter.writerow(row_list)
你可以看到,上面大部分代码与前一个脚本中的代码非常相似。所以,这里只讨论那些有明显区别的代码。
第 2 行代码导入 csv
文件,以便可以使用其中的函数来分析输入文件,写入输出文件。
第 8 行代码,就是在第二个 with
语句下面的那行代码,使用 csv
模块中的 reader
函数创建了一个文件读取对象,名为 filereader
,可以使用这个对象来读取输入文件中的行。同样,第 9 行代码使用 csv
模块的 writer
函数创建了一个文件写入对象,名为 filewriter
,可以使用这个对象将数据写入输出文件。这些函数中的第二个参数(就是 delimiter=','
)是默认分隔符,所以如果你的输入文件和输出文件都是用逗号分隔的,就不需要指定这个参数。这里指定了这个分隔符参数,是为了防备你处理的输入文件或要写入的输出文件具有不同的分隔符,例如,分号(;
)或制表符(\t
)。
第 12 行代码使用 filewriter
对象的 writerow
函数来将每行中的列表值写入输出文件。
假设输入文件和 Python 脚本都保存在你的桌面上,你也没有在命令行或终端行窗口中改变目录,在命令行中输入以下命令,然后按回车键运行脚本(如果你使用 Mac,需要对新的脚本先运行 chmod
命令,使它成为可执行的):
python 2csv_reader_parsing_and_write.py supplier_data.csv\
output_files\2output.csv
你可以看到输出被打印到屏幕上,如图 2-9 所示。
图 2-9:运行 Python 脚本得到的输出
输入文件中的所有行都被打印到了屏幕上,同时被写入到输出文件。你可以看到,Python 内置的 csv
模块处理了嵌入数据的逗号问题,正确地将每一行拆分成了 5 个值。
我们知道了如何使用 csv
模块来读取、处理和写入 CSV 文件,下面开始学习如何筛选出特定的行以及如何选择特定的列,以便可以有效地抽取出需要的数据。
2.2 筛选特定的行
有些时候,你并不需要文件中所有的数据。例如,你可能只需要一个包含特定词或数字的行的子集,或者是与某个具体日期关联的行的子集。在这些情况下,可以用 Python 筛选出特定的行来使用。
你应该很熟悉如何在 Excel 中手动筛选行,但是本章的重点在于提高你的能力,使你既能处理因为体积太大以致 Excel 不能打开的 CSV 文件,又能处理多个 CSV 文件。因为要通过手动处理这些文件,时间花费太多了。
下面各小节演示了在输入文件中筛选出特定行的 3 种方法:
行中的值满足某个条件
行中的值属于某个集合
行中的值匹配于某个模式(正则表达式)
你会发现这些小节中的代码在结构上是一致的。接下来会详细解释这种通用结构,使你可以轻松地修改代码来满足自己的业务规则。
在下面的 3 个小节中,请注意以下结构,从而来理解如何从输入文件中筛选出特定的行:
for row in filereader:
***if value in row meets some business rule or set of rules:***
do something
else:
do something else
这段伪代码展示了用来在输入文件中筛选出特定行的通用代码结构。在下面的小节中,会修改封装在 ***
之间的代码,以使脚本能够满足具体业务规则,抽取出你需要的数据。
2.2.1 行中的值满足某个条件
基础Python
有些时候,当行中的值满足一个具体条件时,才需要保留这些行。例如,你可能会希望在数据集中保留那些成本高于某个具体阈值的行,或者希望保留所有购买日期在一个具体日期之前的行。在这种情况下,你可以检验行中的值是否满足具体的条件,然后筛选出满足条件的行。
下面的示例演示了检验行值是否满足两个具体条件的方法,并将满足条件的行的子集写入一个输出文件。在这个示例中,保留供应商名字为
Supplier Z
或成本大于 $600.00 的行,并将结果写入输出文件。要筛选出满足这些条件的行的子集,在文本编辑器中输入以下代码,将文件保存为 3csv_reader_value_meets_condition.py:1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 with open(input_file, 'r', newline='') as csv_in_file: 7 with open(output_file, 'w', newline='') as csv_out_file: 8 filereader = csv.reader(csv_in_file) 9 filewriter = csv.writer(csv_out_file) 10 header = next(filereader) 11 filewriter.writerow(header) 12 for row_list in filereader: 13 supplier = str(row_list[0]).strip() 14 cost = str(row_list[3]).strip('$').replace(',', '') 15 if supplier == 'Supplier Z' or float(cost) > 600.0: 16 filewriter.writerow(row_list)
第 10 行代码使用
csv
模块的next
函数读出输入文件的第一行,赋给名为header
的列表变量。第 11 行代码将标题行写入输出文件。第 13 行代码取出每行数据中的供应商名字,并赋给名为
supplier
的变量。这行代码使用列表索引取出每行数据的第一个值row[0]
,然后使用str
函数将其转换为一个字符串。在此之后,使用strip
函数删除字符串两端的空格、制表符和换行符。最后,将处理好的字符串赋给变量supplier
。第 14 行代码取出每行数据中的成本,并赋给名为
cost
的变量。这行代码使用列表索引取出每行数据的第四个值row[3]
,然后使用str
函数将其转换为一个字符串。在此之后,使用strip
函数从字符串中删除美元符号。接着使用replace
函数从字符串中删除逗号。最后,将处理好的字符串赋给变量cost
。第 15 行代码创建了一个
if
语句,来检验每行中的这两个值是否满足条件。具体来说,这里想筛选出供应商名字为Supplier Z
或者成本大于 $600.00 的那些行。if
和or
之间的第一个条件检验变量supplier
中的值是否为Supplier Z
。or
和冒号之间的第二个条件检验变量cost
中的值在被转换为浮点数之后,是否大于 600.0。第 16 行代码使用
filewriter
的writerow
函数将满足条件的行写入输出文件。要运行这个脚本,输入以下命令,然后按回车键:
python 3csv_reader_value_meets_condition.py supplier_data.csv\ output_files\3output.csv
在屏幕上你不会看到任何输出,但可以打开输出文件 3output.csv 看一下结果。检查一下,确保结果正确,然后可以修改一下代码,设定不同的供应商或成本阈值,试着筛选一下其他数据。
pandas
pandas 提供了一个
loc
函数,可以同时选择特定的行与列。你需要在逗号前面设定行筛选条件,在逗号后面设定列筛选条件。下面的loc
函数中的条件设置为:Supplier Name 列中姓名包含Z
,或者 Cost 列中的值大于 600.0,并且需要所有的列。在文本编辑器中输入以下代码,将文件保存为 pandas_value_meets_condition.py(这个脚本使用 pandas 来分析 CSV 文件,并将满足条件的行写入输出文件)。#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) data_frame['Cost'] = data_frame['Cost'].str.strip('$').astype(float) data_frame_value_meets_condition = data_frame.loc[(data_frame['Supplier Name']\ .str.contains('Z')) | (data_frame['Cost'] > 600.0), :] data_frame_value_meets_condition.to_csv(output_file, index=False)
在命令行中运行脚本,并给出数据源文件和输出文件。
python pandas_value_meets_condition.py supplier_data.csv\ output_files\pandas_output.csv
在屏幕上你不会看到任何输出,但可以打开输出文件 pandas_output.csv 看一下结果。试试修改一下
loc
函数中的参数,选择出另外一些数据。
2.2.2 行中的值属于某个集合
基础Python
有些时候,当行中的值属于某个集合时,才需要保留这些行。例如,你可能会希望在数据集中保留那些供应商名字属于集合
{Supplier X, Supplier Y}
的行(这里的花括号表示集合,不是 Python 中的字典),或者希望保留所有购买日期属于集合{'1/20/14', '1/30/14'}
的行。在这种情况下,你可以检验行中的值是否属于某个集合,然后筛选出具有属于该集合的值的行。下面的示例演示了检验行值是否是集合成员的方法,并将具有集合中的值的行写入到输出文件。在这个示例中,是要保留那些购买日期属于集合
{'1/20/14', '1/30/14'}
的行,并将结果写入输出文件。要筛选出值属于这个集合的行的子集,在文本编辑器中输入以下代码,并将文件保存为 4csv_reader_value_in_set.py:1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 important_dates = ['1/20/14', '1/30/14'] 7 with open(input_file, 'r', newline='') as csv_in_file: 8 with open(output_file, 'w', newline='') as csv_out_file: 9 filereader = csv.reader(csv_in_file) 10 filewriter = csv.writer(csv_out_file) 11 header = next(filereader) 12 filewriter.writerow(header) 13 for row_list in filereader: 14 a_date = row_list[4] 15 if a_date in important_dates: 16 filewriter.writerow(row_list)
第 6 行代码创建了一个名为
important_dates
的列表变量,其中包含两个特定日期,这个变量就是你的集合。创建包含特定值的变量,然后在代码中引用变量,这种编写代码的方式非常有用。使用了这种方式,如果变量值发生了变化,你只需在一个地方修改代码(就是定义变量的地方),变量值的变化就会反映到代码中所有引用该变量的地方。第 14 行代码取出每一行的购买日期,并将其赋给变量
a_date
。从行列表的索引值row[4]
可知,购买日期在第 5 列。第 15 行代码创建了一个
if
语句来检验a_date
变量中的购买日期是否属于important_dates
这个集合。如果变量值在集合中,下一行代码就将这一行写入输出文件。在命令行中运行下面脚本:
python 4csv_reader_value_in_set.py supplier_data.csv output_files/4output.csv
你可以打开输出文件 4output.csv 来检查结果。
pandas
当行中的值属于某个集合时,如何使用 pandas 筛选出这些行呢?在文本编辑器中输入以下代码,然后将文件保存为 pandas_value_in_set.py(这个脚本分析 CSV 文件,并将值属于某个集合的行写入输出文件):
#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) important_dates = ['1/20/14', '1/30/14'] data_frame_value_in_set = data_frame.loc[data_frame['Purchase Date'].\ isin(important_dates), :] data_frame_value_in_set.to_csv(output_file, index=False)
这里最重要的新命令就是简洁的
isin
。和以前一样,在命令行中运行脚本,并给出源数据文件名和输出文件名:
python pandas_value_in_set.py supplier_data.csv output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 来检查结果。
2.2.3 行中的值匹配于某个模式/正则表达式
基础Python
有些时候,当行中的值匹配了或包含了一个特定模式(也就是正则表达式)时,才需要保留这些行。例如,你可能会希望在数据集中保留所有发票编号开始于“001-”的行,或者希望保留所有供应商名字中包含“Y”的行。在这种情况下,你可以检验行中的值是否匹配或包含某种模式,然后筛选出匹配了或包含了该模式的行。
下面的示例演示了如何检验某个值是否匹配特定的模式,并将具有这种值的行写入输出文件。在这个示例中,保留发票编号由“001-”开头的行,并将结果写入一个输出文件。要筛选出某个值匹配了这个模式的行,在文本编辑器中输入下列代码,然后将文件保存为 5csv_reader_value_matches_pattern.py:
1 #!/usr/bin/env python3 2 import csv 3 import re 4 import sys 5 input_file = sys.argv[1] 6 output_file = sys.argv[2] 7 pattern = re.compile(r'(?P<my_pattern_group>^001-.*)', re.I) 8 with open(input_file, 'r', newline='') as csv_in_file: 9 with open(output_file, 'w', newline='') as csv_out_file: 10 filereader = csv.reader(csv_in_file) 11 filewriter = csv.writer(csv_out_file) 12 header = next(filereader) 13 filewriter.writerow(header) 14 for row_list in filereader: 15 invoice_number = row_list[1] 16 if pattern.search(invoice_number): 17 filewriter.writerow(row_list)
第 3 行代码导入正则表达式(
re
)模块,这样就可以使用re
模块中的函数了。第 7 行代码使用
re
模块的compile
函数创建一个名为pattern
的正则表达式变量。如果你学习了第 1 章,那么应该很熟悉这个函数。r
表示将单引号之间的模式当作原始字符串来处理。元字符
?P<my_pattern_group>
捕获了名为<my_pattern_group>
的组中匹配了的子字符串,以便在需要时将它们打印到屏幕或写入文件。这里要搜索的实际模式是
^001-.*
。插入符号(^
)是一个特殊符号,表示只在字符串开头搜索模式。所以,字符串需要以“001-”开头。句点.
可以匹配任何字符,除了换行符。所以除换行符之外的任何字符都可以跟在“001-”后面。最后,*
表示重复前面的字符 0 次或更多次。.*
组合在一起用来表示除换行符之外的任意字符可以在“001-”后面出现任意次。更通俗的说法是:字符串在“-”后面可以包含任意值,只要字符串开始于“001-”,就会匹配正则表达式。最后,参数
re.I
告诉正则表达式进行大小写敏感的匹配。此参数在这个示例中不是太重要,因为模式是数值型的,但是它说明了在模式中包含字符并且需要进行大小写敏感的匹配时,应该如何设置参数。第 15 行代码使用列表索引从行中取出发票编号,并赋给变量
invoice_number
。在下一行中,将在这个变量中寻找模式。第 16 行代码使用
re
模块的search
函数在invoice_number
的值中寻找模式。如果模式出现在invoice_number
值中,第 17 行代码就将这行写入输出文件。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 5csv_reader_value_matches_pattern.py supplier_data.csv\ output_files\5output.csv
你可以打开输出文件 5output.csv 来查看结果。
pandas
要使用 pandas 筛选出匹配于某个模式的行,在文本编辑器中输入下列代码,然后将文件保存为 pandas_value_matches_pattern.py(这个脚本读取 CSV 文件,将匹配于某个模式的行打印在屏幕上,并将同样的行写入输出文件):
#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) data_frame_value_matches_pattern = data_frame.loc[data_frame['Invoice Number'].\ str.startswith("001-"), :] data_frame_value_matches_pattern.to_csv(output_file, index=False)
使用 pandas 时,可以使用
startwith
函数来搜索数据,不用再使用笨重冗长的正则表达式了。要运行这个脚本,在命令行中输入以下命令,然后按回车键:python pandas_value_matches_pattern.py supplier_data.csv\ output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.3 选取特定的列
有些时候,你并不需要文件中所有的列。在本节示例中,可以使用 Python 选取出你需要的列。
有两种通用方法可以在 CSV 文件中选取特定的列。下面各小节演示了这两种方法:
使用列索引值
使用列标题
2.3.1 列索引值
基础Python
在 CSV 文件中选取特定列的一种方法是使用你想保留的列的索引值。当你想保留的列的索引值非常容易识别,或者在处理多个输入文件时,各个输入文件中列的位置一致(也就是不会发生改变)的时候,这种方法非常有效。例如,如果你只需要保留数据的第一列和最后一列,那么你可以使用
row[0]
和row[-1]
来将每行的第一个值和最后一个值写入文件。在这个示例中,你只想保留供应商姓名和成本这两列。要使用索引值选取这两列,在文本编辑器中输入下列代码,然后将文件保存为 6csv_reader_column_by_index.py:
1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 my_columns = [0, 3] 7 with open(input_file, 'r', newline='') as csv_in_file: 8 with open(output_file, 'w', newline='') as csv_out_file: 9 filereader = csv.reader(csv_in_file) 10 filewriter = csv.writer(csv_out_file) 11 for row_list in filereader: 12 row_list_output = [ ] 13 for index_value in my_columns: 14 row_list_output.append(row_list[index_value]) 15 filewriter.writerow(row_list_output)
第 6 行代码创建了一个列表变量
my_columns
,其中包含了你想保留的两列的索引值。在这个示例中,这两个索引值对应着供应商姓名和成本列。再说一次,应该创建一个包含索引值的变量,然后在代码中引用这个变量。这样,如果索引值需要改变的话,你只需要在一个地方(就是定义my_columns
的地方)修改即可,修改会反映到代码中所有引用my_columns
的地方。第 12~15 行代码是
for
循环下面缩进的部分,所以对于输入文件中的每一行都要执行这些代码。第 12 行代码创建了一个空列表变量row_list_output
。这个变量保存你在每行中要保留的值。第 13 行代码是一个for
循环语句,在my_cloumns
中的各个索引值之间进行迭代。第 14 行代码通过列表的append
函数使用每行中my_columns
索引位置的值为row_list_output
填充元素。这3
行代码生成了一个列表,列表中包含了每行中你要写入输出文件的值。创建列表是有用的,因为filewriter
的writerow
方法需要一个字符串序列或数值序列,而列表row_list_out
正是一个字符串序列。第 15 行代码将row_list_output
中的值写入输出文件。脚本会对输入文件中的每一行执行这些代码。为了确切地理解这一系列操作,下面来看看第一次外部
for
循环做了些什么。在本例中,你处理的是输入文件中的第一行(也就是标题行)。第 12 行代码创建了空列表变量row_list_output
。第 13 行代码是一个for
循环,在my_columns
的值之间迭代。第一次循环时,
index_value
等于 0,所以第 14 行代码中的append
函数将row[0]
(就是供应商姓名字符串)加入row_list_output
。此后,代码回到第 13 行中的for
循环,这一次index_value
等于 3。因为index_value
等于 3,所以第 14 行代码中的append
函数将row[3]
(也就是成本字符串)加入row_list_output
。my_columns
中没有更多的值了,所以第 13 行中的for
循环结束,代码前进到第 15 行。第 15 行代码将row_list_output
中的列表值写入输出文件。然后,代码回到第 11 行中的外部for
循环,开始处理输入文件中的下一行。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 6csv_reader_column_by_index.py supplier_data.csv output_files\6output.csv
你可以打开输出文件 6output.csv 查看一下结果。
pandas
要使用 pandas 根据索引值选取列,在文本编辑器中输入下列代码,然后将文件保存为 pandas_column_by_index.py(这个脚本读取 CSV 文件,将索引值为 0 和 3 的列打印到屏幕,并将同样的行写入输出文件):
#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) data_frame_column_by_index = data_frame.iloc[:, [0, 3]] data_frame_column_by_index.to_csv(output_file, index=False)
这里使用了
iloc
函数来根据索引位置选取列。在命令行中运行以下脚本:
python pandas_column_by_index.py supplier_data.csv\ output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.3.2 列标题
基础Python
在 CSV 文件中选取特定列的第二种方法是使用列标题,而不是索引位置。当你想保留的列的标题非常容易识别,或者在处理多个输入文件时,各个输入文件中列的位置会发生改变,但标题不变的时候,这种方法非常有效。
举例来说,假设你只需要保留发票号码列和购买日期列。要使用列标题选取这两列,在文本编辑器中输入下列代码,然后将文件保存为 7csv_reader_column_by_name.py:
1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 my_columns = ['Invoice Number', 'Purchase Date'] 7 my_columns_index = [] 8 with open(input_file, 'r', newline='') as csv_in_file: 9 with open(output_file, 'w', newline='') as csv_out_file: 10 filereader = csv.reader(csv_in_file) 11 filewriter = csv.writer(csv_out_file) 12 header = next(filereader, None) 13 for index_value in range(len(header)): 14 if header[index_value] in my_columns: 15 my_columns_index.append(index_value) 16 filewriter.writerow(my_columns) 17 for row_list in filereader: 18 row_list_output = [ ] 19 for index_value in my_columns_index: 20 row_list_output.append(row_list[index_value]) 21 filewriter.writerow(row_list_output)
这个示例中的代码比上一个示例要稍微长一点,但是所有代码看起来都很熟悉。此示例中有更多代码的唯一原因就是,你需要先单独处理一下标题行,识别出相应标题行对应的索引值。然后你可以使用索引值保留每行中的值,这些值和要保留的列标题具有同样的索引值。
第 6 行代码创建了一个列表变量
my_columns
,其中包含了两个字符串,即要保留的两列的名字。第 7 行代码创建了一个空列表变量my_columns_index
,要使用两个保留列的索引值来填充它。第 12 行代码在
filereader
对象上使用next
函数从输入文件中读出第一行,并保存在列表变量header
中。第 13 行代码初始化在列标题的索引值中迭代的for
循环。第 14 行代码使用
if
语句和列表索引来检验每个列标题是否在my_columns
中。例如,第一次for
循环时,index_value
等于 0,所以if
语句检验header[0]
(也就是第一个列标题供应商姓名)是否在my_columns
中。因为供应商姓名不在my_columns
中,所以第 15 行代码不会对这个值执行。代码返回第 13 行中的
for
循环,这一次index_value
等于 1。然后,第 14 行代码中的if
语句检验header[1]
(也就是第二个列标题发票号码)是否在my_columns
中。因为发票号码在my_columns
中,所以执行第 15 行代码,将这列的索引值加入到my_columns_index
列表中。然后继续
for
循环,最后将购买日期列的索引值加入my_columns_index
。一旦for
循环结束,第 16 行代码就将my_columns
中的两个字符串写入输出文件。第 18~21 行代码处理输入文件中余下的数据行。第 18 行代码创建一个空列表
row_list_output
来保存你要在每一行中保留的值。第 19 行代码中的for
循环在my_columns_index
中的索引值之间迭代,第 20 行代码将数据行中具有这些索引值的值加入row_list_output
。最后,第 21 行代码将row_list_output
中的值写入输出文件。在命令行中运行以下脚本:
python 7csv_reader_column_by_name.py supplier_data.csv output_files\7output.csv
你可以打开输出文件 7output.csv 查看一下结果。
pandas
要使用 pandas 根据列标题选取列,在文本编辑器中输入下列代码,然后将文件保存为 pandas_column_by_name.py(这个脚本读取 CSV 文件,将发票号码列与购买日期列打印到屏幕,并将同样的列写入输出文件):
#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file) data_frame_column_by_name = data_frame.loc[:, ['Invoice Number', 'Purchase Date']] data_frame_column_by_name.to_csv(output_file, index=False)
这里又一次使用
loc
函数来选取列,这次使用的是列标题。运行以下脚本:
python pandas_column_by_name.py supplier_data.csv output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.4 选取连续的行
有些时候,在文件内容中,工作表头部和尾部都是你不想处理的。例如,文件头部可能是标题和作者信息,文件尾部也可能会列出来源、假设、附加说明和注意事项。在很多情况下,你不需要处理这些内容。
为了演示如何在 CSV 文件中选取连续的行,需要对输入文件做如下修改。
(1) 在电子表格软件中打开 supplier_data.csv。
(2) 在文件头部插入 3 行,就在列标题那行的上面。
在 A1:A3 单元格中随便写一些文字,比如“I don't care about this line”。
(3) 在文件尾部,也就是最后一行数据下面插入 3 行。
在最后一行数据下面 A 列的 3 个单元格中随便写一些文字,比如“I don't want this line either”。
(4) 将文件保存为 supplier_data_unnecessary_header_footer.csv。这个文件应该如图 2-10 所示。
图 2-10:在你需要的行上方和下方具有无关数据的 CSV 文件
现在输入文件中包含了你不需要的头部和尾部信息,修改一下 Python 脚本,使它不读取这些行。
基础Python
要使用基础 Python 选取特定行,这里使用
row_counter
变量来跟踪行编号,以便可以识别和选取想保留的行。从前面的示例中,你已经知道了要保留 13 行数据。在下面的if
代码块中,你可以看到你要写入输出文件中的行就是行索引大于等于 3 并小于等于 15 的行。要使用基础 Python 选取这些行,在文本编辑器中输入下列代码,然后将文件保存为 11csv_reader_select_contiguous_rows.py:
1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 row_counter = 0 7 with open(input_file, 'r', newline='') as csv_in_file: 8 with open(output_file, 'w', newline='') as csv_out_file: 9 filereader = csv.reader(csv_in_file) 10 filewriter = csv.writer(csv_out_file) 11 for row in filereader: 12 if row_counter >= 3 and row_counter <= 15: 13 filewriter.writerow([value.strip() for value in row]) 14 row_counter += 1 15
这里使用
row_counter
变量和一个if
语句来保留需要的行,跳过那些不需要的头部和尾部内容。对于输入文件的前 3 行,因为row_counter
小于 3,所以不执行if
代码块,并将row_counter
的值增加 1。对于输入文件的最后 3 行,
row_counter
大于 15,所以也不执行if
代码块,并将row_counter
的值增加 1。你要保留的行在无用的头部和尾部之间。对于这些行,
row_counter
在 3 和 15 之间。if
代码块处理这些行并将它们写入输出文件。在列表生成式中使用string
模块的strip
函数除去每行两端的空格、制表符和换行符。如果想看看
row_counter
变量的值和每行的内容,可以在现有的writerow
语句上面加上一个print
语句,比如print(row_counter, [value.strip() for value in row])
。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 11csv_reader_select_contiguous_rows.py supplier_data_unnecessary_header_\ footer.csv output_files\11output.csv
你可以打开输出文件 11output.csv 查看一下结果。
pandas
pandas 提供了
drop
函数根据行索引或列标题来丢弃行或列。在下面的脚本中,drop
函数从输入文件中丢弃前 3 行和最后 3 行(也就是行索引为 0,1,2 和 16,17,18 的那些行)。pandas 还提供了功能强大的iloc
函数,你可以使用这个函数根据行索引选取一个单独行作为列索引。最后,使用reindex
函数为数据框重新生成索引。使用 pandas 可以保留列标题行和数据行,除去不需要的头部和尾部。在文本编辑器中输入下列代码,并将文件保存为 pandas_select_contiguous_rows.py:
#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] data_frame = pd.read_csv(input_file, header=None) data_frame = data_frame.drop([0,1,2,16,17,18]) data_frame.columns = data_frame.iloc[0] data_frame = data_frame.reindex(data_frame.index.drop(3)) data_frame.to_csv(output_file, index=False)
要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python pandas_select_contiguous_rows.py supplier_data_unnecessary_header_\ footer.csv output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.5 添加标题行
有些时候,电子表格中没有标题行,但你确实希望所有列都有列标题。在这种情况下,可以使用脚本添加列标题。
为了演示如何使用脚本添加列标题,需要对输入文件做一下修改:
(1) 在电子表格程序中打开 supplier_data.csv。
(2) 删除文件中的第一行(即包含列标题的标题行)。
(3) 将文件保存为 supplier_data_no_header_row.csv。如图 2-11 所示。
图 2-11:包含数据行的 CSV 文件,没有标题行
基础Python
要使用基础 Python 添加列标题,在文本编辑器中输入下列代码,然后将文件保存为 12csv_reader_add_header_row.py:
1 #!/usr/bin/env python3 2 import csv 3 import sys 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 with open(input_file, 'r', newline='') as csv_in_file: 7 with open(output_file, 'w', newline='') as csv_out_file: 8 filereader = csv.reader(csv_in_file) 9 filewriter = csv.writer(csv_out_file) 10 header_list = ['Supplier Name', 'Invoice Number',\ 11 'Part Number', 'Cost', 'Purchase Date'] 12 filewriter.writerow(header_list) 13 for row in filereader: 14 filewriter.writerow(row)
第 10 行代码创建了列表变量
header_list
,其中包含了要作为列标题的 5 个字符串。第 12 行代码将这些列表值写入输出文件的第一行。同样,第 14 行代码将所有数据行写入输出文件,放在标题行下面。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 12csv_reader_add_header_row.py supplier_data_no_header_row.csv\ output_files\12output.csv
你可以打开输出文件 12output.csv 查看一下结果。
pandas
pandas 中的
read_csv
函数可以直接指定输入文件不包含标题行,并可以提供一个列标题列表。要给一个没有标题行的数据集添加标题行,在文本编辑器中输入下列代码,然后将文件保存为 pandas_add_header_row.py:#!/usr/bin/env python3 import pandas as pd import sys input_file = sys.argv[1] output_file = sys.argv[2] header_list = ['Supplier Name', 'Invoice Number',\ 'Part Number', 'Cost', 'Purchase Date'] data_frame = pd.read_csv(input_file, header=None, names=header_list) data_frame.to_csv(output_file, index=False)
要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python pandas_add_header_row.py supplier_data_no_header_row.csv\ output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.6 读取多个CSV文件
本章到目前为止,都在演示如何处理单个 CSV 文件。有些时候,你也只需要处理一个文件。在这些情况下,上面的示例可以告诉你如何使用 Python 程序去处理。尽管只是一个文件,这个文件也可能太大,不能手工处理,因此用程序处理文件还可以减少人为犯错的概率,比如复制 / 粘贴错误和输入错误。
但是,在大多数情况下,你需要处理的文件很多,多到使用手工处理效率非常低或者根本不可行。在这种情况下,Python 会给你惊喜,因为它可以让你自动化和规模化地进行数据处理,远远超过手工处理能够达到的限度。这一小节介绍 Python 内置的 glob
模块,并在本章前面示例的基础上,演示如何规模化地处理 CSV 文件。
为了处理多个 CSV 文件,首先需要创建多个 CSV 文件。下面的示例中创建了 3 个 CSV 文件,但是请记住,这里介绍的技术可以扩展为处理计算机允许的任意多的文件,多到几百个,甚至更多!
第一个 CSV 文件
(1) 打开一个电子表格程序。
(2) 加入图 2-12 所示的数据。
(3) 将文件保存为 sales_january_2014.csv。
图 2-12:第一个 CSV 文件:sales_january_2014.csv
第二个 CSV 文件
(1) 打开一个电子表格程序。
(2) 加入图 2-13 所示的数据。
(3) 将文件保存为 sales_february_2014.csv。
图 2-13:第二个 CSV 文件:sales_february_2014.csv
第三个 CSV 文件
(1) 打开一个电子表格程序。
(2) 加入图 2-14 所示的数据。
(3) 将文件保存为 sales_march_2014.csv。
图 2-14:第三个 CSV 文件:sales_march_2014.csv
文件计数与文件中的行列计数
下面先从一些简单的行列计数开始。行列计数虽然相当基础,但却是熟悉新数据集的好方式。尽管有些时候你知道要处理的输入文件中的内容,但在多数情况下,文件是别人发送给你的,你不会立即知道是哪些内容。这时,计算一下要处理的文件数量以及每个文件中行与列的数量,会对你有所帮助。
要处理前一节中创建的 3 个 CSV 文件,在文本编辑器中输入下列代码,然后将文件保存为 8csv_reader_counts_for_multiple_files.py:
1 #!/usr/bin/env python3
2 import csv
3 import glob
4 import os
5 import sys
6 input_path = sys.argv[1]
7 file_counter = 0
8 for input_file in glob.glob(os.path.join(input_path,'sales_*')):
9 row_counter = 1
10 with open(input_file, 'r', newline='') as csv_in_file:
11 filereader = csv.reader(csv_in_file)
12 header = next(filereader, None)
13 for row in filereader:
14 row_counter += 1
15 print('{0!s}: \t{1:d} rows \t{2:d} columns'.format(\
16 os.path.basename(input_file), row_counter, len(header)))
17 file_counter += 1
18 print('Number of files: {0:d}'.format(file_counter))
第 3~4 行代码导入 Python 内置的 glob
和 os
模块,以使我们可以使用它们提供的函数列出和解析你要处理的文件路径名。glob
模块可以定位匹配于某个特定模式的所有路径名。模式中可以包含 Unixshell 风格的通配符,比如 *
。在上面这个具体示例中,要搜索的模式是 'sales_*'
。这个模式表示要搜索所有文件名以 sales_ 开头并且下划线后面可以是任意字符的文件。因为你创建了 3 个输入文件,所以应该知道使用这段代码可以识别出这 3 个文件,它们的文件名都是以 sales_ 开头的,下划线后面是不同的月份。
以后你可能会想找出一个文件夹下面的所有 CSV 文件,而不是以 sales_ 开头的文件。如果这样,那么你可以简单地将脚本中的模式从 'sales_*'
改变为 '*.csv'
。因为 '.csv'
是所有 CSV 文件名末尾的模式,这样做可以有效地找出所有 CSV 文件。
os
模块包含了用于解析路径名的函数。例如,os.path.basename(path)
返回 path
的基本文件名。即,如果 path
是 C:\Users\Clinton\Desktop\my_input_file.csv
,那么 os.path.basename(path)
返回 my_input_file.csv
。
第 8 行代码是将数据处理扩展到多个文件中的关键语句。此行代码创建了一个 for
循环,在一个输入文件集合中迭代,并使用 glob
模块和 os
模块中的函数创建了一个输入文件列表以供处理。这行代码比较复杂,所以需要仔细地分析一下。os
模块中的 os.path.join()
函数将函数圆括号中的两部分连接在一起。input_path
是包含输入文件的文件夹的路径,'sales_*'
代表任何以模式 'sales_'
开头的文件名。
glob
模块中的 glob.glob()
函数将 'sales_*'
中的星号(*
)转换为实际的文件名。在这个示例中,glob.glob()
函数和 os.path.join()
函数创建了一个包含 3 个输入文件的列表:
['C:\Users\Clinton\Desktop\sales_january_2014.csv',
'C:\Users\Clinton\Desktop\sales_february_2014.csv',
'C:\Users\Clinton\Desktop\sales_march_2014.csv']
然后,这行开头的 for
循环语句对于列表中每个输入文件执行下面缩进的各行代码。
第 15 行代码是一个 print
语句,打印出每个输入文件的文件名、文件中的行数、文件中的列数。print
语句中的制表符 \t
不是必需的,但是在各列之间放上一个制表符可以对齐这 3 列。这行代码使用 {}
占位符将 3 个值传入 print
语句。对于第一个值,使用 os.path.basename()
函数从完整路径名中抽取出基本文件名。对于第二个值,使用 row_counter
变量来计算每个输入文件中的总行数。最后,对于第三个值,使用内置的 len
函数计算出列表变量 header
中的值的数量,这个列表变量中包含了每个输入文件的列标题列表。我们使用这个值作为每个输入文件中的列数。最后,在第 15 行代码打印了每个文件的信息之后,第 17 行代码使用 file_counter
变量中的值显示出脚本处理的文件的数量。
要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 8csv_reader_counts_for_multiple_files.py "C:\Users\Clinton\Desktop"
请注意在命令行中,脚本名称后面是一个文件夹路径。在前面的示例中,这个位置都是输入文件名。在这个示例中,你要处理多个文件,所以必须使用包含所有输入文件的文件夹。
你可以看到 3 个输入文件的文件名和每个文件中的行数与列数被打印到屏幕上。在关于 3 个文件的信息行下面,最后的 print
语句显示了之前处理过的输入文件总数。显示信息如图 2-15 所示:
图 2-15:Python 脚本输出:3 个 CSV 文件中的行数与列数
输出结果显示脚本处理了 3 个文件,每个文件都有 7 行和 5 列。
这个示例演示了如何读取多个 CSV 文件并将每个文件的基本信息打印到屏幕上。在你不熟悉要处理的文件时,将要处理文件的基本信息打印出来是非常有用的。知道了输入文件的数量和每个文件中行与列的数量,你就对数据处理的工作量以及文件内容的一致性有了一个大致的概念。
2.7 从多个文件中连接数据
对于包含相似数据的多个文件,你经常希望将其中的数据连接起来,以使所有数据都在一个文件中。以前你完成这种工作的方式可能是:打开每个文件,将每个工作表中的数据复制粘贴到一个单独的工作表中。这种手动处理方式不但浪费时间,还容易出错。而且,在有些情况下,因为需要合并的文件数量和文件大小的原因,手动处理根本不可能完成。
知道了手动连接数据的局限性之后,接下来看一下如何通过 Python 完成这个任务。这里将会使用本节开头创建的 3 个 CSV 文件来演示如何从多个文件中连接数据。
基础Python
要使用基础 Python 将多个输入文件中的数据垂直连接成一个输出文件,在文本编辑器中输入下列代码,然后将文件保存为 9csv_reader_concat_rows_from_multiple_files.py:
1 #!/usr/bin/env python3 2 import csv 3 import glob 4 import os 5 import sys 6 input_path = sys.argv[1] 7 output_file = sys.argv[2] 8 9 first_file = True 10 for input_file in glob.glob(os.path.join(input_path,'sales_*')): 11 print(os.path.basename(input_file)) 12 with open(input_file, 'r', newline='') as csv_in_file: 13 with open(output_file, 'a', newline='') as csv_out_file: 14 filereader = csv.reader(csv_in_file) 15 filewriter = csv.writer(csv_out_file) 16 if first_file: 17 for row in filereader: 18 filewriter.writerow(row) 19 first_file = False 20 else: 21 header = next(filereader, None) 22 for row in filereader: 23 filewriter.writerow(row)
第 13 行代码是一个
with
语句,用来打开输出文件。在前面介绍写入输出文件的示例中,open
函数中的字符串是'w'
,表示以可写的方式打开输出文件。在这个示例中,使用
'a'
代替'w'
以追加的方式打开输出文件,以使每个输入文件中的数据可以追加(也就是添加)到输出文件中。如果使用可写方式,从一个输入文件中输出的数据会覆盖掉前一个输入文件中的数据,最后的输出文件会只包含最后处理的那个输入文件中的数据。从第 16 行代码开始的
if-else
语句根据第 9 行代码中创建的first_file
变量来区分当前文件是第一个输入文件,还是其后的输入文件。在输入文件中做这个区分的目的是将标题行仅写入输出文件一次。if
代码块处理第一个输入文件,将包括标题行的所有行写入输出文件。else
代码块处理所有余下的输入文件,使用next
方法将每个文件中的标题行赋给一个变量(这样就可以在后面的处理过程中跳过标题行),然后将其余数据行写入输出文件。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 9csv_reader_concat_rows_from_multiple_files.py "C:\Users\Clinton\Desktop"\ output_files\9output.csv
你可以看到输入文件的文件名被打印到屏幕上,如图 2-16 所示。
图 2-16:Python 脚本输出:连接成输出文件的文件名
屏幕上的输出展示了处理过的文件的文件名。此外,脚本还将 3 个输入文件中的数据连接成了一个单独的输出文件 9output.csv,位于桌面上的 output_files 文件夹中。图 2-17 显示了文件中的内容。
图 2-17:输出 CSV 文件,连接了来自于多个输入文件的行
图中显示,脚本成功地连接了来自于 3 个输入文件的数据。输出文件中包含一个标题行和 3 个输入文件中所有的数据行。
在解释代码时,前面提到了在第 13 行代码中为什么要使用
'a'
(追加模式),而不是'w'
(可写模式),还提到了为什么要区分第一个输入文件和其后的输入文件。要实际验证一下的话,你可以将'a'
改成'w'
,然后保存脚本,对 3 个输入文件重新执行一下,看看输出文件有什么变化。同样,你也可以删除if-else
语句,将所有输入文件中的所有行都打印出来,看看输出会如何改变。还有一点需要注意,这个示例中的模式
'sales_*'
相对来说是比较特殊的,也就是说在你的桌面上除了 3 个输入文件之外,不太可能还有文件名以 sales_ 开头的文件。其他情况下,你更可能使用一个不那么特殊的模式,比如'*.csv'
,来搜索所有 CSV 文件。在这种情况下,你不应该在包含所有输入文件的文件夹内创建输出文件。不应该这样做的原因就是,你在打开输出文件的同时还在处理输入文件。这样,如果你的模式是'*.csv'
,输出文件也是个 CSV 文件,那么脚本就会像处理输入文件一样试图处理输出文件,这就会导致问题和错误。这种可能性就是最好在另一个文件夹中处理输出文件的原因,就像在这个示例中所做的一样。pandas
pandas 可以直接从多个文件中连接数据。基本过程就是将每个输入文件读取到 pandas 数据框中,将所有数据框追加到一个数据框列表,然后使用
concat
函数将所有数据框连接成一个数据框。concat
函数可以使用axis
参数来设置连接数据框的方式,axis=0
表示从头到尾垂直堆叠,axis=1
表示并排地平行堆叠。要使用 pandas 将多个输入文件中的数据垂直连接成一个输出文件,在文本编辑器中输入下列代码,然后将文件保存为 pandas_concat_rows_from_multiple_files.py:
#!/usr/bin/env python3 import pandas as pd import glob import os import sys input_path = sys.argv[1] output_file = sys.argv[2] all_files = glob.glob(os.path.join(input_path,'sales_*')) all_data_frames = [] for file in all_files: data_frame = pd.read_csv(file, index_col=None) all_data_frames.append(data_frame) data_frame_concat = pd.concat(all_data_frames, axis=0, ignore_index=True) data_frame_concat.to_csv(output_file, index = False)
这段代码垂直堆叠数据框。如果你需要平行连接数据,那么就在
concat
函数中设置axis=1
。除了数据框,pandas 中还有一个数据容器,称为序列。你可以使用同样的语法去连接序列,只是要将连接的对象由数据框改为序列。有时候,除了简单地垂直或平行连接数据,你还需要基于数据集中的关键字列的值来连接数据集。pandas 提供了类似 SQL join 操作的
merge
函数。如果你很熟悉 SQL join,那么就非常容易理解merge
函数的语法:pd.merge(DataFrame1, DataFrame2, on='key', how='inner')
。Python 的另一个内置模块 NumPy 也提供了若干函数来垂直或平行连接数据。通常是将 NumPy 导入为
np
。然后,要垂直连接数据,你可以使用np.concatenate([array1, array2], axis=0)
、np.vstack((array1, array2))
或np.r_[array1, array2]
。同样,要平行连接数据,你可以使用np.concatenate([array1, array2], axis=1)
、np.hstack((array1, array2))
或np.c_[array1, array2]
。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python pandas_concat_rows_from_multiple_files.py "C:\Users\Clinton\Desktop"\ output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
2.8 计算每个文件中值的总和与均值
有些时候,当你有多个输入文件时,需要对每个输入文件计算一些统计量。本节的示例使用之前创建的 3 个 CSV 文件来展示如何计算每个输入文件中某一列的总计和均值。
基础Python
要使用基础 Python 为多个文件计算某列的总计和均值,在文本编辑器中输入下列代码,然后将文件保存为 10csv_reader_sum_average_from_multiple_files:
1 #!/usr/bin/env python3 2 import csv 3 import glob 4 import os 5 import sys 6 input_path = sys.argv[1] 7 output_file = sys.argv[2] 8 output_header_list = ['file_name', 'total_sales', 'average_sales'] 9 csv_out_file = open(output_file, 'a', newline='') 10 filewriter = csv.writer(csv_out_file) 11 filewriter.writerow(output_header_list) 12 for input_file in glob.glob(os.path.join(input_path,'sales_*')): 13 with open(input_file, 'r', newline='') as csv_in_file: 14 filereader = csv.reader(csv_in_file) 15 output_list = [ ] 16 output_list.append(os.path.basename(input_file)) 17 header = next(filereader) 18 total_sales = 0.0 19 number_of_sales = 0.0 20 for row in filereader: 21 sale_amount = row[3] 22 total_sales += float(str(sale_amount).strip('$').replace(',','')) 23 number_of_sales += 1 24 average_sales = '{0:.2f}'.format(total_sales / number_of_sales) 25 output_list.append(total_sales) 26 output_list.append(average_sales) 27 filewriter.writerow(output_list) 28 csv_out_file.close()
第 8 行代码创建了一个输出文件的列标题列表。第 10 行代码创建了
filewriter
对象,第 11 行代码将标题行写入输出文件。第 15 行代码创建了一个空列表,保存要写入输出文件中的每行输出。因为要为每个输入文件计算总计和均值,所以第 16 行代码将输入文件的文件名追加到
output_list
中。第 17 行代码使用
next
函数除去每个输入文件的标题行。第 18 行代码创建了一个变量total_sales
并将其初始化为 0。第 20 行代码是一个for
循环,在每个输入文件的数据行之间迭代。第 21 行代码使用列表索引取出销售额这列中的值,并赋给变量
sale_amount
。第 22 行代码使用str
函数确保sale_amount
中的值是一个字符串,然后使用strip
函数和replace
函数除去值中的美元符号和逗号。此后使用float
函数将这个值转换为浮点数,并将这个值加到total_sales
中的值上。第 23 行代码给number_of_sales
中的值加 1。第 24 行代码用
total_sales
中的值除以number_of_sales
中的值,为输入文件计算出平均销售额,然后将这个数值格式化成具有两位小数的数值,并转换成字符串,赋给变量average_sales
。第 25 行代码将总销售额作为第二个值添加到
output_list
中。列表中的第一个值是输入文件的名字。这个值在第 16 行代码中被添加到列表中。第 26 行代码将平均销售额作为第三个值添加到output_list
中。第 27 行代码将output_list
中的值写入输出文件。脚本对每个输入文件都运行这些代码,所以输出文件中会包含对应于每个输入文件的一列文件名、一列总销售额和一列平均销售额。
要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python 10csv_reader_sum_average_from_multiple_files.py \ "C:\Users\Clinton\Desktop" output_files\10output.csv
你可以打开输出文件 10output.csv 查看一下结果。
pandas
pandas 提供了可以用来计算行和列统计量的摘要统计函数,比如
sum
和mean
。下面的代码演示了如何对于多个文件中的某一列计算这两个统计量(总计和均值),并将每个输入文件的计算结果写入输出文件。要使用 pandas 计算这两个列统计量,在文本编辑器中输入下列代码,并将文件保存为 pandas_sum_average_from_multiple_files.py:
#!/usr/bin/env python3 import pandas as pd import glob import os import sys input_path = sys.argv[1] output_file = sys.argv[2] all_files = glob.glob(os.path.join(input_path,'sales_*')) all_data_frames = [] for input_file in all_files: data_frame = pd.read_csv(input_file, index_col=None) total_cost = pd.DataFrame([float(str(value).strip('$').replace(',','')) \ for value in data_frame.loc[:, 'Sale Amount']]).sum() average_cost = pd.DataFrame([float(str(value).strip('$').replace(',','')) \ for value in data_frame.loc[:, 'Sale Amount']]).mean() data = {'file_name': os.path.basename(input_file), 'total_sales': total_sales, 'average_sales': average_sales} all_data_frames.append(pd.DataFrame(data, \ columns=['file_name', 'total_sales', 'average_sales'])) data_frames_concat = pd.concat(all_data_frames, axis=0, ignore_index=True) data_frames_concat.to_csv(output_file, index = False)
使用列表生成式将销售额这一列中带美元符号的字符串转换为浮点数,然后使用数据框函数将这个对象转换为一个
DataFrame
,以便可以使用这两个函数计算列的总计和均值。因为输出文件中的每行应该包含输入文件名,以及文件中销售额的总计和均值,所以可以将这 3 种数据组合成一个文本框,使用
concat
函数将这些数据框连接成为一个数据框,然后将这个数据框写入输出文件。要运行这个脚本,在命令行中输入以下命令,然后按回车键:
python pandas_ sum_average_from_multiple_files.py "C:\Users\Clinton\Desktop"\ output_files\pandas_output.csv
你可以打开输出文件 pandas_output.csv 查看一下结果。
本章介绍了很多基础知识,包括读取和分析 CSV 文件、在 CSV 文件中浏览行与列、处理多个 CSV 文件和为多个 CSV 文件计算统计量的方法。如果你一直跟随本章内容练习示例代码,应该已经完成了 12 个 Python 脚本。
你练习本章中示例代码的最大收获是,它们是浏览和处理文件的基础模块。掌握了本章中的示例代码后,你就可以继续学习如何处理 Excel 文件了,这就是下一章的主题。
2.9 本章练习
(1) 对根据具体条件、集合和正则表达式来筛选行数据的一个脚本进行修改,将与示例代码中不同的一组数据打印出来并写入输出文件。
(2) 对根据索引值或列标题来筛选列数据的一个脚本进行修改,将与示例代码中不同的一组数据打印出来并写入输出文件。
(3) 在一个文件夹中创建一组新的 CSV 输入文件,创建另外一个输出文件夹,使用处理多个文件的一个脚本来处理这些新的输入文件,并将结果写入输出文件夹。