如何在Node.js中不借助库读取fs.statSync之外的文件元数据?
原生Node.js读取图片深层元数据(以JPEG EXIF拍摄日期为例)
Node.js 没有原生提供直接提取图片/视频深层元数据的高层API,这类元数据(比如EXIF、文件格式内嵌信息)是按照特定二进制结构存储在文件内容中的,需要手动解析对应格式的二进制数据。下面以读取JPEG文件的拍摄日期(EXIF中的DateTimeOriginal)为例,给出同步实现的基础示例:
核心思路
JPEG文件的EXIF数据存储在APP1标记段(二进制标识为FF E1)中,该段内部包含Exif\0\0标识,后续是TIFF格式的元数据结构,我们需要定位到对应标签(DateTimeOriginal的标签ID为0x9003)并提取其ASCII值。
同步实现示例
const fs = require('fs'); function getJpegExifDateTaken(filePath) { // 同步读取文件为Buffer const buffer = fs.readFileSync(filePath); let offset = 0; // 验证是否为JPEG文件(开头为FF D8) if (buffer.readUInt16BE(offset) !== 0xFFD8) { throw new Error('不是JPEG格式文件'); } offset += 2; // 遍历寻找APP1标记(FF E1) while (offset < buffer.length - 2) { const marker = buffer.readUInt16BE(offset); offset += 2; // 标记段长度(包含长度本身的2字节) const segmentLength = buffer.readUInt16BE(offset); offset += 2; // 找到APP1标记,检查是否为EXIF数据 if (marker === 0xFFE1) { // 验证EXIF标识:"Exif\0\0" if (buffer.toString('ascii', offset, offset + 6) === 'Exif\0\0') { offset += 6; // TIFF头:字节序标识(4949=II=小端,4D4D=MM=大端) const endian = buffer.readUInt16BE(offset); const isLittleEndian = endian === 0x4949; offset += 2; // TIFF头的IFD0偏移(相对于TIFF头起始位置) const ifd0Offset = buffer.readUInt32(offset, isLittleEndian); offset += 4; // 跳转到IFD0位置(TIFF头起始是offset-6,所以总偏移是 (offset-6) + ifd0Offset) let ifdOffset = (offset - 6) + ifd0Offset; // 读取IFD0中的条目数量 const entryCount = buffer.readUInt16(ifdOffset, isLittleEndian); ifdOffset += 2; // 遍历IFD0条目,寻找ExifIFD的偏移(标签ID 0x8769) let exifIfdOffset = null; for (let i = 0; i < entryCount; i++) { const tagId = buffer.readUInt16(ifdOffset, isLittleEndian); if (tagId === 0x8769) { // ExifIFD的偏移值 exifIfdOffset = buffer.readUInt32(ifdOffset + 8, isLittleEndian); break; } ifdOffset += 12; // 每个IFD条目占12字节 } if (!exifIfdOffset) { throw new Error('未找到ExifIFD'); } // 跳转到ExifIFD位置 ifdOffset = (offset - 6) + exifIfdOffset; const exifEntryCount = buffer.readUInt16(ifdOffset, isLittleEndian); ifdOffset += 2; // 遍历ExifIFD条目,寻找DateTimeOriginal(标签ID 0x9003) for (let i = 0; i < exifEntryCount; i++) { const tagId = buffer.readUInt16(ifdOffset, isLittleEndian); if (tagId === 0x9003) { // 该标签是ASCII字符串,长度为20(YYYY:MM:DD HH:MM:SS\0) const valueOffset = buffer.readUInt32(ifdOffset + 8, isLittleEndian); const dateStr = buffer.toString('ascii', (offset - 6) + valueOffset, (offset - 6) + valueOffset + 19); return dateStr; } ifdOffset += 12; } throw new Error('未找到拍摄日期'); } } // 跳过当前标记段的剩余内容 offset += segmentLength - 2; } throw new Error('未找到EXIF数据'); } // 使用示例 try { const dateTaken = getJpegExifDateTaken('./test.jpg'); console.log('拍摄日期:', dateTaken); } catch (err) { console.error(err.message); }
关键说明
- 二进制解析逻辑:所有元数据的读取都依赖对对应文件格式二进制规范的理解,比如JPEG的标记段、TIFF的字节序/IFD结构,这些都需要查阅对应格式的官方规范文档。
- 扩展性:如果需要读取图片尺寸,JPEG的尺寸存储在
SOF0(FF C0)标记段中;PNG的尺寸在IHDR块的前8字节;视频/音频则需要解析对应容器格式(如MP4的moov原子、MP3的ID3标签)。 - 关于字符串匹配的误区:你之前设想的直接将文件转成字符串用正则匹配的方式不可靠,因为元数据是二进制结构化存储的,并非所有元数据都以明文"Date Taken:"形式存在,不同格式的存储方式差异极大,正则匹配无法覆盖所有场景且容易出错。
内容的提问来源于stack exchange,提问作者Native Dev
相关产品推荐
相关产品推荐

