duckdb_zim直接在 DuckDB 中读取.zim文件Kiwix / openZIM 存档——离线维基百科、WikiMed、Stack Exchange、iFixit、Project Gutenberg 等通过 libzim 实现。查询存档的条目、提取文章内容、读取元数据并与 DuckDB 扩展生态系统的其余部分特别是用于处理 ZIM 文章所包含 HTML 的webbed组合使用。状态阶段 1–3v0.2.0。已实现内容扫描、查找、列表、元数据、zim://文件系统、全文搜索 标题建议zim_search/zim_suggest可跨多个存档联合搜索以及实用工具zim_illustration/zim_random/zim_check。v0.2.0 版本中阅读器功能完整已评估并放弃了ATTACH——请参阅路线图。许可证GPL-2.0-or-later。libzim 是 GPL 许可的因此该扩展也是如此。这是有意为之与 MIT 许可的markdown/yaml/webbed系列扩展是不同的请参阅许可证。平台Linux 和 macOSx64 arm64支持全文搜索以及 WebAssembly无搜索功能都能在 CI 中成功构建和测试。暂不支持 Windows请参阅平台。安装可从 DuckDB 社区扩展仓库获取INSTALL zimFROMcommunity;LOADzim;发布的社区构建会跟踪已打标签的版本zim://文件系统、zim_search/zim_suggest以及其他阶段 2–3 的功能随v0.2.0版本一同发布。如需使用最新发布标签之后的更新请从源代码构建见下文。从源代码构建DuckDB 扩展作为子模块拉取gitclone --recurse-submodules https://github.com/teaguesterling/duckdb_zim.gitcdduckdb_zimmake# 需要 vcpkg 工具链来构建 libzim详细信息请参阅 DESIGN.md然后加载构建好的扩展LOADbuild/release/extension/zim/zim.duckdb_extension;快速开始-- 列出所有内容条目不获取内容——即使在大型存档上速度也很快SELECTpath,title,mimetype,sizeFROMread_zim(wikipedia.zim)ORDERBYpath;-- 这是什么类型的存档计数和标志SELECTzim_info(wikipedia.zim);-- 它实际包含什么存档自描述的 MIME 类型直方图SELECTzim_counter(wikipedia.zim);-- MAP(text/html - 11604, image/webp - 987, ...)-- 提取一篇文章的 HTMLSELECTzim_get_text(wikipedia.zim,A/Photosynthesis);关于路径的说明内容路径不含命名空间是A/Photosynthesis而不是C/A/Photosynthesis其形状取决于抓取工具——前缀是路径文本而不是 libzim 命名空间。mwoffliner 生成A/Photosynthesis、I/logo.pngzimit / browsertrix 生成带有尾部斜杠的完整 URL例如www.nhs.uk/medicines/insulin/。该扩展将两者都视为不透明的内容路径在查找时会容忍并规范化掉前导的C/。请参阅docs/libzim-semantics.md了解已验证的模型。核心用法扫描与列表read_zim为每个内容条目生成一行。内容是惰性加载的——除非你同时投影了该列并传递了include_content : true否则它为NULL因此列表扫描永远不会解压存档。-- 前缀列表高效由 libzim 的 findByPath 支持而非全扫描SELECTpath,titleFROMread_zim(wikipedia.zim,path_prefix :A/Cal);-- 基于标题前缀利用标题列表自动补全风格SELECTtitleFROMread_zim(wikipedia.zim,title_prefix :Calc,listing :title);-- 按 MIME 类型过滤排除重定向因为重定向没有 MIME 类型SELECTpath,sizeFROMread_zim(wikipedia.zim,mimetype :text/css);-- 按路径或标题精确查找输出 0 或 1 行SELECT*FROMread_zim(wikipedia.zim,path :A/Photosynthesis);-- 获取内容选择加入。默认BLOB。content_as_varchar : true 会使该列变为 VARCHAR-- 如果条目的字节不是有效的 UTF-8二进制图像、字体等则返回 NULL 而不是被破坏——-- 与 zim_get_text 相同的“永不破坏”规则。SELECTpath,contentFROMread_zim(wikipedia.zim,path :A/Photosynthesis,include_content :true,content_as_varchar :true);-- 批量获取内容但仅针对你想要的 MIME 类型include_content 也接受一个 MIME 类型或列表。-- 匹配的条目会被解压其他条目保持其元数据行内容为 NULL——存档中的图像/字体永远不会被物化。SELECTpath,contentFROMread_zim(wikipedia.zim,include_content :text/html);SELECTpath,contentFROMread_zim(wikipedia.zim,include_content :[text/html,text/css]);read_zim参数include_contenttrue/false或一个 MIME 类型 / MIME 类型列表——支持image/*/*/*通配符——仅加载这些条目的内容、content_as_varchar、include_filepath别名filename、mimetype单个模式或LISTAccept 风格支持image/*/*/*通配符、path、title、path_prefix、title_prefix、listingpath|title、paralleltrue/false见下文。普通的路径顺序扫描默认并行运行在 DuckDB 配置的threads数量上——条目按 libzim簇顺序分区因此每个压缩簇只解压一次。在并行扫描下不保证行顺序在 SQL 中本来就不保证传递parallel : false进行单线程、路径顺序扫描或添加ORDER BY。精确查找、前缀列表和listing : title本质上是单线程的。-- Accept 风格的内容类型匹配所有图像然后如果你希望有优先级可以用 SQL 进一步缩小范围SELECTpath,mimetypeFROMread_zim(wikipedia.zim,mimetype :image/*);SELECTpathFROMread_zim(wikipedia.zim,mimetype :[image/webp,image/png])ORDERBYarray_position([image/webp,image/png],mimetype);-- webp 优先这些是互斥的模式冲突的组合会被拒绝而不是悄悄解决最多选择一个精确键path或title或一个前缀path_prefix或title_prefix精确键不能与前缀/listing组合listing不能与前缀的顺序冲突title_prefixlisting : title可以path_prefixlisting : title会报错。mimetype可以与它们中的任何一个组合。listing以及title_prefix仅针对文章。listing : title选择 libzim 的标题列表——即iterByTitle它仅包含标记为FRONT_ARTICLE的条目文章而不是每个条目。因此它返回的是不同的行集而不仅仅是默认的path列表包含所有条目的重新排序。title_prefix使用相同的标题列表。要按标题顺序获取所有条目请扫描默认的路径列表并在 SQL 中ORDER BY title。列path, title, mimetype, is_redirect, redirect_path, size, content[, file_path]。多存档、通配符和FROM x.zimread_zim接受一个路径、一个通配符或一个列表——每个存档的行会被连接起来include_filepath : true可以区分它们。FROM中的裸.zim文件名会自动重写为read_zim。SELECTcount(*)FROMread_zim(wikis/*.zim);-- 通配符SELECT*FROMread_zim([a.zim,b.zim],include_filepath :true);-- 显式列表SELECT*FROMwikipedia.zim;-- 替换扫描精确查找和前导前缀按存档应用read_zim(*.zim, path : A/Foo)为每个包含A/Foo的存档输出一行。元数据元数据是与内容分开的键空间libzim 的两个不同入口。同时提供表函数和直接标量查找。SELECTkey,valueFROMread_zim_metadata(wikipedia.zim);SELECTzim_metadata(wikipedia.zim,Title);-- 单个值SELECTzim_metadata(wikipedia.zim,Language);-- ISO 639-3可能是逗号分隔列表SELECTzim_metadata_keys(wikipedia.zim);-- LIST(VARCHAR)SELECTzim_counter(wikipedia.zim);-- MAP(mimetype - count)SELECTzim_info(wikipedia.zim);-- STRUCT 包含计数/标志/uuid标量辅助函数zim_get_content(file,path)-- BLOB跟随重定向zim_get_text(file,path)-- 用于文本 MIME 类型的 VARCHAR二进制文件返回 NULL永不破坏zim_has_entry(file,path)-- BOOLEANzim_redirect_target(file,path)-- VARCHAR如果不是重定向则返回 NULLzim_mimetype(file,path)-- VARCHARzim_main_entry(file)-- VARCHAR重定向解析后的落地页路径zim_random(file)-- VARCHAR随机条目的路径zim_check(file)-- BOOLEANlibzim 存档完整性检查zim_illustration(file[,size])-- BLOB封面图像 / favicon默认 48px没有则返回 NULL全文搜索与标题建议如果存档带有 Xapian 全文索引zim_info(file).has_fulltext_index为真zim_search可以查询它SELECTpath,title,score,snippetFROMzim_search(wikipedia.zim,compound heterozygous,max_results :20);-- 分页使用 result_offset两者默认值为 (25, 0)SELECTpathFROMzim_search(wikipedia.zim,photosynthesis,max_results :10,result_offset :10);返回(path, title, score DOUBLE, snippet VARCHAR, file VARCHAR)按相关性排序。score是 Xapian 的排名snippet是尽力提取的高亮摘要如果没有生成则为NULL。没有全文索引的存档——或者无搜索功能的WebAssembly构建——会返回零行而不是报错。使用max_results/result_offset而不是limit/offset因为后者是 SQL 保留字。联合搜索——与read_zim类似第一个参数可以是单个路径、通配符或LIST(VARCHAR)因此一个查询可以同时跨多个存档运行。max_results按存档应用file列说明每条命中来自哪个存档你可以在 SQL 中对它们进行排名/修剪Xapian 分数是按存档的因此不能直接比较SELECTfile,path,title,scoreFROMzim_search(library/*.zim,insulin,max_results :5)ORDERBYscoreDESCLIMIT20;zim_suggest是基于建议索引的标题自动补全——返回(path, title, snippet, file)并且以相同的方式支持联合搜索通配符 /LIST。与zim_search不同它在所有构建上都有效当没有 Xapian 索引时它会回退到标题前缀列表因此在 Wasm 上也可用SELECTpath,titleFROMzim_suggest(wikipedia.zim,Photosyn,max_results :10);SELECTfile,titleFROMzim_suggest(library/*.zim,Photosyn);-- 跨书架端到端读取真实存档遍历一个真实的 zimit 存档——nhs.uk_en_medicines_2025-12.zim约 2k 条目-- 这是什么计数/标志、人类可读标题和自描述的 MIME 类型直方图SELECTzim_info(nhs.uk_en_medicines_2025-12.zim);-- entry_count 2064, uuid, …SELECTzim_metadata(nhs.uk_en_medicines_2025-12.zim,Title);-- NHS Medicines A to ZSELECTzim_counter(nhs.uk_en_medicines_2025-12.zim);-- text/html→1996, image/jpeg→9, …-- 落地页重定向解析后SELECTzim_main_entry(nhs.uk_en_medicines_2025-12.zim);-- www.nhs.uk/medicines/-- 浏览最大的文章——不获取内容所以速度很快SELECTpath,title,sizeFROMread_zim(nhs.uk_en_medicines_2025-12.zim,mimetype :text/html)ORDERBYsizeDESCLIMIT10;-- Insulin, HRT, Propranolol, …-- 以文本形式读取一篇文章的 HTMLSELECTzim_get_text(nhs.uk_en_medicines_2025-12.zim,www.nhs.uk/medicines/insulin/);关于标识符的提醒存档的Name元数据nhs.uk_en_medicines不是磁盘上的文件名主干nhs.uk_en_medicines_2025-12——日期仅存在于文件名中而 kiwix 通过文件名主干提供内容。不要使用Name构建内容 URL。与其他扩展的集成ZIM 文章是HTMLmwoffliner / zimwriterfs / zimit 都生成text/html因此自然的搭档是webbed。duckdb_zim特意不了解 HTML——它把字节交给你让webbed负责解析。这确保 GPL 的影响范围仅限于“读取容器”而将所有丰富的处理留在 MIT 生态系统中。值层集成现在可用将文章 HTML 直接传递给webbed的提取器-- 需要LOAD webbed;-- include_content : text/html 只加载并解压文章 HTML ——-- 存档中的图像和字体将被跳过而不仅仅是在之后被过滤掉。SELECTpath,html_extract_text(content,//h1)[1]ASheading,html_extract_links(content)ASlinksFROMread_zim(wikipedia.zim,include_content :text/html,content_as_varchar :true)WHEREmimetypetext/html;文章正文选择器因抓取工具而异原始文章 HTML 被导航/页眉/页脚等包裹因此请使用指向内容容器的 XPath 来提取正文而不是//body。mwoffliner 维基使用//div[contains(class,mw-parser-output)]zimit / browsertrix 捕获的内容如上面的 NHS 存档保留站点自身的标记通常是//main或//article。确切的选择器取决于抓取工具版本和皮肤。文件系统层集成zim://该扩展注册了一个只读的zim://文件系统因此任何读取路径的函数——DuckDB 自己的read_text/read_blob或webbed的read_html或任何其他通过 DuckDB 文件层的东西——都可以免费组合 ZIM 内容无需耦合也无需将 GPL 链接回这些扩展SELECT*FROMread_text(zim://wikipedia.zim/A/Photosynthesis);-- 一个条目文本形式SELECT*FROMread_blob(zim://wikipedia.zim/I/logo.webp);-- 一个条目字节形式SELECT*FROMread_html(zim://wikipedia.zim/A/Photosynthesis);-- 需要LOAD webbed;SELECT*FROMread_text(zim://wikipedia.zim/A/C*);-- 内容路径通配符语法是内容路径优先zim://archive.zim/content-path。存档组件是本地文件libzim 直接进行内存映射.zim/边界之后的所有内容都是存档内的条目路径。内容路径前导的C/会被容忍并去除重定向会被跟随重定向路径会提供其目标的字节类似于符号链接*/?/[…]通配符会与条目路径匹配。条目在打开时会被物化到内存中——对于文章来说没问题但单个非常大的媒体项会占用与其大小相当的 RAM。-- 通过文件系统通配符提取每篇文章的 h1 并传递给 webbedSELECTregexp_replace(filename,^.*/,)ASentry,html_extract_text(content,//h1)[1]ASheadingFROMread_text(zim://wikipedia.zim/A/*)ORDERBYentry;-- 需要LOAD webbed;端到端离线维基百科整个离线百科全书变成了一个结构化的、可查询的数据集——无需抓取、无需解包、无需联网。对存档使用通配符对每篇文章运行 XPath你就有了一个表你甚至可以在两个不同的离线存档之间进行连接使用共享键在一个查询中协调它们不同的 HTML 约定。在这里NHS 站点一个 zimit 捕获//article和维基百科mwoffliner//div[mw-parser-output]通过药物名称进行交叉引用-- 需要LOAD webbed;WITHnhs_rawAS(-- 解析每个 NHS 页面一次...SELECTregexp_extract(filename,medicines/([^/])/,1)ASmedicine,html_extract_text(content::HTML,//article//p)[1]ASnhs_saysFROMread_text(zim://nhs.zim/www.nhs.uk/medicines/*/about-*/)),nhsAS(SELECT*FROMnhs_rawWHEREnhs_saysISNOTNULL),-- ...对解析后的值进行过滤mergedAS(-- 每行进行一次维基百科点查找无扫描SELECTmedicine,nhs_says,zim_get_text(wikipedia_en_medicine.zim,upper(medicine[1])||medicine[2:])ASwiki_htmlFROMnhs)SELECTmedicine,nhs_says,html_extract_text(wiki_html::HTML,(//div[contains(class,mw-parser-output)]/p[normalize-space()])[1])[1]ASwikipedia_saysFROMmergedWHEREwiki_htmlISNOTNULL;medicinenhs_sayswikipedia_saysamoxicillina type of penicillin antibiotic.an antibiotic … of the aminopenicillin class …metforminused to treat type 2 diabetes…the main first-line medication for type 2 diabetes…warfarinan anticoagulant.used as an anticoagulant medication … deep vein thrombosis …这是一个键查找连接——由 NHS 驱动每行通过标题对维基百科362k 篇文章的存档被探测从未被扫描执行一次zim_get_text点查找。或者使用联合zim_search同时搜索两者并按相关性对齐——两个独立的搜索索引在约 40 毫秒内完成匹配WITHhitsAS(SELECTCASEWHENfileLIKE%/nhs.zimTHENNHSELSEWikipediaENDASsource,html_extract_text(zim_get_text(file,path)::HTML,CASEWHENfileLIKE%/nhs.zimTHEN//article//pELSE(//div[contains(class,mw-parser-output)]/p[normalize-space()])[1]END)[1]ASleadFROMzim_search([nhs.zim,wikipedia_en_medicine.zim],warfarin blood clots,max_results :1))SELECTmax(lead)FILTER(WHEREsourceNHS)ASnhs,max(lead)FILTER(WHEREsourceWikipedia)ASwikipediaFROMhits;zim特意不包含 HTML 处理知识——它把字节交给你让解析扩展来完成其余工作。任何通过 DuckDB 文件层读取的东西都可以组合zim://连接层这里是webbed用于 HTML/XPath同样read_json、read_csv、read_text或read_blob也可以用于存档内的其他负载。实用方案构建你自己的全文索引对于没有 Xapian 索引的存档或者当你想要自己的排序时很有用CREATETABLEcorpusASSELECTpath,title,html_extract_text(content::HTML,//div[contains(class,mw-parser-output)])[1]ASbodyFROMread_zim(wikimed.zim,mimetype :text/html,include_content :true,content_as_varchar :true);PRAGMA create_fts_index(corpus,path,title,body);全语料库链接图——html_extract_links给出 href内部链接是条目路径因此规范化并连接回条目表以构建边列表。HTML → 干净的 Markdown → 本地 LLM——html_to_duck_blocks(content)webbed duck_block_utils生成一个结构化的块树你可以将其渲染为 Markdown作为离线模型的检索上下文。网络捕获zimit存档——zimit / browsertrix 生成的 ZIM 使用完整 URL 作为内容路径www.nhs.uk/medicines/insulin/而不是 mwoffliner 的A/Foo。该扩展保持抓取工具无关——路径是不透明字符串——因此所有操作都是对path列的普通 SQL无需特殊函数-- 这个存档是由哪个抓取工具生成的SELECTzim_metadata(archive.zim,Scraper);-- zimit … vs mwoffliner …-- 捕获的主机以及一个主机下的条目及其重建的源 URLSELECTDISTINCTsplit_part(path,/,1)AShostFROMread_zim(archive.zim);SELECTpath,https://||pathASsource_urlFROMread_zim(archive.zim)WHEREpathLIKEwww.nhs.uk/%ANDmimetypetext/html;-- 直接通过文件系统读取捕获的页面尾部斜杠会被解析SELECT*FROMread_html(zim://archive.zim/www.nhs.uk/medicines/insulin/);-- 需要 LOAD webbed;平台Linuxx64 arm64和macOSx64 arm64在 CI 中构建和测试通过支持全文搜索——为原生三元组拉取了xapian。WebAssembly构建也可用所有三个 emscripten 变体都能编译通过但不支持搜索xapian被排除在 Wasm 三元组之外vcpkg.json中设置了platform: !emscripten因为 libzim icuzstdliblzma可以在 emscripten 下构建但 xapian 不行。在 Wasm 上zim_search可以绑定但返回零行。Windows暂不支持——在 MSVC/mingw 下移植 libzim icu 需要额外的工作因此目前从 CI 中排除。路线图阶段范围状态1read_zim、列表/前缀、read_zim_metadata、zim_metadata/_keys/zim_counter/zim_info、查找标量已实现2zim://文件系统路径 通配符——与webbed/markdown/read_blob组合已实现3zim_searchXapian 全文zim_suggest标题自动补全实用工具zim_illustration/zim_random/zim_checkAccept 风格的mimetype匹配已实现3.5 (v0.3)并行read_zim扫描——跨 DuckDB 线程的簇顺序小块parallel参数已实现3.6 (v0.3)远程存档——通过httpfs按字节范围读取 S3/HTTP ZIM捆绑了一个 libzimIRandomAccessReader补丁全文搜索保持仅本地已实现3.7过滤器下推——WHERE path/title …→ libzim 精确查找WHERE mimetype→ 后过滤惯用 SQL 获得远程便宜的路径已实现4ATTACH x.zim AS … (TYPE zim)不会做——见下文v0.2.0 被视为功能完整。ATTACH是最后一个计划的阶段但已被放弃ZIM 是一个数据集一个逻辑关系 元数据 搜索索引而不是一个多表数据库因此 DuckDB 的read_*函数习惯用法是合适的——而不是目录/ATTACH习惯用法适用于 sqlite/postgres。ZIM 唯一的结构性“表”是其命名空间两个有用的C内容M元数据已经由read_zim/read_zm_metadata提供其余W众所周知的 ≈ 主条目X原始索引块不值得作为行暴露出来。ATTACH唯一一个非表面的好处——一个热句柄——已经由存档池提供别名可以是一行CREATE VIEW。如果将来需要查看W/X命名空间一个选择加入的原始枚举标志放在read_zim上是诚实的工具而不是一个目录。未来的方向如果出现的话是另一种目录形态——一个多存档库浏览/连接一个书架上的多个 ZIM——联合的zim_search/zim_suggest和read_zim(*.zim)已经部分覆盖了。工作原理一个进程范围的池保持每个打开的zim::Archive在查询之间存活因此 libzim 的解压簇缓存保持温暖——每个函数和zim://文件系统共享每个文件的一个打开句柄。所有与 libzim 的接触都隔离在一个小的访问层中src/zim_access.*代码的其余部分是在 DuckDB 无关结构上的普通 DuckDB 绑定。请参阅DESIGN.md和docs/libzim-semantics.md。许可证GPL-2.0-or-later继承自 libzim。分发静态链接 libzim 的构建会使得组合后的作品成为 GPL。如果你需要一个宽松许可的 ZIM 读取器那么这个扩展不是它——但它可以在 GPL 条款下自由使用、修改和再分发。