提取手机 QQ 的聊天图片

ShadowC

| 本文阅读量: -

不断保留之前历史的结果就是,qq 越来越臃肿,聊天历史中的图片既然难以找到或再被发出去,那么所占据的空间就是无意义的。

本文记录了将缓存的图片文件导出并根据文件头增加后缀,以便于查看和归档的过程。

图片文件位置

经过多方搜索和查找,手机 QQ 的缓存图片文件在 Android/data/.com/tencent/mobileqq/TencentMobileQQ/chatpic/chatimg中。在我的手机上,该文件夹下是三位十六进制表示的 4096 个文件夹。猜测是 QQ 将过往的缓存图片用某种哈希算法映射到了这 4096 个 Bucket 中,而子目录下的文件是没有后缀名的。

遇到的问题

  1. /Android/data目录是被保护的目录,在系统文件管理器(MiExplorer)中是无法访问的,通过 FTP 等方式也是无法访问的;常见的文件管理工具也都被禁止直接操作这里。最后是通过 MT 管理器将此处的文件复制到这里;
  2. 缓存的图片是没有后缀名的,但是在 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 为后缀且没有匹配到图片格式,猜测可能是未完成或损坏的文件,但是比例很小,大约万分之几。