简介友益文书8.7.5映山红心系黄英版是一份面向电子书制作初学者与资料整理爱好者的EXE电子书工具包可用于将网页、文档与图片素材组织成便于分发的离线电子书适合学习软件界面、目录树与帮助系统等基础用法。压缩包共15个文件约2.26MB内含主程序exe、配置文件manifest、主题样式css、目录树与界面脚本js、界面图标gif以及示例帮助文件yws和说明txt类型覆盖程序、资源与说明文档体积轻便便于本地解压查看与试验。该资源已有379人浏览学习可作为了解友益文书旧版本结构与电子书打包流程的参考样本帮助读者观察主题资源、脚本与帮助文件之间的配合方式并据此排查素材引用、目录生成和界面显示等常见问题适合作为中初级用户熟悉工具组成与资源组织方式的练习材料。1. 从U盘交付说起exe电子书为什么还有一席之地前阵子一位做内部培训的同行找我说客户那边的电脑只装 Windows发过去的 PDF 要先装阅读器发过去的网页包又得解压找 index问我能不能把几十份讲义塞进一个 exe插上 U 盘双击就能翻。这类需求听着老派但 exe 电子书制作工具一直没死透。友益文书就是这类工具里比较典型的一个把 HTML、图片、纯文本打包成单个可执行文件内置浏览器控件负责渲染目录树、书签、全文搜索一并塞进去。它解决的不是阅读体验而是交付确定性——对方不需要装任何东西双击就能看内容还不容易被随手另存。适合做内部资料分发、课程讲义、产品手册、标书附件的人。网上那些挂着破解版、绿色版名头的包来源不明的 exe 本身就是最大的坑真要长期用还是走正规授权渠道。下面按这东西是什么 → 怎么做一本 → 哪里会翻车的顺序拆。2. 拆开一个 exe 电子书资源嵌入、渲染内核与目录三层结构先把黑匣子打开。很多人以为 exe 电子书是编译出来的其实绝大多数同类工具干的事情非常朴素准备一个宿主程序阅读器外壳把正文资源压缩或加密后附加到 exe 文件末尾运行时宿主程序读自己的文件、把附加段解出来交给内置的渲染控件显示。理解了这一层后面所有参数和坑都能对上号。2.1 单文件 exe 里到底装了什么Windows 的可执行文件遵循 PE 格式头部之后是一串节区.text、.rdata、.rsrc 等节区之后到文件末尾的那段空间业内叫 overlay也就是附加数据。电子书正文、图片、目录索引通常就躺在这里。宿主程序启动时用GetModuleFileName拿到自身路径重新打开自己按编译时写入的偏移量读出 overlay解密解压到内存或临时目录。这意味着两件事第一exe 体积基本等于外壳 压缩后的资源一本 50MB 的图文册子压完可能只有 20MB 出头第二overlay 是明文还是密文取决于制作时有没有勾加密没勾的话用十六进制编辑器就能看到 HTML 片段。想快速判断一个成品是不是资源外挂式可以写个小脚本算一下 overlay 的起止位置。import struct import sys def overlay_info(path: str) - dict: 读取 PE 节表计算附加数据overlay的起始偏移和大小。 with open(path, rb) as f: data f.read() if data[:2] ! bMZ: raise ValueError(不是有效的 DOS/PE 文件头) pe_off struct.unpack_from(I, data, 0x3C)[0] if data[pe_off:pe_off 4] ! bPE\0\0: raise ValueError(PE 签名缺失可能被加壳) # COFF 头里 NumberOfSections 在 PE 签名后第 6 字节节表紧跟在可选头之后 num_sections struct.unpack_from(H, data, pe_off 6)[0] opt_size struct.unpack_from(H, data, pe_off 20)[0] section_start pe_off 24 opt_size last_end 0 for i in range(num_sections): base section_start i * 40 raw_size struct.unpack_from(I, data, base 16)[0] raw_ptr struct.unpack_from(I, data, base 20)[0] last_end max(last_end, raw_ptr raw_size) total len(data) return { file_size: total, sections_end: last_end, overlay_offset: last_end, overlay_size: max(0, total - last_end), overlay_ratio: round(max(0, total - last_end) / total, 4), } if __name__ __main__: print(overlay_info(sys.argv[1]))这段脚本只做一件事遍历节表取所有节区原始数据偏移 原始数据长度的最大值文件总长减去它就是附加数据。参数上没什么可调的传入成品 exe 路径即可。判读标准是overlay_size如果接近 0说明资源被编进了 .rsrc 节区属于真编译路线如果占比很高常见 0.6 以上说明正文就是挂在尾巴上的压缩包。后者在内容保护上天然弱一档这条结论在第 4 章还会用到。2.2 渲染内核选型WebBrowser 控件与 Chromium 的取舍同类工具的渲染层无非两条路一条是 Windows 自带的 WebBrowser 控件底层是 MSHTML也就是 IE11 那套另一条是打包 Chromium 系内核CEF、WebView2 或精简版 Blink。选哪条直接决定了你的排版能做到什么程度以及最终 exe 有多重。维度WebBrowser 控件MSHTMLChromium 系内核单文件体积增量几乎为 0依赖系统组件80MB200MB 起CSS3 / Flex / Grid部分支持Grid 基本不可用完整支持字体渲染依赖 GDI高分屏易发虚DirectWrite缩放更稳启动速度快冷启动 1 秒内慢冷启动 25 秒系统兼容Win7 到 Win11 都在Win7 需额外补丁Win10 稳适合内容纯文本、简单图文、表格富媒体、动画、复杂排版如果资料以文字和表格为主WebBrowser 路线足够体积还小客户老机器也跑得动一旦要做响应式排版、SVG 图表、滚动动效就得接受 Chromium 的体积代价。实操里我一般先问一句交付对象的机器是 2015 年以后的还是更老老机器多就别上 Chromium不然双击之后白屏三秒客户第一反应是你这软件坏了。提示WebBrowser 控件的渲染模式受注册表 FEATURE_BROWSER_EMULATION 影响默认可能跑在 IE7 兼容模式导致border-radius、box-shadow全部失效。工具里如果有使用现代渲染模式之类的勾选项务必打开。2.3 素材目录规范与导入顺序工具再傻瓜素材乱也白搭。我的习惯是在工程目录下固定四个文件夹导入顺序严格按样式 → 图片 → 正文 → 目录走因为多数工具在导入正文时就会解析相对路径图片没就位就会写进一堆裂图占位。manuscript/ ├── assets/ │ ├── book.css # 全局样式字体、行距、表格边框 │ └── cover.png # 封面建议 600×800 以内 ├── images/ │ ├── ch01-fig01.png # 命名规则章节号-图号全小写 │ └── ch01-fig02.png ├── chapters/ │ ├── 001-前言.html │ ├── 002-基础概念.html │ └── 003-操作流程.html └── toc.json # 由脚本生成不手工维护命名上有个血泪经验文件名一律小写、不用空格、不用中文标点。Windows 本身不区分大小写但打包工具内部做路径映射时常常区分Images/和images/在开发机上没事到了客户机上就是一片红叉。章节文件用三位数字前缀保证001、002、010排序正确别用1、2、10否则第十 chapter 会排到第二前面。导入顺序对应到工具里通常是先设样式表路径再批量导入图片目录然后按章节文件顺序导入正文最后让工具根据h1自动生成目录树。如果工具有自动识别标题层级选项建议只认h1和h2h3往里塞会让目录树长得像蜈蚣读者翻两下就烦。2.4 目录、书签与搜索是怎么串起来的目录树本质上是一份标题—文件—锚点的三元组索引工具在编译时把它序列化进资源段。书签是读者手动加的存在用户目录的配置文件里跟 exe 分离所以换台机器书签就没了这点要提前跟客户说清楚。全文搜索则是编译期建倒排或者运行期暴力扫正文前者快但体积涨后者慢但实现简单。判断一本电子书搜索好不好用有个土办法找一篇正文里出现频繁但目录里没有的词搜一下看命中结果能不能点击跳转并高亮。很多工具搜索能出结果点了却跳回封面原因是锚点 ID 在压缩时丢了。这个在自测清单里必须列一条。3. 从零做一本可交付的 exe 电子书素材清洗到产物自测的六步流水线这一章是可以直接抄作业的部分。整套流程我跑过几十本稳定下来的顺序是清洗素材 → 套 HTML 模板 → 生成目录索引 → 配置编译参数 → 编译 → 逐项自测。中间任何一步偷懒后面都要用两倍时间补。3.1 素材清洗与 HTML 模板原始素材大概率来自 Word 或 WPS 导出的 HTML里面的o:p、mso-样式、内联stylefont-family:宋体;font-size:10.5pt一堆。直接导进电子书结果就是排版各章不一致、行距忽大忽小。我一般先跑一遍清洗把 Word 特有的标签和属性剥掉统一交给外部 CSS 管。import re from pathlib import Path # 需要清理的 Word/WPS 残留标签与属性 DROP_TAGS re.compile(r/?(o:p|st1:chsdate|w:[a-z])[^]*, re.I) MSO_STYLE re.compile(r\sstyle[^]*mso-[^]*, re.I) EMPTY_SPAN re.compile(rspan[^]*\s*/span, re.I) def clean_html(raw: str) - str: text DROP_TAGS.sub(, raw) text MSO_STYLE.sub(, text) text EMPTY_SPAN.sub(, text) # 压缩连续空行但保留段落结构 text re.sub(r\n{3,}, \n\n, text) return text.strip() for src in Path(raw).glob(*.html): dst Path(manuscript/chapters) / src.name dst.parent.mkdir(parentsTrue, exist_okTrue) dst.write_text(clean_html(src.read_text(encodingutf-8, errorsignore)), encodingutf-8) print(f清洗完成: {src.name})这段脚本干三件事删掉 Office 命名空间标签、删掉所有mso-开头的内联样式、清掉空 span。参数上DROP_TAGS里的标签名单按你素材的实际脏法增减不用一次到位跑一遍看看还剩什么再补。注意errorsignore是兜底遇到 GBK 混编的老文件不至于直接崩但清洗完最好抽查两章确认没把正文段落吃掉。清洗完套统一模板。模板里只留结构样式全走外部 CSS这样后期改字号、改行距只动一个文件。!DOCTYPE html html langzh-CN head meta charsetutf-8 title{{TITLE}}/title link relstylesheet href../assets/book.css /head body article classchapter>import json import re from pathlib import Path SRC Path(manuscript/chapters) OUT Path(manuscript/toc.json) def natural_key(path: Path): 让 002 排在 010 前面而不是按字符串字典序。 return [int(t) if t.isdigit() else t.lower() for t in re.split(r(\d), path.name)] chapters [] for f in sorted(SRC.glob(*.html), keynatural_key): html f.read_text(encodingutf-8) m re.search(rh1[^]*(.*?)/h1, html, re.S) title re.sub(r[^], , m.group(1)).strip() if m else f.stem body re.sub(r[^], , html) chapters.append({ no: len(chapters) 1, title: title, file: f.name, chars: len(body.strip()), }) OUT.write_text(json.dumps(chapters, ensure_asciiFalse, indent2), encodingutf-8) print(f生成 {len(chapters)} 条目录总字数约 {sum(c[chars] for c in chapters)})natural_key是这段脚本的核心按数字段切分再比较避免10排在2前面。chars字段顺手统计正文字数交付前对一下跟合同里写的字数差多少差太多说明有章节没导进去。生成的 JSON 结构简单工具不认也能自己写个 XSLT 或模板转成目录页。3.3 编译参数怎么设参数没有万能值但有一组不出错的起步配置。下面这张表是我在多台客户机上验证过的保守组合先按这个跑通再按需调。参数起步值调整建议窗口尺寸1024×720客户屏幕普遍 1366×768 时不要超过 1200 宽启动位置屏幕居中多显示器环境下居中比记忆位置稳资源压缩开启图片多时压缩率明显纯文本可关内容加密开启中等只防顺手复制不防逆向别当保险箱禁用右键菜单开启会误伤需要复制引用的场景按客户定单实例运行开启防止客户双击三次开三个窗口图标256×256 ico小于 256 在高分屏任务栏会糊版本信息填全公司名、版本号、版权行都填杀软白名单有帮助临时目录系统默认不要硬编码到某个盘客户端可能没有 D 盘几个参数背后的道理值得说清。压缩和加密是两件事压缩针对体积加密针对别让人一眼看到源码开了加密之后启动会多一次解密开销几十兆的册子大概多 200 毫秒可以接受。单实例运行在部分工具里靠命名互斥量实现如果你的电子书要在同一台机器上开两本不同的这个选项要关掉否则第二本会被第一本的互斥量挡住表现为双击没反应。3.4 产物自测清单编译完别急着发。按下面顺序过一遍全程不超过十分钟能挡掉八成返工。在开发机上双击记录冷启动到首页出现的时间超过 3 秒要查是不是资源太大。断网再双击一次。有些工具会在启动时偷偷请求更新检查断网就卡住这条必测。把系统显示缩放改成 125% 和 150%各看一章重点看表格边框和小字号注释有没有发虚。找一台没装过任何开发环境的干净机器或者新建一个标准用户账户跑一遍。搜索一个生僻词点结果确认能跳到对应段落并高亮。翻到最后一章点关于或版本信息确认版本号、日期、版权行都对。用 2.1 的脚本算一下 overlay 占比跟上一版对比体积异常增大要查是不是重复打包了图片。第 4 条最容易被跳过也最容易出问题。开发机上装了一堆运行库和字体跑得好好的到了客户的标准账户里缺字体、缺 VC 运行库、临时目录没写权限三连击。血泪经验是自测机器越干净越好实在没有就用虚拟机装个原版系统。4. 避坑与排查五个让交付当场翻车的细节这一章按现象 → 原因 → 解决写都是实际交付里撞过的不是理论推演。每条都给出可操作的判断方法照着排查能省下跟客户来回扯皮的时间。4.1 杀毒软件误报双击就被隔离现象开发机没事客户机一解压就被杀软删掉或者双击弹出已阻止此应用。原因通常三条叠加宿主程序用了 UPX 之类压缩壳壳本身就是启发式重点关照对象运行时把资源释放到%TEMP%再执行或加载行为像释放器exe 没有数字签名信誉分为零。三者占一条可能没事占两条基本必报。解决顺序第一编译时关掉加壳/压缩可执行文件选项体积涨一点换平安第二给 exe 加代码签名证书哪怕是组织内部证书也比没有强第三把成品提交到主流杀软的白名单申诉通道一般三到五个工作日第四如果工具支持内存加载资源不落盘把这个选项打开减少释放行为。真要应急让客户把整个文件夹加进杀软排除目录但这只是临时方案不能写进交付文档当标准流程。4.2 高分屏发虚125% 缩放下字体像蒙了层雾现象客户笔记本默认 125% 或 150% 缩放打开电子书正文发虚表格线粗细不均封面图片边缘有毛边。原因进程没有声明 DPI 感知Windows 直接把整个窗口按位图拉伸等于把 1024×720 的画面放大 1.5 倍再显示。WebBrowser 控件走 GDI 渲染这个问题尤其明显。解决优先在工具设置里找高 DPI 支持或DPI 感知勾选项。没有的话给宿主程序加一个外部 manifest 文件声明dpiAware为true/PM。判断是否已生效用任务管理器看进程的DPI 感知列显示每监视器就对了。改完还要复查 2.3 里的样式因为之前为了在模糊状态下看起来清楚调大的字号现在会显得过大得回调一档。4.3 字体缺失与中文乱码现象开发机上是漂亮的思源宋体客户机打开变成宋体加方块个别符号直接显示成问号。原因CSS 里写了font-family: Source Han Serif SC但这个字体只在开发机装过没嵌进资源客户机回退到系统默认字体遇到扩展区汉字或特殊符号就没有字形。解决两条路。稳妥的是只用 Windows 自带字体族正文Microsoft YaHei, SimSun, sans-serif标题Microsoft YaHei, sans-serif跨机器一致性最好。要保留特殊字体就得子集化后以font-face嵌入font-face { font-family: BookSerif; src: url(../assets/bookserif-subset.woff2) format(woff2); font-display: swap; } body { font-family: BookSerif, Microsoft YaHei, SimSun, sans-serif; }font-display: swap保证字体没加载完时先用回退字体显示不至于白屏。子集化只保留正文实际用到的字符几十万字的小说压完通常 13MB可以接受。注意授权商用字体嵌入分发前先看清许可条款。乱码另一常见来源是文件编码清洗脚本统一按 UTF-8 写出模板里meta charsetutf-8不能少这两处对不上就会出现一半正常一半乱码的诡异现象。4.4 路径与盘符依赖换台机器图片全裂现象在自己电脑上一切正常拷到 U 盘、插到客户机封面和插图全变成红叉。原因素材引用用了绝对路径比如D:\项目\images\fig01.png或者用了反斜杠images\fig01.png。打包工具在开发机上能顺着盘符找到文件编译时没报错但资源根本没被收进去运行时自然找不到。解决所有引用统一成正斜杠相对路径且相对于当前 HTML 文件比如../images/fig01.png。清洗阶段加一条检查把绝对路径和反斜杠全揪出来import re from pathlib import Path ABS_PATH re.compile(r(src|href)([A-Za-z]:[\\/]|[\\/]{2})[^]*, re.I) BACKSLASH re.compile(r(src|href)[^]*\\[^]*, re.I) for f in Path(manuscript/chapters).glob(*.html): text f.read_text(encodingutf-8) for pattern, label in ((ABS_PATH, 绝对路径), (BACKSLASH, 反斜杠路径)): for m in pattern.finditer(text): print(f{f.name}: {label} - {m.group(0)})这条检查放在打包前跑输出为空才允许编译。BACKSLASH那条在 Windows 上会误报正常的转义实际用的时候看一眼上下文即可。另外提醒一句客户机可能是没有光驱、没有 D 盘的精简系统任何默认存到 D 盘的行为都要关掉。4.5 内容保护exe 只是容器不是保险箱现象辛辛苦苦加密的册子发布三天就有人在网上贴出解包后的 HTML 和原图。原因exe 电子书的本质是自解压 内置浏览器资源最终要在内存里以明文形态交给渲染引擎任何能在内存里读字符串的工具都能把它捞出来。加密只提高了门槛没有改变必须解密才能显示这个事实。解决思路是调整预期加分层。第一层开内容加密挡住用十六进制编辑器直接翻的人第二层正文图片加半透明水印写上仅限某某用途泄了也能追源第三层真正核心的资产比如计价公式、完整源代码不要放进去电子书只放给人看的部分。见过太多把整套内部培训视频塞进 exe 当宝贝的其实换个思路把视频放内网exe 里只放目录和摘要安全性和体积都好看。注意任何声称绝对无法提取的电子书工具宣传都不可信。判断标准很简单问一句解密后的内容在哪里答不上来的就是没想过这个问题。5. 进阶把重复打包压成一条命令的两个脚本做到第三本册子的时候人就会烦。每本书的流程一模一样改个标题、换批图、重编一次、再自测七项纯体力活。我的做法是把构建和校验拆成两个脚本构建负责把素材变成 dist 目录校验负责给产物留指纹出了问题能第一时间定位是哪一版、哪个文件。先说构建。工具本身如果支持命令行调用直接包一层 shell 就行不支持也没关系用 Python 串起清洗、目录生成、调用工具、复制产物四步中间任何一步失败就中断别带着半成品往下走。#!/usr/bin/env bash set -euo pipefail BOOK_NAMEinternal-handbook BUILD_DIRdist/${BOOK_NAME} rm -rf ${BUILD_DIR} mkdir -p ${BUILD_DIR} python3 scripts/clean_html.py python3 scripts/build_toc.py # 调用制作工具的命令行模式参数名按实际工具改 bookmaker --project ./project.bmk \ --output ${BUILD_DIR}/${BOOK_NAME}.exe \ --icon ./assets/book.ico \ --compress on \ --encrypt on \ --silent python3 scripts/verify_overlay.py ${BUILD_DIR}/${BOOK_NAME}.exe echo 构建完成: ${BUILD_DIR}set -euo pipefail是这套脚本的保命符任何一步非零退出立刻停管道里任何一段失败也算失败。实际用的时候最容易踩的是工具的命令行模式不返回正确退出码编译失败了还返回 0。所以后面必须跟一个产物校验检查文件是否存在、体积是否在合理区间。再说校验。交付前给 dist 目录里的每个文件算 SHA-256连同构建时间、版本号写进 manifest.json一起发出去。客户反馈打不开的时候先让他报 manifest 里的哈希一对就知道是不是文件传坏了。import datetime import hashlib import json from pathlib import Path BUILD Path(dist/internal-handbook) manifest { build_time: datetime.datetime.now().isoformat(timespecseconds), files: [], } for f in sorted(BUILD.rglob(*)): if not f.is_file() or f.name manifest.json: continue digest hashlib.sha256(f.read_bytes()).hexdigest() manifest[files].append({ path: str(f.relative_to(BUILD)), size: f.stat().st_size, sha256: digest, }) (BUILD / manifest.json).write_text( json.dumps(manifest, ensure_asciiFalse, indent2), encodingutf-8) print(f已登记 {len(manifest[files])} 个文件)rglob(*)递归扫整个产物目录排除 manifest 自身避免自指。哈希用 SHA-256 而不是 MD5不是为了防攻击是为了避免不同文件撞哈希的尴尬。这份 manifest 还有个副作用下次改版时把它跟上一版 diff 一下哪些文件变了、体积涨了多少一目了然比凭记忆靠谱。最后说个习惯。早年我图省事编译完自己双击看一眼封面就发包结果有一批册子在客户那边搜索全部失效原因是目录索引的锚点字段在换模板时被覆盖了而我的自测根本没点搜索。那批返工花了两天重发快递。从那以后我每次打包都强制走一遍 3.4 那七条清单一条不打勾就不发哪怕赶时间也照走。这套笨办法后来救过好几次尤其是断网测试和标准账户测试这两条看着多余出事的往往就是它们。希望帮到你。本文还有配套的精品资源点击获取