EPUB弹出窗口式脚注
网上搜到一些国学典籍的EPUB版,虽有古人的注解,但正文和注解混排在一起,当我只想迅速读正文的时候比较碍眼。于是研究了一下 EPUB3 中有关脚注(footnote)的规格定义,写了一个 Python 脚本把所有混在正文中的脚注全部改写成了弹出窗口样式,在 iBooks 里测试通过,略记一笔。
什么是EPUB弹出窗口式脚注
弹出式脚注是 EPUB3 推出的,简单的说就是正文中加一个链接锚点,对应一个脚注模块,点击链接的时候,脚注内容会直接以弹出窗口的形式显示出来。这样就省去了页面跳转这个步骤,更加方便阅读。
一图胜千言,下图是脚本处理后的《三国志》(这个还是混排版的)在 iPad 版 iBooks 下的效果。(原本是像日文电子书那样的竖排EPUB,我把 CSS 里和竖排相关的定义注释掉了)
如何实现EPUB弹出窗口式脚注
要实现这种效果,有三个注意点。
1.正文中的链接锚点。

<p>
太祖武皇帝,沛國譙人也,姓曹,諱操,字孟德,漢相國參之後。
<a epub:type="noteref" href="#fn1">
<sup>1</sup>
</a>
桓帝世,曹騰為中常侍大長秋,封費亭侯。
......
</p>

2.脚注aside模块
<aside epub:type="footnote" id="fn1">
〔曹瞞傳曰:太祖一名吉利,小字阿瞞。王沈魏書曰:其先出於黃帝。當高陽世,陸終之子曰安,是為曹姓。周武王克殷,存先世之後,封曹俠於邾。春秋之世,與於盟會,逮至戰國,為楚所滅。子孫分流,或家於沛。漢高祖之起,曹參以功封平陽侯,世襲爵士,絶而復紹,至今適嗣國於容城。〕
</aside>
在 iBooks 下,如果 epub:type
属性的值为 footnote
,这个 aside
会默认隐藏。只有对应的链接被点击时,其内容才会在弹出窗口中显示。
3.epub 命名空间(namespace)。
上面两处都有一个共同的属性名,epub:type
。一般 EPUB 文档都没有定义 epub 这个命名空间,所以满足以上两点之后直接打开会提示 epub 命名空间没有定义。EPUB 定义 namespace 有两种方式,一种是在 CSS 里定义,一种是在内容页的HTML标签里定义。我测试过,iBooks 无法识别 CSS 里定义的 namespace,所以我采用了另外一种方式。
<html xml:lang="zh-CN" xmlns="http://www.w3.org/1999/xhtml" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:epub="http://www.idpf.org/2007/ops">
用Python脚本处理EPUB的HTML文档
了解了这些基本概念之后,再来看要处理的对象。原EPUB文档中注解夹杂在正文中,以span
标签标记,所以 Python 脚本的基本流程就比较清楚了,这里使用 BeautifulSoup 来解析并更改 HTML 文档树。
- 循环读入所有 EPUB 内容文档并解析
- 给
html
标签加上 epub 命名空间定义 - 获取
p
标签下的 所有span
标签 - 遍历获取的
span
标签,取出文本,并以此创建aside
模块 - 清除
span
标签的内容,更改为链接锚点
原 EPUB 中的 HTML 文档节选 :

<p>太祖武皇帝,沛國譙人也,姓曹,諱操,字孟德,漢相國參之後。
<span class="zhushi">
〔曹瞞傳曰:太祖一名吉利,小字阿瞞。王沈魏書曰:其先出於黃帝。當高陽世,陸終之子曰安,是為曹姓。周武王克殷,存先世之後,封曹俠於邾。春秋之世,與於盟會,逮至戰國,為楚所滅。子孫分流,或家於沛。漢高祖之起,曹參以功封平陽侯,世襲爵士,絶而復紹,至今適嗣國於容城。〕
</span>
桓帝世,曹騰為中常侍大長秋,封費亭侯。
......
</p>

在 OS X 版 iBooks 中的显示效果,正文注释混排。
详细处理方式见下面的 Python 代码 :

# -*- coding: utf-8 -*- from bs4 import BeautifulSoup
import os epub_content_path = 'E:\MachineLearning\sanguozhi_bak.epub\OEBPS\Text'
#zizhitongjian_path='E:\MachineLearning\zizhitongjian.epub\OEBPS\Text' #for f in os.listdir(zizhitongjian_path):
for f in os.listdir(epub_content_path):
#html = os.path.join(zizhitongjian_path,f)
html = os.path.join(epub_content_path,f)
print html
doc = open(html,'rb')
soup = BeautifulSoup(doc) #如果没有HTML标签里没有定义epub namespace,则加上
if not 'xmlns:epub' in soup.html.attrs:
soup.html['xmlns:epub'] = "http://www.idpf.org/2007/ops" #没有使用soup.find_all('span')是为了略过<span>中内嵌<span>的情况
#???假如有嵌套的怎么办呢?
#因为原文里面是每一句话后面都有一个注释(zhushi)
notes = soup.select('p > span') #如果没有找到span标签,进入下一个循环,也就是进入下一个html文件
if not notes:
continue
#反序循环notes 列表
#len(notes)统计有多少个zhushi(note)
for n in range(len(notes)-1, -1, -1):
#n是列表index,number是实际注释序号
note = notes[n]
number = n+1 footnote = soup.new_tag('aside')
footnote['epub:type'] = 'footnote'
footnote['id'] = 'fn%d' % number
footnote.string = note.get_text() #-----------------------change by cici-------------------------------------------------
#-----------------------add a new tag:脚注前缀标识----------------------------------
##需要在脚注前面加上这句话,可以跳转到原文里面去<a href="#fns1">[1]</a>
##<a href="#fns1">[1]</a>
#change by cici
footnotesup = soup.new_tag('a')
footnotesup['href'] = '#fns%d' % number
footnotesup.string = '['+str(number)+']'
#--------------------------------------------------------------------------------------- #---------------------------在注释的前边假如数字[1]--------------------------------
#下面这句是在footnote的string前面加入一个tag footnotesup
footnote.string.insert_before(footnotesup);
#--------------------------------------------------------------------------------------- #为了保证aside模块是按数字顺序逐一插入到段落之后,所以反序读取notes列表
note.parent.insert_after(footnote) #---------------------------------------------------------------------------------------
#note.parent.insert_after(footnotesup)
#--------------------------------------------------------------------------------------- note.clear()
note.name = "a"
del note['class']
note['epub:type'] = 'noteref'
note['href'] = '#fn%d' % number
#自己修改对上标假如id
note['id'] = 'fns%d' % number
sup = soup.new_tag('sup')
sup.string = str(number)
note.append(sup) #print soup.prettify()
doc.close()
doc = open(html,'wb')
doc.write(str(soup))
doc.close()

注释锚点的美化
为了让链接锚点看起来美观一点,我顺手在CSS里给sup
添加了几个定义。其中 text-indent
是为了重置原CSS代码中 p
标签中的定义,其他的就没什么好说的了。

sup {
font-family: Arial;
font-size: 0.5em;
color:#FFF;
background-color: #333;
display: inline-block;
border-radius:0.25em;
/* reset text-indent */
text-indent: 0;
padding:0 0.5em;
box-shadow: 0px 1px 1px #333;
text-shadow: 0 -1px 0 #333;
}

其实在 epub 的 CSS 里定义颜色是一件不太好的事,以 iBooks 为例,主题分纯白、棕褐、夜间三种模式,如果 hardcode 颜色,主题变更时颜色不随之变化就会很难看。不过 iBooks 也似乎没有提供一个办法来解决这种矛盾,所以作罢。
iBooks 对 EPUB3 标准的支持
iBooks 对 EPUB3 的支持也并不完全,除了上文提到的CSS命名空间之外,aside
的 CSS 样式 iBooks 也不支持,此外还有很多槽点。好像这也是苹果的一贯风格——把现有的处于上升趋势的技术拿来为我所用,然后搞一个私有的变种出来,至于标准,就随便随便啦。
苹果现在对 iBooks 似乎也不是很上心了,可能在电子出版方面遇到的阻力很大,没有帮主的现实扭曲立场,在可见的未来也不太可能复制当年在音乐出版上的成功,于是 iBooks 的臭虫一堆也没人修复,新功能也不见有什么添加,似乎已经很久没有更新了。
可惜现在网上流传的四书五经、二十四史之类的 EPUB 制作良莠不齐,HTML定义也不尽相同,所以没法弄一个通用的脚本出来,只能见招拆招。
注意事项:
- 在制作这个之前要求电脑上已经安装了Python2.6(或2.7),没有测试Python3.4版本,并且已经安装了BeautifulSoup库。
- 因为EPUB属于一种压缩文件,需要先将EPUB文件加上".zip"后缀,然后用Winrar或7zip将其解压成***.epub文件夹,再进行代码里面的操作,不然的话,Python程序不能打开EPUB压缩文件。最后用Python程序处理完成之后,同样的过程用7zip将几个文件夹打包压缩成zip压缩包(EPUB阅读器不识别rar压缩的格式),然后将后缀“.zip”去掉就可以了。
对于原作者提供的代码和文件做了一定的修改,现在讲修改的部分贴出来。
1.第43-55行,在脚注的文本前面加上了<a href="#fns1">[1]</a>,实际显示为[1]。点击可以跳转到脚注对应的原文。

#-----------------------change by cici-------------------------------------------------
#-----------------------add a new tag:脚注前缀标识----------------------------------
##需要在脚注前面加上这句话,可以跳转到原文里面去<a href="#fns1">[1]</a>
##<a href="#fns1">[1]</a>
#change by cici
footnotesup = soup.new_tag('a')
footnotesup['href'] = '#fns%d' % number
footnotesup.string = '['+str(number)+']'
#--------------------------------------------------------------------------------------- #---------------------------在注释的前边假如数字[1]--------------------------------
#下面这句是在footnote的string前面加入一个tag footnotesup
footnote.string.insert_before(footnotesup);

2.第70-71行,加上了上标的id,脚注可以通过这个id跳转到原文中。
#自己修改对上标假如id
note['id'] = 'fns%d' % number
参考文章:
后记:效果图
最后修改完成图书《三國志-陳壽》(共享密码:17ib),下图是在Windows+Calibre上现实的效果图。
后续增加:
发现多看的手机和Kindle版本的多看是可以显示脚注的,可能和EPUB3.0的实现方式不同,就跟iBOOKS是类似的,不过这样的确是增加了一个选择的机会。
在博客园非官方月刊这篇文章中作者制作的电子书的确是可以显示脚注的,而且显示的效果还是不错的,多看是目前在国内发现的性能和体验最好的电子书阅读器APP。
多看也在自己的官方论坛贴出了关于如何制作带有脚注EPUB电子书的方法:多看电子书规范扩展开放计划
显示效果如下图(在Android手机平台):
EPUB弹出窗口式脚注的更多相关文章
- OAF_开发系列08_实现OAF通过Popup参数式弹出窗口(案例)
20150711 Created By BaoXinjian
- JS弹出窗口代码大全(详细整理)
1.弹启一个全屏窗口 复制代码代码如下: <html> <body http://www.jb51.net','脚本之家','fullscreen');">; < ...
- jQuery弹出窗口完整代码
jQuery弹出窗口完整代码 效果体验:http://keleyi.com/keleyi/phtml/jqtexiao/1.htm 1 <!DOCTYPE html PUBLIC "- ...
- Android Demo---实现从底部弹出窗口
在前面的博文中,小编简单的介绍了如何制作圆角的按钮以及圆角的图片,伴着键盘和手指之间的舞步,迎来新的问题,不知道小伙伴有没有这样的经历,以App为例,点击头像的时候,会从底部弹出一个窗口,有从相册中选 ...
- tkinter 弹出窗口 传值回到 主窗口
有些时候,我们需要使用弹出窗口,对程序的运行参数进行设置.有两种选择 一.标准窗口 如果只对一个参数进行设置(或者说从弹出窗口取回一个值),那么可以使用simpledialog,导入方法: from ...
- Java Selenium (十二) 操作弹出窗口 & 智能等待页面加载完成 & 处理 Iframe 中的元素
一.操作弹出窗口 原理 在代码里, 通过 Set<String> allWindowsId = driver.getWindowHandles(); 来获取到所有弹出浏览器的句柄, 然 ...
- Python tkinter模块弹出窗口及传值回到主窗口操作详解
这篇文章主要介绍了Python tkinter模块弹出窗口及传值回到主窗口操作,结合实例形式分析了Python使用tkinter模块实现的弹出窗口及参数传递相关操作技巧,需要的朋友可以参考下 本文实例 ...
- jQuery弹出窗口浏览图片
效果预览:http://keleyi.com/keleyi/phtml/jqtexiao/3.htm HTML文件代码: <!DOCTYPE HTML> <html> < ...
- EasyUI弹出窗口实例
效果体验:http://hovertree.com/texiao/jeasyui/1.htm 源代码下载:HovertreeJEasyUI HTML文件代码: <!DOCTYPE html> ...
随机推荐
- linux中 ECShop的文件不能写
解决办法: 1.开放权限 使用命令:chmod -R 777 文件路径 2.关闭SELinux 使用命令:setenforce 0
- poj 2533 Longest Ordered Subsequence 最长递增子序列
作者:jostree 转载请注明出处 http://www.cnblogs.com/jostree/p/4098562.html 题目链接:poj 2533 Longest Ordered Subse ...
- bootstrap和jQuery.Gantt的css冲突问题
bootstrap是广泛使用的一个前端框架, 而jQuery.Gantt在目前也是一个很好用的用于绘制甘特图的插件. 这次在同时使用它们时,发现甘特图显示异常,如图 不加载bootstrap. ...
- jquery 在页面中按回车 响应 事件
为了用户方便我们往往会在用户回车之后做一些事,比如登陆的时候,填完表单过后,我们习惯性的会直接按回车,当然要处理这个,jquery是很简单的,我们来看看怎么做吧. $(document).ready( ...
- dynamic 动态获取object数据
1.替代XXX.GetType().GetProperty("YYY").GetValue(XXX) static object GetPerson() { return new ...
- Linux文件保护禁止修改、删除、移动文件等,使用chattr +i保护
不让用户修改.删除文件等,使用 chattr保护 chattr命令的用法:chattr [ -RV ] [ -v version ] [ mode ] files… 最关键的是在[mode]部分,[m ...
- python with关键字学习
1.with语句时用于对try except finally 的优化,让代码更加美观, 例如常用的开发文件的操作,用try except finally 实现: f=open('file_name', ...
- 创建第一个UI
创建一个2D UI 制作UI时,首先要创建UI的"根".在Unity顶部NGUI菜单中选择Create,然后选择2D UI. 创建完成后,在Scene窗口中,NGUI自动生成了一个 ...
- Codeforces Round #359 div2
Problem_A(CodeForces 686A): 题意: \[ 有n个输入, +\space d_i代表冰淇淋数目增加d_i个, -\space d_i表示某个孩纸需要d_i个, 如果你现在手里 ...
- PHP reset() 函数
定义和用法 reset()函数把数组的内部指针指向第一个元素,并返回这个元素的值.若失败,则返回 FALSE. reset()函数用来将数组指针设置回数组的开始位置.如果需要在一个脚本中多次查看或处理 ...