# iBooks笔记导出
## epub结构
sqlite3 ~/Library/Containers/com.apple.iBooksX/Data/Documents/AEAnnotation/AEAnnotation_v10312011_1727_local.sqlite
sqlite> .tables
**ZAEANNOTATION** Z_METADATA Z_PRIMARYKEY
ZBCCLOUDSYNCVERSIONS Z_MODELCACHE
**ZAEANNOTATION**表:
| 字段名 | 值 | 含义描述推测 |
| ---- | ---- | ---- |
| Z_PK | 4662 | 可能是该条记录在数据库中的主键(Primary Key),用于唯一标识这条批注数据 |
| Z_ENT | 1 | 可能代表实体类型(Entity Type),用于区分数据库中不同类型的记录,此处可能标识为“批注”类实体 |
| Z_OPT | 1 | 可能是版本号或优化字段(Optimization),用于记录数据的更新次数或状态标识 |
| ZANNOTATIONDELETED | 0 | 标识批注是否被删除,0表示未删除,1可能表示已删除 |
| ZANNOTATIONISUNDERLINE | 1 | 标识批注是否为下划线样式,1表示该批注是下划线标注,0可能表示非下划线 |
| ZANNOTATIONSTYLE | 0 | 可能表示批注的样式属性(如颜色、粗细等),0可能对应默认样式 |
| ZANNOTATIONTYPE | 2 | 表示批注的类型,可能用于区分是下划线、高亮、注释等不同类型,2此处可能对应下划线批注 |
| ZPLABSOLUTEPHYSICALLOCATION | 0 | 可能是批注在文档中的绝对物理位置信息,具体含义需结合文档存储格式判断 |
| ZPLLOCATIONRANGEEND | 0 | 批注内容在文档中的位置范围终点,可能是字符偏移量或段落索引等 |
| ZPLLOCATIONRANGESTART | 9 | 批注内容在文档中的位置范围起点,可能是字符偏移量或段落索引等,与终点配合确定批注选中的文本范围 |
| **ZANNOTATIONCREATIONDATE** | 774064827.1 | 批注的创建时间,可能是时间戳格式(需转换为具体日期时间) |
| ZANNOTATIONMODIFICATIONDATE | 774064829.2 | 批注的最后修改时间,时间戳格式,记录批注内容或属性的更新时间 |
| **ZANNOTATIONASSETID** | 768E1CD0B3086166F791683869B12425 | 可能是该批注所属文档的唯一标识(Asset ID),用于**关联批注对应的电子书或文档** |
| ZANNOTATIONCREATORIDENTIFIER | com~apple~iBooks | 标识创建该批注的应用程序,此处表明是苹果的iBooks应用创建的批注 |
| **ZANNOTATIONLOCATION** | epubcfi(/6/20[id155]!/4/412/1,:0,:97) | 采用EPUB格式的CFI(Content Fragment Identifier),**精确标识批注在电子书文档中的位置** |
| ZANNOTATIONNOTE | | 批注的备注内容,此处为空表示该批注没有额外添加的文字备注 |
| **ZANNOTATIONREPRESENTATIVETEXT** | 在听完这个充满暴力、腐败和表里不一的悲剧故事之后,希维格和我都已精疲力尽。我们不知道到底谁比较可恶:是罪不可逭的席格和犹太人防卫联盟,还是恶意侵犯席格的宪法基本人权、并且否认曾许下承诺的政府官员。 | 可能是批注所关联的代表性文本,通常是包含批注选中内容的上下文文本 |
| **ZANNOTATIONSELECTEDTEXT** | 在听完这个充满暴力、腐败和表里不一的悲剧故事之后,希维格和我都已精疲力尽。我们不知道到底谁比较可恶:是罪不可逭的席格和犹太人防卫联盟,还是恶意侵犯席格的宪法基本人权、并且否认曾许下承诺的政府官员 | 被用户选中并添加批注(下划线)的具体文本内容,**优先使用该字段作为内容定位依据**。 |
| ZANNOTATIONUUID | 7097EAE0-0EDB-4552-9AE0-FA6AB390B90B | 批注的唯一标识符(UUID),用于在系统中唯一标识这条批注,避免重复 |
| ZFUTUREPROOFING1 | | 预留字段,可能用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING10 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING11 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING12 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING2 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING3 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING4 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING5 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING6 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING7 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING8 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZFUTUREPROOFING9 | | 预留字段,用于未来功能扩展,目前未使用 |
| ZPLSTORAGEUUID | 774064829.2 | 可能是与存储相关的唯一标识符,用于关联批注在存储系统中的位置或状态 |
| ZPLUSERDATA | | 可能用于存储与用户相关的自定义数据,此处为空表示无额外用户数据 |
## 笔记数据
### 书籍元数据
PLIST_PATH = os.path.expanduser("~/Library/Containers/com.apple.BKAgentService/Data/Documents/iBooks/Books/Books.plist")
{
"Books" => [
……
603 => {
"artistName" => "(美)德肖维茨"
"BKAllocatedSize" => 1388544
"BKBookType" => "epub"
"BKDisplayName" => "最好的辩护-德肖维茨"
"BKGeneratedItemId" => "768E1CD0B3086166F791683869B12425"
"BKGenerationCount" => 1
"BKInsertionDate" => 773817769
"BKPercentComplete" => 1
"book-info" => {
"package-file-hash" => "768E1CD0B3086166F791683869B12425"
}
"genre" => "法律"
"isPreview" => 0
"itemName" => "最好的辩护"
"path" => "/Users/gavin/Library/Mobile Documents/iCloud~com~apple~iBooks/Documents/最好的辩护.epub"
"sourcePath" => "/Users/gavin/Library/Containers/com.apple.iBooksX/Data/Library/Caches/Inbox/最好的辩护-德肖维茨.epub"
"updateDate" => 2025-07-10 05:22:49 +0000
}
……
]
### 笔记数据库
DB_PATH = os.path.expanduser("~/Library/Containers/com.apple.iBooksX/Data/Documents/AEAnnotation/AEAnnotation_v10312011_1727_local.sqlite")
## books
~/Library/Containers/com.apple.iBooksX/Data/Documents/BKLibrary/BKLibrary-1-091020131601.sqlite
gavin@GavinsMAC BKLibrary % sqlite3 ~/Library/Containers/com.apple.iBooksX/Data/Documents/BKLibrary/BKLibrary-1-091020131601.sqlite
sqlite> .tables
ZBCCLOUDSYNCVERSIONS ZBKJALISCOSTATUS Z_MODELCACHE
ZBKCOLLECTION **ZBKLIBRARYASSET** Z_PRIMARYKEY
**ZBKCOLLECTIONMEMBER** Z_METADATA
## 问题
### 20250729
从epubcfi,如epubcfi(/6/20[id155]!/4/412/1,:0,:97)去反推定位内容,没法实现,浪费时间。
**解决思路**:
1. 基本信息
- 笔记数据在这个表中:~/Library/Containers/com.apple.iBooksX/Data/Documents/AEAnnotation/AEAnnotation_v10312011_1727_local.sqlite
- 书籍清单在这个表中:~/Library/Containers/com.apple.iBooksX/Data/Documents/BKLibrary/BKLibrary-1-091020131601.sqlite
- 书籍元数据在这里:~/Library/Containers/com.apple.BKAgentService/Data/Documents/iBooks/Books/Books.plist
- 书籍在这个directory中(epub为文件夹,非压缩文件):~/Library/Mobile Documents/iCloud~com~apple~iBooks/Documents
2. copy以上前三个文件到./data目录下分别命名为:AEAnnotation.sqlite、BKLibrary.sqlite、Books.plist
BKLibrary.sqlite暂不使用,使用Books.plist
3. 解析Books.plist,存在名为booksinfo的defaultdict(dict)中。booksinfo数据结构如下:
booksinfo = {
"768E1CD0B3086166F791683869B12425": {
"BKDisplayName" : "最好的辩护-德肖维茨",
"artistName" : "(美)德肖维茨",
"BKBookType" : "epub",
"BKGeneratedItemId" : "768E1CD0B3086166F791683869B12425",
"itemName" : "最好的辩护",
"path" : "/Users/gavin/Library/Mobile Documents/iCloud~com~apple~iBooks/Documents/最好的辩护.epub"
}
……
}
其中768E1CD0B3086166F791683869B12425为BKGeneratedItemId。
4. 查询AEAnnotation.sqlite,获取ZANNOTATIONCREATIONDATE ZANNOTATIONASSETID ZANNOTATIONLOCATION ZANNOTATIONSELECTEDTEXT ZANNOTATIONUUID
如:
ZANNOTATIONCREATIONDATE 746202137.8, #需要转化为2025/7/30这样的格式。
ZANNOTATIONASSETID 5F2C1C40566C61A2267907E621A664A4
ZANNOTATIONLOCATION epubcfi(/6/98[id61]!/4[1CQAE0-902f5dcbb4a04a858d573ec5ee66e862]/58/1,:28,:226) #需要获取idref = id61,filepos=1CQAE0-902f5dcbb4a04a858d573ec5ee66e862
ZANNOTATIONSELECTEDTEXT 这才是对人而言的自由
ZANNOTATIONUUID 0456103A-7161-451A-B762-1305ECBECBDB
并把数据存到名为annotations的defaultdict(dict)中。annotations的数据结构如下:
annotations = {
'CB9A605DCD687C4FA544DD4BCCD00D43': {
'326CA2CF-3298-45D5-A93F-440E6F2A0B33': {
'creationdate': '2023/7/12',
'filepos': None,
'idref': '008.xhtml',
'note': None,
'selectedtext': '這就是宣傳的恐怖之處'},
'2BD61C93-D1F1-4553-90CB-043A6E06DBA1': {
'creationdate': '2023/7/12',
'filepos': None,
'idref': '008.xhtml',
'note': None,
'selectedtext': '應該有一些系統性的表達機制'},
}
……
}
5. 需要导出的书名作为参数,比如"最好的辩护",书名做从booksinfo中做模糊查询,如匹配到多本,在shell中提示,选择哪一本书。
从booksinfo获取本书的uuid和这本书epub的路径path,进而从annotations中获取这本书的笔记。
6. 依据这本书的路径path,解析epub文件夹下的文件。
假定epub目录为epubdir,xx.opf(package.opf|standard.opf|content.opf)和toc.ncx文件可能的位置在:
epubdir/content.opf epubdir/toc.ncx
epubdir/OEBPS/content.opf epubdir/OEBPS/toc.ncx
epubdir/EPUB/package.opf epubdir/EPUB/toc.ncx
epubdir/item/standard.opf epubdir/item/toc.ncx
- 解析opf文件,存入default(dict)数据结构contentopf:
contentopf = {
"768E1CD0B3086166F791683869B12425": {
id163 : index_split_000.html, # idref: ref
id162 : index_split_001.html,
id161 : index_split_002.html,
id160 : index_split_003.html,
id159 : index_split_004.html,
id158 : index_split_005.html
……
}
}
- 解析toc.ncx文件,存入default(dict)数据结构contenttoc:
contenttoc =
{
"768E1CD0B3086166F791683869B12425": {
{ 'num_1': {'filepos': None, 'label': '\n最好的辩护\n', 'ref': 'index_split_000.html'},
'num_100': { 'filepos': None, 'label': '\n第八章父亲的罪\n',
'num_101': { 'filepos': 'filepos698889', 'label': '\n没有父亲的生活\n', 'ref': 'index_split_016.html'},
'num_102': {'filepos': 'filepos706518', 'label': '\n越狱\n', 'ref': 'index_split_016.html'},
……
}
}
}
7. 使用annotationdata.py获取annotations,把annotations扩充label_path,
通过annotations的assetid从bookinfo获取path路径,在path下找到.opf和.ncx,使用opf_parse.py和toc_parse.py获取label_path
booksnote = {
assetid: { label_path: { uuid: {
'creationdate': '2023/7/12',
'filepos': None,
'idref': '008.xhtml',
'note': None,
'selectedtext': '這就是宣傳的恐怖之處'
}}}}
应该从epubcfi中的id155,去找到html文件,在从笔记内容去匹配对应文件中的位置。
需要有个函数吧html的数据结构dict化,很快可以从内容找到title。
从epubcfi的ZANNOTATIONASSETID找到bookid
从ZANNOTATIONASSETID 依据bookid => epub路径
解析book到dict中。
解析content.opf,中的[id,href]
关联id,title,章节内容
=> bookdict = [id,title,[内容]]
从ZAEANNOTATION的笔记内容,遍历bookdict找到内容,找到title
最后按bookname,title归并笔记
### 202500906
1. 增加QTUI
2. 增加统计和展示
统计
- 周活跃 - 30天每天的阅读时长,柱状图
某本书,根据每条笔记note中ZANNOTATIONCREATIONDATE,如果某天没有note,则阅读时间为0;如果只有一条note,阅读时间为READ_TIME_DAY=60(在config.py中配置);如果note超过1条,计算第一条和最后一条的时间差,作为阅读时长。放在readtime30d这个list中。
- 月活跃 - 30天每天的阅读时长,柱状图
每本书,ZANNOTATIONCREATIONDATE,落在30天前到今天的天数*60min(60mins为每天阅读时间,可配置)
- 已阅读的书籍: 每本平均阅读时长。所有书籍:总阅读时长,年阅读时长,年平均每日阅读时长,累计阅读天数。用气泡图表示。
如果已读完,表ZBKLIBRARYASSET的ZISFINISHED字段为1;
表ZBKLIBRARYASSET的ZDATEFINISHED,读完时间,可以统计今年读完的书籍
*表ZBKLIBRARYASSET的(ZDATEFINISHED - ZCREATIONDATE),得出书本创建到读完的阅读周期,并非真正的阅读时间。
表ZAEANNOTATION的ZANNOTATIONCREATIONDATE,找出最早的一条批注创建时间,(ZDATEFINISHED-ZANNOTATIONCREATIONDATE),大约周期read\_days。近似的用read_days*60min(60mins为每天阅读时间,可配置)
- 本年度已阅读书籍封面图片橱窗,6xn展示,图片西面注上书名,支持把6xn导出成一张图片。
读完时间依据ZISFINISHED为1,ZDATEFINISHED,计算是否今年读完。
封面图片通过在IBOOKS_BOOKS_DIR对应的书籍下查找*cover*.jpg/png/jpeg文件,如果没有,查找cover*html文件,解析该文件获取图片路径(相对于html文件的路径)。如:
,取出../Images/data-url-image.jpeg
,取出Image00007.jpg
### 202500907
1. 使用GPT5生成ipad app版本
- vscode 下载插件swift
- 安装Command Line Tools for Xcode。MacBook一直提醒我安装,正好更新了。