使用 BeautifulSoup 处理 kindle 导出的 HTML 笔记

问题描述

我用手机端的 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 把程序调用做成一个自定义命令
  • ……