不断保留之前历史的结果就是,qq 越来越臃肿,聊天历史中的图片既然难以找到或再被发出去,那么所占据的空间就是无意义的。
本文记录了将缓存的图片文件导出并根据文件头增加后缀,以便于查看和归档的过程。
图片文件位置
经过多方搜索和查找,手机 QQ 的缓存图片文件在 Android/data/.com/tencent/mobileqq/TencentMobileQQ/chatpic/chatimg
中。在我的手机上,该文件夹下是三位十六进制表示的 4096 个文件夹。猜测是 QQ 将过往的缓存图片用某种哈希算法映射到了这 4096 个 Bucket 中,而子目录下的文件是没有后缀名的。
遇到的问题
/Android/data
目录是被保护的目录,在系统文件管理器(MiExplorer)中是无法访问的,通过 FTP 等方式也是无法访问的;常见的文件管理工具也都被禁止直接操作这里。最后是通过 MT 管理器将此处的文件复制到这里;- 缓存的图片是没有后缀名的,但是在 MT 管理器中可以预览。不难猜测图片文件的头部信息中包含了格式相关的信息(在有的地方会被称为 Magic Number),通过读取文件开头的几个字节即可判断大部分图片文件的格式。常见图片格式的文件头有:
- JPEG (jpg),文件头:FFD8FF
- PNG (png),文件头:89504E47
- GIF (gif),文件头:47494638
- Bitmap (bmp),文件头:424D
处理过程
文件压缩导出
大量小文件的复制既缓慢又繁琐,所以先将目标文件夹复制到有操作权限的位置,例如 Downloads
目录下,然后将整个文件夹压缩起来,再整体复制到 PC 上,详细操作在不同环境上可能略有不同。
文件处理
1. 文件集中
分散在多个子文件夹中不太好处理,所以将其整理在一起:
# 在目标目录下
$ mv */* ./
2. 文件后缀添加
既然知道文件格式可以从文件的最开始几个字节提取出来,简单写个程序:
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const (
BufferCapacity = 8
)
type MagicInfo struct {
MagicNum int64
ByteSize int
Suffix string
}
var (
magicInfos = []MagicInfo{
{0xFFD8FF, 3, ".jpg"},
{0x89504E47, 4, ".png"},
{0x47494638, 4, ".gif"},
{0x424D, 2, ".bmp"},
}
)
func main() {
if len(os.Args) <= 1 {
fmt.Println("Please input files or directorys")
return
}
for _, path := range os.Args[1:] {
info, err := os.Stat(path)
if err != nil {
fmt.Printf("%v\n", err)
break
}
if !info.IsDir() {
handleFile(path)
continue
}
entries, err := os.ReadDir(path)
if err != nil {
fmt.Printf("%v\n", err)
break
}
for _, item := range entries {
if item.IsDir() {
continue
}
handleFile(filepath.Join(path, item.Name()))
}
}
}
func handleFile(path string) error {
suffix := getSuffix(path)
if strings.HasSuffix(path, suffix) {
return nil
}
if suffix == "" {
return nil
}
return os.Rename(path, path+suffix)
}
func getSuffix(filepath string) string {
// 1.read file header
f, err := os.Open(filepath)
if err != nil {
fmt.Printf("Failed to open file %s, err: %v\n", filepath, err)
return ""
}
buffer := make([]byte, BufferCapacity)
_, err = f.Read(buffer)
if err != nil {
fmt.Printf("Failed to open file %s, err: %v\n", filepath, err)
return ""
}
// 2. compare header with magickNumbers
for _, item := range magicInfos {
if compareBytes(buffer, item.MagicNum, item.ByteSize) {
return item.Suffix
}
}
return ""
}
// compareBytes compare bytes with a big int, max compare 8 bytes
func compareBytes(target []byte, magicNumber int64, tSize int) bool {
if len(target) < tSize {
return false
}
var n int64
for i := 0; i < tSize; i++ {
n = (n << 8) | int64(target[i])
}
return n == magicNumber
}
在目标目录下执行:
$ go run cmd/main.go chatimg/chatimg/
即可给文件加上对应格式的后缀。
注:有少部分文件是以 .tmp 为后缀且没有匹配到图片格式,猜测可能是未完成或损坏的文件,但是比例很小,大约万分之几。