盘一盘 Python 系列特别篇 - 实战正则表达式

目的:将网页上的表格获取出来。

我们采用新冠肺炎的数据举例,网址如下:

https://www.worldometers.info/coronavirus/

浏览该网页后,我们想获取下图的表格数据。

首先引入用于正则表达式的 RE 包或用于获取网页信息的 urllib 包。

import reimport urllib

接下来从网址中读源代码并转成字符串需要以下三步:

  1. 用 urllib 中 request.urlopen() 函数打开链接存成对象 f
  2. 用 f 中 read() 的函数获取里面的内容 myfile,但是 myfile 的类型是 bytes,而 re 里面的函数都需要 string 作为输入
  3. 用 decode("utf-8") 将 bytes 装成 string

完整的代码如下:

link = "https://www.worldometers.info/coronavirus/"f = urllib.request.urlopen(link)myfile = f.read()info = myfile.decode("utf-8") print(info)

但是这个字符串太长了,我无法找到从 info 字符串里找到上面 Table 源代码所在的地方。这时我们找网页源码来看,操作有三步:

  1. 点击鼠标右键,选择 view page source
  2. 跳转到源代码后,我们知道 Table html 会带有 table id 的关键词,因此按 ctrl + F 找到其位置作为 Table 代码起始位置
  3. 再继续搜索 table 关键词,看到出现 </table> 位置作为 Table 代码终止位置

整个操作如下面动图所示:

第一步 - 获取整个 Table 的字符串

锁定好位置,其实我们只需知道表示 Table 代码的首尾的若干字符即可,以 <table 开始,以 </table> 结束,中间无数个(可用 * 表示)字符(可用通配符 . 表示)。

定义其模式 pat 如下,并用 findall 获取整个 Table 的字符串,返回是个列表,索引 0 位置的字符串。

pat = r'<table.*</table>'table_str = re.findall(pat, info)table_str[0]

该字符串还是很长,但至少已经缩减到 Table 层面了,Table 无非就是由若干行组成的嘛,让我们把注意力放在每行的代码上。

第二步 - 获取 Table 中每行的字符串

细看一下,我们发现一个规律,即每行代码以 <tr> 开始,以 </tr> 结束,如下图所示。

那定义其模式就简单了,r'<tr.*?</tr>',关键点是这个问号 ?,它代表是非贪婪模式匹配,即以尽可能少的方式来匹配,这样我们就可以把 Table 中多行就找出来。代码如下:

row_pat = r'<tr.*?</tr>'row_str = re.findall(row_pat, table_str[0])print( len(row_str) )row_str

返回的结果是一个包含 128 个元素的列表(表示这个 Table 有 128 行),接下来就需要把 Table 每一行的元素一一取出。

第三步 - 获取每行字符串中的各种信息

我们来看看表格,发现所有行分三种模式:

  1. 第一行:都是粗体字,而且分两行写
  2. 中间行:第一个是字符串,后面都是数字
  3. 最后一行:第一个是字符串,后面都是数字

再看这三种类型的行对应的源代码

第一行

中间行

最后一行

设计他们的模式,并用 compile 函数创建带特定模式的对象。

first_row_pat = r'<th width=.+?>(.+?)<br />(.+?)</th>'mid_row_pat = r'<td style=.+?>(.+?)</td>'last_row_pat = r'<td>(?:<strong>|)(.+?)(?:</strong>|)</td>'
first_row_obj = re.compile(first_row_pat)mid_row_obj = re.compile(mid_row_pat)last_row_obj = re.compile(last_row_pat)

将每行获取出来的元素存在列表中,起名为 table。

table = []
for i, row in enumerate(row_str):    if i == 0:        table.append(first_row_obj.findall(row))    elif i == len(row_str)-1:         table.append(last_row_obj.findall(row))    else:        table.append(mid_row_obj.findall(row))table

结果基本正确,有三个问题:

  1. 有些字符串包含 ‘%&nbsp' 字符,这是代表空白符,之后我们会处理
  2. 有地方是出现了 <a class ... href=...></a>,这代表在网页的表格中 China 一词上有超链接。如下图,带下划线的词都带超链接。
  3. Diamond Pricess 是斜体,对应的 html 源代码如 <span style=...></an>

我们要进一步处理以上三个问题,首先处理超链接和斜体(第 2 和 3 问题),代码如下:

pat1 = r'<a.+?>(.+?)</a>'pat2 = r'<span.+?>(.+?)</span>'
table1 = table.copy()for i, row in enumerate(table):    if i !=0 and i !=len(table1)-1:        if row[0].startswith(' <a'):            table1[i][0] = re.findall(pat1, row[0])[0]        elif row[0].startswith(' <span'):            table1[i][0] = re.findall(pat2, row[0])[0]        else:            table1[i][0] = table1[i][0].strip()table1

最后将结果转换成数据帧(DataFrame),用 Pandas。

第四步 - 整理成 DataFrame

先引入 Pandas 包,并把 table1 转成 DataFrame。

import pandas as pd
df = pd.DataFrame(table1)df

结果无敌难看,有两点要改进:

  1. 把第一栏每个国家或地区的名称当成行标签(index)
  2. 把第一行标题当成列标签(columns)

先搞定行标签。

df = df.set_index(0)df

再搞定列标签。

new_header = df.iloc[0]df = df[1:]df.columns = new_headerdf

看起来完美,除了左上角有个讨厌的 (Country, Other) 和 0,它们分别是列标签名称和行标签名称,改成自己喜欢的就行。

df.index.name = 'Region'df.columns.name = ''df

现在看起来真的完美了,等等,列标签里还有讨厌的 ‘%&nbsp',打印出来看看:

names = [i[0]+' '+i[1] for i in df.columns.values]names
['Total Cases',
 '%&nbsp;of&nbsp;All Cases',
 'New Cases',
 'Total Deaths',
 'New Deaths',
 'Total Recovered',
 'New Recovered',
 'Active Cases',
 'Serious, Critical',
 'Death Rate',
 'Tot&nbsp;Cases/ 1M pop']

将 ‘%&nbsp' 换成空白符就行了。

names = [name.replace('&nbsp;', ' ') for name in names]names
['Total Cases',
 '% of All Cases',
 'New Cases',
 'Total Deaths',
 'New Deaths',
 'Total Recovered',
 'New Recovered',
 'Active Cases',
 'Serious, Critical',
 'Death Rate',
 'Tot Cases/ 1M pop']

再更新 DataFrame 的列标签。

下一篇
举报

扫码关注云+社区

领取腾讯云代金券