引言 pandas中的read_html()
函数是将HTML的表格转换为DataFrame的一种快速方便的方法,这个函数对于快速合并来自不同网页上的表格非常有用。 在合并时,不需要用爬虫获取站点的HTML。但是,在分析数据之前,数据的清理和格式化可能会遇到一些问题。在本文中,我将讨论如何使用pandas的read_html()
来读取和清理来自维基百科的多个HTML表格,以便对它们做进一步的数值分析。
基本方法 在第一个例子中,我们将尝试解析一个表格。这个表格来自维基百科页面中明尼苏达州的政治部分(https://en.wikipedia.org/wiki/Minnesota)。
read_html
的基本用法非常简单,在许多维基百科页面上都能运行良好,因为表格并不复杂。首先,要导入一些库 ,在后面的数据清理中都会用到:
1 2 3 4 5 6 import pandas as pd import numpy as np import matplotlib.pyplot as plt from unicodedata import normalize table_MN = pd.read_html('https://en.wikipedia.org/wiki/Minnesota')
特别注意,上面代码中得到的table_MN
是页面上所有表格的列表:
1 2 3 print(f'Total tables: {len(table_MN)}') Total tables: 38
很难在38张表格中找到你需要的那张,要想容易地找出来,可以设置match
参数,如下面的代码所示,用mathch
参数指明要选择标题为“Election results from statewide races”的那张表格。
1 2 3 4 5 table_MN = pd.read_html('https://en.wikipedia.org/wiki/Minnesota', match='Election results from statewide races') len(table_MN) # 输出 1
1 2 df = table_MN[0] df.head()
输出:
显然,用Pandas能够很容易地读取到了表格,此外,从上面的输出结果可以看出,跨多行的Year
列也得到了很好地处理,这要比自己写爬虫工具专门收集数据简单多了。
总的来说,这样的操作看起来还不错,然而,如果用df.info()
来查看数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 <class 'pandas.core.frame.DataFrame'> RangeIndex: 24 entries, 0 to 23 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Year 24 non-null int64 1 Office 24 non-null object 2 GOP 24 non-null object 3 DFL 24 non-null object 4 Others 24 non-null object dtypes: int64(1), object(4) memory usage: 1.1+ KB
如果想对这些数据进行分析,需要将GOP
、DFL
和其他类型为object
的列转换为数值。
如果这么操作:
1 df['GOP'].astype('float')
系统就会报错:
1 ValueError: could not convert string to float: '42.4%'
最有可能的罪魁祸首是%
,下面用pandas的replace()
函数删除它。
1 df['GOP'].replace({'%':''}, regex=True).astype('float')
效果看起来不错:
1 2 3 4 5 6 7 8 9 0 42.4 1 36.2 2 42.4 3 44.9 <...> 21 63.3 22 49.1 23 31.9 Name: GOP, dtype: float64
注意,必须使用参数regex=True
才能完美地删除,因为%
是字符串的一部分,而不是完整的字符串值。
现在,我们可以用pd.to_numeric()
和apply()
替换所有的%
值,并将其转换为数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 df = df.replace({'%': ''}, regex=True) df[['GOP', 'DFL', 'Others']] = df[['GOP', 'DFL', 'Others']].apply(pd.to_numeric) df.info() # 输出 <class 'pandas.core.frame.DataFrame'> RangeIndex: 24 entries, 0 to 23 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Year 24 non-null int64 1 Office 24 non-null object 2 GOP 24 non-null float64 3 DFL 24 non-null float64 4 Others 24 non-null float64 dtypes: float64(3), int64(1), object(1) memory usage: 1.1+ KB
输出:
这个基本过程进展顺利,下面看一个有点难度的。
高级的数据清理方法 前面的例子展示了基本概念,数据清理是任何数据科学项目都不可或缺的,下面看一个有点难度的示例。在接下来的示例中继续使用维基百科,但是这些方法同样适用于其他含有表格的HTML页面。
例如读取美国GDP的数据表:
现在,就不能用match
参数指定要获得的那个表格标题——因为这表格没有标题,但是可以将其值设置为“Nominal GDP”,这样依然能匹配到我们想要的表格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 table_GDP = pd.read_html('https://en.wikipedia.org/wiki/Economy_of_the_United_States', match='Nominal GDP') df_GDP = table_GDP[0] df_GDP.info() # 输出 <class 'pandas.core.frame.DataFrame'> RangeIndex: 41 entries, 0 to 40 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Year 41 non-null object 1 Nominal GDP(in bil. US-Dollar) 41 non-null float64 2 GDP per capita(in US-Dollar) 41 non-null int64 3 GDP growth(real) 41 non-null object 4 Inflation rate(in percent) 41 non-null object 5 Unemployment (in percent) 41 non-null object 6 Budget balance(in % of GDP)[107] 41 non-null object 7 Government debt held by public(in % of GDP)[108] 41 non-null object 8 Current account balance(in % of GDP) 41 non-null object dtypes: float64(1), int64(1), object(7) memory usage: 3.0+ KB
不出所料,数据清理是避免不了得了。根据前面的经验,先删除%
。
1 df_GDP['GDP growth(real)'].replace({'%': ''}, regex=True).astype('float')
很遗憾,报错了:
1 ValueError: could not convert string to float: '−5.9\xa0'
问题的根源在于有一个隐藏字符xa0
,它导致了错误,它是一个特殊字符,即“non-breaking Latin1 (ISO 8859-1) space”,对应的实体是  
,即空格。
我所使用的一个方法是使用replace
直接替换,这种方法奏效了,但我担心它将来是否会与其他字符产生冲突。
在深入研究了Unicode这个坑之后,我决定使用normalize
来清理这个值。
我还发现,在其他的一些表格的数据中也有多余的空格。于是编写了一个函数,对所有文本进行清理。
1 2 3 4 5 6 7 from unicodedata import normalize def clean_normalize_whitespace(x): if isinstance(x, str): return normalize('NFKC', x).strip() else: return x
用applymap
将这个函数用于整个DataFrame上:
1 df_GDP = df_GDP.applymap(clean_normalize_whitespace)
需要注意的是:applymap
函数非常慢,所以在使用applymap
时应该慎重。
applymap
函数是一个非常低效的pandas函数,不推荐你经常使用它。但在本例中,DataFrame很小,像这样的清理又很棘手,所以我认为这是一个有用的权衡。
applymap
不能处理列名称,例如:
1 2 3 4 df_GDP.columns[7] # 输出 'Government debt held by public(in\xa0% of GDP)[108]'
在列的名称中有可怕的xa0%
。解决此问题的方法有多种,在这里还是继续使用clean_normalize_whitespace()
函数,将列转换为Series对象,并使用apply
来调用这个函数。有点麻烦了,不知道pandas在以后的版本是否会考虑到这里的问题,让操作简化。
1 2 3 4 5 df_GDP.columns = df_GDP.columns.to_series().apply(clean_normalize_whitespace) df_GDP.columns[7] # 输出 'Government debt held by public(in % of GDP)[108]'
现在我们清理掉了一些隐藏的字符。下一步会怎样呢?
再试一次:
1 2 3 4 df_GDP['GDP growth(real)'].replace({'%': ''}, regex=True).astype('float') # 输出 ValueError: could not convert string to float: '−5.9 '
真的很棘手。如果你仔细观察,你可能会发现:−
和-
看起来有点不同,但真的很难看出,在Unicode中,破折号和减号之间实际上是有区别的。
幸运的是,我们也可以使用replace
来清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 df_GDP['GDP growth(real)'].replace({'%': '', '−': '-'}, regex=True).astype('float') # 输出 0 -5.9 1 2.2 2 3.0 3 2.3 4 1.7 <...> 38 -1.8 39 2.6 40 -0.2 Name: GDP growth(real), dtype: float64
现在来关注列Year
,例如表示“2020年”的值是2020(est)
,需要去掉其中的(est)
,还要将列转换为整数型。
1 2 3 4 5 6 7 8 9 10 11 df['Year'].replace({'%': '', '−': '-', '\(est\)': ''}, regex=True).astype('int') # 输出 0 2020 1 2019 2 2018 3 2017 4 2016 <...> 40 1980 Name: Year, dtype: int64
在DataFrame中的各列的值,除了整数型之外,其他的是浮点数型,在转化的时候,如果使用pd.numeric()
虽然能够实现,但略显笨拙。我们可以使用astype()
同时又不需要为每一列手动输入类型信息。
astype()
函数可以接受含有列名和数据类型的字典。这真的很有用,直到我写了这篇文章我才知道这一点。下面是对列与其数据类型映射字典:
1 2 3 4 5 6 7 8 9 10 11 col_type = { 'Year': 'int', 'Nominal GDP(in bil. US-Dollar)': 'float', 'GDP per capita(in US-Dollar)': 'int', 'GDP growth(real)': 'float', 'Inflation rate(in percent)': 'float', 'Unemployment (in percent)': 'float', 'Budget balance(in % of GDP)[107]': 'float', 'Government debt held by public(in % of GDP)[108]': 'float', 'Current account balance(in % of GDP)': 'float' }
如果你觉得键入上面这个词典很慢,可以用下面的快捷方法。要注意,这样建立的字典,默认值为float
,还需要手动将Year
对应的值修改为int
:
1 2 3 4 5 6 7 8 9 10 11 12 dict.fromkeys(df_GDP.columns, 'float') # 输出 {'Year': 'float', 'Nominal GDP(in bil. US-Dollar)': 'float', 'GDP per capita(in US-Dollar)': 'float', 'GDP growth(real)': 'float', 'Inflation rate(in percent)': 'float', 'Unemployment (in percent)': 'float', 'Budget balance(in % of GDP)[107]': 'float', 'Government debt held by public(in % of GDP)[108]': 'float', 'Current account balance(in % of GDP)': 'float'}
再创建了一个字典,其中包含要替换的值:
1 clean_dict = {'%': '', '−': '-', '\(est\)': ''}
现在我们可以调用这个DataFrame的replace
方法,转换为所需的类型,并获得干净的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 df_GDP = df_GDP.replace(clean_dict, regex=True).replace({'-n/a ': np.nan}).astype(col_type) df_GDP.info() # 输出 <class 'pandas.core.frame.DataFrame'> RangeIndex: 41 entries, 0 to 40 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Year 41 non-null int64 1 Nominal GDP(in bil. US-Dollar) 41 non-null float64 2 GDP per capita(in US-Dollar) 41 non-null int64 3 GDP growth(real) 41 non-null float64 4 Inflation rate(in percent) 41 non-null float64 5 Unemployment (in percent) 41 non-null float64 6 Budget balance(in % of GDP)[107] 40 non-null float64 7 Government debt held by public(in % of GDP)[108] 41 non-null float64 8 Current account balance(in % of GDP) 40 non-null float64 dtypes: float64(7), int64(2) memory usage: 3.0 KB
结果如下所示:
为了证明上述操作的效果,我们可以把这些数据绘制成图表:
1 2 plt.style.use('seaborn-whitegrid') df_clean.plot.line(x='Year', y=['Inflation rate(in percent)', 'Unemployment (in percent)'])
如果你紧跟我的思路,可能已经注意到链式方式调用replace
的方法:
1 .replace({'-n/a ': np.nan})
我这样做的原因是我不知道如何使用第一个字典replace
来清理n/a
。我认为问题的症结在于:我无法预测这些数据的清理顺序,所以不得不分两个阶段来执行替换。
如果读者有更好的方法,请不吝赐教。
完整的代码 最后,把上面的过程,集中用下面的代码实现。从HTML网页上的表格获取数据,并把这些数据转化为DataFrame对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import pandas as pd import numpy as np from unicodedata import normalize def clean_normalize_whitespace(x): """ Normalize unicode characters and strip trailing spaces """ if isinstance(x, str): return normalize('NFKC', x).strip() else: return x # Read in the Wikipedia page and get the DataFrame table_GDP = pd.read_html( 'https://en.wikipedia.org/wiki/Economy_of_the_United_States', match='Nominal GDP') df_GDP = table_GDP[0] # Clean up the DataFrame and Columns df_GDP = df_GDP.applymap(clean_normalize_whitespace) df_GDP.columns = df_GDP.columns.to_series().apply(clean_normalize_whitespace) # Determine numeric types for each column col_type = { 'Year': 'int', 'Nominal GDP(in bil. US-Dollar)': 'float', 'GDP per capita(in US-Dollar)': 'int', 'GDP growth(real)': 'float', 'Inflation rate(in percent)': 'float', 'Unemployment (in percent)': 'float', 'Budget balance(in % of GDP)[107]': 'float', 'Government debt held by public(in % of GDP)[108]': 'float', 'Current account balance(in % of GDP)': 'float' } # Values to replace clean_dict = {'%': '', '−': '-', '\(est\)': ''} # Replace values and convert to numeric values df_GDP = df_GDP.replace(clean_dict, regex=True).replace({ '-n/a ': np.nan }).astype(col_type)
总结 pandas的read_html()
函数对于快速解析页面中的 HTML表格非常有用,尤其是维基百科页面。从HTML页面直接获得的数据,通常不会像你所需要的那样干净,并且清理各种Unicode字符可能会非常耗时。本文展示的几种技术可以用于清理数据、并将其转换为正确的数字格式。如果你需要从维基百科或其他HTML表格中获取数据,这些技巧应该可以为你节省一些时间。
参考: https://pbpython.com/pandas-html-table.html