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> ...
随机推荐
- Arrays 标准库算法
Binary Search public static int binarySearch0(Object[] a, int fromIndex, int toIndex, Object key) { ...
- C#与.NET
1 .NET Framework的核心是其运行库执行环境,即公共语言运行库(CLR)或.NET运行库,一般将CLR控制下运行的代码称为托管代码(managed code). 在CLR在执行编写好的代码 ...
- html5在手机端关于 map area中的自适应
https://github.com/stowball/jQuery-rwdImageMaps用这一个插件可自适应!!!
- SQL 字符替换
--匹配所有字符替换 )),'被替换','替换') --匹配给定位子替换 update 表名 set 列=stuff(列名,从一开始数位数,往后数几位,替换)
- WPF异步调用WCF服务
wpf调用wcf时,第一次访问总耗时到达几秒,影响界面的用户体验,因此在wpf加载界面和加载数据时采用异步加载,即异步访问wcf服务, 由于是否采用异步加载和服务端无关,仅仅由客户端自己根据需要来选择 ...
- asp.net+MVC--1
1.MVC入门 1)第一个路由: /*任何应用程序启动时发生的动作都应该存在于单独的类中,并且仅在该方法中按照正确顺序调用*/ protected void Application_St ...
- Mysql ID重新排列
我们经常会遇到,在删除数据库某条记录时,原来的ID排序会有间隔,比如删除了ID为8的数据,这个表的ID排序就会从7直接到9, 那我们如何解决这个ID重新排列的问题呢? 只需一下三步: 1.删除这个表的 ...
- python log 层次结构
文件结构 - run.py - b -- __init__.py run.py import logging import b log = logging.getLogger("" ...
- SQL的多表操作
多表更新: 假定我们有两张表,一张表为Product表存放产品信息,其中有产品价格列Price:另外一张表是ProductPrice表,我们要将ProductPrice表中的价格字段Price更新为P ...
- 角色控制器 Character Controller
Unity中,1个单位尺寸代表1米.即在Unity中创建一个Cube的尺寸是1x1x1米大小. Unity推荐把人的身高定为大约2个Unity单位高度(2米). 为了截取角色的全身照,需要把角色Ins ...