随手写个拿来即用的 → Android资源文件查重脚本

·  阅读 2130
随手写个拿来即用的 → Android资源文件查重脚本

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

0x1、起因

🤡 昨天下午干完活,离下班还有一个钟,摸起了鱼儿来,看到有群友在 开车吃瓜群技术问题

热心助人的 杰哥 嗅到了 又可以练手 的契机,反正等下班,看着挺简单,说不定自己也有用,丢下一句 下班前给你 后就开始了折腾。


0x2、脚本思路

① 基本思路

  • 一个项目包含多个模块,一般丢在同一个目录,递归遍历过滤获取所有资源文件就好,得出一个资源文件列表;
  • 对资源文件列表进行遍历,排除某些目录或文件(如/build,AndroidManifest.xml),将文件名相同的文件放一起;
  • 将遍历结果,格式化输出到文件中;

② 实现细节

  • 1、检索所有资源文件

利用 os.listdir() 可获取当前目录下所有文件,os.chdir() 能切换当前目录,遍历判断:文件夹→往下递归,资源文件→保存,非资源文件→跳过,当然为了使检索过程更直观,可把文件类型打印出来,这一步能得到资源列表;

  • 2、分析名字重复的资源文件

遍历上一步获取到的资源列表,利用 any()函数,批量判断资源是否在排除列表中,同时获取文件md5,接着这样的键值对 文件名:list('md5→文件绝对路径') 保存到字典中;

  • 3、格式化输出

对上一步中产出的重名文件字典进行处理,一些空判断,然后是遍历,拼接输出文本,最后输出到结果文件中。

整体流程比较简单,主要是一些细节处理。

③ 使用与效果演示

来看下脚本的具体效果吧 (运行需安装Python环境,官网下个安装包傻瓜下一步就好):

直接 双击打开 或命令行键入 python repeat_res_check.py 都可:

接着目录下会生成两个文件:

  • name_repeat_result.txt → 文件名相同的结果文件,适用于抽取同名文件不同文件内容比对(md5不同)

  • md5_repeat_result.txt → md5相同的但文件名不同的文件,适用于资源瘦身减少重复文件~

读者也可按需对资源类型、排除目录/文件进行定制,直接改这里,所以其实并不局限于Android项目查重:

如果有Python基础,还可以自行扩展,比如索引资源被引用位置,进行多文件等的批量替换~


0x3、完整代码

# -*- coding: utf-8 -*-
# !/usr/bin/env python
"""
-------------------------------------------------
   File     : repeat_res_check.py
   Author   : CoderPig
   date     : 2021-11-04 17:28 
   Desc     : 重复资源检索脚本
-------------------------------------------------
"""
import os
import threading as t
import hashlib

search_path = os.getcwd()  # 待分析目录,默认脚本所有路径,可自行写死

# 输出文件,依次为:名字重复、md5重复
name_repeat_result_file = os.path.join(os.getcwd(), "name_repeat_result.txt")
md5_repeat_result_file = os.path.join(os.getcwd(), "md5_repeat_result.txt")

# 暂存结果
res_list = []  # 资源文件列表
name_repeat_dict = {}
md5_repeat_dict = {}

# 资源后缀元组 (按需自己加)
res_suffix_tuple = ('.xml', '.jpg', '.png', '.webp', '.JPG', '.PNG', '.svg', '.SVG', '.webp', '.py')

# 排除目录列表或文件 (按需自己加)
exclude_dir_list = ["build{}".format(os.path.sep),
                    ".idea{}".format(os.path.sep),
                    "AndroidManifest.xml"]

# 文件读写锁
lock = t.RLock()


# 检索所有资源文件
def search_all_res_files(path):
    os.chdir(path)
    items = os.listdir(os.curdir)
    for item in items:
        path = os.path.join(item)
        # 获取路径分割后的最后部分,即文件名
        file_name = path.split(os.path.sep)[-1]
        absolute_path = "{}{}{}".format(os.getcwd(), os.path.sep, path)
        # 判断是否为目录,是往下递归
        if os.path.isdir(path):
            print("[-]", absolute_path)
            search_all_res_files(path)
            os.chdir(os.pardir)
        # 只检测资源文件
        elif file_name.endswith(res_suffix_tuple):
            print("[!]", absolute_path)
            res_list.append(absolute_path)
        else:
            print("[+]", absolute_path)


# 分析名字重复资源文件
def analysis_repeat_name_files():
    print("分析名字重复文件...")
    for res in res_list:
        if not any(name in res for name in exclude_dir_list):
            res_file_name = res.split(os.path.sep)[-1]
            file_md5 = get_file_md5(res)
            if name_repeat_dict.get(res_file_name) is None:
                name_repeat_dict[res_file_name] = ["{} → {}".format(file_md5, res)]
            else:
                name_repeat_dict[res_file_name].append("{} → {}".format(file_md5, res))
    if len(name_repeat_dict.keys()) == 0:
        print("未检测到名字重复资源...")
    else:
        format_output(name_repeat_dict, "名字重复", name_repeat_result_file)


# 分析md5重复资源文件
def analysis_repeat_md5_files():
    print("分析md5重复文件...")
    for res in res_list:
        if not any(name in res for name in exclude_dir_list):
            file_md5 = get_file_md5(res)
            if md5_repeat_dict.get(file_md5) is None:
                md5_repeat_dict[file_md5] = {res}
            else:
                md5_repeat_dict[file_md5].add(res)
    if len(name_repeat_dict.keys()) == 0:
        print("未检测到md5重复资源...")
    else:
        format_output(md5_repeat_dict, "md5重复", md5_repeat_result_file)


# 格式化输出
def format_output(origin_dict, hint, result_file):
    print("生成{}分析结果...".format(hint))
    output_content = ''
    if len(origin_dict.keys()) == 0:
        output_content += "未检测到{}资源...".format(hint)
    else:
        output_content = "共检索到资源文件:【{}】个\n共检索到{}资源文件:【{}】个\n\n"
        repeat_file_count = 0
        for (k, v) in origin_dict.items():
            if len(v) > 1:
                repeat_file_count += 1
                output_content += "{} {} {}\n".format('=' * 18, k, '=' * 18)
                for value in v:
                    output_content += "{}\n".format(value)
                output_content += "\n\n"
        output_content = output_content.format(len(res_list), hint, repeat_file_count)
        write_str_data(output_content, result_file)


# 获取文件md5
def get_file_md5(file_name):
    m = hashlib.md5()
    try:
        with lock:
            with open(file_name, 'rb') as f:
                while True:
                    data = f.read(4096)
                    if not data:
                        break
                    m.update(data)
            return m.hexdigest()
    except OSError as reason:
        print(str(reason))


# 把内容写入文件
def write_str_data(content, file_path):
    with lock:
        try:
            with open(file_path, "w+", encoding='utf-8') as f:
                f.write(content + "\n", )
                print("输出结果文件:{}\n".format(file_path))
        except OSError as reason:
            print(str(reason))


if __name__ == '__main__':
    print("当前检索目录:{}".format(search_path))
    search_all_res_files(search_path)
    print("\n资源文件检索完毕,开始进行分析...\n")
    if len(res_list) == 0:
        print("未检测到资源文件")
    else:
        analysis_repeat_name_files()
        analysis_repeat_md5_files()
复制代码

以上就是本节的全部内容,测了敝司几个项目都可以正常运行,欢迎读者测试反馈,提出改进建议,谢谢~

人生苦短,我用Python🐍~