简介:友益文书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] != b"MZ": raise ValueError("不是有效的 DOS/PE 文件头") pe_off = struct.unpack_from("<I", data, 0x3C)[0] if data[pe_off:pe_off + 4] != b"PE\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 控件(MSHTML) | Chromium 系内核 |
|---|---|---|
| 单文件体积增量 | 几乎为 0,依赖系统组件 | 80MB~200MB 起 |
| CSS3 / Flex / Grid | 部分支持,Grid 基本不可用 | 完整支持 |
| 字体渲染 | 依赖 GDI,高分屏易发虚 | DirectWrite,缩放更稳 |
| 启动速度 | 快,冷启动 1 秒内 | 慢,冷启动 2~5 秒 |
| 系统兼容 | 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>和<h2>,<h3>往里塞会让目录树长得像蜈蚣,读者翻两下就烦。
2.4 目录、书签与搜索是怎么串起来的
目录树本质上是一份"标题—文件—锚点"的三元组索引,工具在编译时把它序列化进资源段。书签是读者手动加的,存在用户目录的配置文件里,跟 exe 分离,所以换台机器书签就没了,这点要提前跟客户说清楚。全文搜索则是编译期建倒排或者运行期暴力扫正文,前者快但体积涨,后者慢但实现简单。
判断一本电子书搜索好不好用,有个土办法:找一篇正文里出现频繁但目录里没有的词,搜一下,看命中结果能不能点击跳转并高亮。很多工具搜索能出结果,点了却跳回封面,原因是锚点 ID 在压缩时丢了。这个在自测清单里必须列一条。
3. 从零做一本可交付的 exe 电子书:素材清洗到产物自测的六步流水线
这一章是可以直接抄作业的部分。整套流程我跑过几十本,稳定下来的顺序是:清洗素材 → 套 HTML 模板 → 生成目录索引 → 配置编译参数 → 编译 → 逐项自测。中间任何一步偷懒,后面都要用两倍时间补。
3.1 素材清洗与 HTML 模板
原始素材大概率来自 Word 或 WPS 导出的 HTML,里面的<o:p>、mso-样式、内联style="font-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(r"<span[^>]*>\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(parents=True, exist_ok=True) dst.write_text(clean_html(src.read_text(encoding="utf-8", errors="ignore")), encoding="utf-8") print(f"清洗完成: {src.name}")这段脚本干三件事:删掉 Office 命名空间标签、删掉所有mso-开头的内联样式、清掉空 span。参数上,DROP_TAGS里的标签名单按你素材的实际脏法增减,不用一次到位,跑一遍看看还剩什么再补。注意errors="ignore"是兜底,遇到 GBK 混编的老文件不至于直接崩,但清洗完最好抽查两章,确认没把正文段落吃掉。
清洗完套统一模板。模板里只留结构,样式全走外部 CSS,这样后期改字号、改行距只动一个文件。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>{{TITLE}}</title> <link rel="stylesheet" href="../assets/book.css"> </head> <body> <article class="chapter">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"), key=natural_key): html = f.read_text(encoding="utf-8") m = re.search(r"<h1[^>]*>(.*?)</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_ascii=False, indent=2), encoding="utf-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保证字体没加载完时先用回退字体显示,不至于白屏。子集化只保留正文实际用到的字符,几十万字的小说压完通常 1~3MB,可以接受。注意授权,商用字体嵌入分发前先看清许可条款。乱码另一常见来源是文件编码,清洗脚本统一按 UTF-8 写出,模板里<meta charset="utf-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(encoding="utf-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_NAME="internal-handbook" BUILD_DIR="dist/${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(timespec="seconds"), "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_ascii=False, indent=2), encoding="utf-8") print(f"已登记 {len(manifest['files'])} 个文件")rglob("*")递归扫整个产物目录,排除 manifest 自身避免自指。哈希用 SHA-256 而不是 MD5,不是为了防攻击,是为了避免不同文件撞哈希的尴尬。这份 manifest 还有个副作用:下次改版时把它跟上一版 diff 一下,哪些文件变了、体积涨了多少,一目了然,比凭记忆靠谱。
最后说个习惯。早年我图省事,编译完自己双击看一眼封面就发包,结果有一批册子在客户那边搜索全部失效,原因是目录索引的锚点字段在换模板时被覆盖了,而我的"自测"根本没点搜索。那批返工花了两天重发快递。从那以后我每次打包都强制走一遍 3.4 那七条清单,一条不打勾就不发,哪怕赶时间也照走。这套笨办法后来救过好几次,尤其是断网测试和标准账户测试这两条,看着多余,出事的往往就是它们。希望帮到你。
本文还有配套的精品资源,点击获取