前几天在一台共享服务器上处理PDF,手里几十份合同要批量转成纯文本,结果pdftotext这个命令压根不存在。更憋屈的是,软件装不装不是我说了算——机器是公司的,权限是运维的,sudo 密码在你手上也没有用,sudoers 里根本没有你这一号人。类似的坑相信很多做数据处理、跑脚本的人都踩过:你只有自己的 home 目录,其他一切都要靠“用户态安装”来解决。
这次的主角是poppler,一个 PDF 渲染与解析工具集,里面包含了pdftotext、pdfimages、pdfinfo这一票命令行工具。很多时候你并不需要装一整个 PDF 阅读器,只需要这几个小工具就能完成文本抽取、图片提取、元数据查看这些活。问题在于,没有 sudo 权限时,apt install poppler-utils、yum install poppler这些常规操作直接作废。本文就用一套完整的“编译安装到用户目录”方案,把 poppler 装进$HOME/local,全程不需要 root,顺便把编译过程中最容易踩的依赖坑、环境变量坑、动态链接坑一次说清楚。
1. 先定位问题:你究竟卡在哪一层
1.1 没有 sudo 权限的典型场景
没有 sudo 权限这件事,看起来只有一种情况,实际细分下来至少有下面几种:
- 共享开发机/跳板机:多个团队共用一台 Linux 服务器,出于安全和管理需要,普通用户的 sudo 权限被收回。这种情况最常见。
- 容器环境:你工作在别人构建好的 Docker 容器里,容器只给了普通用户身份,没有 root。
- 远程执行环境:通过 CI/CD 平台、scheduler(比如 Slurm 的登录节点)执行任务,系统是标准镜像,但你没有安装软件包的权限。
- 伪 sudo 限制:更憋屈的是,你在 sudoers 里,但远程执行
sudo命令时提示sudo: sorry, you must have a tty to run sudo。这是 ssh 非交互式执行命令时没有分配 TTY,加上对方 sudoers 配置了requiretty导致的。也就是说,哪怕权限在,你也用不了。
我那次遇到的情况就是第一种——机器管理员明确说了:“装什么软件走流程,别自己动系统。”问题是走流程审批少说两三天,我的任务当天就要出结果。所以,最现实的方案就是:把软件装到自己的用户目录下,不碰系统目录,不污染全局环境。
1.2 为什么 poppler 对权限这么敏感
poppler 并非单一的可执行文件,而是一套以动态库为核心的软件集合。系统自带的包管理器安装 poppler 时,会向/usr/bin、/usr/lib、/usr/share这些系统目录写入文件,没有 root 权限就动不了这些位置。从源码编译时,默认的安装前缀也是/usr/local,同样需要 root。
真正麻烦的还不是 poppler 本身,而是它的依赖链。poppler 要正常编译运行,通常依赖 fontconfig、freetype2、libjpeg、libpng、zlib、openjp2(JPEG 2000 支持)等一堆库。系统里如果已有这些库的开发和运行时版本,那编译还简单;如果系统缺了某一个库,或者版本太旧,你就得先把那个库也编译安装到自己的用户目录。这一环扣一环的,就是“用户态编译安装”最考验耐心的部分。
注意:没有 sudo 不代表你不能编译软件。GCC、Make、CMake 这些开发工具往往是系统镜像里自带的,只要能找到
gcc和make,编译这条路就没堵死。
2. 方案对比:三条用户态安装路线的取舍
在动手编译之前,先把候选方案过一遍。很多时候不是“会不会编译”的问题,而是“有没有更快的路”。我用过三种方案,优缺点都很明显。
2.1 方案 A:源码编译安装到 $HOME/local(推荐)
这是最通用、最可控的方案。从 poppler 官方发布页下载源码包,手动配置编译选项,把安装路径指定为$HOME/local,所有生成的可执行文件、库文件、头文件都放进自己的目录。之后通过环境变量让系统“找到”它们。
适用场景:
- 机器上确实缺少 poppler,且没有 conda 等包管理器。
- 你需要定制编译选项,比如裁剪掉用不到的 Qt/Glib 绑定,只保留命令行工具。
- 你想完全掌控安装路径,方便多版本并存。
优点是不会污染系统,缺点是编译耗时长、依赖要自己解决。如果你机器上系统库齐全,编译 poppler 本体大概几分钟;如果依赖缺失需要顺带编译依赖,那时间就要按半小时到一小时预估了。
2.2 方案 B:借助 Miniforge/Conda 安装(最快)
如果你的机器上有 conda 或者允许你在 home 目录安装 Miniforge,那么这个方案是最省心的。
# 在 home 目录安装 Miniforge(需要一个可执行安装包) wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh bash Miniforge3-Linux-x86_64.sh # 创建独立环境并安装 poppler conda create -n pdf python=3.9 -y conda activate pdf conda install -c conda-forge poppler -y装完后,pdftotext就在 conda 对应环境的bin/目录下。conda 最大的优势是依赖全部由它自己管理,不会和系统库打架,对 GLIBC 版本、系统库缺失这些烦心事基本免疫。代价是你得接受 conda 本身占用的磁盘空间(几个 G 很常见),以及下载速度可能不理想。
2.3 方案 C:寻找静态编译的现成二进制(碰运气)
有人会把 poppler 及其依赖全部静态编译成一个独立的可执行文件,你下载下来直接就能跑,ldd一看全是not a dynamic executable。这种包在 GitHub 的 releases 里偶尔能找到,但维护者少、更新慢、架构未必匹配。它适合只想要pdftotext一个命令、不想折腾编译环境的场景,但不是一个长期可依赖的路线。
这三条路线怎么选,我给个直观的判断标准:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 机器有 conda 或允许装 Miniforge | B | 最快、最省心 |
| 无 conda,希望稳定可控 | A | 不依赖外部运行时,通用性最强 |
| 只需要单个命令跑一次 | C | 下载即用,零依赖 |
| 需要定制编译特性(如取消 Qt 依赖) | A | 编译选项可控 |
我当时的环境没有 conda,也没有现成静态包,所以走的是方案 A,后面整个实操过程也都围绕方案 A 展开。如果你只需要结果,跳到方案 B 就能收工;如果你想了解“在什么都不能装的机器上怎么把软件捏出来”,请继续往下看。
3. 核心实操:从源码把 poppler 装进 $HOME/local
3.1 环境摸底清单
动手编译之前,花五分钟把环境弄清楚,能省后面一个小时的排错时间。我通常按以下顺序检查:
# 1. 确认系统发行版与架构 cat /etc/os-release uname -m # 2. 确认开发工具是否可用 which gcc g++ make cmake pkg-config gcc --version | head -1 cmake --version | head -1 # 3. 检查关键依赖是否已存在(以 fontconfig 为例) pkg-config --modversion fontconfig ldconfig -p | grep -E 'fontconfig|freetype'如果pkg-config报错找不到某项,说明对应依赖的开发包可能没装。注意,pkg-config的搜索路径是PKG_CONFIG_PATH指定的,系统库一般在/usr/lib/pkgconfig、/usr/lib/x86_64-linux-gnu/pkgconfig这些地方。确认系统里已有的依赖,能帮你减少不必要的源码编译劳动。
3.2 依赖处理策略:能复用的绝不编译
poppler 编译时通过 CMake 查找依赖,查找过程很大程度上依赖pkg-config。所以核心策略是:
- 系统已有且版本不算太旧的库,直接复用,不做多余动作。
- 系统缺失或版本过旧(特别是头文件缺失)的库,才手动编译到
$HOME/local。 - 编译 poppler 时,把
$HOME/local加入CMAKE_PREFIX_PATH,让 CMake 能同时发现系统库和自己编译的库。
poppler 的常见依赖及其用途如下:
| 依赖 | 作用 | 缺失时的表现 |
|---|---|---|
| fontconfig | 字体配置与匹配 | CMake 报 Could NOT find Fontconfig |
| freetype2 | 字体光栅化 | 同上,或链接报错 |
| libjpeg / libpng / zlib | 图像解码 | 编译选项报错 |
| openjp2 | JPEG 2000 支持 | ENABLE_LIBOPENJPEG 选项异常 |
| cairo | 渲染输出(可选) | 影响特定工具编译 |
| nss | PDF 签名验证(可选) | 非必需,可关闭 |
如果系统缺少 fontconfig 和 freetype2,我建议先编译这两个依赖,因为它们几乎是绕不开的硬依赖。以 fontconfig 为例,标准的用户态编译三步走:
wget https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.14.2.tar.xz tar xf fontconfig-2.14.2.tar.xz cd fontconfig-2.14.2 ./configure --prefix=$HOME/local make -j4 make installfreetype2 同理:
wget https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.xz tar xf freetype-2.13.2.tar.xz cd freetype-2.13.2 ./configure --prefix=$HOME/local make -j4 make install编译依赖时有一点要特别留心:./configure时如果把 prefix 指定为$HOME/local,但系统里已有旧版 fontconfig,要注意PKG_CONFIG_PATH的顺序,确保你新编译的版本优先被找到,否则可能出现“头文件是新版、库文件是旧版”的错配,链接阶段会报一些莫名其妙的 undefined reference。
3.3 poppler 本体编译步骤
依赖就绪之后,就可以编译 poppler 本体了。到 poppler 的官方发布页(poppler.freedesktop.org)或 freedesktop 的 GitLab release 页面下载源码包,我常用的是poppler-xx.x.tar.xz。
wget https://poppler.freedesktop.org/poppler-24.02.0.tar.xz tar xf poppler-24.02.0.tar.xz cd poppler-24.02.0poppler 从 0.XX 时代之后逐步转向 CMake 构建,新版基本都是 CMake。创建一个独立的 build 目录编译,避免源码目录里到处都是生成文件:
mkdir build && cd build cmake .. \ -DCMAKE_INSTALL_PREFIX=$HOME/local \ -DCMAKE_PREFIX_PATH=$HOME/local \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_GTK_TESTS=OFF \ -DBUILD_QT5=OFF \ -DBUILD_QT6=OFF \ -DENABLE_GLIB=OFF \ -DENABLE_GTK_DOC=OFF \ -DENABLE_LIBOPENJPEG=openjpeg2 \ -DENABLE_UNSTABLE_API_ABI_HEADERS=ON逐项说明这几个参数:
CMAKE_INSTALL_PREFIX=$HOME/local:核心参数。它决定了make install把文件装到哪,可执行文件会在$HOME/local/bin,库文件在$HOME/local/lib。CMAKE_PREFIX_PATH=$HOME/local:让 CMake 在$HOME/local下搜索依赖,尤其当你手动编译过 fontconfig/freetype 时,必须加这一项。BUILD_QT5=OFF、BUILD_QT6=OFF、ENABLE_GLIB=OFF:裁剪掉不用的 GUI 绑定和 glib 绑定。我们只要命令行工具,没必要为这些绑定引入额外的依赖编译风险。如果你需要编程接口(C++ API),保留默认即可。ENABLE_LIBOPENJPEG=openjpeg2:开启 JPEG 2000 支持。这个对处理扫描版 PDF 有帮助,如果 openjp2 不存在,也可以改为ENABLE_LIBOPENJPEG=none先绕过去。ENABLE_UNSTABLE_API_ABI_HEADERS=ON:部分工具(比如pdftocairo)需要不稳定 API 头文件,开启后避免找不到头文件的报错。
接下来编译和安装:
make -j4 make install-j4表示用 4 个并行任务编译。如果机器核数多,可以适当加大,比如-j8,能明显缩短编译时间。安装完成后,$HOME/local/bin下应该有pdftotext、pdfinfo、pdfimages、pdftoppm等一系列工具。检查一下:
ls $HOME/local/bin/3.4 环境变量配置:让系统找到你装的东西
编译安装完成后,最后一步也是最关键的一步:配置环境变量。如果你不配置,直接敲pdftotext大概率会提示command not found,因为 shell 只会在PATH里找命令。
需要配置三个环境变量,并且建议写进~/.bashrc或~/.profile,让每次登录都自动生效:
export PATH=$HOME/local/bin:$PATH export LD_LIBRARY_PATH=$HOME/local/lib:$HOME/local/lib64:$LD_LIBRARY_PATH export PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig:$HOME/local/lib64/pkgconfig:$PKG_CONFIG_PATHPATH:让 shell 找到pdftotext等可执行文件。LD_LIBRARY_PATH:让动态链接器运行时找到libpoppler.so等动态库。这一步尤其重要,否则你会见到pdftotext: error while loading shared libraries: libpoppler.so.XX: cannot open shared object file: No such file or directory。PKG_CONFIG_PATH:让你之后编译其他依赖 poppler 的软件时,pkg-config能找到 poppler 的.pc文件。
配置好之后,source ~/.bashrc让配置生效,然后用两个命令验证安装结果:
which pdftotext pdftotext -vpdftotext -v会输出版本信息,看到版本号说明安装成功,动态库链接也没问题。进一步用ldd $(which pdftotext)可以查看它依赖的动态库路径,确认没有异常。
注意:环境变量的顺序有讲究。
PATH中$HOME/local/bin要放在最前面,确保优先于系统路径。如果系统里恰好也有一个旧版 pdftotext,顺序不对就会调用到旧版。LD_LIBRARY_PATH 同理,优先指向新版本库,避免运行时加载了系统旧库导致 ABI 不兼容。
4. 踩坑实录:装 poppler 时的 5 个典型问题
编译安装 poppler 的过程中,我前前后后踩过不少坑,有些坑能靠报错信息直接定位,有些则要折腾半天。这里整理一份问题排查速查表,并展开说说每个问题的解决办法。
4.1 问题排查速查表
| 报错/现象 | 原因 | 解决办法 |
|---|---|---|
| CMake Error: Could NOT find Fontconfig | 系统缺 fontconfig 开发包 | 编译 fontconfig 到$HOME/local,并加CMAKE_PREFIX_PATH |
| Could not find poppler-cpp | 调用方编译环境找不到 poppler 依赖 | 设置PKG_CONFIG_PATH指向$HOME/local/lib/pkgconfig |
| error while loading shared libraries: libpoppler.so.XX | 运行时找不到动态库 | 设置LD_LIBRARY_PATH指向$HOME/local/lib |
| 编译到一半报 undefined reference | 依赖库版本不匹配或链接顺序问题 | 清理 build 目录重新 cmake,检查PKG_CONFIG_PATH顺序 |
| make 时编译特别慢 | 并行任务数设置不合理 | 用-j$(nproc)或-j4提升并行度 |
4.2 逐条展开:报错背后的排查思路
第一条:CMake 找不到 Fontconfig
这可能是用户态编译最常遇到的第一个拦路虎。报错长这样:
CMake Error at /usr/share/cmake-3.x/Modules/FindFontconfig.cmake:60 (message): Could NOT find Fontconfig (missing: FONTCONFIG_LIBRARIES FONTCONFIG_INCLUDE_DIR)原因很简单:系统装了 runtime 库(能跑程序),但没装-dev开发包(头文件、.so软链和.pc文件)。要么 sudo 装开发包(没权限),要么自己编译一个到用户目录。我当时的做法是下载 fontconfig 源码,configure --prefix=$HOME/local编译安装,之后在 cmake 参数里加-DCMAKE_PREFIX_PATH=$HOME/local才通过。注意 fontconfig 本身又依赖 freetype2,如果 freetype 也缺失,得先把 freetype 装上,这就是依赖链的典型展开。
第二条:pkg-config找不到 poppler 的 .pc 文件
编译完 poppler 后,如果你自己写了一个调用 poppler 的小程序,用pkg-config --cflags --libs poppler去拿编译参数,大概率会失败。因为pkg-config默认只在系统目录找.pc文件。这个问题的解决方式是设置:
export PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig:$PKG_CONFIG_PATH之后再跑pkg-config --modversion poppler,能弹出版本号就说明路径生效了。
第三条:动态库运行时找不到
这是最经典的“编译成功、运行失败”案例。make install之后pdftotext就在那,但一运行就报错:
pdftotext: error while loading shared libraries: libpoppler.so.123: cannot open shared object file: No such file or directory原因是程序运行时,动态链接器按LD_LIBRARY_PATH、系统缓存/etc/ld.so.cache的顺序查找.so文件,而你安装的 lib 目录不在里面。设置好LD_LIBRARY_PATH即可解决:
export LD_LIBRARY_PATH=$HOME/local/lib:$HOME/local/lib64:$LD_LIBRARY_PATH第四条:编译到一半 undefined reference
这个坑最隐蔽。我记得有一次编译 poppler 时,链接阶段反复报undefined reference to 'FT_...',一看就是 freetype 相关符号找不齐。查了很久发现,系统里存在一个旧版 freetype 的库,而PKG_CONFIG_PATH里我把自己编译的新版放在后面,导致 pkg-config 返回的是系统旧版的路径,把旧版的.so链接进去了,但头文件又是新版,符号对不上。解决办法是调整PKG_CONFIG_PATH,把$HOME/local路径放在最前面,同时清掉 build 目录重新配置,确保 CMake 缓存里没有残留的旧路径。
第五条:编译时间过长
poppler 本体不算特别大,但如果并行任务数太少,而机器核数很多,确实会浪费时间。一次在 32 核机器上用默认单任务编译,等了将近 20 分钟才结束,后来直接杀掉进程,用make -j32重编,三分钟就完了。编译大型项目时,建议用-j$(nproc)自动获取核数,比较省事。
4.3 一个很容易被忽略的坑:版本不对应
poppler 的库版本号更新很频繁,libpoppler.so后面的数字几乎每个大版本都会变。这就带来一个连锁问题:系统里可能有某个软件(比如系统自带的 PDF 查看器)依赖的是旧版 libpoppler,你把自己编译的新版路径加到LD_LIBRARY_PATH后,那个软件可能会因为找不到旧版.so而罢工,或者更坑的是,加载到新版库后因 ABI 不兼容直接崩溃。
解决思路有两个:
- 尽量把
LD_LIBRARY_PATH的作用范围控制在当前 shell 或脚本里,不要全局导出,比如在要跑 poppler 命令的脚本里再 export。 - 如果一定要全局加,观察是否影响其他程序,必要时编写一个 wrapper 脚本,只在你需要时加载新路径。
我个人的经验是:把环境变量写进脚本而不是全局 .bashrc。比如专门写一个pdf_env.sh,用 poppler 工具时先source它,这样既不影响系统原有程序,也不用手动敲一串 export。
5. 一个更省事的变体:用 Conda 环境绕开所有依赖
5.1 什么时候值得直接切到 Conda
如果你觉得上面这一整套“检测依赖、编译依赖、编译主程序、配置环境变量”的流程对你来说太复杂,而且机器上允许你安装 Miniforge,那我强烈建议直接用 Conda 方案。很多人对 Conda 的认知停留在“Python 环境管理工具”,实际上 Conda 本身是一个通用的包管理系统,对 C/C++ 库同样管理得服服帖帖。
Conda 最牛的地方在于:它把所有依赖都装在自己的目录里,完全不碰系统目录。这意味着你不会遇到“系统库版本太旧”“GLIBC 版本不够”“没有 sudo 装不了开发包”等一系列问题。对没有 sudo 权限的用户来说,Conda 几乎是救星级别的存在。
安装 Miniforge 的流程前面已经写过,这里补充几个细节:
wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh bash Miniforge3-Linux-x86_64.sh -b -p $HOME/miniforge3-b表示静默安装,-p指定安装路径。静默安装意味着不需要交互式确认,适合脚本化操作。安装完成后,用$HOME/miniforge3/bin/conda直接调用,不需要激活 base 环境。
创建 poppler 环境:
$HOME/miniforge3/bin/conda create -n pdf -y $HOME/miniforge3/bin/conda install -n pdf -c conda-forge poppler -y装完后,poppler 的工具在$HOME/miniforge3/envs/pdf/bin/下。使用方式有两种:
# 方式一:激活环境,然后正常使用 source $HOME/miniforge3/etc/profile.d/conda.sh conda activate pdf pdftotext -v # 方式二:不激活环境,直接用绝对路径 $HOME/miniforge3/envs/pdf/bin/pdftotext -v方式二的好处是:完全不改动当前 shell 的环境变量,调用完毕就结束,对系统没有一丝副作用。这在写脚本时尤其方便——你可以在脚本里写死工具的路径,避免依赖用户手动激活 conda 环境。
5.2 Conda 方案的坑
Conda 最明显的坑是下载速度。conda-forge 的包服务器在国外,国内网络环境下拉取依赖可能很慢,甚至超时。解决方式通常是配置 conda 镜像源,把default_channels和custom_channels指向国内镜像。至于具体镜像地址,用公共的 conda 镜像即可,很多高校和云厂商都提供。
另一个坑是 Conda 环境里的 glibc 兼容性。Conda 官方支持的最低 glibc 版本是 2.17(CentOS 7 级别),如果你的系统比这个还老,Conda 可能会有兼容问题。不过这个情况在今天的主流 Linux 发行版上已经非常罕见了。
5.3 Conda 和编译方案怎么选
给一个更直接的建议:
- 如果你是临时要用,只想快点把 PDF 转出文本来,用 Conda。
- 如果你长期在一台受限服务器上工作,需要稳定、可重复的软件环境,编译安装到
$HOME/local是更干净的选择,因为你完全知道每个文件在哪、什么版本。 - 如果你要在脚本里调用这些工具,Conda 的绝对路径方案最省事,不会因为环境变量污染影响其他任务。
我自己后来在两台不同服务器上分别用了这两种方案,一台配了 Conda,一台用编译安装。就日常体验而言,编译安装在 PATH 通畅后基本不操心,Conda 则要记得环境激活或者写绝对路径。两者都能解决问题,看你的使用习惯和机器条件。
6. 一些实操体会与扩展建议
6.1 关于权限受限环境的长期策略
经过这次安装 poppler 的折腾,我摸索出一套应对“无 sudo 权限”的通用打法:
- 先检查有没有替代的包管理器,比如 conda、pip、rustup 的 cargo 安装等。很多时候你不需要自己编译,工具链已经帮你处理好了依赖。
- 再检查系统已有哪些库,能复用的尽量复用,只编译缺失层。
- 编译安装时统一使用同一个 prefix,比如
$HOME/local,不要把不同的库散落在不同目录,否则环境变量会越搞越乱。 - 环境变量尽量局部化,写进脚本而不是全局导出,避免影响系统其他程序。
- 养成记录的习惯。编译安装的依赖、版本、cmake 参数,都值得记下来。下次再换一台机器,照着记录走,效率会高很多。
这套打法不仅适用于 poppler,也适用于任何用户态软件。我后来用同样的方法装过其他几个工具,包括 JSON 处理工具jq的源码版、zip 压缩工具、以及某个内部需要的命令行工具,都是同一套思路:源码下载、configure --prefix=$HOME/local、make、make install、配置环境变量。
6.2 关于 poppler 本身的额外使用建议
最后再分享两个 poppler 使用中的小经验。
一是中文 PDF 的文本提取。pdftotext在默认情况下对中文 PDF 支持其实不错,但偶尔会遇到提取出来全是乱码或者空白的情况,这通常跟 PDF 内嵌字体编码有关。可以尝试pdftotext -enc UTF-8 input.pdf output.txt,显式指定 UTF-8 编码。如果是扫描件,没有文本层,pdftotext也救不了你,这时候得配合 OCR,poppler 提供的pdftoppm先把每页转成图片,再走 OCR 流程。我在处理一批扫描版合同时就是这么做的,先pdftoppm -png -r 300按 300 DPI 输出图片,再喂给 OCR 引擎,识别率稳定在 95% 以上。
二是批量处理时的文件命名。用pdfimages提取图片时,它会按image-001.jpg这样的规则自动命名,如果 PDF 里图片很多,注意输出目录要单独建一个,避免文件混在一起不好收拾。这些细节文档里不会写,但实际跑一遍就会明白。
编译安装 poppler 这件事本身不算复杂,真正繁琐的是依赖管理和环境配置。如果你也面临类似处境,希望这份记录能帮你少走几步弯路,一次就把pdftotext跑起来。