使用 BeautifulSoup 处理 kindle 导出的 HTML 笔记
- Python
- 2016-10-06
- 120热度
- 0评论
导航
问题描述
我用手机端的 kindle App 看完一本书,标注了一些重要句子和段落,想整理一下发表到博客上,于是使用电子邮件方式导出了 HTML 格式的笔记。但是遇到了问题:1、原文件中包含了 CSS 代码,无法直接复制到 WordPress 的文章编辑页中(会被自动去掉),格式很乱;2、某些笔记项是空的,需要把这些多余部分去掉,如果手动删除这些零碎的 HTML 代码,会非常麻烦。
于是,我准备写一段 Python 程序,将笔记文件转换为纯 HTML 文档(去掉 CSS,仍然保持结构清晰),并且去掉多余的空白笔记项。
这个程序中,主要会用到 BeautifulSoup 模块,我曾在爬虫项目中用过,非常适合处理 HTML 和 XML 文件。这里是 Beautiful 的官方中文文档。
本程序中,以处理《我敢在你怀里孤独》的笔记为例,且此 HTML 文件位于脚本文件的同级目录下。
下面开始记录程序编写步骤。最终完整程序见文章底部。
1、构建程序框架
第一步,写好程序框架,读取原笔记文件的内容,封装成 soup 对象。打印 soup 对象,确认一切正常。
#!/usr/bin/env python
# encoding: utf-8
from bs4 import BeautifulSoup
# 防止中文乱码
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
def main():
filename = '我敢在你怀里孤独 - Notebook.html'
with open(filename) as f:
filecontent = f.read()
soup = BeautifulSoup(filecontent, 'lxml')
print soup
if __name__ == '__main__':
main()
2、提取关键信息
观察 HTML 代码可以发现,笔记信息都包含在 <div class="bodyContainer"> 标签下,所以我们只关心这个标签下的内容。把上面获取 soup 对象的语句改为
soup = BeautifulSoup(filecontent, 'lxml').find('div', {'class': 'bodyContainer'})
运行一下,发现包含 CSS 代码的无用部分都已经不见了。输出的结果大概是这个样子(只贴出了一部分):
<div class="bodyContainer"> <div class="notebookFor"> 笔记本导出 </div> <div class="bookTitle"> 我敢在你怀里孤独 </div> <div class="authors"> 刘若英 </div> <div class="citation"> </div> <hr/> <div class="sectionHeading"> 推荐序 全在一杯里 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 27 </div> <div class="noteText"> 我喜欢刘若英,不是她某一个阶段,而是整场花开的过程。读这本书,奶茶只有一杯,冷冷热热,醇醇淡淡,全在一杯里。 </div><div class="sectionHeading"> 推荐序 孤独力──情感高度成熟的指标 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 67 </div> <div class="noteText"> 一般的观念里,孤独这个字让我们想到的是悲伤、无奈、无助……负面的情绪。然而温尼科特所讲的自在独处,特别是在别人面前还是可以留在自己的孤独里的能力,反而是一个人情感高度成熟的指标。 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 68 </div> <div class="noteText"> </div><div class="sectionHeading"> 我还想要继续,这样矛盾的人生! </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 127 </div> <div class="noteText"> 事实上,以事情的本质来说,这世上没有所谓“平凡”的事。 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 128 </div> <div class="noteText"> 事情只有“多数人做”或是“少数人做”,“做得到”或是“做不到”,“愿意做”或是“不愿意做”的差别而已。结婚生子这件事,也许符合了“多数人做”、“愿意做”,而我刚好也“做得到”而已。这件对大部分人来说(也许)算是稀松平常的事,却有可能是我生命中将面临的最大挑战。因为结婚、生子,对我来说是“最最不平凡,也最最具有挑战的事情”。 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 144 </div> <div class="noteText"> 人不会真心羡慕自己从未真正感受过的事物。 </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 144 </div> <div class="noteText"> </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 169 </div> <div class="noteText"> </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 169 </div> <div class="noteText"> </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 171 </div> <div class="noteText"> </div><div class="noteHeading"> 标注(<span class="highlight_yellow">黄色</span>) - 位置 174 </div> <div class="noteText"> </div>
还是比较乱,需要进一步处理。
3、提取有用信息
上面代码中的有用的 div 有:
- bookTitle,书名
- authors,作者
- sectionHeading,区块标题,(整理为 h4)
- noteText,具体的笔记项(空白项除外),(整理为有序列表)
noteHeading 的内容我不需要,可以忽略。
书名和作者信息很容易得到:
bookTitle = '《%s》' % soup.find('div', {'class': 'bookTitle'}).string.strip()
authors = soup.find('div', {'class': 'authors'}).string.strip()
info = 'bookTitle: %s
\nauthor(s): %s
' % (bookTitle, authors)
print info
难点在于如何获取区块标题和笔记内容(其实也没有多难)。我的想法是,遍历所有的 div,把感兴趣的内容格式化之后,存到一个叫做 notes 的字符串变量里。代码如下:
notes = ''
for iterDiv in soup.find_all('div'):
# 排除空项
if iterDiv.text.isspace():
pass
# 注意,iterDiv['class'] 是一个列表
elif iterDiv['class'][0] == 'sectionHeading':
# 从第二次开始,每次遇到 sectionHeading,都要加上有序列表的结尾标签
if notes != '':
notes += ' </ol>\n'
notes += '<h4>%s</h4>\n <ol>\n' % iterDiv.text.strip()
elif iterDiv['class'][0] == 'noteText':
notes += ' <li>%s</li>\n' % iterDiv.text.strip()
notes += ' </ol>\n'
print notes
运行之后,一切正常。
4、输出结果
现在把结果组合一下,然后输出为新的 html 文件。
with open('notesOutput.html', 'w') as f:
finalContent = '%s\n<hr/>%s' % (info, notes)
f.write(finalContent)
print '输出完毕'
输出结果大致如下(以下为部分内容):
bookTitle: 《我敢在你怀里孤独》<br/>
author(s): 刘若英<br/>
<hr/><h4>推荐序 全在一杯里</h4>
<ol>
<li>我喜欢刘若英,不是她某一个阶段,而是整场花开的过程。读这本书,奶茶只有一杯,冷冷热热,醇醇淡淡,全在一杯里。</li>
</ol>
<h4>推荐序 孤独力──情感高度成熟的指标</h4>
<ol>
<li>一般的观念里,孤独这个字让我们想到的是悲伤、无奈、无助……负面的情绪。然而温尼科特所讲的自在独处,特别是在别人面前还是可以留在自己的孤独里的能力,反而是一个人情感高度成熟的指标。</li>
</ol>
<h4>我还想要继续,这样矛盾的人生!</h4>
<ol>
<li>事实上,以事情的本质来说,这世上没有所谓“平凡”的事。</li>
<li>事情只有“多数人做”或是“少数人做”,“做得到”或是“做不到”,“愿意做”或是“不愿意做”的差别而已。结婚生子这件事,也许符合了“多数人做”、“愿意做”,而我刚好也“做得到”而已。这件对大部分人来说(也许)算是稀松平常的事,却有可能是我生命中将面临的最大挑战。因为结婚、生子,对我来说是“最最不平凡,也最最具有挑战的事情”。</li>
<li>人不会真心羡慕自己从未真正感受过的事物。</li>
<li>现在回想起来,祖父母给我的教育重点,并非考试要考几分,或是要如何如何之类的规范,他们给予我很大的自由,但也清楚地告诉我,哪些事不能做,或是哪些事该怎么做,换句话来说,他们在意的是“规矩”、是“教养”。</li>
<li>在规矩的范围内,我可以自由地过自己的生活,就算在人群中,也可以安安静静、人畜无害地独处。我又何必无故逼自己逃亡?</li>
<li>就像突然学会骑脚踏车的快感般,从此我迷恋上一个人的旅行。一直到现在。</li>
<li>从那之后,我一直维持着独居的生活状态二十几年。叔本华曾经说过类似的话,“要么孤独,要么庸俗”,言下之意他非常享受孤独,认为唯有孤独可以带来精彩与伟大。这道理我真的懂得。</li>
<li>在不同的时代,人需要不同的印记,以证明自己达到某种被定义的标准,成为被接受的某种人。</li>
<li>到现在,我并不在意物质上的辛苦,只有自己一个人也无所谓,每天都吃一样的餐点也不在乎,只要生活有趣,那一天的生活就值回票价。</li>
<li>每隔一段时间,我就把在外面的东西搬回家里,那对我来说,也许就是所谓“旅程的完结”。然后在家里,重新打包整装,准备再出发,从这角度来看,家又是“旅程的起点”。这些过程很重要。</li>
<li>若没有“家”这根据地,旅行只是无尽的漂流吧!但对某些人来说,所谓“家”这个地方,只是有个固定收账单、各类信件、包裹的地点。</li>
<li>这也是我的矛盾,我既期待浪迹天涯,又觉得有个固定的家是件重要的事。因为,我们最终都需要有“回去”的地方。</li>
<li>我希望永远握有自己最终的选择权。如同我的人生最重要的一句话“选择我所能承受的”。如果,将自己关在家里算是“自囚”,那也是我自己的选择。只要我想,随时可以释放自己;只要我想,随时可以改变那样的状态。“嘿!我握有主控权喔!”我可以开心地对自己这样说。</li>
<li>当然会有海浪,当然会有黑夜,即便我们能欣赏它的美,也会有孤单,害怕不被了解的时候。别怕,虽然我知道你不怕,因为我们都会陪伴你,不管有声无声的。</li>
<li>我知道你不怕,因为你清楚世界的变化,而你总保留了一块没有变,最纯粹的初衷与梦想。</li>
</ol>
<h4>请不要在我身边灵魂出窍X卢广仲</h4>
<ol>
<li>在MSN的年代,我们可以用文字喝酒聊天聊整晚,喝到醉躺卧榻到天亮。MSN当时的内容比较深刻,看着绿灯的闪烁,等待文字的出现,甚至从对方的昵称、反应时间去猜测想象对方的状态,却又保有自己。比现在任何一种通讯方式都浪漫。广仲说,现在就算是因为工作需求装了LINE,但还是坚持关成静音,好让他不会时时被讯息干扰。想要看手机的时候再看,保有自己对接收讯息的主导权。</li>
<li>“我们常常忘了自己是人,不是讯息接收器”,网络、脸书、媒体,接收讯息的时间永远都不够,感觉很热闹,“当我发现我是孤独的时候,反而是种很好的状态,孤独可以让你更强壮。”广仲说。就因为现在人和人之间的连结太多元也太频繁,独处反倒变得珍贵,成了意识上得一直去寻找的一种平静。</li>
至此,一开始的目标已经实现了。整个程序源码如下:
#!/usr/bin/env python
# encoding: utf-8
from bs4 import BeautifulSoup
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
def main():
filename = '我敢在你怀里孤独 - Notebook.html'
with open(filename) as f:
filecontent = f.read()
soup = BeautifulSoup(filecontent, 'lxml').find('div', {'class': 'bodyContainer'})
# print soup
# 获取书名与作者信息
bookTitle = '《%s》' % soup.find('div', {'class': 'bookTitle'}).text.strip()
authors = soup.find('div', {'class': 'authors'}).text.strip()
info = 'bookTitle: %s<br/>\nauthor(s): %s<br/>' % (bookTitle, authors)
print info
# 获取笔记列表
notes = ''
for iterDiv in soup.find_all('div'):
# 排除空项
if iterDiv.text.isspace():
pass
# 注意,iterDiv['class'] 是一个列表
elif iterDiv['class'][0] == 'sectionHeading':
# 从第二次开始,每次遇到 sectionHeading,都要加上有序列表的结尾标签
if notes != '':
notes += ' </ol>\n'
notes += '<h4>%s</h4>\n <ol>\n' % iterDiv.text.strip()
elif iterDiv['class'][0] == 'noteText':
notes += ' <li>%s</li>\n' % iterDiv.text.strip()
notes += ' </ol>\n'
# print notes
with open('notesOutput.html', 'w') as f:
finalContent = '%s\n<hr/>%s' % (info, notes)
f.write(finalContent)
print '输出完毕'
if __name__ == '__main__':
main()
5、更进一步
为了方便以后的使用,本程序还可以改进。感兴趣的朋友可以做进一步研究,我偷懒不折腾了。比如:
- 现在源文件路径是写死在程序中的,可改为从命令行获取参数
- 利用 alias 把程序调用做成一个自定义命令
- ……