Python爬虫之为BeautifulSoup添加索引查找

最近在帮金融教授爬取优先股的数据,要求不能过滤掉部分信息缺失的数据并将缺失部分用”N/A”填充。这样一来必须要使用正则表达式将原始数据切成很小片,很不方便,好在有解析利器 BeautifulSoup,但是不知道什么原因BeautifulSoup只能索引到多个同类子节点的第一个节点。不能索引给我造成了极大困扰,有时候甚至还是需要使用纯正则来解析数据。思前想后我决定自己为其添加索引功能以备不时之需。

下面我们通过例子来讲解如何为BeautifulSoup添加:

  • 关键字索引
  • 列表索引

##元数据

这是要摘取几千条数据中的一个

我们来看它的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
In [37]: p
Out[37]: '<tr bgcolor="#CFCFCF">\n\t\t<td>\n\t\t<font face="arial, helvetica, sans-serif" size="2">\n\t\t\t<a
href="search.cfm?tickersymbol=ADK-A&amp;sopt=symbol"><b>ADK-A</b></a><br
/>00650W409\n\t\t</font>\n\t\t</td>\n\n\t\t<td>\n\t\t<font face="arial, helvetica, sans-serif" size="2"><b>AdCare Health
Systems, 10.875% Series A Cumulative Redeemable Preferred Stock</b></font>\n\n\n<font size="2">\n\n\t<br /><font face=""
color="Green">IPO:\xa011/07/12</font>\xa0\xa0\xa0\n\n\n\n\n\t\xa0\xa0\xa0<a
href="http://www.sec.gov/Archives/edgar/data/1004724/000104746912010172/a2211594z424b5.htm" target="SECEDGAR"><b>IPO
Prospectus @ SEC EDGAR</b></a>\n\t\n\t\t\xa0\xa0\xa0Call
Date:\xa012/01/17\n\t</font>\n\n\n\n\n\t\t</td>\n\n\t\t\n\t\t\n\n\t\t\n\t\t\n\t\t\n\n\t\t\n\t\t\n\t\t\n\n\n\n\n<td
align="center">\n\t<font size="2">\n\t\n\t\n\t\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t<a
href="https://www.nyse.com/quote/XASE:ADKpA" target="QUOTE">AMEX</a><br />\n\t\t\t<a
href="https://www.nyse.com/quote/XASE:ADKpA"
target="CHART">Chart</a>\n\t\t\n\n\t\n\n\t</font>\n\n\t\n\n</td>\n\n\n\n\t\t\n\t\t\n\t\t\n\t</tr>'

##数据结构

它的结构应该是这样的

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
<tr>
<td>
<font>
<a>
<b>
ADK-A
</b>
</a>
<br/>
00650W409
</font>
</td>
<td>
<font>
<b>
AdCare Health Systems, 10.875% Series A Cumulative Redeemable Preferred Stock
</b>
</font>
<font>
<br/>
<font>
IPO: 11/07/12
</font>
<a href="http://www.sec.gov/Archives/edgar/data/1004724/000104746912010172/a2211594z424b5.htm"">
<b>
IPO Prospectus @ SEC EDGAR
</b>
</a>
Call Date: 12/01/17
</font>
</td>
<td>
<font>
<a>
AMEX
</a>
<br/>
<a>
Chart
</a>
</font>
</td>
</tr>

对于大多数数据来说,我们完全可以根据这个结构写一个正则表达式来进行抓取,将所需要的内容替换为提取的符号即可进行匹配。但是如果里面某一项内容缺失,比如没有给出IPO日期,则整个表达式不能匹配到该条数据并将其排除在外,造成数据缺失。更不能将缺失的内容进行填补。所以我们先将内容用正则切割成几个小部分(比如按td标签切分成3块),然后再分别用正则匹配。这样很麻烦。

##BeautifulSoup树形结构图解

所以我们有了BeautifulSoup这样的神器,只要输入tr.td.font.a.b就能从前往后索引到‘ADK-A’,这条数据。‘’

但是,不知道为什么,BS只支持第一条个节点的直接索引。比如我想要第二个td标签的第二个b标签,很遗憾,我们并不能使用索引直接链式调用

好在天无绝人之路,BS提供了一种间接方法,迭代,来调用其他的子节点。说白了就是写个循环是可以接触到这些隐藏的节点的。

我们来看下图

实线代表可以直接使用链式调用可以接触到的节点,虚线代表使用迭代可以接触到的节点。实际上G1我们同样可以直接在A下面调用,但是即便这样我们还是不能达到我们想要的。那怎么办呢?曲线救国嘛,好在我们可以遍历整个树,把树上的节点全部摘下来放在我们改过结构的树上。

##使用Hook的树

首先来看我们关键的组件,钩子Hook.它其实本身有点像二叉树或者链表的节点,一头指向父级元素,一头指向子级元素(的集合)。

我们从图中不难看出,hook处在两个BS树节点的中间,其中父级元素指向的节点为自身所代表的BS树节点(因为可以直接调用到),子级元素指向一个分类过后的字典,我们可以通过字典来对类别进行关键字搜索。每一个类别都是一个列表,这样我们可以按顺序排列后按顺序索引。更完整的例子如下图

值得注意的是,父级Hook的子集是一个子级hook的集合,因为hook可以代表本身的BS树节点。

##实现

看图比较复杂,其实代码很简单,首先我们创建一个hook的类

1
2
3
4
5
class Hook():

def __init__(self,root):
self.root = root
self.child = {}
  • self.root 是本身的BS树节点
  • self.child 是一个字典,可以使用关键字索引

然后使用递归,从BS的某一节点开始,将分支的内容转移到新的树上并分类。

1
2
3
4
5
6
7
8
9
10
def load_node(roottag,child=None):
hook = Hook(roottag)
for each in roottag:
if isinstance(each,Tag):
subhook = load_node(each,hook.child)
if each.name in hook.child:
hook.child[each.name].append(subhook)
else:
hook.child[each.name]=[subhook,]
return hook
  • 首先先初始化一个hook实例,并将其“挂”在当前节点
  • 然后迭代子元素,判断是否为Tag(我们需要的节点类型),如果不是就过滤掉
  • 递归调用本函数,将该Tag作为该函数的当前节点,返回一个与之对应的hook
  • 将新的hook添加到我们的字典中

##演示

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
In [27]: test
Out[27]: <__main__.Tode at 0x10e7e32e8>

In [28]: test.child
Out[28]:
{'td': [<__main__.Tode at 0x10e7e3358>,
<__main__.Tode at 0x10e7e3ef0>,
<__main__.Tode at 0x10e7e31d0>]}

In [29]: test.child['td'][0]
Out[29]: <__main__.Tode at 0x10e7e3358>

In [30]: test.child['td'][0].child
Out[30]: {'font': [<__main__.Tode at 0x10e7e3278>]}

In [31]: test.child['td'][0].child['font'][0]
Out[31]: <__main__.Tode at 0x10e7e3278>

In [32]: test.child['td'][0].child['font'][0].child
Out[32]: {'a': [<__main__.Tode at 0x10e7e3d30>], 'br': [<__main__.Tode at 0x10e7e3940>]}

In [33]: test.child['td'][0].child['font'][0].child['a'][0]
Out[33]: <__main__.Tode at 0x10e7e3d30>

In [34]: test.child['td'][0].child['font'][0].child['a'][0].root
Out[34]: <a href="search.cfm?tickersymbol=ADK-A&amp;sopt=symbol"><b>ADK-A</b></a>

In [35]: test.child['td'][0].child['font'][0].root
Out[35]:
<font face="arial, helvetica, sans-serif" size="2">
<a href="search.cfm?tickersymbol=ADK-A&amp;sopt=symbol"><b>ADK-A</b></a><br/>00650W409
</font>

In [36]: test.child['td'][0].root
Out[36]:
<td>
<font face="arial, helvetica, sans-serif" size="2">
<a href="search.cfm?tickersymbol=ADK-A&amp;sopt=symbol"><b>ADK-A</b></a><br/>00650W409
</font>
</td>

嗯,就是这样,还是需要正则提取,但是少了切片会快很多。上面的数据可以作为练习,试着删掉某一块数据

以后会更新部分方法使调用过程更加方便