提出问题
当我们写爬虫进行数据结构化时,经常遇到一些表格数据,无论是web表格还是Excel
、Word
or PDF
中的表格,都很常见;如果表格非常规范化,我们在做结构化时可以说非常省事,一般认为某列对应我们的某字段值即可;但是,大多数时候,表格往往写的比较随意,某一字段值对应的数据并不是在固定的列。
举个例子,表格A对应的表头如下(这里用列表表示):
[“企业名称”, “处罚决定书文号”, “违法行为”, “处罚内容”, “处罚决定机关名称”, “处罚决定日期”]
相应的我们有一个python对象,其字段值为:
1 | class PunishmentItem(object): |
于是我们在提取的时候,理想的方式是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 | item = PunishmentItem() |
table.row_len
表示表格的行数row_values
表示表格第i行所有单元格的值,其结构为列表
以上只是代码逻辑,并不具有可执行性
解决过程
接下来的问题是如何建立这样一个index_dict
:
首先我们要建立name
–企业名称、irregularities
–违法行为
…类似的映射关系
A方案
1 | item_dict = {"name": "企业名称", "irregularities": "违法行为", "punishment_text": "处罚内容", "department": "处罚决定机关名称", "public_date": "处罚决定日期", "number": "处罚决定书文号"} |
我们的代码逻辑大概如下:
1 | for header in table_header: |
table_header
表示表格的头部,用列表表示table_header.index(header)
意为获取header在table_header中的索引值
B方案
1 | item_dcit = {"企业名称": "name", "违法行为": "irregularities", "处罚内容": "punishment_text", |
相应的代码逻辑为:
1 | for header in table_header: |
通过以上代码,我们就完成了对index_dict
的初始化。
下面又有了一个问题,接下来遇到了一系列的表格,其表头为以下几种:
[“处罚决定书文号”, “公司名称”, “违法行为”, “处罚内容”, “处罚决定机关名称”, “处罚决定日期”]
[“处罚决定文书号”, “单位名称”, “违法行为”, “处罚结果”, “处罚决定机关名称”, “处罚决定日期”]
[“处罚决定文书号”, “公司名称”, “违法事实”, “处罚结果”, “处罚机关名称”, “行政处罚决定日期”]
很明显,我们上面的方式失效了,对于这几种表头形式,会出现漏掉一些字段索引的情况。
我们对item_dict
进行改进:
A方案的改进
1 | item_dict = {"name": ["企业名称", "公司名称", "单位名称"], "irregularities": ["违法行为", "违法事实"], "punishment_text": ["处罚内容", "处罚结果"], "department": ["处罚决定机关名称", "处罚机关名称"], "public_date": ["处罚决定日期", "行政处罚决定日期"], "number": "处罚决定书文号", "处罚决定文书号"} |
相应的代码逻辑为:
1 | for header in table_header: |
B方案的改进
1 | item_dict = {"企业名称": "name", "公司名称": "name", "单位名称": "name", "违法行为": "irregularities", "违法事实": "irregularities", "处罚内容": "punishment_text", "处罚结果": "punishment_text", "处罚决定机关名称": "department", "处罚机关名称": "department", "处罚决定日期": "public_date", "行政处罚决定日期": "public_date", "处罚决定书文号": "number", "处罚决定文书号": "number"} |
相应的代码逻辑为:
1 | for header in table_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 | for header in table_header: |
B方案的进一步改进:
1 | item_dict = {"名称": "name", "公司": "name", "违法": "irregularities", "处罚内容": "punishment_text", "处罚结果": "punishment_text", "机关名称": "department", "处罚决定日期": "public_date", "决定书文号": "number", "决定文书号": "number"} |
相应的代码逻辑为:
1 | for header in table_header: |
整体看上去,B方案确实比A方案逻辑上更简单些
需要注意的是,这里进一步改进的方案,认真思考可以看出,是包含没改进之前的那种方案的,即使没有在item_dict
中写上被包含的内容,而是写的全部内容也是可以的,所以使用起来更加灵活。
以下附上提取网页表格的完整代码:
1 | from bs4 import BeautifulSoup |