安全风险 - 检测设备是否为模拟器

devtools/2024/12/23 0:28:29/

在很多安全机构的检测中,关于模拟器的运行环境一般也会做监听处理,有的可能允许执行但是会提示用户,有的可能直接禁止在模拟器上运行我方APP

如何判断当前 app 是运行在Android真机,还是运行在模拟器? 可能做 Framework 的朋友思维会更开阔一些,不过现在也可以跟我这门外汉一起来稍微了解下

    • 攻略过程
      • 基础思考
      • 进阶思考
    • easyprotector 框架解析
    • 剥离 easyprotector框架 模拟器检测功能
      • CommandUtil (公共、基础)
      • EmulatorCheckUtil (核心)
      • EmulatorCheckCallback (配置)
      • CheckResult(配置)
      • 调用方式

攻略过程

其实我已经很久没有用过模拟器了,不过可以肯定的是模拟器与真机的本质区别大概率在于运行载体

Android 有非常多的模拟器,我已知的有官方自带的 Genymotion 模拟器,三方平台的夜神模拟器、天天模拟器等,所以想要完全鉴别出设备的运行环境,其实应该是存在一定问题的,我们只能说尽可能保证一定的容错率(我有想过很多应用平台提供的云机,但好像大多提供的都是真机,所以此项不在考虑范围之内)

基础思考

以下的一些思考主要结合了 如何判断是否是模拟器还是真机、全面检测设备是否模拟器、一行代码帮你检测Android模拟器、安卓逆向环境检测–模拟器 等多篇新旧文章

  • IMEI 设备识别码:用于唯一标识移动设备(放弃

模拟器的 IMEI 可以修改,早期平板可能没有IMEI,但是随着时代发展很多平板设备已拥有了属于自己的IMEI

  • MAC地址:物理地址,硬件地址,也称局域网地址,由网络设备制造商生产时烧录在网卡(放弃

模拟器的MAC地址是固定的几种,但是这些固定的地址随着模拟器类型递增,没有找到合适的,同时mac地址现在可以被模拟…

  • 通过调用公开或者隐藏的系统API判断放弃

因为调用结果可以轻易被修改,比如直接修改Android的源代码或者借助 Xposed Framework 进行修改(这种场景我虽未参与,但是应该可以参考Java的反射机制)

  • 功能验证:初期模拟器功能并不完善,可以采用类似 打电话、发短信 等方式进行功能测试,但是后续随着模拟器升级已补全对应功能 (放弃)
public boolean isSimulator1() {String url = "tel:" + "10086";Intent intent = new Intent();intent.setData(Uri.parse(url));intent.setAction(Intent.ACTION_DIAL);// 是否可以处理跳转到拨号的 Intentboolean canResolveIntent = intent.resolveActivity(mContext.getPackageManager()) != null;return !canResolveIntent;
}
  • 设备IDS、特有文件验证(放弃

涉及到敏感权限时需要申请权限,根据授权结果容易出现误判,同时影响用户体验

    private static String[]known_numbers = {"15555215554","15555215556","15555215558","15555215560","15555215562","15555215564","15555215566","15555215568","15555215570","15555215572","15555215574","15555215576","15555215578","15555215580","15555215582","15555215584",};public static Boolean CheckPhoneNumber(Context context){TelephonyManager telephonyManager =(TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);String phonenumber =telephonyManager.getLine1Number();for(String number :known_numbers){if(number.equalsIgnoreCase(phonenumber)){Log.v("Result:","Find PhoneNumber!");return true;}}Log.v("Result:","Not Find PhoneNumber!");return false;}

特有文件检测 - 权限要求

在这里插入图片描述

设备IDS检测 - 权限要求

在这里插入图片描述

  • CPU检测方法:现在的模拟器基本可以做到模拟手机号码,手机品牌,cpu信息等,比如逍遥/夜神模拟器读取 ro.product.board 进行了处理,能得到预先设置的cpu信息(放弃
public static boolean checkIsNotRealPhone() {String cpuInfo = readCpuInfo();if ((cpuInfo.contains("intel") || cpuInfo.contains("amd"))) {return true;}return false;
}
public static String readCpuInfo() {String result = "";try {String[] args = {"/system/bin/cat", "/proc/cpuinfo"};ProcessBuilder cmd = new ProcessBuilder(args);Process process = cmd.start();StringBuffer sb = new StringBuffer();String readLine = "";BufferedReader responseReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"));while ((readLine = responseReader.readLine()) != null) {sb.append(readLine);}responseReader.close();result = sb.toString().toLowerCase();} catch (IOException ex) {}return result;
}

进阶思考

在一篇Blog内有看到这样一副图,值得借鉴,因为我最后使用的 easyprotector 框架就做了硬件信息检测

在这里插入图片描述

Tip:有兴趣的可以参考以下方法扩展 easyprotector 框架内的模拟器检测部分

模拟器框架文件

 
UNEXPORT void AntiEmulator::check_file() {char *(path[]) = {"/system/bin/androVM-prop",   //检测androidVM"/system/bin/microvirt-prop", //检测逍遥模拟器--新版本找不到特征"/system/lib/libdroid4x.so",  //检测海马模拟器"/system/bin/windroyed",      //检测文卓爷模拟器"/system/bin/nox-prop",       //检测夜神模拟器--某些版本找不到特征"/system/lib/libnoxspeedup.so",//检测夜神模拟器"/system/bin/ttVM-prop",      //检测天天模拟器"/data/.bluestacks.prop",     //检测bluestacks模拟器  51模拟器"/system/bin/duosconfig",     //检测AMIDuOS模拟器"/system/etc/xxzs_prop.sh",   //检测星星模拟器"/system/etc/mumu-configs/device-prop-configs/mumu.config", //网易MuMu模拟器"/system/priv-app/ldAppStore",   //雷电模拟器"/system/bin/ldinit",             //雷电模拟器"/system/bin/ldmountsf",          //雷电模拟器"/system/app/AntStore",          //小蚁模拟器"/system/app/AntLauncher",       //小蚁模拟器"vmos.prop",                     //vmos虚拟机"fstab.titan",                   //光速虚拟机"init.titan.rc",                 //光速虚拟机"x8.prop",                       //x8沙箱和51虚拟机"/system/lib/libc_malloc_debug_qemu.so", //AVD QEMU"/system/bin/microvirtd","/dev/socket/qemud","/dev/qemu_pipe"};for (int i = 0; i < sizeof(path) / sizeof(char*); i++){if (Syscall::check_file_or_dir_exists(path[i])){LOGI("check_file  %s file existing", path[i]);// TODO 风险}}
}

easyprotector 框架解析

关于 easyprotector框架 文档可以参考 一行代码帮你检测Android模拟器(更新至1.1.0) 会更详细一些

我之所以在 github 选这个框架,主要有几点原因

  • 模拟器检测框架有限
  • 该框架star高
  • 检测方面考虑全面(检测渠道、设备型号、硬件制造商、主板名称、基带信息、第三方应用数量、传感器数量、是否支持蓝牙、是否支持相机、是否支持闪光灯等)
  • 能力之内可以适当扩展检测项
  • 为了避免误判设备类型,内部加入条件判断,只有满足3项模拟器特征才会判定为模拟器

直接通过框架源码,查看下模拟器检测的执行过程

  1. 调用了 EasyProtectorLib.checkIsRunningInEmulator 方法

在这里插入图片描述

  1. EasyProtectorLib 中找到了 checkIsRunningInEmulator 实际调用了 EmulatorCheckUtil

在这里插入图片描述

  1. 查看 EmulatorCheckUtil - readSysProperty 源码即可

源码中 ro.buildro.productgsm.version 含义,做Framework朋友可能比较了解,主要用于检测一些系统级信息

在这里插入图片描述

之前有提到功能扩展和延伸,大家可自行在该类源码中进行扩展,不过最好另起方法,有自信的话改原方法也行

在这里插入图片描述


剥离 easyprotector框架 模拟器检测功能

因为 easyprotector框架 中涉及的功能比较多,我习惯性只抽出了我所需要的部分

CommandUtil (公共、基础)

简单来看主要是通过反射机制获取一些系统的公共资源信息

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;/*** Project Name:EasyProtector* Package Name:com.lahm.library* Created by lahm on 2018/6/8 16:23 .*/
public class CommandUtil {private CommandUtil() {}private static class SingletonHolder {private static final CommandUtil INSTANCE = new CommandUtil();}public static final CommandUtil getSingleInstance() {return SingletonHolder.INSTANCE;}public String getProperty(String propName) {String value = null;Object roSecureObj;try {roSecureObj = Class.forName("android.os.SystemProperties").getMethod("get", String.class).invoke(null, propName);if (roSecureObj != null) value = (String) roSecureObj;} catch (Exception e) {value = null;} finally {return value;}}public String exec(String command) {BufferedOutputStream bufferedOutputStream = null;BufferedInputStream bufferedInputStream = null;Process process = null;try {process = Runtime.getRuntime().exec("sh");bufferedOutputStream = new BufferedOutputStream(process.getOutputStream());bufferedInputStream = new BufferedInputStream(process.getInputStream());bufferedOutputStream.write(command.getBytes());bufferedOutputStream.write('\n');bufferedOutputStream.flush();bufferedOutputStream.close();process.waitFor();String outputStr = getStrFromBufferInputSteam(bufferedInputStream);return outputStr;} catch (Exception e) {return null;} finally {if (bufferedOutputStream != null) {try {bufferedOutputStream.close();} catch (IOException e) {e.printStackTrace();}}if (bufferedInputStream != null) {try {bufferedInputStream.close();} catch (IOException e) {e.printStackTrace();}}if (process != null) {process.destroy();}}}private static String getStrFromBufferInputSteam(BufferedInputStream bufferedInputStream) {if (null == bufferedInputStream) {return "";}int BUFFER_SIZE = 512;byte[] buffer = new byte[BUFFER_SIZE];StringBuilder result = new StringBuilder();try {while (true) {int read = bufferedInputStream.read(buffer);if (read > 0) {result.append(new String(buffer, 0, read));}if (read < BUFFER_SIZE) {break;}}} catch (Exception e) {e.printStackTrace();}return result.toString();}
}

EmulatorCheckUtil (核心)

import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.text.TextUtils;import static android.content.Context.SENSOR_SERVICE;
import static cn.com.huaan.fund.acts.base.safe.CheckResult.RESULT_EMULATOR;
import static cn.com.huaan.fund.acts.base.safe.CheckResult.RESULT_MAYBE_EMULATOR;
import static cn.com.huaan.fund.acts.base.safe.CheckResult.RESULT_UNKNOWN;/*** Project Name:EasyProtector* Package Name:com.lahm.library* Created by lahm on 2018/6/8 15:01 .*/
public class EmulatorCheckUtil {private EmulatorCheckUtil() {}private static class SingletonHolder {private static final EmulatorCheckUtil INSTANCE = new EmulatorCheckUtil();}public static final EmulatorCheckUtil getSingleInstance() {return SingletonHolder.INSTANCE;}public boolean readSysProperty(Context context, EmulatorCheckCallback callback) {if (context == null)throw new IllegalArgumentException("context must not be null");int suspectCount = 0;//检测硬件名称CheckResult hardwareResult = checkFeaturesByHardware();switch (hardwareResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("hardware = " + hardwareResult.value);return true;}//检测渠道CheckResult flavorResult = checkFeaturesByFlavor();switch (flavorResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("flavor = " + flavorResult.value);return true;}//检测设备型号CheckResult modelResult = checkFeaturesByModel();switch (modelResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("model = " + modelResult.value);return true;}//检测硬件制造商CheckResult manufacturerResult = checkFeaturesByManufacturer();switch (manufacturerResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null)callback.findEmulator("manufacturer = " + manufacturerResult.value);return true;}//检测主板名称CheckResult boardResult = checkFeaturesByBoard();switch (boardResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("board = " + boardResult.value);return true;}//检测主板平台CheckResult platformResult = checkFeaturesByPlatform();switch (platformResult.result) {case RESULT_MAYBE_EMULATOR:++suspectCount;break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("platform = " + platformResult.value);return true;}//检测基带信息CheckResult baseBandResult = checkFeaturesByBaseBand();switch (baseBandResult.result) {case RESULT_MAYBE_EMULATOR:suspectCount += 2;//模拟器基带信息为null的情况概率相当大break;case RESULT_EMULATOR:if (callback != null) callback.findEmulator("baseBand = " + baseBandResult.value);return true;}//检测传感器数量int sensorNumber = getSensorNumber(context);if (sensorNumber <= 7) ++suspectCount;//检测已安装第三方应用数量int userAppNumber = getUserAppNumber();if (userAppNumber <= 5) ++suspectCount;//检测是否支持闪光灯boolean supportCameraFlash = supportCameraFlash(context);if (!supportCameraFlash) ++suspectCount;//检测是否支持相机boolean supportCamera = supportCamera(context);if (!supportCamera) ++suspectCount;//检测是否支持蓝牙boolean supportBluetooth = supportBluetooth(context);if (!supportBluetooth) ++suspectCount;//检测光线传感器boolean hasLightSensor = hasLightSensor(context);if (!hasLightSensor) ++suspectCount;//检测进程组信息CheckResult cgroupResult = checkFeaturesByCgroup();if (cgroupResult.result == RESULT_MAYBE_EMULATOR) ++suspectCount;if (callback != null) {StringBuffer stringBuffer = new StringBuffer("Test start").append("\r\n").append("hardware = ").append(hardwareResult.value).append("\r\n").append("flavor = ").append(flavorResult.value).append("\r\n").append("model = ").append(modelResult.value).append("\r\n").append("manufacturer = ").append(manufacturerResult.value).append("\r\n").append("board = ").append(boardResult.value).append("\r\n").append("platform = ").append(platformResult.value).append("\r\n").append("baseBand = ").append(baseBandResult.value).append("\r\n").append("sensorNumber = ").append(sensorNumber).append("\r\n").append("userAppNumber = ").append(userAppNumber).append("\r\n").append("supportCamera = ").append(supportCamera).append("\r\n").append("supportCameraFlash = ").append(supportCameraFlash).append("\r\n").append("supportBluetooth = ").append(supportBluetooth).append("\r\n").append("hasLightSensor = ").append(hasLightSensor).append("\r\n").append("cgroupResult = ").append(cgroupResult.value).append("\r\n").append("suspectCount = ").append(suspectCount);callback.findEmulator(stringBuffer.toString());}//嫌疑值大于3,认为是模拟器return suspectCount > 3;}private int getUserAppNum(String userApps) {if (TextUtils.isEmpty(userApps)) return 0;String[] result = userApps.split("package:");return result.length;}private String getProperty(String propName) {String property = CommandUtil.getSingleInstance().getProperty(propName);return TextUtils.isEmpty(property) ? null : property;}/*** 特征参数-硬件名称** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByHardware() {String hardware = getProperty("ro.hardware");if (null == hardware) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = hardware.toLowerCase();switch (tempValue) {case "ttvm"://天天模拟器case "nox"://夜神模拟器case "cancro"://网易MUMU模拟器case "intel"://逍遥模拟器case "vbox":case "vbox86"://腾讯手游助手case "android_x86"://雷电模拟器result = RESULT_EMULATOR;break;default:result = RESULT_UNKNOWN;break;}return new CheckResult(result, hardware);}/*** 特征参数-渠道** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByFlavor() {String flavor = getProperty("ro.build.flavor");if (null == flavor) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = flavor.toLowerCase();if (tempValue.contains("vbox")) result = RESULT_EMULATOR;else if (tempValue.contains("sdk_gphone")) result = RESULT_EMULATOR;else result = RESULT_UNKNOWN;return new CheckResult(result, flavor);}/*** 特征参数-设备型号** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByModel() {String model = getProperty("ro.product.model");if (null == model) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = model.toLowerCase();if (tempValue.contains("google_sdk")) result = RESULT_EMULATOR;else if (tempValue.contains("emulator")) result = RESULT_EMULATOR;else if (tempValue.contains("android sdk built for x86")) result = RESULT_EMULATOR;else result = RESULT_UNKNOWN;return new CheckResult(result, model);}/*** 特征参数-硬件制造商** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByManufacturer() {String manufacturer = getProperty("ro.product.manufacturer");if (null == manufacturer) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = manufacturer.toLowerCase();if (tempValue.contains("genymotion")) result = RESULT_EMULATOR;else if (tempValue.contains("netease")) result = RESULT_EMULATOR;//网易MUMU模拟器else result = RESULT_UNKNOWN;return new CheckResult(result, manufacturer);}/*** 特征参数-主板名称** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByBoard() {String board = getProperty("ro.product.board");if (null == board) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = board.toLowerCase();if (tempValue.contains("android")) result = RESULT_EMULATOR;else if (tempValue.contains("goldfish")) result = RESULT_EMULATOR;else result = RESULT_UNKNOWN;return new CheckResult(result, board);}/*** 特征参数-主板平台** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByPlatform() {String platform = getProperty("ro.board.platform");if (null == platform) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;String tempValue = platform.toLowerCase();if (tempValue.contains("android")) result = RESULT_EMULATOR;else result = RESULT_UNKNOWN;return new CheckResult(result, platform);}/*** 特征参数-基带信息** @return 0表示可能是模拟器,1表示模拟器,2表示可能是真机*/private CheckResult checkFeaturesByBaseBand() {String baseBandVersion = getProperty("gsm.version.baseband");if (null == baseBandVersion) return new CheckResult(RESULT_MAYBE_EMULATOR, null);int result;if (baseBandVersion.contains("1.0.0.0")) result = RESULT_EMULATOR;else result = RESULT_UNKNOWN;return new CheckResult(result, baseBandVersion);}/*** 获取传感器数量*/private int getSensorNumber(Context context) {SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);return sm.getSensorList(Sensor.TYPE_ALL).size();}/*** 获取已安装第三方应用数量*/private int getUserAppNumber() {String userApps = CommandUtil.getSingleInstance().exec("pm list package -3");return getUserAppNum(userApps);}/*** 是否支持相机*/private boolean supportCamera(Context context) {return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);}/*** 是否支持闪光灯*/private boolean supportCameraFlash(Context context) {return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);}/*** 是否支持蓝牙*/private boolean supportBluetooth(Context context) {return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);}/*** 判断是否存在光传感器来判断是否为模拟器* 部分真机也不存在温度和压力传感器。其余传感器模拟器也存在。** @return false为模拟器*/private boolean hasLightSensor(Context context) {SensorManager sensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); //光线传感器if (null == sensor) return false;else return true;}/*** 特征参数-进程组信息*/private CheckResult checkFeaturesByCgroup() {String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");if (null == filter) return new CheckResult(RESULT_MAYBE_EMULATOR, null);return new CheckResult(RESULT_UNKNOWN, filter);}
}

EmulatorCheckCallback (配置)

回调监听,可以获取到具体检测结果

public interface EmulatorCheckCallback {void findEmulator(String emulatorInfo);
}

CheckResult(配置)

对检测结果进行类别划分,方便管理

public class CheckResult {public static final int RESULT_MAYBE_EMULATOR = 0;//可能是模拟器public static final int RESULT_EMULATOR = 1;//模拟器public static final int RESULT_UNKNOWN = 2;//可能是真机public int result;public String value;public CheckResult(int result, String value) {this.result = result;this.value = value;}
}

调用方式

 val readSysProperty = EmulatorCheckUtil.getSingleInstance().readSysProperty(context, null)if (readSysProperty) {//根据需要进行风险提示等相关业务ToastUtils.showToast("您当前可能运行在模拟器设备,请谨防安全风险!")}

http://www.ppmy.cn/devtools/46318.html

相关文章

6-Django项目--分页模块化封装参数共存

目录 utils/page_data.py 分页模块化封装 在app当中创建一个python package 在当前包里面创建py文件 参数共存 完整代码 utils/page_data.py --包里创建py文件. # -*- coding:utf-8 -*- from django.utils.safestring import mark_safe from copy import deepcopyclass…

黄仁勋的AI时代:英伟达GPU革命的狂欢与挑战

在最近的COMPUTEX 2024大会上&#xff0c;英伟达创始人黄仁勋发布了最新的Blackwell GPU。这次发布不仅标志着英伟达在AI领域的又一次飞跃&#xff0c;也展示了其对未来技术发展的战略规划。本文将详细解析英伟达最新技术的亮点&#xff0c;探讨其在AI时代的市场地位和未来挑战…

探索Python爬虫:实战演练,打造你的数据采集利器

在这个信息爆炸的时代&#xff0c;数据成为了最宝贵的资源之一。Python&#xff0c;以其简洁的语法和强大的库支持&#xff0c;成为了数据采集和处理的首选语言。本文将带领你走进Python爬虫的世界&#xff0c;通过一系列实战演练&#xff0c;教你如何构建自己的数据采集工具。…

Vue:网络请求axios

Axios 是一个基于 Promise 的网络请求库。 安装 npm install axios引入 <script setup> import axios from axios </script>常用 API axios.get(url[, config]) axios.get(/user,{params: {id: 1234,} }) .then(response > console.log(response)) .catch(…

python-bert模型基础笔记0.1.02

python-bert模型基础笔记0.1.00 bert的适合的场景bert多语言和中文模型bert模型两大类官方建议模型模型中名字的含义标题bert系列模型包含的文件bert系列模型参数微调与迁移学习区别参考链接bert的适合的场景 裸跑都非常优秀,句子级别(例如,SST-2)、句子对级别(例如Multi…

弘君资本股市资讯:“20cm”跌停!这些个股重挫,什么原因?

多家收到2023年年报问询函的A股上市公司&#xff0c;今天上午股价团体重挫&#xff0c;多股盘中甚至“20cm”跌停。 记者发现&#xff0c;在A股上市公司2023年年报发表后&#xff0c;交易所近期对上市公司密布宣布年报问询函&#xff0c;到目前&#xff0c;交易所对上市公司20…

MapReduce学习之MapJoin案例实现

MapReduce学习之MapJoin案例实现 1.当前main方法所在的入口类 package com.shujia.mr.mapJoin;import com.shujia.mr.reduceJoin.ReduceJoin; import com.shujia.mr.reduceJoin.ReduceJoinMapper; import com.shujia.mr.reduceJoin.ReduceJoinReducer; import org.apache.had…

【python】修改目标检测的xml标签(VOC)类别名

需求&#xff1a; 在集成多个数据集一同训练时&#xff0c;可能会存在不同数据集针对同一种目标有不同的类名&#xff0c;可以通过python脚本修改数据内的类名映射&#xff0c;实现统一数据集标签名的目的。 代码&#xff1a; # -*- coding: utf-8 -*- # Time : 2023/9/11 1…