news 2026/10/13 5:46:17

PyInstaller源码安装与打包:从Python到Zookeeper部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyInstaller源码安装与打包:从Python到Zookeeper部署

简介:这是PyInstaller 4.7的官方源码压缩包,面向需要将Python脚本转换为独立可执行程序的开发者,尤其适合在分布式、云原生等环境中交付Python应用。资源共404个文件,以Python源码(319个py)为主,辅以C源文件与头文件(23个c、25个h)以及exe、icns、ico等平台相关文件,同时包含构建脚本、配置文件和文档说明,压缩后仅2.65MB,结构精炼完整。已有631人下载学习。通过阅读源码,可以深入理解PyInstaller的分析、编译、打包三阶段工作机制:动态追踪import依赖、生成捆绑包、处理非Python资源;C实现部分覆盖启动器、归档解压、异常对话框等底层模块,适合想二次开发或排查打包问题的中高级开发者。该版本针对新Python版本提供支持,并优化了多依赖场景下的处理能力,对于使用Zookeeper等协调服务的Python应用,也能借助它生成无需预装解释器的交付产物,提升部署效率。

1. 拿到 pyinstaller-4.7.tar.gz 之后:为什么还要自己从源码装

PyPI 下载页上那个pyinstaller-4.7.tar.gz,很多老手也是一扫而过,毕竟平时一条pip install pyinstaller就能拿到预编译好的 wheel。但这个 tar.gz 才是 PyInstaller 最原始的形态:里面pyi_launch.c、pyi_archive.c、pyi_pythonlib.c这些 C 源码,才是真正决定打包产物能不能启动的 bootloader。需要从源码包开始装的人,通常是被 wheel 坑过的那批:Python 刚升到新版本,官方还没出配套 wheel;内网 PyPI 源不完整,拉不到包;又想改启动画面、改解压策略,只能在源码层动手。下面就用 4.7 这个版本,把下载、构建、打包、避坑整条链路讲透。

2. 从源码包开始装:PyPI 下载与本地构建

2.1 PyPI 官网定位 tar.gz:文件名版本号背后的含义

在 PyPI 上搜 pyinstaller,进入 Download files 页面,能看到pyinstaller-4.7.tar.gz和一堆 wheel 文件并列。文件名结构是项目名+版本号+格式,但信息量不小:tar.gz 代表源码包,没有平台限制,装上以后为你当前平台重新编译 bootloader;wheel 则带有 cp39-cp39-win_amd64 这种标签,只能装到对应 Python 版本和系统架构上。如果你打算修改 bootloader 源码,或者遇到 wheel 与当前 Python 不兼容,源码包是唯一可走的路。

4.7 这个版本号也值得留意。PyInstaller 每个版本都会针对 Python 新版本做支持,4.7 发布时正好卡在 Python 3.10 刚出道的阶段,对 3.10 的支持还在磨合期。你的环境如果是 Python 3.8 或 3.9,直接装 wheel 最省事;但如果是刚升级的 Python 3.10,wheel 可能装不上,这时候下载 tar.gz 从源码构建反而可控。

下载源码包我通常用 pip download,而不是在浏览器里点链接,因为 pip 会校验哈希并放到指定目录:

pip download pyinstaller==4.7 --no-binary :all: -d ./src sha256sum ./src/pyinstaller-4.7.tar.gz tar -tzf ./src/pyinstaller-4.7.tar.gz | head -20

--no-binary :all:是让 pip 忽略 wheel,强制拿源码包;-d ./src把文件存到当前目录下 src 文件夹。sha256sum 用来核对 PyPI 页面提供的哈希,这一步在内网分发时不能省。tar -tzf 列出包内顶层结构,重点看有没有 bootloader 和 PyInstaller 目录,这两个是硬指标,缺了后面必编译失败。

2.2 本地安装源码包:pip install 指定本地路径

拿到 tar.gz 后,最稳妥的安装方式是把它直接交给 pip:

pip install ./src/pyinstaller-4.7.tar.gz

pip 会把源码包走一遍构建流程,生成临时 wheel 再装进当前环境。如果之前装过其他版本的 PyInstaller,先pip uninstall pyinstaller,否则可能因为已安装的 dist-info 冲突导致模块指向混乱。PyInstaller 4.7 在 Windows 上依赖 pywin32-ctypes,在 Linux 上依赖 altgraph,这些 pip 会自动装上,不用自己处理。

如果你打算改 C 源码,就先解压再以开发模式安装:

tar -xzf ./src/pyinstaller-4.7.tar.gz cd pyinstaller-4.7 pip install -e .

开发模式下,Python 层代码修改后立刻生效,但 bootloader 的 C 源码改动需要重新构建。PyInstaller 的构建逻辑是:打包时如果发现当前环境缺少匹配的 bootloader,会调用本机编译器自动编译一次。这意味着 Windows 上要有 Visual Studio Build Tools,Linux 上要有 gcc 和 Python 开发头文件。如果缺编译器,通常会看到Python.h not found或command 'gcc' failed之类的报错,装好环境后重新执行打包即可。

装完确认版本时,我建议不要只敲pyinstaller --version,因为 PATH 里可能有多个 Python 环境。用下面两行对比:

pyinstaller --version python -m PyInstaller --version

第一行走 PATH,第二行走当前解释器。如果输出不一致,说明环境串了。我的习惯是后面所有打包都统一用python -m PyInstaller,这样模块版本和 Python 解释器一定匹配,不会出现用 Python 3.9 的 pyinstaller 去打包 Python 3.8 项目这种乌龙。

2.3 装完先验证:bootloader 文件与平台绑定

版本号正确只是第一步,真正决定成败的是 bootloader 是否被正确编译。你可以用一段 Python 找到安装目录,再检查里面的引导文件:

python -c "import PyInstaller; print(PyInstaller.__file__)" python -c "import PyInstaller, os; print(os.path.join(os.path.dirname(PyInstaller.__file__), 'bootloader'))" find $(python -c "import PyInstaller, os; print(os.path.dirname(PyInstaller.__file__))") -path "*/bootloader/*" -type f | head

在 Windows 上,bootloader 目录下应看到 run.exe 与 runw.exe;Linux 上应看到名为 run 的 ELF 文件。如果找不到 bootloader,说明安装时构建步骤被跳过或失败。可以手动执行一次构建:

cd pyinstaller-4.7/bootloader python waf configure build

注意 waf 是 bootloader 的构建脚本,只有源码包里才有。执行python waf configure build会把所有平台相关的引导文件编出来。这一步需要网络访问,因为 waf 会下载一些辅助头文件,内网环境要提前配好 pip 源和 HTTPS 代理。如果不需要定制 bootloader,其实用默认的预编译引导文件就够了,不必手动执行 waf。

还要验证平台匹配。用file命令看一眼引导文件的格式:

file $(python -c "import PyInstaller, os; print(os.path.join(os.path.dirname(PyInstaller.__file__), 'bootloader', 'Linux-64bit', 'run'))")

输出ELF 64-bit表示这是 Linux 版本,在 Windows 上对应的是 PE32+ 格式。很多新人把源码包从 Linux 下载后拿到 Windows 上装,装完发现 pyinstaller 命令行能用,但打出来的 exe 根本不是 Windows 应用程序。PyInstaller 平台绑定的特性要求单平台打包必须在对应操作系统上完成,交叉编译只能靠 CI 多平台任务。

3. 把脚本打包成 exe:命令参数与 spec 文件的真实结构

3.1 最简单的一行命令:-F 与 -D 的取舍

命令行工具就绪后,开始打包一个真正的脚本。PyInstaller 的打包过程官方叫三步:分析、编译、捆绑,听起来复杂,实际操作就是一行命令加一个 spec 文件。

假设入口是 app.py:

python -m PyInstaller --onefile --clean app.py

--onefile等价于-F,--clean清掉 build 目录的历史缓存。跑完后 dist 里只有一个 app(Windows 上是 app.exe),看起来非常干净。这里立刻会遇到第一个决定:单文件还是目录模式。

单文件模式的优点是交付物只有一个文件,发给别人不需要带目录;缺点是启动时要由 bootloader 把整个程序解压到系统临时目录,文件越大启动越慢。目录模式(--onedir或-D)生成一个包含 exe 和依赖文件的目录,启动快,便于单独替换动态库,但交付时要整目录打包。我一般这样选:给非技术同事的内网小工具用-F;要打进 Docker 镜像或做 CI 部署的服务端应用用-D,因为少一层解压,且出问题能直接看依赖文件是否缺失。

--onefile的一个冷门参数是--runtime-tmpdir,可以指定解压目录。默认在%TEMP%/_MEIxxxxxx,如果系统 temp 在机械盘,启动会明显变慢。有需求的场景可以指定到本地 SSD 路径,但不建议写死到固定绝对路径,因为目标机器未必有同样的目录。通常让它默认就够。

3.2 spec 文件:Analysis、PYZ、EXE、COLLECT 各管什么

用命令行打包只是第一步,稍微复杂一点就要改 spec。先用--name指定产物名,PyInstaller 会生成同名 spec:

python -m PyInstaller --onefile --name app --clean app.py

生成的 app.spec 本质是 Python 脚本,里面四个核心结构是关键。

Analysis 是整个依赖分析的中心。它读取入口脚本,追踪所有 import,把结果拆成 pure、binaries、datas 三组。下面这个 spec 是从实际项目简化的:

a = Analysis( ['app.py'], pathex=['/project/src'], binaries=[], datas=[('config.ini', '.')], hiddenimports=['handler_a'], hookspath=['./hooks'], runtime_hooks=[], ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name='app', console=True, icon='app.ico', ) coll = COLLECT( exe, a.binaries, a.datas, name='app', )

参数说明:pathex是额外搜索路径;datas每个元素是 (源文件, 目标目录),目标目录相对于最终产物的根;hiddenimports是手动补充在分析阶段没扫到的模块;hookspath指向自定义 hook 脚本目录,用于扩展分析逻辑。EXE 的console=True表示保留控制台窗口,GUI 程序改成 False。exclude_binaries=True只出现在-D模式,它告诉构建器不要在 EXE 里重复嵌入动态库,由 COLLECT 统一收集。

改完 spec 后,重复执行的命令不再是 app.py,而是 spec 文件:

python -m PyInstaller app.spec

PyInstaller 看到参数里有 .spec 就直接按 spec 执行,不会再做初始分析。这种方式的收益是固定的数据文件路径、隐藏导入和资源文件都写在同一份版本可控的文本里,多人维护时不会因为命令行参数不一样而打出不同结果。

3.3 数据文件、动态导入与隐藏导入的配置

PyInstaller 对静态 import 的分析很准,但对动态加载无能为力。比如这段代码:

# 插件按需加载 plugin_name = 'plugin_' + user_selection plugin = __import__(plugin_name)

PyInstaller 看到的 import 是字符串拼接,不可能知道 plugin_a、plugin_b 需要被打包。解决办法是在 spec 的 hiddenimports 里显式列出:

hiddenimports=['plugin_a', 'plugin_b']

数据文件也一样。代码里open('config.json')不会让 PyInstaller 自动把 config.json 放进产物,因为它只跟字节码和二进制打交道。命令行可以用 --add-data 一次性传递:

python -m PyInstaller --add-data "config.json:." --onefile app.py

注意不同平台的分隔符:Windows 用分号,Linux 和 macOS 用冒号。如果分隔符用错,PyInstaller 不一定报错,但运行时会在临时目录找不到文件。我的建议是数据文件多于两个时,直接在 spec 的 datas 列表里写,避免跟 shell 转义搞在一起。

另一个隐藏问题是第三方库自己的动态导入。PyInstaller 通过 hook 机制为流行库打补丁,hook 文件会额外收集那些靠__import__或 pkg_resources 加载的子模块。4.7 自带了大量 hook,比如 kazoo、requests、lxml 都有对应 hook。如果你发现某个库打包后提示缺模块,先看 PyInstaller 安装目录下hooks/stdhooks有没有对应文件,有的话通常是版本不匹配,升级或降级该库可以解决;没有的话,才需要写自定义 hook。

4. 打包避坑指南:六类高频翻车现场

打包报错分三类:构建时报错、启动时闪退、运行时导入失败。前两类可以从控制台和临时目录的日志里拿到线索,第三类要下点功夫。下面这几条是我在 Windows 和 Linux 上都栽过的案例,按现象、原因、解决的顺序写。

4.1 中文路径导致 exe 启动闪退

现象:exe 在自己机器正常,发给同事后闪退,双击无任何提示。最后发现路径里有中文,比如D:\项目\软件包\app.exe。

原因:PyInstaller 4.7 的 bootloader 在解析命令行和临时目录时,对非 ASCII 路径支持不完善。C 层调用的是窄字符 API,中文路径被截断或转乱码。

解决:交付时统一改成英文路径;如果产品名有中文,只在窗口标题体现,安装路径仍用英文。也可以修改 bootloader 源码里的路径处理函数后重新编译,但维护成本高,不如直接约束交付目录。注意 Windows 用户名是中文时,%TEMP%也可能包含中文,此时单文件模式解压也会失败。可以把--runtime-tmpdir指到一个英文路径规避。

4.2 ModuleNotFoundError:动态 import 没被扫到

现象:构建过程没有报错,运行 exe 后提示ModuleNotFoundError。常见于 pkgutil、importlib 动态加载的插件。

原因:静态分析器只能看到字面量 import,字符串拼接不可解析。

解决:把动态导入的模块名写进 spec 的hiddenimports。另一种做法是在入口文件顶部加import handler_a之类的显式引入,让分析器扫到。注意,如果模块名本身也是动态生成的,比如getattr(__import__(module), 'handler'),还得检查该模块是否依赖其他内部模块,可能需要一并 hiddenimports。排查时用pyi-archive-viewer dist/app.exe看模块列表,确认缺失项。

4.3 单文件模式启动慢与“程序无响应”假象

现象:-F打包的 exe 双击后半天没窗口,任务管理器显示无响应。体感像是死了。

原因:单文件模式启动时,bootloader 把整个包解压到临时目录。包体积越大,解压越慢,看起来像假死。

解决:换成-D目录模式或减小打包体积;如果仍要单文件,用--runtime-tmpdir指定一个位于内存盘的路径,并检查杀软是否临时扫描解压文件导致延迟。启动后可以先显示一个启动画面,PyInstaller 支持 splash image,但 4.7 里需要额外配置。

4.4 Linux 打的包到 Windows 跑不了

现象:在 Linux CI 上打包,产物拷到 Windows 双击提示不是有效的 Win32 程序。

原因:PyInstaller 不跨平台编译,Linux 生成 ELF,Windows 需要 PE。bootloader 也是平台相关。

解决:每个目标平台分别打包。Windows 上可以装 PyInstaller 后直接打包;Linux 上只能生成 Linux 可执行文件。云原生的 CI 可以配置多个 runner,Windows job 和 Linux job 各自构建产物。macOS 场景同理,需要 macOS runner。

4.5 UPX 压缩后直接崩溃

现象:打包后的 exe 在某些机器上启动即崩溃,事件日志里是 0xc0000005,或者干脆静默退出。

原因:系统里装了 UPX,PyInstaller 检测到后自动对引导文件和动态库做了压缩。UPX 版本与 bootloader 不兼容时,解压后代码段被破坏。

解决:打包加--noupx。在 4.7 中如果系统里有 upx,默认就会启用,所以出现莫名崩溃时优先检查是不是 UPX 干的。也可以卸载 upx 或设置 PATH 不包含 upx。upx 省下的空间对 Python 程序而言通常只有几 MB,收益不大,风险却不小。

4.6 杀软误报与加壳的取舍

现象:exe 刚生成就被 Windows Defender 拦截,或者压缩包发出去被安全网关标记为病毒。

原因:PyInstaller 产物自带自解压特征,行为有点像常见的恶意下载器,杀软对无签名的自解压程序误报率高。

解决:商用分发给 exe 做 Authenticode 签名;内网使用可以在打包机上加白名单。签名能显著降低误报,但签名证书有成本。另一个可选思路是用--key加密 Python 字节码,但这只是增加逆向门槛,不改变自解压特征。最实际的是在 README 里附上 SHA256 哈希和构建环境说明,遇到误报配合安全团队验证。

以上六类问题里,至少三类的排查方式是把构建日志和运行日志打开、把产物用 pyi-archive_viewer 列一遍。很多坑不是 PyInstaller 本身有问题,而是环境不一致放大了问题。

5. 在 Zookeeper/云原生场景下用 PyInstaller:依赖打包与配置分离

5.1 kazoo 客户端为什么能被 PyInstaller 识别

PyInstaller 4.7 对 Zookeeper 生态的支持主要体现在 Python 客户端 kazoo 上。kazoo 为了保证跨平台连接稳定性,内部使用了动态选择 handler 的机制,底层依赖 gevent、eventlet 或 threading 模型。如果只依赖静态分析,PyInstaller 很可能只抓到 kazoo.client 一个顶层模块,等代码运行到zk.start()时才去实例化 handler,这时找不到对应实现,程序直接抛异常。

PyInstaller 4.7 的 hooks 目录里带了hook-kazoo.py,它会把这个动态选择过程补全,把 kazoo 的 handlers、components 等相关子模块一并收进 Analysis。这也正好印证了一个排查思路:遇到第三方库打包后缺模块,先看自带 hook 有没有覆盖,而不是盲目加 hiddenimports。如果你自己维护了一个类似 kazoo 这样带动态加载的库,可以仿照官方 hook 格式写一个自定义 hook。

一个典型的验证方式是打包一个使用 kazoo 的最小脚本,然后用pyi-archive-viewer检查模块列表里是否包含 kazoo.handlers、kazoo.recipe 等子模块。如果列表里只有 kazoo.client,说明 hook 没有生效,大概率是 kazoo 版本比 4.7 期望的版本新,hook 内的匹配规则失效,此时可以把 kazoo 降级到 2.8 左右的旧版测试。

5.2 把 Zookeeper 地址和节点配置塞进 spec,运行时再覆盖

Zookeeper 脚本通常有多个环境:开发测试、预发、生产。每套环境的 ZK 地址、根节点路径都不一样。如果把配置写死进 Python 代码,每次换环境都要重新打包,这个做法不推荐。正确做法是运行时从环境变量读取配置,spec 里只带一份默认配置作为兜底。

代码示例:

import os from kazoo.client import KazooClient hosts = os.environ.get("ZK_HOSTS", "localhost:2181") root = os.environ.get("ZK_ROOT", "/app/demo") zk = KazooClient(hosts=hosts) zk.start() children = zk.get_children(root) print(children) zk.stop()

打包时把默认配置作为数据文件一起打进产物:

python -m PyInstaller --onedir --name zk_app --add-data "zk_default.ini:etc" zk_app.py

这里的zk_default.ini是开发环境默认值,etc是产物里的目标目录。运行时程序读取配置文件的路径要注意:单文件模式下,程序解压在sys._MEIPASS指到的临时目录,配置文件也在那里;目录模式下,配置文件就在 exe 同级目录下。所以读取逻辑一般写成:

import os import sys base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) cfg_path = os.path.join(base, "etc", "zk_default.ini") print("config path:", cfg_path)

sys._MEIPASS是 PyInstaller 在运行时注入的变量,指向解压目录。在源码环境下没有这个变量,代码就会 fallback 到当前脚本目录,这样开发与打包后都能读到同一个文件。这个技巧在处理数据文件时非常实用。

5.3 云原生环境下的执行体:容器镜像里只放一个可执行文件

云原生部署的诉求是镜像里不要装一堆运行时依赖,最好只放一个启动命令。PyInstaller 能够把 Python 解释器与依赖全打进去,所以 Dockerfile 可以非常简洁:

FROM debian:stable-slim COPY dist/zk_app/ /opt/app/ CMD ["/opt/app/zk_app"]

注意这里用的是 debian 而不是 alpine。PyInstaller 4.7 打包出来的 libpython 和第三方库通常链接的是 glibc,而 Alpine 默认用 musl,直接跑大概率报错。如果你确实要在 Alpine 里用,必须在打包环境也基于 Alpine 构建,并且 PyInstaller 的 bootloader 要针对 musl 重新编译,这条路成本高,不建议新手走。

另一个云原生场景的注意点是动态库依赖。比如 kazoo 依赖其他 C 库,打包时 PyInstaller 会把 .so 复制到产物目录,但系统级依赖不会复制。启动镜像时如果提示缺 libssl、libcrypto,就在 Dockerfile 里用 apt 补上:

RUN apt-get update && apt-get install -y libssl3 libc6

如果目标镜像需要最小化,可以用 multipart 构建:先用 python-slim 镜像打包出 dist 目录,再把 dist 拷贝到干净的 debian 运行镜像。这样运行镜像里没有 pip、没有源码,攻击面更小,多进程环境下的安全暴露也少很多。

还有一点关于多进程:如果你的 Zookeeper 应用用 multiprocessing 同时起多个进程,注意 PyInstaller 打包后的程序在 Windows 上没有现成的 fork,spawn 模式会重新执行主模块。此时if __name__ == '__main__':必须是单独入口,否则子进程会把整个逻辑重复跑一遍。这个坑在分布式脚本里经常出现,打包后和源码环境下的行为完全不一样。

6. 交付前的最后一步:验证产物与启动自检

6.1 pyi-archive_viewer 看捆绑包

打包完成不等于可以交付。我拿到 dist 里的产物,第一件事是验证里面装的东西对不对。

pyi-archive_viewer dist/zk_app/zk_app

这个工具会打开交互式界面,输入l列出归档内容,输入x 模块名可以提取某个文件到当前目录,输入q退出。检查的关键点有两个:一是 kazoo 相关模块是否齐全,二是数据文件是否在归档里。如果发现缺东西,回 spec 补 hiddenimports 再重新打包。

6.2 启动日志自检,验证 Zookeeper 连接

接下来在干净环境里做一次真实启动。我会临时把 ZK_HOSTS 指向测试集群,运行产物并观察日志。用环境变量覆盖配置,确认程序从环境变量读值而不是用默认值。

ZK_HOSTS=zk-test.internal:2181 ./dist/zk_app/zk_app

等程序跑完,检查三件事:进程能正常启动并停留在监听状态;Zookeeper 连接日志显示成功拿到节点列表;Ctrl+C 能优雅退出。如果退出时出现 segfault,不要忽略,多半是 bootloader 或某个动态库的适配问题,不是业务代码的小毛病。

从那以后我每次交付前,都在一个没装 Python 的全新容器或虚拟机里,走一遍归档检查和启动自检。第一次觉得多此一举,直到有一次在干净环境里发现产物依赖了开发机上才有的某个 DLL,才知道打包产物根本不干净。现在这套流程已经是肌肉记忆了,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:46:16

人脸多任务学习与运动重定向端到端网络

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:45:48

EMC设计前置:从风险识别到整改的工业案例实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:45:16

基于pytest+allure+yaml的接口自动化框架搭建实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:45:13

Bash select命令实战:告别read+case,轻松写出交互式菜单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:44:31

RSA加密算法从原理到实战:公钥私钥、数字签名与OpenSSL操作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:44:02

率失真曲线平滑绘制:Matlab与Python的PCHIP插值实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华