这是我参与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🐍~