python 提取表格内容的通用方式

提出问题

当我们写爬虫进行数据结构化时,经常遇到一些表格数据,无论是web表格还是ExcelWord or PDF中的表格,都很常见;如果表格非常规范化,我们在做结构化时可以说非常省事,一般认为某列对应我们的某字段值即可;但是,大多数时候,表格往往写的比较随意,某一字段值对应的数据并不是在固定的列。

举个例子,表格A对应的表头如下(这里用列表表示):

[“企业名称”, “处罚决定书文号”, “违法行为”, “处罚内容”, “处罚决定机关名称”, “处罚决定日期”]

相应的我们有一个python对象,其字段值为:

1
2
3
4
5
6
7
8
9
class PunishmentItem(object):
__slots__ = (
"name", # 行政相对人名称
"irregularities", # 违规行为
"punishment_text", # 处罚内容
"department", # 处罚决定机关名称
'public_date', # 时间
'number', # 文号
)

于是我们在提取的时候,理想的方式是name对应表格第一列,number对应第二列,依此类推……

但是实际上可能还有表格B,其对应的表头为:

[“处罚决定书文号”, “企业名称”, “违法行为”, “处罚内容”, “处罚决定机关名称”, “处罚决定日期”]

我们发现其文书号和企业名称顺序是反的,那我们可能会想在程序中加个判断如果是B表格我们就调整索引值…

可是,事实上可能还存在表格C、D、E……,其表头顺序各不相同,我们要在程序中写无数个if else吗,显然是不可以的。

初步思路

很自然的就想到是否可以在一开始建立一个字典,其表示我们的字段值对应的列,如:

1
index_dict = {"name": -1, "irregularities": -1, "punishment_text": -1, "department": -1, "public_date": -1, "number": -1}

每个key对应的value就表示该字段值对应的表格中的列的索引值,我们提取字段时,只需要遍历该字典的key,取出value(我们开始时将其全部设置为-1),去表格的value列中取就可以了,大概的逻辑如下:

1
2
3
4
5
item = PunishmentItem()
for i in range(table.row_len):
row_values = table.get_value(i)
for key in index_dict:
setattr(item, key, row_values[index_dict[key]])
  • table.row_len 表示表格的行数
  • row_values表示表格第i行所有单元格的值,其结构为列表

以上只是代码逻辑,并不具有可执行性

解决过程

接下来的问题是如何建立这样一个index_dict

首先我们要建立name–企业名称、irregularities违法行为…类似的映射关系

A方案

1
item_dict = {"name": "企业名称", "irregularities": "违法行为", "punishment_text": "处罚内容", "department": "处罚决定机关名称", "public_date": "处罚决定日期", "number": "处罚决定书文号"}

我们的代码逻辑大概如下:

1
2
3
4
5
for header in table_header:
for key in item_dict:
if header == item_dict[key]:
index_dict[key] = table_header.index(header)
break
  • table_header表示表格的头部,用列表表示
  • table_header.index(header)意为获取header在table_header中的索引值

B方案

1
2
item_dcit = {"企业名称": "name", "违法行为": "irregularities", "处罚内容": "punishment_text",
"处罚决定机关名称": "department", "处罚决定日期": "public_date", "处罚决定书文号": "number"}

相应的代码逻辑为:

1
2
3
for header in table_header:
if header in item_dict:
index_dict[item_dict[header]] = table_header.index(header)

通过以上代码,我们就完成了对index_dict的初始化。

下面又有了一个问题,接下来遇到了一系列的表格,其表头为以下几种:

[“处罚决定书文号”, “公司名称”, “违法行为”, “处罚内容”, “处罚决定机关名称”, “处罚决定日期”]

[“处罚决定文书号”, “单位名称”, “违法行为”, “处罚结果”, “处罚决定机关名称”, “处罚决定日期”]

[“处罚决定文书号”, “公司名称”, “违法事实”, “处罚结果”, “处罚机关名称”, “行政处罚决定日期”]

很明显,我们上面的方式失效了,对于这几种表头形式,会出现漏掉一些字段索引的情况。

我们对item_dict进行改进:

A方案的改进

1
item_dict = {"name": ["企业名称", "公司名称", "单位名称"], "irregularities": ["违法行为", "违法事实"], "punishment_text": ["处罚内容", "处罚结果"], "department": ["处罚决定机关名称", "处罚机关名称"], "public_date": ["处罚决定日期", "行政处罚决定日期"], "number": "处罚决定书文号", "处罚决定文书号"}

相应的代码逻辑为:

1
2
3
4
5
for header in table_header:
for key in item_dict:
if header in item_dict[key]:
index_dict[key] = table_header.index(header)
break

B方案的改进

1
item_dict = {"企业名称": "name", "公司名称": "name", "单位名称": "name", "违法行为": "irregularities", "违法事实": "irregularities", "处罚内容": "punishment_text", "处罚结果": "punishment_text", "处罚决定机关名称": "department", "处罚机关名称": "department", "处罚决定日期": "public_date", "行政处罚决定日期": "public_date", "处罚决定书文号": "number", "处罚决定文书号": "number"}

相应的代码逻辑为:

1
2
3
for header in table_header:
if header in item_dict:
index_dict[item_dict[header]] = table_header.index(header)

相比较B方案改进前,代码逻辑并没有改变;并且与A方案比较,B方案少一层循环,应该更好

通过改进之后,就可以解决实质上是同样的含义但是在表头中表述不太一样的情况,例如将name字段对应的所有表述可能都添加到我们的item_dict中,如"name": ["企业名称", "公司名称", "单位名称", "公司名", "相关公司"](A方案),"企业名称": "name", "公司名称": "name", "单位名称": "name", "公司名": "name", "相关公司": "name"(B方案)的形式。

思考:这种方式是否还存在问题,是否还有可以改进的空间?

问题:我们要解析的表格非常多,但是我们并不确定name字段对应的所有表述可能,我们只能不断测试发现了新的表述方式,然后将其添加到item_dict中,这中间也是有点麻烦的;原因就在于我们在比较的时候用的完全等于,考虑是否可以用一种包含的关系

例如,我们知道name字段对应的有”企业名称”, “公司名称”, “单位名称”, “公司名”, “相关公司”等,我们发现其都包含比较重要的几个词,”公司“和”企业“和”单位“,所以当我们判断表头中含有”公司“或”名称“或”单位“时,便可认为其对应name字段,但是前提是其他表头不能含有这两个词。所以,使用包含关系的方式,要求我们能找到每个字段的表头表述方式的最核心并且又独特的内容

A方案的进一步改进

1
item_dict = {"name": ["企业", "公司", "单位"], "irregularities": ["违法"], "punishment_text": ["处罚内容", "处罚结果"], "department": ["机关名称"], "public_date": ["处罚决定日期"], "number": ["决定书文号", "决定文书号"]}

相应的代码逻辑为:

1
2
3
4
5
6
7
8
9
10
for header in table_header:
for key in item_dict:
flag = 0
for value in item_dict[key]:
if value in header:
index_dict[key] = table_header.index(header)
flag = 1
break
if flag:
break

B方案的进一步改进:

1
item_dict = {"名称": "name", "公司": "name", "违法": "irregularities", "处罚内容": "punishment_text", "处罚结果": "punishment_text", "机关名称": "department", "处罚决定日期": "public_date", "决定书文号": "number", "决定文书号": "number"}

相应的代码逻辑为:

1
2
3
4
5
for header in table_header:
for key in item_dict:
if key in header:
index_dict[item_dict[key]] = table_header.index(header)
break

整体看上去,B方案确实比A方案逻辑上更简单些

需要注意的是,这里进一步改进的方案,认真思考可以看出,是包含没改进之前的那种方案的,即使没有在item_dict中写上被包含的内容,而是写的全部内容也是可以的,所以使用起来更加灵活

以下附上提取网页表格的完整代码:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from bs4 import BeautifulSoup


class PunishmentItem(object):
__slots__ = (
"name", # 行政相对人名称
"irregularities", # 违规行为
"punishment_text", # 处罚内容
"department", # 处罚决定机关名称
'public_date', # 时间
'number', # 文号
)


class TableHeader(object):

def __init__(self):
self.index_dict = {"name": -1, "irregularities": -1, "punishment_text": -1, "department": -1, "public_date": -1,
"number": -1}
self.item_dict_A = {"name": ["企业", "公司", "单位"], "irregularities": ["违法"], "punishment_text": ["处罚内容", "处罚结果"],
"department": ["机关名称"], "public_date": ["处罚决定日期"],
"number": ["决定书文号", "决定文书号"]}

self.item_dict_B = {"企业": "name", "公司": "name", "单位": "name", "违法": "irregularities", "处罚内容": "punishment_text",
"处罚结果": "punishment_text", "机关名称": "department", "处罚决定日期": "public_date",
"决定书文号": "number", "决定文书号": "number"}

def init_index_a(self, table):
soup = BeautifulSoup(table, 'lxml')
tr_list = soup.select('tr')
td_list = tr_list[0].select('td')
for td in td_list:
header = td.getText().replace(' ', '').strip()
for key in self.item_dict_A:
flag = 0
for value in self.item_dict_A[key]:
if value in header:
self.index_dict[key] = td_list.index(td)
flag = 1
break
if flag:
break
for tr in tr_list:
if tr_list.index(tr) == 0:
continue
td_list = tr.select('td')
item = PunishmentItem()
for key in self.index_dict:
setattr(item, key, td_list[self.index_dict[key]].getText().replace(' ', '').strip())
# print(getattr(item, key))
yield item

def init_index_b(self, table):
soup = BeautifulSoup(table, 'lxml')
tr_list = soup.select('tr')
td_list = tr_list[0].select('td')
for td in td_list:
header = td.getText().replace(' ', '').strip()
for key in self.item_dict_B:
if key in header:
self.index_dict[self.item_dict_B[key]] = td_list.index(td)
break
for tr in tr_list:
if tr_list.index(tr) == 0:
continue
td_list = tr.select('td')
item = PunishmentItem()
for key in self.index_dict:
setattr(item, key, td_list[self.index_dict[key]].getText().replace(' ', '').strip())
# print(getattr(item, key))
yield item


if __name__ == "__main__":
test = TableHeader()
table_html = """<table border="1" bordercolor="#000000" cellpadding="2" cellspacing="0" style="width: 680px; border-collapse: collapse">
<tbody>
<tr>
<td><p style="text-align: center">企业名称</p> </td>
<td><p style="text-align: center">行政处罚决定书文号</p> </td>
<td><p style="text-align: center">违法行为类型</p> </td>
<td><p style="text-align: center">行政处罚内容</p> </td>
<td style="width: 80px"><p style="text-align: center">作出行政处罚决定机关名称</p> </td>
<td style="width: 80px"><p style="text-align: center">作出行政处罚决定日期</p> </td>
</tr>
<tr>
<td style="text-align: center">交通银行股份有限公司(简称交通银行)</td>
<td><p style="text-align: center">银反洗罚决字〔2018〕1号</p> </td>
<td style="text-align: center">未按照规定履行客户身份识别义务、未按照规定报送大额交易报告或者可疑交易报告、与身份不明的客户进行交易或者为客户开立匿名账户、假名账户</td>
<td style="text-align: center">2017年1月1日至2017年6月30日,交通银行未按照规定履行客户身份识别义务,根据《中华人民共和国反洗钱法》第三十二条第(一)项规定,处以50万元罚款;未按照规定报送大额交易报告或者可疑交易报告,根据《中华人民共和国反洗钱法》第三十二条第(三)项规定,处以40万元罚款;存在与身份不明的客户进行交易的行为,根据《中华人民共和国反洗钱法》第三十二条第(四)项规定,处以40万元罚款;对交通银行合计处以130万元罚款,并根据《中华人民共和国反洗钱法》第三十二条第(一)项、第(三)项和第(四)项规定,对相关责任人共处以8万元罚款。</td>
<td><p style="text-align: center">中国人民银行</p> </td>
<td style="text-align: center">2018年7月26日</td>
</tr>
<tr>
<td style="text-align: center">平安银行股份有限公司(简称平安银行)</td>
<td style="text-align: center">银反洗罚决字〔2018〕2号</td>
<td style="text-align: center">未按照规定履行客户身份识别义务、未按照规定保存客户身份资料和交易记录、未按照规定报送大额交易报告或者可疑交易报告</td>
<td style="text-align: center">2017年1月1日至2017年6月30日,平安银行未按照规定履行客户身份识别义务,根据《中华人民共和国反洗钱法》第三十二条第(一)项规定,处以50万元罚款;未按照规定保存客户身份资料和交易记录,根据《中华人民共和国反洗钱法》第三十二条第(二)项规定,处以40万元罚款;未按照规定报送大额交易报告或者可疑交易报告,根据《中华人民共和国反洗钱法》第三十二条第(三)项规定,处以50万元罚款;对平安银行合计处以140万元罚款,并根据《中华人民共和国反洗钱法》第三十二条第(一)项、第(二)项和第(三)项规定,对相关责任人共处以14万元罚款。</td>
<td><p style="text-align: center">中国人民银行</p> </td>
<td style="text-align: center">2018年7月26日</td>
</tr>
<tr>
<td style="text-align: center">上海浦东发展银行股份有限公司(简称浦发银行)</td>
<td style="text-align: center">银反洗罚决字〔2018〕3号</td>
<td style="text-align: center">未按照规定履行客户身份识别义务、未按照规定保存客户身份资料和交易记录、未按照规定报送大额交易报告或者可疑交易报告、与身份不明的客户进行交易</td>
<td style="text-align: center">2017年1月1日至2017年6月30日,浦发银行未按照规定履行客户身份识别义务,根据《中华人民共和国反洗钱法》第三十二条第(一)项规定,处以50万元罚款;未按照规定保存客户身份资料和交易记录,根据《中华人民共和国反洗钱法》第三十二条第(二)项规定,处以30万元罚款;未按照规定报送大额交易报告或者可疑交易报告,根据《中华人民共和国反洗钱法》第三十二条第(三)项规定,处以50万元罚款;存在与身份不明的客户进行交易的行为,根据《中华人民共和国反洗钱法》第三十二条第(四)项规定,处以40万元罚款;对浦发银行合计处以170万元罚款,并根据《中华人民共和国反洗钱法》第三十二条第(一)项、第(二)项、第(三)项和第(四)项规定,对相关责任人共处以18万元罚款。</td>
<td><p style="text-align: center">中国人民银行</p> </td>
<td style="text-align: center">2018年7月26日</td>
</tr>
<tr>
<td style="text-align: center">中国银河证券股份有限公司(简称银河证券)</td>
<td style="text-align: center">银反洗罚决字〔2018〕4号</td>
<td style="text-align: center">未按照规定履行客户身份识别义务、与身份不明的客户进行交易</td>
<td style="text-align: center">2016年1月1日至2017年6月30日,银河证券未按照规定履行客户身份识别义务,根据《中华人民共和国反洗钱法》第三十二条第(一)项规定,处以50万元罚款;存在与身份不明的客户进行交易的行为,根据《中华人民共和国反洗钱法》第三十二条第(四)项规定,处以50万元罚款;对银河证券合计处以100万元罚款,并根据《中华人民共和国反洗钱法》第三十二条第(一)项和第(四)项规定,对相关责任人共处以6万元罚款。</td>
<td><p style="text-align: center">中国人民银行</p> </td>
<td style="text-align: center">2018年7月26日</td>
</tr>
<tr>
<td style="text-align: center">中国人寿保险股份有限公司(简称中国人寿)</td>
<td style="text-align: center">银反洗罚决字〔2018〕5号</td>
<td style="text-align: center">未按照规定保存客户身份资料和交易记录、未按照规定报送大额交易报告或者可疑交易报告</td>
<td style="text-align: center">2015年7月1日至2016年6月30日,中国人寿未按照规定保存客户身份资料和交易记录,根据《中华人民共和国反洗钱法》第三十二条第(二)项规定,处以30万元罚款;未按照规定报送大额交易报告或者可疑交易报告,根据《中华人民共和国反洗钱法》第三十二条第(三)项规定,处以40万元罚款;对中国人寿合计处以70万元罚款,并根据《中华人民共和国反洗钱法》第三十二条第(二)项和第(三)项规定,对相关责任人共处以6万元罚款。</td>
<td><p style="text-align: center">中国人民银行</p> </td>
<td style="text-align: center">2018年7月26日</td>
</tr>
</tbody>
</table>
"""
test.init_index_a(table_html)
test.init_index_b(table_html)