《python数据分析(第2版)-阿曼多.凡丹戈》读书笔记第5章-数据的检索、加工与存储
第5章数据的检索、加工与存储
现实中,各种形式的数据随处可见。我们不仅可以从网络、电子邮件和FTP中取得数据,也可通过实验研究或者市场调查来获得数据。要想全面总结不同格式数据的获取方法,恐怕要占用大量的篇幅,不是几页就能讲全的。大部分情况下,数据在分析之前或之后我们都需要将其存储起来。关于数据的存储问题,本章也有讨论。第8章将讲解各种数据库(关系数据库和NoSQL数据库)及其API的有关知识。本章探讨的主题如下。
- 利用NumPy和Pandas对CSV文件进行写操作
- 二进制.npy格式和pickle格式
- 利用PyTables储存数据
- Pandas DataFrame与HDF5仓库之间的读写操作
- 用pandas读写Excel
- 使用REST web服务和JSON
- 使用Pandas读写JSON
- 解析RSS和Atom订阅
- 利用Beautiful Soup解析HTML
5.1利用NumPy和pandas对CSV文件进行写操作
前几章我们学过读取CSV文件的内容,其实,对CSV文件进行写操作同样也很简单,只不过使用的函数和方法不同罢了。首先,生成一些数据,将来它们会以CSV格式保存。下面的代码给随机数生成器指定种子并生成一个3×4的NumPy数组。
我们将一个数组元素的值设为NaN。
1 import numpy as np 2 import pandas as pd 3 4 np.random.seed(42) 5 6 a = np.random.randn(3, 4) 7 a[2][2] = np.nan 8 print(a)
上述代码打印输出的数组如下。
[[ 0.49671415 -0.1382643 0.64768854 1.52302986] [-0.23415337 -0.23413696 1.57921282 0.76743473] [-0.46947439 0.54256004 nan -0.46572975]]
NumPy的savetxt()函数是与loadtxt()相对应的一个函数,它能以诸如CSV之类的区隔型文件格式保存数组。下面的代码可用来保存刚创建的那个数组。
1 np.savetxt('np.csv', a, fmt='%.2f', delimiter=',', header=" #1, #2, #3, #4")
上面的函数调用中,我们规定了用以保存数组的文件的名称、数组、可选格式、间隔符(默认为空格符)和一个可选的标题。
Tips:有关格式参数的详细说明,请参考Python的官方文档。
通过编辑器,例如在Windows系统上的Notepad或者cat命令,即cat np.csv,我们可以查看刚才所建的np.csv文件的具体内容,具体如下。
利用随机数组来创建Pandas DataFrame,代码如下。
1 df = pd.DataFrame(a) 2 print(df)
就像你所看到的那样,Pandas会自动替我们给数据取好列名。
0 1 2 3
0 0.496714 -0.138264 0.647689 1.523030
1 -0.234153 -0.234137 1.579213 0.767435
2 -0.469474 0.542560 NaN -0.465730
利用Pandas的to_csv()方法可以为CSV文件生成一个DataFrame,代码如下。
1 df.to_csv('pd.csv', float_format='%.2f', na_rep="NAN!")
对于这个方法,我们需要提供文件名、类似于NumPy的savetxt()函数的格式化参数的可选格式串和一个表示NaN的可选字符串。Pd.csv 文件的内容如下。
下面的代码引自本书代码包中的ch-05.ipynb文件。
1 import numpy as np 2 import pandas as pd 3 4 np.random.seed(42) 5 6 a = np.random.randn(3, 4) 7 a[2][2] = np.nan 8 print(a) 9 np.savetxt('np.csv', a, fmt='%.2f', delimiter=',', header=" #1, #2, #3, #4") 10 df = pd.DataFrame(a) 11 print(df) 12 df.to_csv('pd.csv', float_format='%.2f', na_rep="NAN!")
5.2二进制.npy与pickle格式
大部分情况下,用CSV格式来保存文件是一个不错的主意,因为大部分程序设计语言和应用程序都能处理这种格式,交流起来非常方便。然而这种格式有一个缺陷,就是它的存储效率不是很高,原因是CSV及其他纯文本格式中含有大量空白符。而后来发明的一些文件格式,如zip、bzip和gzip等,压缩率则有了显著提升。
以下代码引自本书代码包中的ch-05.ipynb文件,它对各种格式的存储利用情况进行了比较。
1 import numpy as np 2 import pandas as pd 3 from tempfile import NamedTemporaryFile 4 from os.path import getsize 5 6 np.random.seed(42) 7 a = np.random.randn(365, 4) 8 9 tmpf = NamedTemporaryFile() 10 np.savetxt(tmpf, a, delimiter=',') 11 print("Size CSV file", getsize(tmpf.name)) 12 13 tmpf = NamedTemporaryFile() 14 np.save(tmpf, a) 15 tmpf.seek(0) 16 loaded = np.load(tmpf) 17 print("Shape", loaded.shape) 18 print("Size .npy file", getsize(tmpf.name)) 19 20 df = pd.DataFrame(a) 21 df.to_pickle(tmpf.name) 22 print("Size pickled dataframe", getsize(tmpf.name)) 23 print("DF from pickle\n", pd.read_pickle(tmpf.name))
NumPy为自己提供了一种专用的格式,称为.npy,可以用于存储NumPy数组。在进一步说明这种格式之前,我们先来生成一个365×4的NumPy数组并给各个元素填充上随机值。这个数组可以看成是一年中4个随机变量的每日观测值的模拟,如一个气象站内传感器测到的温度、湿度、降雨量和气压读数。这里,我们将使用Python标准的NamedTemporaryFile来存储数据,这些临时文件随后会自动删除。
下面将该数组存入一个CSV文件并检查其大小,代码如下。
1 tmpf = NamedTemporaryFile() 2 np.savetxt(tmpf, a, delimiter=',') 3 print("Size CSV file", getsize(tmpf.name))
这个CSV文件的大小如下。
Size CSV file 32616
首先以NumPy.npy格式来保存该数组,随后载入内存并检查数组的形状以及该.npy文件的大小,具体代码如下。
1 tmpf = NamedTemporaryFile() 2 np.save(tmpf, a) 3 tmpf.seek(0) 4 loaded = np.load(tmpf) 5 print("Shape", loaded.shape) 6 print("Size .npy file", getsize(tmpf.name))
为了模拟该临时文件的关闭与重新打开过程,我们在上述代码中调用了seek()函数。数组的形状以及文件大小如下。
Shape (365, 4)
Size .npy file 11808
不出所料,.npy文件的大小只有CSV文件的1/3左右。实际上,利用Python可以存储任意复杂的数据结构,也可以用序列化格式来存储Pandas的DataFrame或者Series数据结构。
Tips:在Python中,pickle是将Python对象存储到磁盘或其他介质时采用的一种格式,这个格式化的过程叫作序列化(pickling)。之后我们可以从存储器中重建该Python对象,这个逆过程称为反序列化(unpickling),详情请参考Python官方文档。序列化技术经过多年的发展,已经出现了多种pickle协议。当然,并非所有的Python对象都能够序列化;不过,借助诸如dill之类的模块,可以将更多种类的Python对象序列化。如有可能,最好使用cPickle模块(标准Python发行版中都含有此模块),因为它是由C语言编写的,所以运行起来会更快一些。
首先用前面生成的NumPy数组创建一个DataFrame,接着用to_pickle()方法将其写入一个pickle对象中,然后用read_pickle()函数从这个pickle对象中检索该DataFrame。
1 df = pd.DataFrame(a) 2 df.to_pickle(tmpf.name) 3 print("Size pickled dataframe", getsize(tmpf.name)) 4 print("DF from pickle\n", pd.read_pickle(tmpf.name))
该DataFrame经过序列化后,尺寸略大于.npy文件,这一点我们通过下列代码进行确认。
Size pickled dataframe 12244 DF from pickle 0 1 2 3 0 0.496714 -0.138264 0.647689 1.523030 1 [TRUNCATED] 59 -2.025143 0.186454 -0.661786 0.852433 … … … … [365 rows x 4 columns]
注意:如果没有temp权限的读写权限,会报错:PermissionError: [Errno 13] Permission denied: 'd:\\Temp\\tmpbjsbk4qw',如果要避免这个错误,可以不使用temp路径,参见下例。
1 import uuid 2 tmpf = str(uuid.uuid4()) # 生成一个随机文件名 3 print(tmpf)
输出:1baacaf1-d943-4a78-88eb-a859a7f17944
5.3使用PyTables存储数据
层次数据格式(Hierarchical Data Format,HDF)是一种存储大型数值数据的技术规范,起源于超级计算社区,目前已经成为一种开放的标准。本书将使用HDF的最新版本,也就是HDF5,该版本仅仅通过组(group)和数据集(dataset)这两种基本结构来组织数据。数据集可以是同类型的多维数组,而组可以用来存放其他组或者数据集。也许读者已经发现了,这里的“组”跟层次式文件系统中的“目录”非常像。
HDF5最常见的两个主要Python程序库如下。
- H5y
- PyTables
本例中使用的是PyTables。不过,这个程序库需要用到的一些依赖项如下。
- NumPy:第1章中已经安装好了NumPy。
- Numexpr:该程序包在计算包含多种运算的数组表达式时,其速度要比NumPy快许多倍。
- HDF5。
Tips:如果使用HDF5的并行版本,则需要安装MPI。HDF5可以从网站下载,然后运行下列命令进行安装。这个安装过程可能需要几分钟的时间。具体命令如下。
1 $ gunzip < hdf5-X.Y.Z.tar.gz | tar xf – 2 $ cd hdf5-X.Y.Z 3 $ make 4 $ make install
一般情况下,我们使用的程序包管理器都会提供HDF5,不过我们最好还是选择目前最新的稳定版本(stable version)。截至编写本书期间为止,最新的版本号是1.8.12。
据Numexpr自称,它在某些方面的运算速度要比NumPy快得多,因为它支持多线程,而且自己的虚拟机是C语言实现的。此外,PyPi也提供了Numexpr和PyTables,所以我们可以利用pip命令来安装它们,具体如下。
pip install Numexpr
pip install numexpr tables
pip install h5py
此外,我们需要生成一些随机数并用它们来给一个NumPy数组赋值。下面我们创建一个HDF5文件并把这个NumPy数组挂载到根节点上,代码如下。
1 tmpf = "pytable_demo.h5" 2 h5file = tables.open_file(tmpf, mode='w') 3 root = h5file.root 4 h5file.create_array(root, "array", a) 5 h5file.close()
读取这个HDF5文件并显示文件大小,代码如下。
1 h5file = tables.open_file(tmpf, "r") 2 print(getsize(tmpf))
我们看到,文件大小为13 824。在读取一个HDF5文件并获得该文件的句柄后,就可以通过常规方式来遍历文件,从而找到我们所需的数据。因为这里只有一个数据集,所以遍历起来非常简单。下面的代码将通过iterNodes()和read()方法取回NumPy数组。
1 for node in h5file.root: 2 b = node.read() 3 print(type(b), b.shape) 4 5 h5file.close()
该数据集的形状和类型果然不出我们所料,显示如下。
<class 'numpy.ndarray'> (365, 4)
以下代码取自本书代码包中的ch-05.ipynb文件。
1 import numpy as np 2 import tables 3 from os.path import getsize 4 import uuid 5 6 np.random.seed(42) 7 a = np.random.randn(365, 4) 8 9 tmpf = "pytable_demo.h5" 10 h5file = tables.open_file(tmpf, mode='w') 11 root = h5file.root 12 h5file.create_array(root, "array", a) 13 h5file.close() 14 15 # h5file = tables.open_file(tmpf.name, "r") 16 h5file = tables.open_file(tmpf, "r") 17 print(getsize(tmpf)) 18 19 for node in h5file.root: 20 b = node.read() 21 print(type(b), b.shape) 22 23 h5file.close()
5.4Pandas DataFrame与HDF5仓库之间的读写操作
HDFStore类可以看作是Pandas中负责HDF5数据处理部分的一种抽象。借助一些随机数据和临时文件,我们可以很好地展示这个类的功能特性,具体步骤如下。
我们将临时文件的路径传递给HDFStore的构造函数,然后创建一个仓库。
1 tmpf ="pytable_df_demo2.h5" 2 store = pd.io.pytables.HDFStore(tmpf) 3 print(store)
上述代码将打印输出该仓库的文件路径及其内容,不过此刻它还没有任何内容。
<class 'pandas.io.pytables.HDFStore'> File path: pytable_df_demo2.h5
Empty
HDFStore提供了一个类似字典类型的接口,例如我们可以通过Pandas中DataFrame的查询键来存储数值。为了将包含随机数据的一个DataFrame存储到HDFStore中,可以使用下列代码。
1 df = pd.DataFrame(a) 2 store['df'] = df 3 print(store)
现在,该仓库存放了如下数据。
<class ‘pandas.io.pytables.HDFStore’> File path: pytable_df_demo.h5 Frame (shape->[365,4])
我们可以通过3种方式来访问DataFrame,分别是使用get()方法访问数据,利用类似字典的查询键访问数据,或者使用点运算符号来访问数据。下面我们分别进行演示。
1 print("Get", store.get('df').shape) 2 print("Lookup", store['df'].shape) 3 print( "Dotted", store.df.shape)
该DataFrame的形状同样也可以通过3种不同的方式进行访问。
Get (365, 4) Lookup (365, 4) Dotted (365, 4)
为了删除仓库中的数据,我们既可以使用remove()方法,也可以使用del运算符。当然,每个数据项只能删除一次。下面我们从仓库删除DataFrame,具体代码如下。
1 del store['df'] 2 print("After del\n", store)
这个仓库再次变空。
After del <class ‘pandas.io.pytables.HDFStore’> File path: pytable_df_demo.h5 Empty
属性is_open的作用是指出仓库是否处于打开状态。为了关闭一个仓库,可以调用close()方法。下面代码展示了关闭仓库的方法并针对仓库的状态进行了相应的检查。
1 print("Before close", store.is_open) 2 store.close() 3 print("After close", store.is_open)
一旦关闭,该仓库就会退出打开状态,显示如下。
Before close True
After close False
为读写HDF数据,Pandas还提供了两种方法,一种是DataFrame的to_hdf()方法,另一种是顶级的read_hdf()函数。下面示例代码展示了调用to_hdf()方法读取数据的过程。
1 df.to_hdf('test.h5', 'data', format='table') 2 print(pd.read_hdf('test.h5', 'data', where=['index>363']))
用于读写操作的应用程序接口的参数包括文件路径、仓库中组的标识符以及可选的格式串。这里的格式有两种,一种是固定格式,另一种是表格格式。固定格式的优点是速度要更快一些,缺点是无法追加数据,也不能进行搜索。表格格式相当于PyTables的Table结构,可以对数据进行搜索和选择操作。下面是通过查询DataFrame得到的数据。
0 1 2 3 364 0.753342 0.381158 1.289753 0.673181
以下代码引自本书代码包中的ch-05.ipynb文件。
1 import numpy as np 2 import pandas as pd 3 4 np.random.seed(42) 5 a = np.random.randn(365, 4) 6 7 tmpf ="pytable_df_demo2.h5" 8 store = pd.io.pytables.HDFStore(tmpf) 9 print(store) 10 11 df = pd.DataFrame(a) 12 store['df'] = df 13 print(store) 14 15 print("Get", store.get('df').shape) 16 print("Lookup", store['df'].shape) 17 print( "Dotted", store.df.shape) 18 19 del store['df'] 20 print("After del\n", store) 21 22 print("Before close", store.is_open) 23 store.close() 24 print("After close", store.is_open) 25 26 df.to_hdf('test.h5', 'data', format='table') 27 print(pd.read_hdf('test.h5', 'data', where=['index>363']))
5.5使用Pandas读写Excel文件
现实生活中,许多重要数据都是以Excel文件的形式存放的。当然,如果需要,我们也可以将其转换为可移植性更高的诸如CSV之类的格式。不过,利用Python来操作Excel文件会更加方便。在Python的世界里,为实现同一目标的项目通常不止一个,如提供Excel I/O操作功能的项目就是如此。只要安装了这些模块,我们就能让Pandas具备读写Excel文件的能力。只是这些方面的说明文档不是很完备,其原因是Pandas依赖的这些项目往往各自为战并且发展极为迅猛。这些Pandas程序包对于Excel文件也很挑剔,要求这些文件的后缀必须是.xls或者.xlsx;否则就会报错。
ValueError: No engine for filetype: ‘’
好在这个问题非常容易解决,举例来说,当创建一个临时文件时,只提供合适的后缀即可。如果需要的多个模块一个都没有安装的话,就会收到如下的错误信息。
ImportError: No module named openpyxl.workbook
只要用下面的命令安装openpyxl,就可以杜绝这样的错误提示,具体命令如下。
$ pip3 install openpyxl xlsxwriter xlrd
模块openpyxl源于PHPExcel,它提供了针对.xlsx文件的读写功能。
此外,模块xlsxwriter也需要读取.xlsx文件,而模块xlrd能用来析取.xls和.xlsx文件中的数据。
Tips:关于padas读取excel,可以参考这里https://www.cnblogs.com/downmoon/p/12447551.html
官方:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_excel.html
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html#pandas.read_excel
下面我们先来生成用于填充Pandas中DataFrame的随机数,然后用这个DataFrame创建一个Excel文件,接着再用Excel文件重建DataFrame并通过mean()方法来计算其平均值。对于Excel文件的工作表,我们既可以为其指定一个从0开始计数的索引,也可以为其规定一个名称。以下代码引自本书代码包中的ch-05.ipynb文件。
1 import numpy as np 2 import pandas as pd 3 # from tempfile import NamedTemporaryFile 4 5 np.random.seed(42) 6 a = np.random.randn(365, 4) 7 8 tmpf = "excel_demo2.xlsx" 9 #NamedTemporaryFile(suffix='.xlsx') 10 11 df = pd.DataFrame(a) 12 print(tmpf) 13 print(df) 14 # df.to_excel(tmpf, sheet_name='Random Data',index=False) 15 with pd.ExcelWriter(tmpf) as writer: 16 df.to_excel(writer, sheet_name='Random_Data',index=False) 17 print("Means\n", pd.read_excel(tmpf, 'Random_Data').mean())
我们通过to_excel()方法创建Excel文件,具体如下。
with pd.ExcelWriter(tmpf) as writer: df.to_excel(writer, sheet_name='Random_Data',index=False)
下面我们使用顶级read_excel()函数来重建DataFrame,代码如下。
1 print("Means\n", pd.read_excel(tmpf, 'Random_Data').mean())
下面我们输出平均值。
excel_demo2.xlsx ... [365 rows x 4 columns] Means 0 0.037860 1 0.024483 2 0.059836 3 0.058417 dtype: float64
5.6使用REST Web服务和JSON
表述性状态转移(Representational State Transfer,REST)Web服务采用的是REST架构风格。对于HTTP(S)来说,可以使用GET、POST、PUT和DELETE方法,这些方法对应数据项的创建、请求、更新及删除操作。
使用REST风格的API时,数据项是通过统一资源标识符(URI)进行标识的。虽然REST并非官方标准,但是其应用极为广泛,所以我们必须对它进行深入了解。Web服务经常使用JavaScript对象表示法(JavaScript Object Notation,JSON)来交换数据。使用这种格式时,数据会按照JavaScript表示法的要求进行加工。这种表示法类似于Python的列表和字典的语法。利用JSON,通过组合列表和字典,可以定义任意复杂的数据。为了解释这一点,我们将使用一个相当于字典类型的JSON字符串为例进行说明,这个字符串用来提供某IP地址的地理位置信息。
{“country”:”Netherlands”,”dma_code”:”0”,”timezone”:”Europe\/Amsterdam “,”area_code”:”0”,”ip”:”46.19.37.108”,”asn”:”AS196752”,”continent_cod E”:”EU”,”isp”:”Tilaa V.O.F.”,”longitude”:5.75,”latitude”:52.5,”country_ code”:”NL”,”country_code3”:”NLD”}
以下是引自ch-5.ipynb文件中的代码。
1 import json 2 3 json_str = '{"country":"Netherlands","dma_code":"0","timezone":"Europe\/Amsterdam","area_code":"0","ip":"46.19.37.108","asn":"AS196752","continent_code":"EU","isp":"Tilaa V.O.F.","longitude":5.75,"latitude":52.5,"country_code":"NL","country_code3":"NLD"}' 4 5 data = json.loads(json_str) 6 print("Country", data["country"]) 7 data["country"] = "Brazil" 8 print(json.dumps(data))
Python为我们提供了一个简单易用的标准JSON API。下面我们用loads()函数来解析JSON字符串。
1 data = json.loads(json_str)
下列代码可以用来访问变量country的值。
1 Print “Country”, data[“country”]
上述代码的输出结果如下。
Country Netherlands
我们修改变量country的取值并利用该新JSON数据来创建一个字符串。
1 Data[“country”] = “Brazil” 2 Printjson.dumps(data)
得到的这个JSON的country变量具有一个新值。与字典类似,这里数据项之间的顺序是任意的。
Country Netherlands {"country": "Brazil", "dma_code": "0", "timezone": "Europe/Amsterdam", "area_code": "0", "ip": "46.19.37.108", "asn": "AS196752", "continent_code": "EU", "isp": "Tilaa V.O.F.", "longitude": 5.75, "latitude": 52.5, "country_code": "NL", "country_code3": "NLD"}
5.7使用Pandas读写JSON
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_json.html
利用上面例子中的JSON字符串,可以轻而易举地创建一个pandas Series。这个pandas提供的read_json()函数,可以用来创建pandas Series或者pandas DataFrame数据结构。
以下示例代码引自本书代码包中的ch-05.ipynb文件。
1 import pandas as pd 2 3 json_str = '{"country":"Netherlands","dma_code":"0","timezone":"Europe\/Amsterdam","area_code":"0","ip":"46.19.37.108","asn":"AS196752","continent_code":"EU","isp":"Tilaa V.O.F.","longitude":5.75,"latitude":52.5,"country_code":"NL","country_code3":"NLD"}' 4 5 data = pd.read_json(json_str, typ='series') 6 print("Series\n", data) 7 8 data["country"] = "Brazil" 9 print("New Series\n", data.to_json())
调用read_json()函数时,既可以向其传递一个JSON字符串,也可以为其指定一个JSON文件的路径。上面的例子中,我们是利用JSON字符串来创建pandas Series的。
1 Data = pd.read_json(json_str, typ=’series’) 2 Print(“Series\n”, data)
在得到的Series中,键是按字母顺序排列的。
Series country Netherlands dma_code 0 timezone Europe/Amsterdam area_code 0 ip 46.19.37.108 asn AS196752 continent_code EU isp Tilaa V.O.F. longitude 5.75 latitude 52.5 country_code NL country_code3 NLD dtype: object
我们再次修改country的值并用to_json()方法将其从pandas Series转换成JSON字符串。
1 Data[“country”] = “Brazil” 2 Print(“New Series\n”, data.to_json())
在这个新JSON字符串中,键的顺序被保留了下来,不过,country的值却变了。
New Series {"country":"Brazil","dma_code":"0","timezone":"Europe\/Amsterdam","area_code":"0","ip":"46.19.37.108","asn":"AS196752","continent_code":"EU","isp":"Tilaa V.O.F.","longitude":5.75,"latitude":52.5,"country_code":"NL","country_code3":"NLD"}
5.8解析RSS和Atom订阅
简易信息聚合(Really Simple Syndication,RSS)和Atom订阅常用于订阅博客和新闻。虽然这两种订阅的类型不同,却都遵循发布订阅模式。比如,Packt出版公司网站就提供了书籍和文章方面的订阅功能,只要用户订阅后,就能与网站内容的最新更新保持同步。在Python的feedparser模块的帮助下,我们无需了解相关的技术细节,就能处理RSS和Atom订阅。要想安装这个feedparser模块,可以使用下列pip命令。
$ pip3 install feedparser
解析完RSS文件后,就可以通过句点来访问基础数据了。以下代码将解析Packt Publishing网站的订阅源并打印内容数量。
1 import feedparser as fp 2 3 # rss = fp.parse("http://www.packtpub.com/rss.xml") 4 rss = fp.parse("http://feed.cnblogs.com/blog/u/22045/rss/") 5 6 print("# Entries", len(rss.entries))
内容的数量如下,不过,每次运行程序时该数字都可能发生变化。
# Entries 10
这里可以显示含有Python这个单词的条目的标题和摘要,代码如下。
1 for i, entry in enumerate(rss.entries): 2 if "数据分析" in entry.summary: 3 print(i, entry.title) 4 print(entry.summary)
就本次运行而言,结果如下。需要注意的是,运行该代码时,内容会有所不同,如果过滤条件限制太严格,还可能一条符合要求的内容也没有。
5.9使用Beautiful Soup解析HTML
超文本标记语言(Hypertext Markup Language,HTML)是用于创建网页文档的一种基础性技术。HTML由各种元素组成,而这些元素由尖括号中的所谓标签组成,如<html>。标签通常是成对出现的,标签对中的第1个标签是开始标签,第2个标签是结束标签,这些标签以树状结构组织在一起。HTML的相关规范草案于1991年由Berners-Lee公布,当时仅包括18个HTML元素。HTML的正式定义出现在1993年,是由国际互联网工程任务组(Internet Engineering Task Force,IETF)颁布的。1995年,IETF发布HTML 2.0标准;2013年,又发行了最新的HTML版本,即HTML5。与XHTML和XML比较,HTML不是一个非常严格的标准。
我们知道,现代浏览器容错性已经有了长足进步,另一方面,这也为Web页面中不符合标准的非结构化数据的滋生提供了温床。我们不仅可以将HTML视为一个硕大的字符串,而且可以运用正则表达式对其执行各种字符串操作。不过,这种方法仅适用于比较简单的项目。
工作中,笔者曾经接触过专业级别的网络信息搜集项目,经验证明我们需要更加先进的方法。在现实世界中,我们有时需要以编程的方式来提交HTML表单,尤其是在登录、切换页面和处理Cookie时。当我们从网页上抓取数据时,常见的问题是我们无法完全控制所抓取的页面,因此经常需要修改自己的代码。此外,有些网站的所有者不喜欢别人以编程的方式访问其内容,所以他们会处心积虑地设置各种障碍,有的甚至直接禁用这种访问方式。考虑到这些因素,我们应该优先考虑诸如REST API之类的信息搜集方法。
如果结果只能通过抓取页面的方式来搜集信息,建议使用Python的Beautiful Soup API。这个应用程序接口不仅可以从HTML文件中抽取数据,同时还支持XML文件。对于新的项目来说,建议使用Beautiful Soup 4,因为Beautiful Soup 3目前已经停止开发了。我们可以使用如下命令来安装Beautiful Soup 4。Easy_install命令的用法与此类似。
1 $ pip3 install beautifulsoup4 lxml
如果这种方法行不通,还可以把自己的代码跟Beautiful Soup直接封装到一起。为了演示如何解析HTML,本书的代码包中提供了一个名为loremIpsum.html的页面文件,该文件是网站http://loripsum.net/的生成程序制作的。此后,我们对这个文件进行了一些修改。文件的内容取自公元前 1 世纪西塞罗的拉丁文作品,这是创建网站模型的一种惯用形式。图5-1中显示的是网页的上面部分。
本例中用的工具是Beautiful Soup 4和Python常规的正则表达式程序库。
以下代码的作用是导入程序库。
1 from bs4 import BeautifulSoup 2 import re
然后我们打开HTML文件并新建一个BeautifulSoup对象,具体代码如下。
1 soup = BeautifulSoup(open('loremIpsum.html'))
通过使用句点标记法,我们可以方便地访问第一个<div>元素,这个元素的作用是组织元素并提供样式。访问第1个div元素的代码如下。
1 print("First div\n", soup.div)
输出内容是一个HTML片段,其中可以看到第1个<div>标签及其所含内容。
First div <div class="tile"> <h4>Development</h4> 0.10.1 - July 2014<br/> </div>
Tips:对于这个div元素来说,其类属性的值为tile,这个属性的作用是为该元素指定CSS样式。层叠样式表(Cascading Style Sheets,CSS)是一种描述网页元素样式的语言。通过CSS类可以方便地控制Web页面的外观,因此CSS规范的应用极为广泛。有了CSS,我们可以方便地定义元素的布局、字体和颜色,这对于内容和外观的隔离帮助很大。而内容和外观的隔离,又让设计工作变得更加简单、清晰。
我们可以像访问字典那样来访问标签的属性,下面以输出<div>标签的类属性的值为例进行说明。
1 print(“First div class”, soup.div[‘class’])
我们利用点号,可以访问任意深度的元素。下面以输出首个<dfn>标签中的文本为例进行说明。
1 print(“First dfn text”, soup.dl.dt.dfn.text)
输出的是一行拉丁文字(英文的意思是:Solisten, I pray)。
First dfn text Quareattende, quaeso
有时,我们只对HTML页面中的超链接感兴趣,例如我们可能只想知道哪些页面具有外向链接。在HTML文档中,链接是用<a>标签定义的,通过这个标签的href属性,就能找到外向链接的URL。BeautifulSoup类中有一个简单易用的find_all()方法,后面将会经常用到;通过这个方法,我们可以找到文档中所有的超链接。
1 for link in soup.find_all(‘a’): 2 print(“Link text”, link.string, “URL”, link.get(‘href’))
这里,我们从文档中找到了以下3个URL相同但文字相异的链接。
Link text loripsum.net URL http://loripsum.net/ Link text Poterat autem inpune; URL http://loripsum.net/ Link text Is es profecto tu. URL http://loripsum.net/
Find_all()方法就介绍到这里,下面演示如何访问所有<div>标签中的内容。
1 # Omitting find_all 2 for i, div in enumerate(soup('div')): 3 print(i, div.contents)
属性contents中存放的是一个HTML元素列表。
['\n', <h4>Development</h4>, '\n 0.10.1 - July 2014', <br/>, '\n'] ['\n', <h4>Official Release</h4>, '\n 0.10.0 June 2014', <br/>, '\n'] ['\n', <h4>Previous Release</h4>, '\n 0.09.1 June 2013', <br/>, '\n']
为了便于查找,每个标签的ID都是唯一的。下面的代码将选取ID为official的那个<div>元素并打印输出第3个元素。
1 #Div with id=official 2 official_div = soup.find_all("div", id="official") 3 print("Official Version", official_div[0].contents[2].strip())
许多页面都是根据访问者的输入或者外部数据即时生成的,网上购物网站上的页面尤其如此。当我们跟动态网站打交道时,必须牢记所有标签的属性值随时都可能发生变化。对于大型网站来说,自动生成的ID会产生一个大型的字母数字字符串。因此,这时最好不要使用完全匹配方式进行查找,而要使用正则表达式。下面介绍如何通过模式匹配进行查找。上面的代码片段输出的内容是在某个网站上查找一款软件产品时所返回的版本号和月份。
Official Version 0.10.0 June 2014
我们知道,class是Python编程语言中的一个关键字。为查询标签的类属性,必须使用class_作为匹配符。下面展示如何求已经定义了类属性的<div>标签的数量。
1 print("# elements with class", len(soup.find_all(class_=True)))
如你期望的那样,我们找到了3个标签。
# elements with class 3
下面计算带有“tile”类的<div>标签的数目。
1 tile_class = soup.find_all("div", class_="tile") 2 print("# Tile classes", len(tile_class))
实际上,文档中存在两个含有tile类的<div>标签以及1个含有notile类的<div>标签,因此有如下写法。
# Tile classes 2
下面定义一个匹配所有<div>标签的正则表达式。
1 Print(“# Divs with class containing tile”, len(soup.find_all(“div”, 2 Class_=re.compile(“tile”))))
这次找到了3个符合要求的标签。
# Divs with class containing tile 3
使用CSS时,可以利用模式来匹配文档中的元素,这些模式通常称为CSS选择器。我们可以利用BeautifulSoup类提供的CSS选择器来选择页面元素。通过select()函数,我们可以匹配带有notile类的<div>元素如下。
1 Print(“Using CSS selector\n”, soup.select(‘div.notile’))
下面是输出内容。
Using CSS selector [<div class=”notile”> <h4>Previous Release</h4> 0.09.1 June 2013<br/> </div>]
HTML有序列表看起来与项目编号列表非常接近,由一个<ol>标签和若干<li>标签组成,其中每个列表项对应一个<li>标签。就像Python的列表一样,select()函数返回的内容也可以进行切分。图5-2展示了有序列表。
下面的代码将选择有序列表中的前两项内容。
1 Print(“Selecting ordered list list items\n”, soup.select(“ol > li”)[:2])
这两个列表项的内容如下。
Selecting ordered list list items [<li>Cur id non ita fit?</li>, <li>In qua si nihil est praeter Rationem, sit in una virtute finis bonorum;</li>]
利用CSS选择器的袖珍型语言,可以选择第二个列表项。注意,这里是从⒈开始算起的。具体代码如下。
1 Print(“Second list item in ordered list”, soup.select(“ol>li:nth-of-type(2)”))
下面是列表中的第二项内容,翻译成英语,大意是“In which, if there is nothing contrary to reason, let him be the power of the end of the good things in one”。
Second list item in ordered list [<li>In qua si nihil est praeter Rationem, sit in una virtute finis bonorum;</li>]
当用浏览器阅览页面时,可以通过特定的正则表达式来检索匹配的文本节点。以下代码展示了如何借助text属性来找出所有包含字符串“2014”的文本节点。
1 Print(“Searching for text string”, soup.find_all(text=re.compile(“2014”)))
得到的文本节点如下。
Searching for text string [u’\n 0.10.1 – July 2014’, u’\n 0.10.0 June 2014’]
上面对BeautifulSoup类的功能做了简要介绍,此外,Beautiful Soup还能用于修改HTML和XML文档。它不仅能够用来排错,还能美化打印效果以及处理不同的字符集。下面的示例代码引自ch-05.ipynb文件。
1 from bs4 import BeautifulSoup 2 import re 3 4 soup = BeautifulSoup(open('loremIpsum.html'),"lxml") 5 6 print("First div\n", soup.div) 7 print("First div class", soup.div['class']) 8 9 print("First dfn text", soup.dl.dt.dfn.text) 10 11 for link in soup.find_all('a'): 12 print("Link text", link.string, "URL", link.get('href')) 13 14 # Omitting find_all 15 for i, div in enumerate(soup('div')): 16 print(i, div.contents) 17 18 19 #Div with id=official 20 official_div = soup.find_all("div", id="official") 21 print("Official Version", official_div[0].contents[2].strip()) 22 23 print("# elements with class", len(soup.find_all(class_=True))) 24 25 tile_class = soup.find_all("div", class_="tile") 26 print("# Tile classes", len(tile_class)) 27 28 print("# Divs with class containing tile", len(soup.find_all("div", class_=re.compile("tile")))) 29 30 print("Using CSS selector\n", soup.select('div.notile')) 31 print("Selecting ordered list list items\n", soup.select("ol > li")[:2]) 32 print("Second list item in ordered list", soup.select("ol > li:nth-of-type(2)")) 33 34 print("Searching for text string", soup.find_all(text=re.compile("2014")))
我们鼓励读者阅读5.11节列出的参考资料,以便进一步深入学习Beautiful Soup的高级功能,例如搜索返回节点的子节点,获取返回节点的第n个父节点,获取返回节点的第n个兄弟节点以及其他高级功能。
5.10小结
本章介绍了检索、加工与存储不同格式数据的方法。这些格式包括CSV、NumPy .npy、Python pickle、JSON、RSS和HTML等格式。其中,我们用到了NumPy pandas、JSON、feedparser以及Beautiful Soup等程序库。
第6章将为读者讲解利用Python显实数据可视化的重要主题。分析数据时,可视化是一种比较常见的任务。它能够展现数据中各个变量之间的关系。通过数据可视化技术,还可以形象地展示出数据的统计特性。
第5章完。
随书源码官方下载:
https://www.ptpress.com.cn/shopping/buy?bookId=bae24ecb-a1a1-41c7-be7c-d913b163c111
需要登录后免费下载。