京东算法分析

news2024/9/25 23:24:28

# 1.首先charles抓包发现每个请求Url后都接了一个sign的参数且每次都不一样。也没有其他的一些别的特别参数,那么关键问题就是分析sign参数的生成了

# 2.jadx反编译,寻找sign的生成的位置

> 直接搜索sign参数匹配的出来的结果太多了,一时间不好区分哪个是真的。于是使用getSign为前缀搜索

> 运气不错搜索到一个native方法 前缀也是getSign的方法名,打开这个类。找下引用位置进一步确认下是否为计算sign的类

> 最终在BaseApplication里面找到这个类引用,可以确定这个就是计算sign的类

# 3.开始分析sign方法的入参

frida hook这个方法得到结果如下:

1

2

3

4

5

6

7

8

9

10

11

function main(){

     Java.perform(function(){

       var BitmapkitUtils =  Java.use("com.jingdong.common.utils.BitmapkitUtils")

       BitmapkitUtils.getSignFromJni.overload('android.content.Context''java.lang.String''java.lang.String''java.lang.String''java.lang.String''java.lang.String').implementation = function( context,  str,  str2,  str3,  str4,  str5){

             console.log("签名入参为: ","str         =>",str,"str2=>",str2,"str3=>",str3,"str4=>",str4,"str5=>",str5)

            var result = this.getSignFromJni(context,str,str2,str3,str4,str5)

             console.log('签名sign为 ',result)

            return result

      }

     })

 }

> 根据hook结果稍加分析大概知道各个参数代表的意义:

>context:上下文对象

>str:url路径

>str2:请求body信息

>str3:55a9c688729bb118  为KEY 16位长度,且每次请求都固定

>str4:android 为平台

>str5:版本号

> 查看源码大概得出str3 的为installationId生成规则是UUID 随机16位长度,在app第一次安装的时候创建保存在缓存中.

# 4.使用unidbg进行黑盒调用

> 1.第一步当然是补环境了:

> 需要补充返回一个application对象,偷个懒想从AbstractJni 这里copy一份

> 重新运行一下又报错了,这个错是找不到对应的methodId、于是想着把Application换成Activity也可以,毕竟Activity也是可以获取Application对象的

1

2

3

4

5

6

7

8

9

10

  @Override

     public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {

         if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {

             //返回appliation

             return vm.resolveClass("android/app/Activity",

                    vm.resolveClass("android/content/ContextWrapper",

                             vm.resolveClass("android/content/Context"))).newObject(null);

        }

         return super.getStaticObjectField(vm, dvmClass, signature);

     }

> 重新运行就没问题了,接下来继续报错补环境,返回apk的路径。很简单

1

2

3

4

5

6

7

8

9

 @Override

 public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {

     //sourceDir 代表当前apk目录

     if("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)){

         StringObject stringObject = new StringObject(vm, APK_PATH);

         return stringObject;

     }

     return super.getObjectField(vm, dvmObject, signature);

 }

> 图上这个返回刚开始不知道传具体什么参数,于是打算看看源码。结果没有反编译出来具体的函数实现。于是用frida hook了这个方法拿到图下的返回 

> 第一个参数为:app安装目录  第二个参数为META-INF 目录 第三个是RSA

> 既然这样就直接返回RSA公钥了

1

2

3

4

5

6

7

8

9

@Override

     public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {

             byte[] unzip = vm.unzip("META-INF/xxxx.RSA");

             System.out.println("unzip " new String(unzip));

             return new ByteArray(vm, unzip);

         }

         return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);

     }

> 继续补充环境

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 @Override

 public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

     if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {

         DvmObject<?> objectArg = varArg.getObjectArg(0);

         try {

             PKCS7 pkcs7 = new PKCS7((byte[]) objectArg.getValue());

             return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);

         catch (ParsingException e) {

             e.printStackTrace();

         }

  

     }

     return super.newObject(vm, dvmClass, signature, varArg);

 }

> 继续补充环境,需补充如下

1

2

3

4

5

6

7

8

9

10

11

12

 @Override

     public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {

             PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();

             X509Certificate[] certificates = pkcs7.getCertificates();

             DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);

             return object;

  

  

         }

         return super.callObjectMethod(vm, dvmObject, signature, varArg);

     }

> 继续补充环境,从反编译的源码中copy 一份objectToBytes方法即可然后构造ByteArray返回即可

1

2

3

4

5

 if("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)){

     DvmObject<?> objectArg = varArg.getObjectArg(0);

     byte[] bytes = objectToBytes(objectArg.getValue());

     return new ByteArray(vm,bytes);

 }

# 5.开始黑盒调用计算sign的方法

1

2

3

4

5

6

7

8

9

10

11

 List<Object> params = new ArrayList<>();

        params.add(dalvikVM.getJNIEnv());

        params.add(0);

        DvmClass context = dalvikVM.resolveClass("android/content/Context");

        params.add(dalvikVM.addLocalObject(context.newObject(null)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));

        Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4+1, params.toArray())[0];

> 运行报错,还缺少环境

1

2

3

4

5

6

7

8

 @Override

 public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {

     if("java/lang/StringBuffer-><init>()V".equals(signature)){

         StringBuffer stringBuffer = new StringBuffer();

         return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

     }

     return super.newObjectV(vm, dvmClass, signature, vaList);

 }

> 重新继续补充环境 这部分环境补充比较简单 直接上最后运行结果

> 黑盒调用没问题了,接下来开始分析sign具体是怎么生成的。全部代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

 package jd;

  

 import com.github.unidbg.AndroidEmulator;

 import com.github.unidbg.Module;

 import com.github.unidbg.linux.android.AndroidEmulatorBuilder;

 import com.github.unidbg.linux.android.AndroidResolver;

 import com.github.unidbg.linux.android.dvm.*;

 import com.github.unidbg.linux.android.dvm.apk.Apk;

 import com.github.unidbg.linux.android.dvm.array.ArrayObject;

 import com.github.unidbg.linux.android.dvm.array.ByteArray;

 import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;

 import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;

 import com.github.unidbg.memory.Memory;

 import org.apache.log4j.Level;

 import org.apache.log4j.Logger;

 import sun.security.pkcs.PKCS7;

 import sun.security.pkcs.ParsingException; 

 import java.io.*;

 import java.security.cert.X509Certificate;

 import java.util.ArrayList;

 import java.util.List;

  

 public class JD extends AbstractJni {

  

  

     private static final String SO_PATH = "";

     private static final String APK_PATH = "";

  

     private static final String INSTALL_ID = "55a9c688729bb118";

     private static final String PLAT_FROM = "android";

     private static final String VERSION = "10.4.6";

     private AndroidEmulator androidEmulator;

  

     public static void main(String[] args) {

         Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);

         JD jd = new JD();

         jd.start();

     }

  

  

     public void start() {

         androidEmulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.jingdong.android")

                 .build();

          Memory androidEmulatorMemory = androidEmulator.getMemory();

          androidEmulatorMemory.setLibraryResolver(new AndroidResolver(23));

          VM dalvikVM = androidEmulator.createDalvikVM(new File(APK_PATH));

         DalvikModule module = dalvikVM.loadLibrary(new File(SO_PATH), false);

         dalvikVM.setJni(this);

         Module moduleModule = module.getModule();

         dalvikVM.callJNI_OnLoad(androidEmulator, moduleModule);

         List<Object> params = new ArrayList<>();

         params.add(dalvikVM.getJNIEnv());

         params.add(0);

         DvmClass context = dalvikVM.resolveClass("android/content/Context");

         params.add(dalvikVM.addLocalObject(context.newObject(null)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));

         Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4 1, params.toArray())[0];

         DvmObject<?> object = dalvikVM.getObject(numbers.intValue());

         System.out.println("加密结果为:" + object.getValue());

     }

  

     @Override

     public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {

         if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {

             //返回appliation

             return vm.resolveClass("android/app/Activity",

                     vm.resolveClass("android/content/ContextWrapper",

                             vm.resolveClass("android/content/Context"))).newObject(null);

         }

         return super.getStaticObjectField(vm, dvmClass, signature);

     }

  

     @Override

     public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {

             byte[] unzip = vm.unzip("META-INF/xx.RSA");

             System.out.println("unzip " new String(unzip));

             return new ByteArray(vm, unzip);

         else if ("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)) {

             DvmObject<?> objectArg = varArg.getObjectArg(0);

             byte[] bytes = objectToBytes(objectArg.getValue());

             return new ByteArray(vm, bytes);

         }

         return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);

     }

  

     public static byte[] objectToBytes(Object obj) {

         try {

             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

             ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

             objectOutputStream.writeObject(obj);

             objectOutputStream.flush();

             byte[] byteArray = byteArrayOutputStream.toByteArray();

             objectOutputStream.close();

             byteArrayOutputStream.close();

             return byteArray;

         catch (IOException e) {

             return null;

         }

     }

  

     @Override

     public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {

             ByteArray byteArray = varArg.getObjectArg(0);

             try {

                 PKCS7 pkcs7 = new PKCS7(byteArray.getValue());

                 return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);

             catch (ParsingException e) {

                 e.printStackTrace();

             }

  

         }

         return super.newObject(vm, dvmClass, signature, varArg);

     }

  

     @Override

     public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {

             PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();

             X509Certificate[] certificates = pkcs7.getCertificates();

             DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);

             return object;

  

  

         }

         return super.callObjectMethod(vm, dvmObject, signature, varArg);

     }

  

     @Override

     public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {

         //sourceDir 代表当前apk目录

         if ("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)) {

             StringObject stringObject = new StringObject(vm, APK_PATH);

             return stringObject;

         }

         return super.getObjectField(vm, dvmObject, signature);

     }

  

  

     @Override

     public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {

         if ("java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;".equals(signature)) {

             StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();

             DvmObject<?> objectArg = vaList.getObjectArg(0);

             stringBuffer.append(objectArg.getValue().toString());

             return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

         else if ("java/lang/Integer->toString()Ljava/lang/String;".equals(signature)) {

             Integer integer = (Integer) dvmObject.getValue();

             return new StringObject(vm, integer.toString());

         }else if("java/lang/StringBuffer->toString()Ljava/lang/String;".equals(signature)){

             StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();

             return new StringObject(vm, stringBuffer.toString());

         }

         return super.callObjectMethodV(vm, dvmObject, signature, vaList);

     }

  

     @Override

     public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {

         if ("java/lang/StringBuffer-><init>()V".equals(signature)) {

             StringBuffer stringBuffer = new StringBuffer();

             return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

         else if ("java/lang/Integer-><init>(I)V".equals(signature)) {

             int intArg = vaList.getIntArg(0);

             Integer integer = Integer.valueOf(intArg);

             return vm.resolveClass("java/lang/Integer").newObject(integer);

         }

         return super.newObjectV(vm, dvmClass, signature, vaList);

     }

 }

# 6.ida+unidbg分析sign的具体如何生成的

ida导入so 包  export导出中找到sign生成方法  截图如下:

F5生成伪代码

根据方法的具体逻辑修改了下方法名称 方便直观查看。初步观察是讲传入参数通过key=value 的方式拼接起来

通过unidbg的执行日志中也可以发现这点

接下来开始从函数返回的地方开始往上分析,单独调用执行返回如下:

st=1648374072802&sign=8102aefd41782d405174725cd433c5a0&sv=111

从上可知要分析的为st 、sign、sv这个三个值

1.st生成分析:生成一个时间戳 长度为13位的 用于计算sign

2.sign的生成:

分析伪代码找到sign的生成方法为sub_126AC,该方法上面的伪代码都是一些拼接字符串的操作,就是把入参拼接起来。

> 接下来开始unidbg hook该方法

> 然后开启无尽的S 单步执行往下走当程序走到switch判断时 发现bl指令跳转到sub_10de4函数  多次在这个位置debugger 到 发现 到 sv =111 走的是 case 2   还遇到过sv = 110 走的是case 1 、这部分我们只分析为2的情况也就是走sub_10DE4,至于为啥呢?就是因为这后面走的加密算法看起来简单些 像md5 

> 查看下函数sub_10de4入参,hook该函数

> 打印mr1,为一串固定的字符串

> mr2 为0x1 固定值

> mr3 打印如下,为前面拼接的参数串 长度为0x106

> 接着分析sub_10de4 这个函数,点击进入sub_12ECC函数 ,这里为啥不分析sub_12FF0 呢 因为具体看了下没啥特别的。

hook sub_12ECC函数分析下入参

> 参数1:打印看看 ,看不太出来是什么 但是是64位  看后面的伪代码这个值主要是 跟 md5魔数有关系

> 参数2: 固定字符串 80306f4370b39fd5630ad0529f77adb6

> 参数3: 0x1

> 参数4: 为明文的参数拼接串

> functionId=personinfoBusiness&body={"callCJH":"1","callNPS":"1","closeJX":"0","headTaskRefresh":"1","locationArea":"0_0_0_0","menuStaticSource":"0","menuTimeStamp":"1631586010000"}&uuid=55a9c688729bb118&client=android&clientVersion=10.4.6&st=1648455172760&sv=111

> 参数5:为长度 0x106

#开始分析代码

>  从上面分析 v27 应该就是 md5 魔数 数组咯 大致为

>  v27[1]= 0x68449237;

>  v27[2]= 0x7FCC3DA5;

>  v27[3] = 0x88D90FBB;

>  v27[4] = 0x5AE99AEE;

>  继续往下分析

这个区域是具体的实现的位置,flag 是之前传入的0x1 所以走的是if 判断  。这个do-while 大致是 从v27中取具体魔数  s 往下走 v18 = 0x37 猜测应该是不断从v27中取值  还原算法时v27 就是这个下面这个数组

1

2

 {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A,

0xE9, 0x5A};

> 继续S 往下走 0x38 就是传入固定字符串的第一个字符   80306f4370b39fd5630ad0529f77adb6

>  r0 = 0x66 是从拼接的字符串中取出的第一个 

>  r0=0x66  r1=0x0 r2=0x37 r3=0x1 r4=0x38    

>  R0 = R0 ^ R2  = 0x51 

>  R0 = R0 ^ R4  = 0x69

> 按照汇编 分析猜测 后面就是 将前面异或的结果 加上R2 0x37  然后在 R2 = R2 ^ R0     

1

*(v5 - 1) = md5_init_key_index ^ *(_BYTE *)(key + v17);// EOR.W R2, R2, R1    STRB.W R2, [R10,#-1]

> 根据ida 上伪代码 结和汇编 大致可知 这部分是每执行一次do while 就修改一次v5的索引对应的值  一直循环到循环到v5数据长度,所以结合伪代码 用C还原 sign 的算法 如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

 void encryption(char *data) {

     uint32_t v1, v2, v3 = 0;

     unsigned char init_key[60] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB,    0x0F, 0xD9, 0x88, 0xEE, 0x9A,

                                   0xE9, 0x5A};

     char *sign = "80306f4370b39fd5630ad0529f77adb6";

     int data_length = strlen(data);

     unsigned char md5_init_key = 0;

     unsigned char sign_key = 0;

     do {

         v1 = v3 & 0xF;

         v2 = v3 & 7;

         v3 = v3 + 1;

         sign_key = sign[v2];

         md5_init_key = init_key[v1];

         md5_init_key = md5_init_key ^ ((md5_init_key ^ *data ^sign_key) + md5_init_key);

         *data = md5_init_key ^ sign_key;

         data++;

     while (v3 != data_length);

 }

> 用python 来调用C代码  跟unidbg 黑盒调用结果比对如下:

> 总结: 某东的sign有三个算法 这边只还原了其中比较简单的 sv =111 的这种 。还原这部分算法也花的时间挺多的,结合unidbg 来分析算法 真的方便。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/934843.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sql:SQL优化知识点记录(三)

&#xff08;1&#xff09;explain之select_type和table介绍 简单的查询类型是&#xff1a;simple 外层 primary&#xff0c;括号里subquery 用到了临时表&#xff1a;derived &#xff08;2&#xff09;explain之select_type介绍 trpe反映的结果与我们sql是否优化过&#xff…

家宽用户家庭网的主要质量问题是什么?原因有哪些

1 引言 截至2020年底&#xff0c;我国家庭宽带&#xff08;以下简称“家宽”&#xff09;普及率已达到96%。经过一年多的发展&#xff0c;当前&#xff0c;家庭宽带的市场空间已经饱和。运营商在家宽市场的竞争也随之从新增用户数的竞争转移到家宽品质的竞争。 早期运营商的家…

软件测试收费标准

软件测试 软件测试收费主要根据工作量进行评估&#xff0c;主流的评估方式有以下两种 一、按照工作量评估 工作量从以下4个方面体现&#xff1a; 1、功能性 功能点数量正相关&#xff0c;功能复杂程度正相关例如一个算法逻辑相对复杂的功能点对应需要设计的用例会更多。 2、…

【PyQt】下载文件时弹出提示用户选择保存文件位置的对话框

1 需求 在界面软件中&#xff0c;用户点击下载某个文件&#xff0c;此时软件需要提示用户选择保存到电脑的某个位置&#xff0c;然后软件才能将文件保存到用户指定的电脑文件夹中。 2 代码 # 需引入的库 import os import sys from PyQt5.QtWidgets import QFileDialogsrc .…

复数基本概念

复数是为了解决无法对负数开根号的问题&#xff0c;其为实数的延伸&#xff1a; 复数表示为&#xff1a; a b i abi abi 其中 a 、b 为实数&#xff0c;i 为虚数单位&#xff0c;且 i 2 − 1 i^2 -1 i2−1。实数 a 称为虚数的实部&#xff0c;b 称为虚数的虚部&#xff…

【韩顺平 零基础30天学会Java】数组、排序和查找(2days)

数组、排序、查找和多维数组 数组可以存放多个同一类型的数据。数组也是一种数据类 型&#xff0c;是引用数据类型。 定义一个数组 double[] hens {3,5,1,3.4,2,50} 遍历数组得到数组所有元素的和 hens[下标]&#xff0c;下标是从0开始编号的。 可以通过数组名.lenght得到数组…

爬取boss直聘简单案例

前提准备 以chrome自动化为例 下载浏览器驱动 最新版本&#xff1a;Chrome for Testing availability (googlechromelabs.github.io) 旧版本&#xff1a;ChromeDriver - WebDriver for Chrome - Downloads (chromium.org) 查看chrome的版本 设置->关于chrome 如图116版本…

React 如何获取上一次 state 的值

React 如何获取上一次 state 的值 一、用 ref 存储上一次的 state 类似 usePrevious function usePrevious(value) {const ref useRef();useEffect(() > {ref.current value;});return ref.current; }二、通过 setState 的入参改为函数获取

Nginx详解 第二部分:Nginx配置文件详解(附详细配置实例)

Part2 一、Nginx配置文件1.1 主配置文件详解1.2 子配置文件 二、全局配置部分2.1 修改启动的工作进程数&#xff08;worker process) 优化2.2 cpu与worker process绑定&#xff08;优化三)2.3 PID 路径修改 优化2.4 修改工作进程的优先级 优化2.7 调试工作进程打开的文件的个数…

专题:平面、空间直线参数方程下的切线斜率问题

本文研究平面、空间直线在参数方程形式下&#xff0c;切线斜率&#xff08;即导数&#xff09;如何表示的问题。 如上图所示。 设 y f ( x ) &#xff0c; x φ ( t ) &#xff0c; y ψ ( t ) 当 t t 0 时&#xff0c; x x 0 &#xff0c; y y 0 &#xff0c;即点 A 坐…

拼多多开放平台的API接口可以获取拼多多电商数据。以下是API接口流程

使用拼多多开放平台的API接口可以获取拼多多电商数据。以下是一般的API接口流程&#xff1a; 1. 注册开发者账号&#xff1a;首先&#xff0c;您需要在拼多多开放平台注册一个开发者账号。通过开发者账号&#xff0c;您可以获得API密钥和其他必要的信息。 2. 鉴权与认证&…

最详细的Django安装与启动

Django安装与配置 Python支持版本&#xff1a; Django 3.2支持的Python版本为3.6、3.7、3.8和3.9&#xff0c;而Django 4.1只支持Python 3.8、3.9和3.10。 因为考虑到新的python版本更新和新的mysql数据库版本&#xff0c;由此使用 python3.8和Django 4.1和MYSQL8.0 Django…

计算机网络-笔记-第二章-物理层

目录 二、第二章——物理层 1、物理层的基本概念 2、物理层下面的传输媒体 &#xff08;1&#xff09;光纤、同轴电缆、双绞线、电力线【导引型】 &#xff08;2&#xff09;无线电波、微波、红外线、可见光【非导引型】 &#xff08;3&#xff09;无线电【频谱的使用】 …

统计学补充概念-17-线性决策边界

概念 线性决策边界是一个用于分类问题的线性超平面&#xff0c;可以将不同类别的样本分开。在二维空间中&#xff0c;线性决策边界是一条直线&#xff0c;将两个不同类别的样本分隔开来。对于更高维的数据&#xff0c;决策边界可能是一个超平面。 线性决策边界的一般形式可以表…

如何查看友商的Camera配流情况

有时候&#xff0c;我们想看下竞品友商机器上&#xff0c;使用到camera的app 申请的分辨率情况。 如果只是抓logcat日志&#xff0c;我们可能没法从中获取到有效信息&#xff0c;毕竟一般出货的机器版本&#xff0c;基本已经把大部分相关日志都给关闭了。 那有没其它方式&…

画出你的数据故事:Python中Matplotlib使用从基础到高级

摘要&#xff1a; Matplotlib是Python中广泛使用的数据可视化库&#xff0c;它提供了丰富的绘图功能&#xff0c;用于创建各种类型的图表和图形。本文将从入门到精通&#xff0c;详细介绍Matplotlib的使用方法&#xff0c;通过代码示例和中文注释&#xff0c;帮助您掌握如何在不…

java练习8.100m小球落地

题目: 如一个小球从100米高度自由落下&#xff0c;每次落地后就反跳回原高度的一半。 那么求它在第10次落地时&#xff0c;共经过多少米&#xff1f;第10次反弹多高&#xff1f; public static void main(String[] args) {/*假如一个小球从100米高度自由落下&#xff0c;每次落…

CAS相关知识

CAS&#xff08;比较并交换&#xff09;&#xff0c;它是一条CPU并发原语&#xff0c;它的功能是判断内存中某个位置的值是否和期望值一致&#xff0c;如果一致的话就去更该为新的值&#xff0c;这个过程是原子的。 Unsafe类中的compareAndSwapInt方法&#xff0c;是native方法…

五种重要的 AI 编程语言

推荐&#xff1a;使用 NSDT场景编辑器 助你快速搭建3D应用场景 简而言之&#xff1a;决定从哪种语言开始可能会令人生畏。 不用担心&#xff01;本文将解释 AI 中使用的最流行编程语言背后的基础知识&#xff0c;并帮助您决定首先学习哪种语言。对于每种语言&#xff0c;我们将…

如何通过开源项目学习编程?

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…