基于Inspeckage的安卓APP抓包逆向分析——以步道乐跑APP为例

引言:本人最近稍微弄懂了inspeckage的用法,特在此以步道乐跑APP为例,较详细记录地记录APP抓包与简单的逆向分析过程,用于备忘与共同学习!另外,温馨提醒,本文图片较多,建议连接WiFi阅读!

目录:

一、准备工作

1、需要用到的APP

2、安装与配置

二、开始抓取数据

1、Inspeckage监测

2、HttpCanary抓取

三、数据分析

1、HttpCanary数据部分

2、Inspeckage数据部分

四、代码实现(Python) 

1、AES-CBC-PKCS5加解密

2、md5加密

3、请求数据构建

4、发送请求与解析响应

5、总代码(包括其他接口与其他数据)


正文:

一、准备工作

1、需要用到的APP

  • VMOS Pro
  • HttpCanary
  • JustTrustMe
  • Inspeckage

已经在蓝奏云安排上了,请自行下载!

链接:

https://huanxingke.lanzoui.com/b02069isj

密码:

lptiyu

2、安装与配置

(1)VMOS Pro的配置

Ⅰ、安装在真机上;

Ⅱ、本人为了更好的效果开了会员,用的是下图的虚拟机:

Ⅲ、成功加载虚拟机并打开后,会看到如下页面,点击文件传输:

Ⅳ、点击我要导入:

Ⅴ、点击安装包,选中Inspeckage和JustTrustMe,确认后将自动安装:

Ⅵ、安装步道乐跑:先在真机上安装步道乐跑APP,然后还是点击我要导入 –> 应用 –> 找到步道乐跑 –> 确认安装;

Ⅶ、然后回到主页,点击进入Xpose:

Ⅷ、点击左上角三杠 –> 模块 –> 选中Inspeckage和JustTrustMe模块,然后重启虚拟机以激活模块:

Ⅸ、重启虚拟机后,JustTrustMe模块已默认激活成功,然后配置Inspeckage:进入Inspeckage,当显示Module enable和Server start时,初始化成功,如下图:

(2)HttpCanary的配置

Ⅰ、安装在真机上;

Ⅱ、打开HttpCanary,点击左上角三杠 –> 点击左下角设置 –> 点击SSL证书设置,如图:

Ⅲ、点击安装证书,如图,然后按提示进行:

Ⅳ、返回上一页面,选择目标应用,如图:

Ⅴ、点击右上角+号,选择VOMS Pro,如图,注意不要选择其他应用,以免干扰:

至此,准备工作完成,准备开始抓取数据。

 

二、开始抓取数据

1、Inspeckage监测

(1)首先,请先在步道乐跑APP上登录好你的账号,然后退出;

(2)进入Inspeckage,按下图选择监测步道乐跑APP:

(3)选择好之后,保留虚拟机在后台运行,回到真机浏览器,输入127.0.0.1:8008,若看到如下界面,则配置初步成功,注意此时App is running为false,左上角开关为OFF:

(4)保留真机浏览器在后台,不要关闭,然后进行下一步;

2、HttpCanary抓取

(1)打开HttpCanary,点击右下角小飞机,小飞机变绿代表抓包开始;

(2)然后直接返回,注意不要清理后台,调出虚拟机,此时HttpCanary将会驻在屏幕右下角位置,如图,然后点击LAUNCH APP:

(3)此时Inspekage将会唤醒步道乐跑APP,抓包与监测工作均将开始,如图,可看到HttpCanary已有抓包数据:

(4)然后点击真机的方框导航键(手势导航的是按住屏幕底部上滑),调出真机浏览器后,刷新一下刚才的页面,如果App is running为true则监测成功,此时打开左上角开关,就可以获取监测的数据了,成功界面如图所示:

至此,抓包与监测工作均已开始并已经获取到步道乐跑APP启动后的网络请求数据,可以开始分析数据了。

 

三、数据分析

1、Httpcanary数据部分

(1)全屏打开HttpCanary,可以看到已经抓取到很多请求了,先拉到最底部,从最开始的请求寻找起,看有没有比较特别的、可能符合我们需要的请求;

(2)如下图,可以发现有一个请求含有Login关键词,我们可以下意识地想到:这可能是跟用户登录有关的请求,这在APP抓包上还是挺重要的一部分:

(3)那我们点击打开这一请求,点击请求 –> 点击右下角预览,如图:

(4)我们可以看到有几个值得我们留意的字段:token、access_token、refresh_token、timestamp、nonce、jpush_id、sign:

Ⅰ、前三者均为token类字段,按照经验,此类字段一般是由服务器生成的,所以我们暂时先不考虑;

Ⅱ、timestamp为时间戳,nonce为随机字符串,均是起防止重放攻击作用的,可以不予考虑(见博主@koastal的博文:https://blog.csdn.net/koastal/article/details/53456696);

综上,最值得我们考虑的就是sign值了——其实我们一开始就应该特别留意到这个sign值了,因为这是很常用的加密算法的字段,并且我们还可以发现它与md5加密后的格式十分相似,先默默记下来;

(5)然后我们再看一下响应吧,如图:

(6)很明显,响应中的data值为base64编码,但当我们拿去解密的时候,得到的是乱码:

¶"Z¥ÆÍǠЩ<í/ÅtlÎÝοaó±8Êã½BV-0ÃúCÃæOÒ;Çÿ(^ò±°Î?t(:t1ÂTº	ådä0ãùºÆ^Ü~.KÝÜ[õõ»9+ÕI5ùÄs©îÁ^Çw[Ï8
Ïϼ#>,ÌeÔPÅ¿Vú2Êç7ZzÇF£ÙÈÊn©

所以很显然,这个字段在base64之前就已经被加密过一次了,这便应当引起我们的兴趣了;

得到了以上数据之后,我们便可以去Inspeckage网页上寻找对应加密方法了。

2、Inspeckage数据部分

(1)回到Inspeckage网页,我们需要知道,网页上的Crypto(一般是签名算法,如AES)和Hash(哈希算法,如md5)是我们寻找加密方法的来源;

(2)我们先看一下Crypto里的数据吧,点击Crypto,如图(温馨提示:请先把左上角的开关关闭为OFF再来分析数据哦,否则网页的动态变化会影响我们的数据寻找过程哦):

(3)我们可以看到已经监测到很多数据了,在上面已经说到了sign可能是md5加密的,也就是Hash,所以我们先不在Crypto里寻找sign,而是先寻找data值,那么要怎么寻找呢:

Ⅰ、根据请求顺序寻找:我们知道data值是包含login字段请求的响应,而此请求在HttpCanary中属于最早发送的一批请求,那我们便应该在Inspeckage网页上也拉到最底部来寻找;

Ⅱ、使用浏览器自带的查找功能,如图:

然后输入data值来匹配寻找即可,注意:只需要复制data值开头的几位字符来寻找即可,因为页面是显示不完全的,如果使用整个data值来匹配,反而找不到结果;

(4)由(3)中的方法,我们成功找到了data的加密算法,如图:

放大后:

将它复制下来就更明显了:

(为了保护隐私, 部分数据使用*号代替)
SecretKeySpec(Wet2C8d34f62ndi3,AES) , Cipher[AES/CBC/PKCS5Padding] IV: K6iv85jBD8jgf32D (tiJapcbNx6AL0JmpPO0VL8V0bM7dEc4dvwth87E4HMrjE
L1CVi0ww5qO+kPD5k8L0jvH/xooXvKxsADOP5x0KAsSOpV0McJUugnlZOQwm+P5usZe3H4uS93cW/X1uzkQK4jVSY0eNfnEG3Op7hDBAl6PjccMd1uVzzgNz88VvCM+LMxl1FDFv1b6MsoC5zdaesdGoxiP******** , {"uid":"*******","access_token":"333C877C9429E26D4FCDC404******","refresh_token":"CC304EDBA40FF2F2AEA2D28C******","refresh_expire":1623509738})

也就是说data进行base64前的加密算法为AES-CBC对称算法,字符串使用的是PKCS5Padding格式,其中SecretKey值为:Wet2C8d34f62ndi3,偏移量iv为K6iv85jBD8jgf32D,加密前的字符串为:

{"uid":"*******","access_token":"333C877C9429E26D4FCDC404******","refresh_token":"CC304EDBA40FF2F2AEA2D28C******","refresh_expire":1623509738}

data值成功破译!

(5)按照同样的方法,我们点击Hash部分来尝试破译sign值:

(6)这里有个小技巧,因为网页默认是不完整显示的,所以会导致某些值看不到,这是我们可以点击网页里的一些>>符号可以将其展开完全显示,避免匹配失败:

(7)不出意外,sign值算法与加密前的字符串也被我们找到了:

复制下来:

Algorithm(MD5) [access_token006C31A39AED1ECAAA28C32554******jpush_id1507bfd3f7684******mobileDeviceId185818969******mobileModelBRQ-AN00
mobileOsVersion7.1.2nonce320975ostype1refresh_token9EF86DB536CEB764B430EC2BA4******school_id***student_num******timestamp1620917736token006C31A39AED1ECAAA28C325****uid******version86version_name3.3.6rDJiNB9j7vD2 : 7aeb494fc0063ec5c60cfd7ef8373929]

所以果然是md5加密!加密前的字符串为:

access_token006C31A39AED1ECAAA28C32554******jpush_id1507bfd3f7684******mobileDeviceId185818969******mobileModelBRQ-AN00
mobileOsVersion7.1.2nonce320975ostype1refresh_token9EF86DB536CEB764B430EC2BA4******school_id***student_num******timestamp1620917736token006C31A39AED1ECAAA28C325****uid******version86version_name3.3.6rDJiNB9j7vD2

至此,简单的逆向分析已经大功告成了!其他接口、其他数据加密算法都可以使用类似的方法来寻找和破译!可以开始码代码了!

 

四、代码实现(Python)

1、AES-CBC-PKCS5加解密(crypto.py)

(1)先看一下相关库的安装与使用:博客园:https://www.cnblogs.com/niuu/p/10107212.html

(2)代码实现:

from Crypto.Cipher import AES
import base64
import re# 加解密
class Crypto(object):def __init__(self):# pubkey值self.key = 'Wet2C8d34f62ndi3'.encode('utf-8')# 偏移量self.iv = b'K6iv85jBD8jgf32D'# AES-CBC对称加密self.mode = AES.MODE_CBC# AES-CBC-PKCS5格式化字符串self.bs = 16self.PADDING = lambda s: s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)# AES-CBC加密def AESEncrypt(self, text):generator = AES.new(self.key, self.mode, self.iv)crypt = generator.encrypt(self.PADDING(text).encode("utf-8"))# 加密后转base64crypted_str = base64.b64encode(crypt)result = crypted_str.decode()return result# 解密def AESDecrypt(self, text):text = base64.b64decode(text)cryptos = AES.new(self.key, self.mode, self.iv)plain_text = cryptos.decrypt(text)data = bytes.decode(plain_text)# 转中文data = data.replace(r'/', '/').encode().decode('unicode_escape')# print(json.dumps(data))# 格式化pat = re.compile(r'<html>(.*?)</html>')html = pat.findall(data)html = html[0] if html else ""html_ = html.replace('"', "'")# 转为字典data = json.loads(data.replace(html, html_).strip('"').strip('u000e').strip('u0007').strip('u0004'))return data

2、md5加密(Md5.py)

import hashlibdef Md5(text):text = text.encode()m = hashlib.md5()m.update(text)return m.hexdigest()

3、请求数据构建(data.py)

from crypto import *
import time# 打*号的数据涉及隐私, 请自行获取
data = {'version': '86', 'version_name': '3.3.6', 'mobileModel': 'BRQ-AN00', 'mobileDeviceId': '******', 'mobileOsVersion': '7.1.2', 'ostype': '1', 'student_num': '******', 'school_id': '***', 'uid': '******', 'token': '***************', 'timestamp': '', 'nonce': '******', 'access_token': '***************', 'refresh_token': '*****************', 'jpush_id': '*****************', 'sign': '*****************'}
timestamp = int(time.time())
token = '*************'
refresh_token = '************'
sign_data = 'access_token{}jpush_id*********mobileDeviceId**************mobileModelBRQ-AN00mobileOsVersion7.1.2nonce********ostype1refresh_token{}school_id***student_num********timestamp{}token{}uid******version86version_name3.3.6rDJiNB9j7vD2'.format(token, refresh_token, timestamp, token)
sign = Md5(sign_data)
data['token'] = token
data['access_token'] = token
data['refresh_token'] = refresh_token
data['timestamp'] = str(timestamp)
data['sign'] = sign
print(sign)
print(data)

4、发送请求与解析响应(req.py)

import urllib.parse
import requestsurl = "https://api2.lptiyu.com/v3/api.php/Login/RefreshToken"
headers = {'Cookie': 'PHPSESSID=*******************', 'Connection': 'close', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Content-Length': '299', 'User-Agent': 'Dalvik/2.1.0(Linux;U;Android7.1.2;BRQ-AN00Build/NZH54D)', 'Host': 'api2.lptiyu.com', 'Accept-Encoding': 'gzip'}
data = {'version': '86', 'version_name': '3.3.6', 'mobileModel': 'BRQ-AN00', 'mobileDeviceId': '***************', 'mobileOsVersion': '7.1.2', 'ostype': '1', 'student_num': '************', 'school_id': '***', 'uid': '*******', 'token': '*************', 'timestamp': '******', 'nonce': '*********', 'access_token': '**********', 'refresh_token': '***********', 'jpush_id': '***************', 'sign': '*************'}
data = urllib.parse.urlencode(data)
response = requests.post(url=url, headers=headers, data=data).json()
print(response)

5、总代码(包括其他接口与其他数据)

from Crypto.Cipher import AES
import urllib.parse
import requests
import hashlib
import random
import string
import base64
import time
import json
import re
import os# 加解密
class Crypto(object):def __init__(self):# pubkey值self.key = 'Wet2C8d34f62ndi3'.encode('utf-8')# 偏移量self.iv = b'K6iv85jBD8jgf32D'# AES-CBC对称加密self.mode = AES.MODE_CBC# AES-CBC-PKCS5格式化字符串self.bs = 16self.PADDING = lambda s: s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)# AES-CBC加密def AESEncrypt(self, text):generator = AES.new(self.key, self.mode, self.iv)crypt = generator.encrypt(self.PADDING(text).encode("utf-8"))# 加密后转base64crypted_str = base64.b64encode(crypt)result = crypted_str.decode()return result# 解密def AESDecrypt(self, text):text = base64.b64decode(text)cryptos = AES.new(self.key, self.mode, self.iv)plain_text = cryptos.decrypt(text)data = bytes.decode(plain_text)# 转中文data = data.replace(r'/', '/').encode().decode('unicode_escape')# print(json.dumps(data))# 格式化pat = re.compile(r'<html>(.*?)</html>')html = pat.findall(data)html = html[0] if html else ""html_ = html.replace('"', "'")# 转为字典data = json.loads(data.replace(html, html_).strip('"').strip('u000e').strip('u0007').strip('u0004'))return data# md5加密@staticmethoddef Md5(text):text = text.encode()m = hashlib.md5()m.update(text)return m.hexdigest()# 生成sign/data值
class Md5Encrypt(object):def __init__(self, **kwargs):# 原始json数据self.data = {# 可自定义的值"jpush_id": "","mobileDeviceId": "","mobileModel": "","mobileOsVersion": "7.1.2",# 客户端版本号, 不建议修改"version": "86","version_name": "3.3.6","ostype": "1",# 标志值# 学校id, 默认-1为未知"school_id": "-1",# 时间戳"timestamp": "",# 六位随机数字字符串"nonce": ""}# sign末端的一个固定值self.sign_str = 'rDJiNB9j7vD2'# 用户uidself.uid = kwargs['uid'] if 'uid' in kwargs else None# 学号self.student_num = kwargs['student_num'] if 'student_num' in kwargs else None# 设置其他值for key, value in kwargs.items():self.data[key] = value# 登录data值def loginData(self, code, phone):# 继承data属性data = self.data# 更新标志值# 固定为1data['type'] = '1'# 手机号data['phone'] = str(phone)# 验证码, 30天内只能获取一次data['code'] = str(code)# 时间戳timestamp = str(int(time.time()))# 六位随机数字字符串nonce = str(random.randint(100000, 999999))# 放入datadata['timestamp'] = timestampdata['nonce'] = nonce# 生成原始sign值# 一定要先对字典排序sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str# 加密sign值sign = Crypto().Md5(sign_data)# sign值放入datadata['sign'] = sign# 加密datadata = Crypto().AESEncrypt(json.dumps(data))# 传输的data原始json值data = {'key': data}# 转换成form-data类型data = urllib.parse.urlencode(data)# 返回data值return data# 获取用户信息def userData(self, token):# 继承data属性data = self.data# 删除jpush_id值del data['jpush_id']# 更新标志值# 用户iddata['uid'] = self.uid# token值data['token'] = token# 时间戳timestamp = str(int(time.time()))# 六位随机数字字符串nonce = str(random.randint(100000, 999999))# 放入datadata['timestamp'] = timestampdata['nonce'] = nonce# 生成原始sign值# 一定要先对字典排序sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str# 加密sign值sign = Crypto().Md5(sign_data)# sign值放入datadata['sign'] = sign# 转换成form-data类型data = urllib.parse.urlencode(data)# 返回data值return data# 获取cookiedef ipData(self, token):# 继承data属性data = self.data# 更新标志值# 固定为2data['type'] = '2'# 学号data['student_num'] = self.student_num# 用户iddata['uid'] = self.uid# token值data['token'] = token# 时间戳timestamp = str(int(time.time()))# 六位随机数字字符串nonce = str(random.randint(100000, 999999))# 放入datadata['timestamp'] = timestampdata['nonce'] = nonce# 生成原始sign值# 一定要先对字典排序sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str# 加密sign值sign = Crypto().Md5(sign_data)# sign值放入datadata['sign'] = sign# 转换成form-data类型data = urllib.parse.urlencode(data)# 返回data值return data# 刷新token值def refreshData(self, token, refresh_token):# 继承data属性data = self.data# 更新标志值# 学号data['student_num'] = self.student_num# 用户iddata['uid'] = self.uid# token值data['token'] = token# access_token值(与token相同)data['access_token'] = token# refresh_token值data['refresh_token'] = refresh_token# 时间戳timestamp = str(int(time.time()))# 六位随机数字字符串nonce = str(random.randint(100000, 999999))# 放入datadata['timestamp'] = timestampdata['nonce'] = nonce# 生成原始sign值# 一定要先对字典排序sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str# 加密sign值sign = Crypto().Md5(sign_data)# sign值放入datadata['sign'] = sign# 转换成form-data类型data = urllib.parse.urlencode(data)# 返回data值return data# 获取排行榜def rankData(self, token, page):# 继承data属性data = self.data# 删除jpush_id值if 'jpush_id' in data:del data['jpush_id']if 'sign' in data:del data['sign']# 更新标志值# 固定为1data['type'] = '1'# 固定为1data['category'] = '1'# 学号data['student_num'] = self.student_num# 用户iddata['uid'] = self.uid# token值data['token'] = token# 页码data['page'] = str(page)# 时间戳timestamp = str(int(time.time()))# 六位随机数字字符串nonce = str(random.randint(100000, 999999))# 放入datadata['timestamp'] = timestampdata['nonce'] = nonce# 生成原始sign值# 一定要先对字典排序sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str# 加密sign值sign = Crypto().Md5(sign_data)# sign值放入datadata['sign'] = sign# 转换成form-data类型data = urllib.parse.urlencode(data)# 返回data值return data# 发送请求
class GetResponse(object):def __init__(self, **kwargs):# 请求头self.headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8','User-Agent': 'Dalvik/2.1.0(Linux;U;Android7.1.2;BRQ-AN00Build/NZH54D)','Host': 'api2.lptiyu.com','Accept-Encoding': 'gzip'}# 读取用户信息userData = User().getUser()# 虚拟客户端信息jpush_id = userData['jpush_id']mobileDeviceId = userData['mobileDeviceId']mobileModel = userData['mobileModel']# 用户标识信息# 用户uidif 'uid' not in userData:self.encrypter = Md5Encrypt(jpush_id=jpush_id, mobileDeviceId=mobileDeviceId, mobileModel=mobileModel)self.login(autoLogin=False)else:self.uid = userData['uid']self.token = userData['access_token']self.refresh_token = userData['refresh_token']# 学号if 'student_num' not in userData:print('正在获取学号...')self.encrypter = Md5Encrypt(jpush_id=jpush_id, mobileDeviceId=mobileDeviceId, mobileModel=mobileModel, uid=self.uid)self.user()else:self.student_num = userData['student_num']# 重载加密器self.encrypter = Md5Encrypt(jpush_id=jpush_id, mobileDeviceId=mobileDeviceId, mobileModel=mobileModel, uid=self.uid, student_num=self.student_num)print('初始化完成!')# 重新登录def login(self, code=None, phone=None, autoLogin=True):if not autoLogin:print('请先重新登录!')phone = input('请输入手机号码: ')code = input('请输入验证码: ')print('正在登录......')url = 'https://api2.lptiyu.com/v3/api.php/Login/quickLoginV300'data = self.encrypter.loginData(code=code, phone=phone)response = requests.post(url=url, headers=self.headers, data=data).json()if 'data' not in response:raise Exception("登录失败!")tokenData = Crypto().AESDecrypt(response['data'])self.uid = tokenData['uid']self.token = tokenData['access_token']self.refresh_token = tokenData['refresh_token']# 保存token数据User().saveUser(userData=tokenData)return tokenData# 获取用户信息def user(self):url = 'https://api2.lptiyu.com/v3/api.php/User/User'data = self.encrypter.userData(token=self.token)response = requests.post(url=url, headers=self.headers, data=data).json()if 'data' not in response:raise Exception("登录失效!")user = Crypto().AESDecrypt(response['data'])self.student_num = user['student_num']User().saveUser(userData=user)return user'''# 获取cookiedef getIp(self):url = 'https://api2.lptiyu.com/v3/api.php/System/getIp'data = self.encrypter.ipData(self.token)response = requests.post(url=url, headers=self.headers, data=data)try:cookies = response.cookiescookies = requests.utils.dict_from_cookiejar(cookies)self.headers["Cookie"] = list(cookies.keys())[0] + '=' + list(cookies.values())[0]except:raise Exception("登录失效!")return cookies'''# 刷新token值def refreshToken(self):url = 'https://api2.lptiyu.com/v3/api.php/Login/RefreshToken'data = self.encrypter.refreshData(token=self.token, refresh_token=self.refresh_token)response = requests.post(url=url, headers=self.headers, data=data).json()if 'data' not in response:raise Exception("登录失效!")tokenData = Crypto().AESDecrypt(response['data'])self.token = tokenData['access_token']self.refresh_token = tokenData['refresh_token']# 保存token数据User().saveUser(userData=tokenData)return tokenData# 获取排行榜def getTotalRank(self, page):url = 'https://api2.lptiyu.com/v3/api.php/Run/getTotalRank'data = self.encrypter.rankData(token=self.token, page=page)response = requests.post(url=url, headers=self.headers, data=data).json()if 'data' not in response:raise Exception("登录失效!")rankData = Crypto().AESDecrypt(response['data'])['rank_list']return rankData# 操作user文件
class User(object):def __init__(self):# token文件路径self.file = 'user.json'# 构建客户端信息def createInfo(self):s = string.ascii_letters + string.digitsjpush_id = "".join(random.choice(s) for _ in range(0, 19))mobileDeviceId = "".join(random.choice(string.digits) for _ in range(0, 15))mobileModel = "".join(random.sample(string.digits, 3)) + '-' + "".join(random.sample(s, 4))user = {'jpush_id': jpush_id, 'mobileDeviceId': mobileDeviceId, 'mobileModel': mobileModel}with open(self.file, 'w') as fp:json.dump(user, fp)# 从本地获取User信息def getUser(self):if not os.path.exists(self.file):self.createInfo()with open(self.file, 'r') as fp:userData = json.load(fp)return userData# 保存User数据def saveUser(self, userData):with open(self.file, 'r') as fp:userData_ = json.load(fp)for key, value in userData.items():userData_[key] = valuewith open(self.file, 'w') as fp:fp.write(json.dumps(userData_))if __name__ == '__main__':handler = GetResponse()fp = open('lptiyu.csv', 'w', encoding='utf-8')fp.write('排名,姓名,已跑次数n')for j in range(1, 4):rankData_ = handler.getTotalRank(page=j)for i in rankData_:score = i['score_num']rank = i['rank']name = i['name']fp.write('%s,%s,%sn' % (rank, name, score))fp.close()

好了,今天的总结与分享就到这里,感谢你的阅读!

Published by

风君子

独自遨游何稽首 揭天掀地慰生平