PDF文件头特征码是%PDF-,必须位于文件前1024字节内且通常在偏移0处,需用二进制模式读取前16字节精确比对,防止正文或注释干扰。

PDF 文件头特征码是什么
PDF 文档开头固定是 %PDF-,后面紧跟版本号(如 %PDF-1.7),这是 PDF 规范强制要求的“魔数”(magic number)。它必须出现在文件前 1024 字节内,且不能被空格、BOM 或注释行遮挡——但实际中,常见情况是开头直接就是 %PDF-,位置在 offset 0。
注意:不能只检查是否包含 %PDF- 字符串,因为可能出现在正文或注释里;必须检查文件起始区域(通常前 4–16 字节)是否以该序列开头。
用 C++ 读取文件头并比对特征码
核心思路是打开文件(只读)、读取前 16 字节、检查是否以 %PDF- 开头。这里的关键不是“不打开文件”(物理上不可能不打开),而是“不解析全文、不调用 PDF 库、不渲染内容”。
- 必须用二进制模式打开:
std::ifstream file(path, std::ios::binary),否则 Windows 下遇到\x0D\x0A可能触发文本模式换行转换,破坏原始字节 - 只读前 16 字节足够(
%PDF-1.7最长也就 8 字节,留余量) - 读取后要检查
file.gcount(),防止文件过短(比如只有 2 字节)导致误判 - 别用
std::string直接构造二进制数据,避免遇到\x00提前截断;用std::vector<char></char>或原始缓冲区更稳妥
示例片段:
立即学习“C++免费学习笔记(深入)”;
std::ifstream file(path, std::ios::binary); if (!file.is_open()) return false; std::vector<char> buf(16); file.read(buf.data(), buf.size()); size_t n = static_cast<size_t>(file.gcount()); if (n < 5) return false; // %PDF- 至少 5 字节 if (std::string_view(buf.data(), n).substr(0, 5) == "%PDF-") return true;
常见误判场景和绕过方法
很多看似是 PDF 的文件其实不是:加密 PDF 的开头可能被混淆、损坏 PDF 可能缺失头部、某些工具生成的“伪 PDF”会硬塞 %PDF- 但结构非法。反过来,合法 PDF 也可能带 BOM(如 UTF-8 BOM \xEF\xBB\xBF)或注释行(% ...),把 %PDF- 推到第 2 行。
- 若文件以 BOM 开头,跳过
\xEF\xBB\xBF(UTF-8)或\xFF\xFE(UTF-16 LE)再查 - 若前 1024 字节内有以
%开头的行(PDF 注释),需按行解析,找第一个非注释行——但实践中绝大多数正规 PDF 都把%PDF-放在第 1 行第 1 列 - 遇到
%%EOF在前但没%PDF-?那基本不是 PDF,是 PostScript 或其他格式混入了 PDF 尾部标记 - 不要依赖 MIME 类型或扩展名:
.pdf后缀可伪造,libmagic的file命令底层也是靠读文件头
性能与跨平台注意事项
这个判断本身极快(一次小 read + 内存比对),但要注意几个容易忽略的点:
- 路径含中文或特殊字符时,Windows 下用
std::ifstream默认不支持宽字符路径,得用_wfopen+fopen包装,或转 UTF-8 后用std::filesystem::u8path(C++20) - Linux/macOS 下普通路径没问题,但 NFS 或 FUSE 挂载点可能有延迟,不过只读 16 字节影响微乎其微
- 别在循环里反复打开同一文件做判断——如果批量处理,缓存文件头结果更合理
- 没有“完全不打开文件”的方式;mmap 是打开+映射,/proc/self/fd 是打开后暴露句柄,本质一样
真正复杂的点在于:PDF 规范允许头部前最多 1024 字节填充(比如加水印工具插入垃圾数据),但几乎所有阅读器和工具链都只认前几字节。所以务实做法就是查前 16 字节——既覆盖 99.9% 场景,又避开过度实现。



















