Android平台如何提取docx、txt、rtf格式文件的元数据
Android平台提取docx、txt、rtf文件元数据实现方案
以下是生产环境验证过的可行实现,覆盖要求的author、title、creator、creationDate字段提取,不需要依赖重型付费SDK。
前置权限配置
先在Manifest里声明对应版本的文件读取权限,否则会抛权限异常:
<!-- Android 12及低版本读存储权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <!-- Android 13及以上读文档类媒体文件权限 --> <uses-permission android:name="android.permission.READ_MEDIA_DOCUMENTS" />
1. DOCX格式元数据提取
DOCX本质是遵循OOXML规范的zip压缩包,核心元数据统一存放在压缩包内的docProps/core.xml路径下,两种实现方式按需选:
- 轻量自实现:直接把文件当zip包解压,读取core.xml里的对应节点映射字段即可,几乎无额外体积开销
- 快速实现:引入裁剪版的POI依赖,不用自己处理zip解析和XML节点映射,核心代码如下:
import org.apache.poi.ooxml.POIXMLProperties import org.apache.poi.xwpf.usermodel.XWPFDocument import java.io.InputStream fun parseDocxMetadata(inputStream: InputStream): Map<String, String?> { val result = mutableMapOf<String, String?>( "author" to null, "title" to null, "creator" to null, "creationDate" to null ) XWPFDocument(inputStream).use { doc -> val coreProps = doc.properties.coreProperties result["author"] = coreProps.creator result["creator"] = coreProps.creator result["title"] = coreProps.title result["creationDate"] = coreProps.created?.toString() } return result }
注意:别直接引入官方全量POI依赖,打包后会让APK体积暴涨10M以上,纯提取元数据优先选自实现方案,或者用社区裁剪过的POI精简包,能把依赖体积压到几百K。
2. TXT格式元数据提取
别费劲找TXT的通用内嵌元数据方案,这个格式从诞生起就没有统一的元数据存储标准,分两类场景处理:
- 通用场景:通过系统
MediaStore接口读取文件系统级的元数据,能拿到文件名(可作为title兜底)、文件创建时间,没有统一的author、creator字段,默认返回空即可 - 特殊场景:部分编辑器生成的带YAML头的Markdown/TXT文件,可以逐行读取文件开头识别YAML块,解析自定义的作者、标题字段,仅对特定来源的文件生效
核心MediaStore查询代码:
import android.content.ContentUris import android.content.Context import android.provider.MediaStore fun parseTxtMetadata(context: Context, fileId: Long): Map<String, String?> { val result = mutableMapOf<String, String?>( "author" to null, "title" to null, "creator" to null, "creationDate" to null ) val queryUri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), fileId) context.contentResolver.query( queryUri, arrayOf( MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.DATE_ADDED ), null, null, null )?.use { cursor -> if (cursor.moveToFirst()) { result["title"] = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)) result["creationDate"] = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_ADDED)).toString() } } return result }
注意:Android 10及以上开启分区存储后,不要硬编码文件路径直接访问,统一通过ContentResolver拿输入流,避免触发权限报错。
3. RTF格式元数据提取
RTF是纯文本标记格式,所有元数据都存在文件头的控制字段里,不需要引入第三方库,只读文件前2KB内容(元数据不会存在文件正文段)做正则匹配即可,控制字和字段的映射关系:
\author后跟随的内容为作者/创建者\title后跟随的内容为标题\creatim后跟随的\yr/\mo/\dy/\hr/\min/\sec字段拼接为创建时间
核心解析代码:
import java.io.InputStream import java.util.regex.Pattern fun parseRtfMetadata(inputStream: InputStream): Map<String, String?> { val result = mutableMapOf<String, String?>( "author" to null, "title" to null, "creator" to null, "creationDate" to null ) // 只读前2KB头内容,减少IO开销 val header = String(inputStream.readNBytes(2048), Charsets.US_ASCII) // 匹配标题 val titlePattern = Pattern.compile("\\\\title\\s+([^;\\\\}]+)") titlePattern.matcher(header).takeIf { it.find() }?.let { result["title"] = it.group(1).trim() } // 匹配作者/创建者 val authorPattern = Pattern.compile("\\\\author\\s+([^;\\\\}]+)") authorPattern.matcher(header).takeIf { it.find() }?.let { val author = it.group(1).trim() result["author"] = author result["creator"] = author } // 匹配创建时间 格式示例:\creatim\yr2024\mo6\dy13\hr15\min22\sec0 val timePattern = Pattern.compile("\\\\creatim\\\\yr(\\d+)\\\\mo(\\d+)\\\\dy(\\d+)\\\\hr(\\d+)\\\\min(\\d+)\\\\sec(\\d+)") timePattern.matcher(header).takeIf { it.find() }?.let { result["creationDate"] = "${it.group(1)}-${it.group(2).padStart(2, '0')}-${it.group(3).padStart(2, '0')} ${it.group(4).padStart(2, '0')}:${it.group(5).padStart(2, '0')}:${it.group(6).padStart(2, '0')}" } return result }
踩过的坑:部分小众编辑器导出的RTF会在元数据字段里插入转义符、嵌套花括号,正则匹配时要补充转义过滤逻辑,不然很容易出现内容截断、匹配错位的问题。
通用兼容注意事项
- 所有文件解析逻辑必须放在子线程执行,不要在主线程做IO和解析操作,避免触发ANR
- 遇到加密的DOCX、损坏的RTF文件要做好异常捕获,不要直接崩溃
- 时间字段建议统一转成时间戳存储,方便后续做格式化展示
内容的提问来源于stack exchange,提问作者devger
相关产品推荐
相关产品推荐

