最近在帮金融教授爬取优先股的数据,要求不能过滤掉部分信息缺失的数据并将缺失部分用"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&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&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&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&sopt=symbol"><b>ADK-A</b></a><br/>00650W409 </font> </td> |
嗯,就是这样,还是需要正则提取,但是少了切片会快很多。上面的数据可以作为练习,试着删掉某一块数据
以后会更新部分方法使调用过程更加方便