技术标签: python爬虫案例 app python app安全
引言:本人最近稍微弄懂了inspeckage的用法,特在此以步道乐跑APP为例,较详细记录地记录APP抓包与简单的逆向分析过程,用于备忘与共同学习!另外,温馨提醒,本文图片较多,建议连接WiFi阅读!
目录:
正文:
一、准备工作
1、需要用到的APP
已经在蓝奏云安排上了,请自行下载!
链接:
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 = 16
self.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"))
# 加密后转base64
crypted_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 hashlib
def 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 requests
url = "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 = 16
self.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"))
# 加密后转base64
crypted_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加密
@staticmethod
def 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'
# 用户uid
self.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
# 更新标志值
# 固定为1
data['type'] = '1'
# 手机号
data['phone'] = str(phone)
# 验证码, 30天内只能获取一次
data['code'] = str(code)
# 时间戳
timestamp = str(int(time.time()))
# 六位随机数字字符串
nonce = str(random.randint(100000, 999999))
# 放入data
data['timestamp'] = timestamp
data['nonce'] = nonce
# 生成原始sign值
# 一定要先对字典排序
sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str
# 加密sign值
sign = Crypto().Md5(sign_data)
# sign值放入data
data['sign'] = sign
# 加密data
data = 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']
# 更新标志值
# 用户id
data['uid'] = self.uid
# token值
data['token'] = token
# 时间戳
timestamp = str(int(time.time()))
# 六位随机数字字符串
nonce = str(random.randint(100000, 999999))
# 放入data
data['timestamp'] = timestamp
data['nonce'] = nonce
# 生成原始sign值
# 一定要先对字典排序
sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str
# 加密sign值
sign = Crypto().Md5(sign_data)
# sign值放入data
data['sign'] = sign
# 转换成form-data类型
data = urllib.parse.urlencode(data)
# 返回data值
return data
# 获取cookie
def ipData(self, token):
# 继承data属性
data = self.data
# 更新标志值
# 固定为2
data['type'] = '2'
# 学号
data['student_num'] = self.student_num
# 用户id
data['uid'] = self.uid
# token值
data['token'] = token
# 时间戳
timestamp = str(int(time.time()))
# 六位随机数字字符串
nonce = str(random.randint(100000, 999999))
# 放入data
data['timestamp'] = timestamp
data['nonce'] = nonce
# 生成原始sign值
# 一定要先对字典排序
sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str
# 加密sign值
sign = Crypto().Md5(sign_data)
# sign值放入data
data['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
# 用户id
data['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))
# 放入data
data['timestamp'] = timestamp
data['nonce'] = nonce
# 生成原始sign值
# 一定要先对字典排序
sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str
# 加密sign值
sign = Crypto().Md5(sign_data)
# sign值放入data
data['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']
# 更新标志值
# 固定为1
data['type'] = '1'
# 固定为1
data['category'] = '1'
# 学号
data['student_num'] = self.student_num
# 用户id
data['uid'] = self.uid
# token值
data['token'] = token
# 页码
data['page'] = str(page)
# 时间戳
timestamp = str(int(time.time()))
# 六位随机数字字符串
nonce = str(random.randint(100000, 999999))
# 放入data
data['timestamp'] = timestamp
data['nonce'] = nonce
# 生成原始sign值
# 一定要先对字典排序
sign_data = ''.join([(i + data[i]) for i in sorted(data)]) + self.sign_str
# 加密sign值
sign = Crypto().Md5(sign_data)
# sign值放入data
data['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']
# 用户标识信息
# 用户uid
if '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
'''
# 获取cookie
def 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.cookies
cookies = 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.digits
jpush_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] = value
with 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,%s\n' % (rank, name, score))
fp.close()
好了,今天的总结与分享就到这里,感谢你的阅读!
文章浏览阅读1.6k次,点赞5次,收藏20次。【有害垃圾】:电池(1 号、2 号、5 号)、过期药品或内包装等;【可回收垃圾】:易拉罐、小号矿泉水瓶;【厨余垃圾】:小土豆、切过的白萝卜、胡萝卜,尺寸为电池大小;【其他垃圾】:瓷片、鹅卵石(小土豆大小)、砖块等。文件结构|----classes.txt # 标签种类|----data-txt\ # 数据集文件集合|----images\ # 数据集图片|----labels\ # yolo标签。_垃圾回收数据集
文章浏览阅读272次。之前写到 通过封装的API 已经可以做到使用redis进行缓存天气信息但是这一操作每次都由客户使用时才进行更新 不友好 所以应该自己实现半小时的定时存入redis 使用quartz框架 首先添加依赖build.gradle中// Quartz compile('org.springframework.boot:spring-boot-starter-quartz'..._cityid=101280803
文章浏览阅读1.8k次,点赞2次,收藏8次。对于使用触发事件来反应的按钮传递参数如下:可以通过lambda对function的参数传递:t.Bind(wx.EVT_BUTTON, lambda x, textctrl=t: self.input_fun(event=x, textctrl=textctrl))前提需要self.input_fun(self,event,t):传入参数而同时两个Frame之间的参数传..._wxpython frame.bind
文章浏览阅读1.9k次。最近接到一个任务要开发消消乐小游戏,当然首先就想到乐cocosCreator来作为开发工具。开发本身倒没有多少难点。消消乐的开发官网发行的书上有专门讲到。下面主要总结一下开发中遇到的问题以及解决方法屏幕适配由于设计尺寸是750*1336,如果适应高度,则在iphonX下,内容会超出屏幕宽度。按宽适应,iphon4下内容会超出屏幕高度。所以就需要根据屏幕比例来动态设置适配策略。 onLoad..._750*1336
文章浏览阅读745次,点赞21次,收藏21次。web项目的框架,通常更简单的数据源。21世纪的今天,随着社会的不断发展与进步,人们对于信息科学化的认识,已由低层次向高层次发展,由原来的感性认识向理性认识提高,管理工作的重要性已逐渐被人们所认识,科学化的管理,使信息存储达到准确、快速、完善,并能提高工作管理效率,促进其发展。论文主要是对银行贷款管理系统进行了介绍,包括研究的现状,还有涉及的开发背景,然后还对系统的设计目标进行了论述,还有系统的需求,以及整个的设计方案,对系统的设计以及实现,也都论述的比较细致,最后对银行贷款管理系统进行了一些具体测试。_vue3重构信贷管理系统
文章浏览阅读774次。题目描述原题目戳这里小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。乌龟棋的棋盘是一行 NNN 个格子,每个格子上一个分数(非负整数)。棋盘第 111 格是唯一的起点,第 NNN 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。乌龟棋中 MMM 张爬行卡片,分成 444 种不同的类型( MMM 张卡片中不一定包含所有 444 种类型的卡片,见样例),每种类型的卡片上分别标有 1,2,3,41, 2, 3, 41,2,3,4 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数
文章浏览阅读1.5k次。吐槽内存泄露 ? 内存暴涨 ? OOM ?首先提一下我自己曾经历过多次内存泄露,到底有几次? 我自己心里悲伤的回想了下,造成线上影响的内存泄露事件有将近5次了,没上线就查出内存暴涨次数可能更多。这次不是最惨,相信也不会是最后的内存的泄露。有人说,内存泄露对于程序员来说,是个好事,也是个坏事。 怎么说? 好事在于,技术又有所长进,经验有所心得…. 毕竟不是所有程序员都写过OOM的服务…. 坏事..._python内存泄露
文章浏览阅读747次。1.sensor typeTYPE_ACCELEROMETER=1 TYPE_MAGNETIC_FIELD=2 (what's value mean at x and z axis)TYPE_ORIENTATION=3TYPE_GYROSCOPE=4 TYPE_LIGHT=5(in )TYPE_PRESSURE=6TYPE_TEMPERATURE=7TYPE_PRO_draft sensor
文章浏览阅读581次。/* * Copyright (c) 2009 湖南师范大学数计院 一心飞翔项目组 * All Right Reserved * * 文件名:matrix.cpp 定义Point、Node、Matrix类的各个方法 * 摘 要:定义矩阵类,包括矩阵的相关信息和方法 * * 作 者:刘 庆 * 修改日期:2009年7月19日21:15:12 **/
文章浏览阅读1.7w次,点赞6次,收藏20次。HTML不再推荐页面中使用框架集,因此HTML5删除了<frameset>、<frame>和<noframes>这三个元素。不过HTML5还保留了<iframe>元素,该元素可以在普通的HTML页面中使用,生成一个行内框架,可以直接放在HTML页面的任意位置。除了指定id、class和style之外,还可以指定如下属性:src 指定一个UR..._iframe allow-top-navigation
文章浏览阅读785次,点赞29次,收藏12次。Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,
文章浏览阅读358次。“随着天网工程的建设,中国已经建成世界上规模最大的视频监控网,摄像头总 数超过2000万个,成为世界上最安全的国家。视频图像及配套数据已经应用在反恐维稳、治安防控、侦查破案、交通行政管理、服务民生等各行业各领域。烁博科技视频安全核心能力:精准智能数据采集能力:在建设之初即以应用需求为导向,开展点位选择、设备选型等布建工作,实现前端采集设备的精细化部署。随需而动的AI数据挖掘能力:让AI所需要的算力、算法、数据、服务都在应用需求的牵引下实现合理的调度,实现解析能力的最大化。完善的数据治理能力:面_2018年8月由于某知名视频监控厂商多款摄像机存在安全漏洞