当前位置: 首页 > news >正文

某漫画网站JS逆向反混淆流程分析

文章目录

  • 1. 写在前面
  • 1. 接口分析
  • 2. 反混淆分析

【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章

作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!

1. 写在前面

  前段时间有几个小伙伴咨询过关于某漫画网站的图片数据如何下载获取,看了一下觉得这个网站蛮适合初学者或者逆向分析爱好者练手的!它涉及到反调试、数据解密、JS反混淆、Cookie反爬虫、TLS指纹的检测


分析目标

aHR0cHM6Ly93d3cuY29sYW1hbmdhLmNvbS9tYW5nYS1tZjg3NDEyNy8xLzU2Lmh0bWw=

初看时有小伙伴也提出过使用自动化的方式来获取图片链接再下载,但是这个链接是临时的。自动化是可以的,但只能是等待所有服务端下发的图片内容加载完毕渲染呈现到页面后使用截图的方式来获取,如下所示:

在这里插入图片描述

1. 接口分析

打开网站准备调试分析之前是有一个反调试的,一般这种大多通过动态生成的函数或代码片段触发!然后过这种反调试的方案是很多的(还有一些大佬开源分享的绝大场景下通杀的方案)如下所示:

在这里插入图片描述

这里我们也是可以通过重写构造函数与其原型方法拦截且移除动态生成代码中反调试语句,代码如下所示:

(function () {'use strict';const OriginalFunction = Function;Function = function (...args) {handleDebuggerRemoval(args);logStackTrace("Function");return OriginalFunction(...args);};Function.prototype = OriginalFunction.prototype;Function.prototype.constructor = function (...args) {handleDebuggerRemoval(args);logStackTrace("Function.constructor");return OriginalFunction(...args);};/*** 移除字符串参数中的 "debugger" 语句* @param {Array} args - 参数数组*/function handleDebuggerRemoval(args) {for (let i = 0; i < args.length; i++) {if (typeof args[i] === "string") {args[i] = args[i].replace(/debugger/g, "");}}}function logStackTrace(context) {const stackTrace = new Error().stack;log(`[${context}] Call Stack:`, stackTrace);if (DEBUG?.deb === 0) {debugger;}log(`[${context}] =============== End ===============`);}
})();

过了反调试之后,我们首先去看一下发包的情况。其实初次看的话没有明确的特征告诉我们从哪里下手,只能花点时间来各方面来分析一下,如下所示:

在这里插入图片描述

点击可发现这个接口貌似就是图片请求加载的发包(不过注意请求的是.enc.webp)大概率是经过处理的,而且在Cookies中也是添加了某些关键的字段,如下所示:

在这里插入图片描述

这里猜测在后续的请求中可能是需要携带这个Cookie参数请求的

在这里插入图片描述

这种场景下通过经验来梳理一下流程分析我们可以从网页加载的源码中来开始,它这种实时章节的加载大概率是不断的拼接后续的漫画图来获取资源的!然后在首次请求页面资源的时候肯定有基础的数据或者一些特征可以挖掘的

这里我们过掉反调试之后重方一下页面请求(请求记得过一下TLS检测)并保证Cookie请求的时候携带了__cf__bkm参数,如下所示:

在这里插入图片描述

可以看到请求的HTML内容中有一串密文(C_DATA)这个就是需要去解密的,解密后会拿到当前漫画章节中的详情信息JSON数据

2. 反混淆分析

它这个JS代码都是经过混淆的!不要硬看,浪费时间。核心逻辑基本都在custom.js、read.js文件中,先把JS拿下来反混淆静态分析一下!找到解密C_DATA的地方,混淆代码如下所示:

在这里插入图片描述

整个这块拿下来先解一下混淆,静态分析就很清晰了。处理解密C_DATA的混淆源码还原之后的JS代码如下所示:

 if (__cad.isInReadPage()) {let decryptedData;__cad.useCodeIndex = 1;try {decryptedData = window.devtools.jsd("USJZOHqNw84GoMA9",window.devtools.jsc.enc.Base64.parse(window.C_DATA).toString(window.devtools.jsc.enc.Utf8));if (decryptedData === '') {__cad.useCodeIndex = 2;decryptedData = window.devtools.jsd("c9UPIOaql84fJIoz",window.devtools.jsc.enc.Base64.parse(window.C_DATA).toString(window.devtools.jsc.enc.Utf8));}window.devtools.jse(decryptedData);} catch (error) {__cad.useCodeIndex = 2;decryptedData = window.devtools.jsd("c9UPIOaql84fJIoz",window.devtools.jsc.enc.Base64.parse(window.C_DATA).toString(window.devtools.jsc.enc.Utf8));}window.devtools.jse(decryptedData);const decodedUrls = window.devtools.jsc.enc.Base64.parse(window.image_info.urls__direct).toString(window.devtools.jsc.enc.Utf8);window.__images_yy = decodedUrls.split("|SEPARATER|");window.__specialDisplay = 1;if (!window.image_info.img_type) {window.__specialDisplay = 0;}
}

直接在控制台把进行解密的JS代码执行可以看到明文的C_DATA数据,如下所示:

在这里插入图片描述

来!接下来分析一下上面还原之后的JS代码到底做了些什么。首先可以看到入口则是检测是否处于阅读页面,开始对C_DATA密文数据进行解密操作,它这个解密的逻辑基本都是一样的,先尝试使用默认的第一个密钥加B64的解码,数据钥匙解出来没有继续尝试切换使用第二个密钥!最后解密图片的URL信息并分割URL列表,最后的话是设置显示的操作

下面作者根据反混淆之后的JS代码使用Python算法来实现对C_DATA的解密操作,代码实现所示:

import base64
from loguru import logger
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Protocol.KDF import scryptdef base64Decode(base64Str):return base64.b64decode(base64Str).decode('utf-8')def aesDecrypt(encData, key):key_bytes = key.encode('utf-8')cipher = AES.new(key_bytes, AES.MODE_ECB)decrypted = unpad(cipher.decrypt(encData), AES.block_size)return decrypted.decode('utf-8')def jsd(key, encryptedData):decodedData = base64Decode(encryptedData)encData = base64.b64decode(decodedData)return aesDecrypt(encData, key)def decryptCData(c_data):key1 = 'USJZOHqNw84GoMA9'decryptedData = jsd(key1, c_data)logger.info(f"解密数据:{decryptedData}")if __name__ == '__main__':c_data = '' # 密文数据decryptCData(c_data)

这里直接到浏览扣一个加密数据丢进去测试,得到运行如下所示:

在这里插入图片描述
在这里插入图片描述

通过下面混淆代码调试标记出来的的几处不难发现大致的流程

在这里插入图片描述

对混淆的JS代码简单做一下还原可以更加直观有效的帮助分析。__cad[_0x3b6833(0x591)]实则就是一个setCookieValue的操作,通过获取上面JSON数据中的enc_code2enc_code1的值来对下面Cookies中的值进行一个解密操作,如下所示:

在这里插入图片描述

接下来,针对还原后的JS代码来进行分析,代码如下所示:

let decryptedValue = window.devtools.jsd(_0x447fdd,window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8)
);if (decryptedValue === '') {decryptedValue = window.devtools.jsd("RMjidK1Dgv0Ojuhm", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8));
}if (!decryptedValue.startsWith(mh_info.mhid + '/')) {decryptedValue = window.devtools.jsd("RMjidK1Dgv0Ojuhm", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8));
}let cookieOptions = { "expires": 0.005 };
__cad.cookie(_0x29107e, decryptedValue, cookieOptions);let decryptedValue2 = window.devtools.jsd(_0x447fdd,window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8)
);if (decryptedValue2 === '') {decryptedValue2 = window.devtools.jsd("HNoYX7fJXcM1PWAK", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8));
}// 转换解密后的值为整数
let valueAsInt = parseInt(decryptedValue2);// 如果转换失败(NaN),再次尝试解密
if (String(valueAsInt) === "NaN") {decryptedValue2 = window.devtools.jsd("HNoYX7fJXcM1PWAK", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8));
}// 存储第二个cookie
let cookieOptions2 = { "expires": 0.005 };
__cad.cookie(_0x3ee2e4, decryptedValue2, cookieOptions2);

通过对上面还原后的JS代码进行静态分析可以发现,初始化的时候是给了一个密钥,然后假设解密是空的,就会使用默认的密钥进行解密!如果解密值不符合预期(不以mh_info.mhid/开头),则重试解密,enc_code1的流程差不多

在这里插入图片描述

接下来我们看一下devtools.jsd的解密算法调用,用的什么

在这里插入图片描述

这里我们根据调试以及反混淆后的JS代码还原一下对mh_info参数中的字段解密,加密算法如下所示:

const CryptoJS = require('crypto-js');function aesDecrypt(encData, key) {const parsedKey = CryptoJS.enc.Utf8.parse(key);const decrypted = CryptoJS.AES.decrypt(encData, parsedKey, {mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});return CryptoJS.enc.Utf8.stringify(decrypted);
}function parseBase64(encodedStr) {return CryptoJS.enc.Base64.parse(encodedStr);
}function decryptProcess(encCode1, encCode2, pageId, mhId) {const key1 = "ZsfOA40m7kWjodMH";const parsedEncCode2 = parseBase64(encCode2).toString(CryptoJS.enc.Utf8);const parsedEncCode1 = parseBase64(encCode1).toString(CryptoJS.enc.Utf8);let decryptedEncCode2;try {decryptedEncCode2 = aesDecrypt(parsedEncCode2, key1);if (!decryptedEncCode2 || !decryptedEncCode2.startsWith(`${mhId}/`)) {decryptedEncCode2 = aesDecrypt(parsedEncCode2, key2);}} catch (e) {decryptedEncCode2 = aesDecrypt(parsedEncCode2, key2);}return {cookie: { key: `_tkb_${pageId}`, value: decryptedEncCode2 },};
}// 测试数据
const mh_info = {"startimg": 1,"enc_code1": "cDJSdkkyUFUzbVZrUXZ1S213TFBuQT09","mhid": "873947","enc_code2": "Q1FrNTVrRGZHZjhQM3dEdkg0cU4vYnVmTU9RWjBWdzMzYmhYSlpyKzM0QjN3cmxFSTdYV1VVWUlXRkNMVHhhNw==","mhname": "捉刀人","pageid": 7557687,"pagename": "56","pageurl": "1/57.html","readmode": 3,"maxpreload": 10,"defaultminline": 1,"domain": "img.colamanga.com","manga_size": "","default_price": 0,"price": 0,"use_server": "","webPath": "/manga-mf874127/"
};const result = decryptProcess(mh_info.enc_code1, mh_info.enc_code2, mh_info.pageid, mh_info.mhid);
console.log(result);

注意一下上面算法解密所使用到的AES密钥是每天都在更新的哈

在这里插入图片描述
解决完Cookie生成解密后我们来看最终的图片如何才能去下载的!从前往后分析的话已经拿到了C_DATA数据并解密,通过对解密数据中的Key成功解密获取到Cookie参数,下面就需要知道完整的图片地址,携带Cookie去请求即可,如下继续分析:

在这里插入图片描述

图片地址生存获取的JS混淆代码同样需要还原,还原如下所示:

window.getpice = function (pageIndex) {let imageUrl = '';if (!window.image_info.img_type) {let currentLine = window.lines[chapter_id].use_line;let imageIndex = parseInt(window.mh_info.startimg) + pageIndex - 1;let fileName = __cr.PrefixInteger(imageIndex, 4) + ".jpg";if (window.image_info.imgKey != undefined && window.image_info.imgKey !== '') {fileName = __cr.PrefixInteger(imageIndex, 4) + ".enc.webp";}let baseDomain;let sanitizedDomain = currentLine.replace("img.", '');sanitizedDomain = document.domain.replace("www.", '');let cookieValue = __cad.getCookieValue();let pageId = mh_info.pageid;let cookieKey = cookieValue[0] + pageId.toString();let encodedPath = __cad.cookie(cookieKey);if (encodedPath == null) {__cad.setCookieValue();encodedPath = __cad.cookie(cookieKey);}if (mh_info.use_server === '') {baseDomain = `//img.${sanitizedDomain}/comic/${encodeURI(encodedPath)}${fileName}`;} else {baseDomain = `//img${mh_info.use_server}.${sanitizedDomain}/comic/${encodeURI(encodedPath)}${fileName}`;}imageUrl = baseDomain;} else {let imagePath = window.__images_yy[pageIndex - 1];if (window.image_info.img_type === '1') {imageUrl = __cr.switchWebp(imagePath, window.mh_info.manga_size);} else {imageUrl = imagePath;}}return imageUrl;
};

先获取当前章节的线路信息再计算图片序号,根据序号生成图片文件名JPG然后替换它的主域名。其中也进行了一些Cookie的设置操作最终拿到完整图片路径

在这里插入图片描述

在这里插入图片描述

最后的图片数据则是通过AES解密二进制图片数据,然后就可以直接下载了!_0x1d85d5是密文对象,包含了加密的图片数据,解密的结果_0x5183f2则是图片的二进制数据(WordArray类型

var key = "KZTC0WwWqyeStZD2";
var _0x5183f2 = window.CryptoJS.AES.decrypt(_0x1d85d5, key, {'iv': window.CryptoJS.enc.Utf8.parse("0000000000000000"),'mode': window.CryptoJS.mode.CBC,'padding': window.CryptoJS.pad.Pkcs7
});

貌似不携带Cookie里面的参数也是可以的,感兴趣的自己尝试

相关文章:

某漫画网站JS逆向反混淆流程分析

文章目录 1. 写在前面1. 接口分析2. 反混淆分析 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Pyth…...

React 中事件机制详细介绍:概念与执行流程如何更好的理解

React 的事件机制是一个非常重要的概念&#xff0c;它涉及到 React 如何处理用户的交互事件。React 的事件系统与传统的 DOM 事件系统有所不同&#xff0c;它在底层使用了事件委托和合成事件&#xff08;Synthetic Events&#xff09;来优化性能。下面&#xff0c;我们将从 Rea…...

Day04-后端Web基础(Maven基础)

目录 Maven课程内容1. Maven初识1.1 什么是Maven?1.2 Maven的作用1.2.1 依赖管理1.2.2 项目构建1.2.3 统一项目结构 2. Maven概述2.1 Maven介绍2.2 Maven模型2.3 Maven仓库2.4 Maven安装2.4.1 下载2.4.2 安装步骤 3. IDEA集成Maven3.1 配置Maven环境3.1.2 全局设置 3.2 Maven项…...

vue3模板语法+响应式基础

模板语法 1. disabled指令&#xff0c;可以用于禁用按钮 <button :disabled"isButtonDisabled">Button</button> //:disabled是一个指令&#xff0c;用于根据isButtonDisabled的值来动态控制按钮的禁用状态。 使用场景&#xff1a; 1.防止用户重复点击…...

【面试题】简单聊一下什么是云原生、什么是k8s、容器,容器与虚机相比优势

云原生&#xff08;Cloud Native&#xff09; 定义&#xff1a;云原生是一种构建和运行应用程序的方法&#xff0c;旨在充分利用云计算的优势。它涵盖了一系列技术和理念&#xff0c;包括容器化、微服务架构、自动化部署与管理等。特点&#xff1a;云原生应用程序被设计为可弹性…...

数据挖掘实训:天气数据分析与机器学习模型构建

随着气候变化对各行各业的影响日益加剧&#xff0c;精准的天气预测已经变得尤为重要。降雨预测在日常生活中尤其关键&#xff0c;例如农业、交通和灾害预警等领域。本文将通过机器学习方法&#xff0c;利用历史天气数据预测明天是否会下雨&#xff0c;具体内容包括数据预处理、…...

STM32如何使用内部晶振作为晶振

目录 前言 首先说明一下芯片内部并没有时钟&#xff0c; 而是内部振荡。使用内部振荡的好处是外部无需设计晶振电路 &#xff0c;再说的简单点 &#xff0c;不用外部晶振依然可以让单片机正常运转。 环境&#xff1a; 芯片&#xff1a;STM32F103C8T6 Keil&#xff1a;V5.24…...

【Maui】导航栏样式调整

前言 .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架&#xff0c;用于使用 C# 和 XAML 创建本机移动和桌面应用。 使用 .NET MAUI&#xff0c;可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。 .NET MAUI 是一款开放源代码应用&#xff0c;是 X…...

【黑马程序员三国疫情折线图——json+pyechart=数据可视化】

json数据在文末 将海量的数据处理成我们肉眼可以进行分析的形式&#xff0c;数据的可视化&#xff0c;可以分为两个步骤&#xff1a; 数据处理&#xff1a;利用三方网站厘清json层次格式化&#xff0c;再对文件的读取、检查是否符合JSON规范以及规范化、JSON格式的转化&#…...

如何实现多级缓存?

本文重点说一说在Java应用中&#xff0c;多级缓存如何实现。 多级缓存是比较常见的一种性能优化的手段&#xff0c;一般来说就是本地缓存分布式缓存。 本地缓存一般采用Caffeine和Guava&#xff0c;这两种是性能比较高的本地缓存的框架。他们都提供了缓存的过期、管理等功能。…...

Saas数据库迁移单租户数据

1、背景 租户使用Saas系统&#xff0c;用一段时间后要将系统、数据搬迁到自建服务器。该Saas系统没有按租户分库&#xff0c;且数据库数据量太大&#xff0c;需要将单租户的数据抽取出来。Saas系统使用Mysql5.7数据库&#xff0c;主要使用INFORMATION_SCHEMA.COLUMNS表进行数据…...

LeetCode100之括号生成(22)--Java

1.问题描述 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例1 输入&#xff1a;n 3 输出&#xff1a;["((()))","(()())","(())()","()(())","()()()&qu…...

阿里云ios镜像源

阿里云镜像源&#xff1a;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 下载centos7...

芯片:为何英伟达的GPU能在AI基础设施领域扮演重要角色?

英伟达的GPU之所以能在AI基础设施领域扮演重要角色&#xff0c;主要源于其硬件架构的优势以及其与深度学习算法的高度兼容性。以下是几个关键因素&#xff1a; 1. 并行计算能力 GPU&#xff08;图形处理单元&#xff09;本质上是为处理大量并行计算任务而设计的。与CPU相比&a…...

Linux系统之hostname相关命令基本使用

Linux系统之hostname相关命令基本使用 一、检查本地系统版本二、hostname命令的帮助说明中文帮助说明 三、hostname命令的基本使用1. 查看计算机名2. 查看本机上所有IP地址3. 查看主机FQDN4. 查看短主机名 四、hostnamectl命令的使用1. 查看主机详细信息2. 设置主机名3. hostna…...

Domain Adaptation(李宏毅)机器学习 2023 Spring HW11 (Boss Baseline)

1. 领域适配简介 领域适配是一种迁移学习方法,适用于源领域和目标领域数据分布不同但学习任务相同的情况。具体而言,我们在源领域(通常有大量标注数据)训练一个模型,并希望将其应用于目标领域(通常只有少量或没有标注数据)。然而,由于这两个领域的数据分布不同,模型在…...

在php中,Fiber、Swoole、Swow这3个协程都是如何并行运行的?

文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons&#xff1a;JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram&#xff0c;自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 &#xff1f; 5 IDEA必装的插件&…...

SQLite PRAGMA

SQLite的PRAGMA命令是一种特殊的命令&#xff0c;用于在SQLite环境中控制各种环境变量和状态标志。PRAGMA值可以被读取&#xff0c;也可以根据需求进行设置【0†source】。 PRAGMA命令的语法格式如下&#xff1a; 要查询当前的PRAGMA值&#xff0c;只需提供该PRAGMA的名字&am…...

使用python调用JIRA6 REST API及遇到的问题

JIRA认证方式简述 JIRA接口调用有两种认证方式访问Jira Rest API&#xff0c;基本认证⽅式(⽤户名和密码)和OAuth1认证方式。 基本认证⽅式&#xff1a;因为⽤户名和密码会被浏览器重复地请求和发送&#xff0c;即使采⽤ SSL/TLS 发送&#xff0c;也会有安全隐患&#xff0c;…...

基于STM32的智能电表可视化设计:ESP8266、AT指令集、python后端Flask(代码示例)

一、项目概述 随着智能家居的普及&#xff0c;智能电表作为家庭用电管理的重要工具&#xff0c;能够实时监测电流、电压及功率&#xff0c;并将数据传输至后台进行分析和可视化。本项目以STM32C8T6为核心&#xff0c;结合交流电压电流监测模块、ESP8266 Wi-Fi模块、OLED显示屏…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

生成 Git SSH 证书

&#x1f511; 1. ​​生成 SSH 密钥对​​ 在终端&#xff08;Windows 使用 Git Bash&#xff0c;Mac/Linux 使用 Terminal&#xff09;执行命令&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" ​​参数说明​​&#xff1a; -t rsa&#x…...

OpenLayers 分屏对比(地图联动)

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能&#xff0c;和卷帘图层不一样的是&#xff0c;分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

Java + Spring Boot + Mybatis 实现批量插入

在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法&#xff1a;使用 MyBatis 的 <foreach> 标签和批处理模式&#xff08;ExecutorType.BATCH&#xff09;。 方法一&#xff1a;使用 XML 的 <foreach> 标签&#xff…...

代码随想录刷题day30

1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币&#xff0c;另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额&#xff0c;返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器

一、原理介绍 传统滑模观测器采用如下结构&#xff1a; 传统SMO中LPF会带来相位延迟和幅值衰减&#xff0c;并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF)&#xff0c;可以去除高次谐波&#xff0c;并且不用相位补偿就可以获得一个误差较小的转子位…...