开发环境
-----自动填表/打卡/上报原理
其实也就是模拟一个数据包,调用今日校园的api;实现登入功能;然后查找未填写的表;倘若表的序列号已经存在你的代码包里面,就会自动将包数据发到职教云服务器上面去,倘若没有,将会把表自动打包程json;然后你只需要编辑下json数据,以后这个序列的包就可以自动提交了
-----使用说明
理论上支持大部分学校(IAP登录方式,自己先试试能用不)和任意表单内容的自定义。目前已知两种学工号登录方式,均已实现。运行以下命令即可
pip install -r requirements.txt
python3 DailyCP.py 学校全名 学号 密码 定位地址
注意系统时间是否设置正确
Linux下可以使用corntab -e设置定时任务让程序每天自动打卡(下面的是设置0-9点触发的)
0 9 * * * python3 DailyCP.py 学校全名 学号 密码 定位地址
import requests
import json
import io
import random
import time
import re
import pyDes
import base64
import uuid
import sys
import os
from Crypto.Cipher import AES
class DailyCP:
def __init__(self, schoolName="合肥学院"):
self.key = "ST83=@XV"#dynamic when app update
self.session = requests.session()
self.host = ""
self.loginUrl = ""
self.isIAPLogin = True
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37",
#"X-Requested-With": "XMLHttpRequest",
"Pragma": "no-cache",
"Accept": "application/json, text/plain, */*",
#"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
# "User-Agent": "okhttp/3.12.4"
})
extension = {"deviceId":str(uuid.uuid4()),"systemName":"保密级操作系统","userId":"5201314","appVersion":"8.1.13","model":"天河一号","lon":0.0,"systemVersion":"初号机","lat":0.0}
self.session.headers.update({"Cpdaily-Extension": self.encrypt(json.dumps(extension))})
self.setHostBySchoolName(schoolName)
def setHostBySchoolName(self,schoolName):
ret = self.request("https://static.campushoy.com/apicache/tenantListSort")
school = [j for i in ret["data"] for j in i["datas"] if j["name"] == schoolName]
if len(school) == 0:
print("不支持的学校或者学校名称错误,以下是支持的学校列表")
print(ret)
exit()
ret = self.request("httpsz://mobile.campushoy.com/v6/config/guest/tenant/info?ids={ids}".format(ids=school[0]["id"]))
self.loginUrl = ret["data"][0]["ampUrl"]
if ret == "":
print("学校并没有申请入驻今日校园平台")
exit()
print("{name}的登录地址{url}".format(name=schoolName,url=self.loginUrl))
self.host = re.findall(r"//(.*?)/",self.loginUrl)[0]
#ret = self.request(ret["data"][0]["ampUrl"],parseJson=False).url
#self.isIAPLogin = "campusphere" in ret
#if not self.isIAPLogin:
# print("注意:包含AuthServer的登陆方式并未测试!且每一个学校的登录方式都不一样。")
#ret = re.findall(r"//(.*?)/",ret)
#if len(ret) == 0:
# exit()
#self.host = ret[0]
def encrypt(self,text):
k = pyDes.des(self.key, pyDes.CBC, b"\x01\x02\x03\x04\x05\x06\x07\x08", pad=None, padmode=pyDes.PAD_PKCS5)
ret = k.encrypt(text)
return base64.b64encode(ret).decode()
def passwordEncrypt(self,text:str,key:str):
pad = lambda s: s + (len(key) - len(s) % len(key)) * chr(len(key) - len(s) % len(key))
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
text = pad("TdEEGazAXQMBzEAisrYaxRRax5kmnMJnpbKxcE6jxQfWRwP2J78adKYm8WzSkfXJ"+text).encode("utf-8")
aes = AES.new(str.encode(key), AES.MODE_CBC,str.encode("ya8C45aRrBEn8sZH"))
return base64.b64encode(aes.encrypt(text))
def request(self,url:str,body=None,parseJson=True,JsonBody=True,Referer=None):
url = url.format(host=self.host)
if Referer != None: self.session.headers.update({"Referer":Referer})
if body == None:ret = self.session.get(url)
else:
self.session.headers.update({"Content-Type": ("application/json" if JsonBody else "application/x-www-form-urlencoded")})
ret = self.session.post(url,data=(json.dumps(body) if JsonBody else body))
if parseJson:return json.loads(ret.text)
else:return ret
def decrypt(self,text):
k = pyDes.des(self.key, pyDes.CBC, b"\x01\x02\x03\x04\x05\x06\x07\x08", pad=None, padmode=pyDes.PAD_PKCS5)
ret = k.decrypt(base64.b64decode(text))
return ret.decode()
def checkNeedCaptcha(self, username):
url = "https://{host}/iap/checkNeedCaptcha?username={username}".format(host=self.host,username=username)
ret = self.session.get(url)
ret = json.loads(ret.text)
return ret["needCaptcha"]
def generateCaptcha(self):
url = "https://{host}/iap/generateCaptcha?ltId={client}&codeType=2".format(host=self.host,client=self.client)
ret = self.session.get(url)
return ret.content
def getBasicInfo(self):
return self.request("https://{host}/iap/tenant/basicInfo","{}")
def login(self, username, password, captcha=""):
if "campusphere" in self.loginUrl:return self.loginIAP(username,password,captcha)
else: return self.loginAuthserver(username,password,captcha)
def loginIAP(self, username, password, captcha=""):
self.session.headers.update({"X-Requested-With": "XMLHttpRequest"})
ret = self.session.get("https://{host}/iap/login?service=https://{host}/portal/login".format(host=self.host)).url
client = ret[ret.find("=")+1:]
ret = self.request("https://{host}/iap/security/lt","lt={client}".format(client=client),True,False)
client = ret["result"]["_lt"]
#self.encryptSalt = ret["result"]["_encryptSalt"]
body = {
"username": username,
"password": password,
"lt": client,
"captcha": captcha,
"rememberMe": "true",
"dllt": "",
"mobile": ""
}
ret = self.request("https://{host}/iap/doLogin",body,True,False)
if ret["resultCode"] == "REDIRECT":
self.session.get(ret["url"])
return True
else: return False
def checkNeedCaptchaAuthServer(self,username):
ret = self.request("http://{host}/authserver/needCaptcha.html?username={username}&pwdEncrypt2=pwdEncryptSalt".format(username=username),parseJson=False).text
return ret == "true"
def loginAuthserver(self,username,password,captcha=""):
ret = self.request(self.loginUrl,parseJson=False)
body = dict(re.findall(r''',ret.text))
salt = dict(re.findall(r''',ret.text))
body["username"] = username
if "pwdDefaultEncryptSalt" in salt.keys():
body["password"] = self.passwordEncrypt(password,salt["pwdDefaultEncryptSalt"])
else:
body["password"] = password
ret = self.request(ret.url,body,False,False,Referer=self.loginUrl).url
print(self.session.cookies)
print("出错了")
return True
def getCollectorList(self):
body = {
"pageSize": 10,
"pageNumber": 1
}
ret = self.request("https://{host}/wec-counselor-collector-apps/stu/collector/queryCollectorProcessingList",body)
return ret["datas"]["rows"]
def getNoticeList(self):
body = {
"pageSize": 10,
"pageNumber": 1
}
ret = self.request("https://{host}/wec-counselor-stu-apps/stu/notice/queryProcessingNoticeList",body)
return ret["datas"]["rows"]
def confirmNotice(self, wid):
body = {
"wid": wid
}
ret = self.request("https://{host}/wec-counselor-stu-apps/stu/notice/confirmNotice",body)
print(ret["message"])
return ret["message"] == "SUCCESS"
def getCollectorDetail(self, collectorWid):
body = {
"collectorWid": collectorWid
}
return self.request("https://{host}/wec-counselor-collector-apps/stu/collector/detailCollector",body)["datas"]
def getCollectorFormFiled(self, formWid, collectorWid):
body = {
"pageSize": 50,
"pageNumber": 1,
"formWid": formWid,
"collectorWid": collectorWid
}
return self.request("https://{host}/wec-counselor-collector-apps/stu/collector/getFormFields",body)["datas"]["rows"]
def submitCollectorForm(self, formWid, collectWid, schoolTaskWid, rows, address):
body = {
"formWid": formWid,
"collectWid": collectWid,
"schoolTaskWid": schoolTaskWid,
"form": rows,
"address": address
}
ret = self.request("https://{host}/wec-counselor-collector-apps/stu/collector/submitForm",body)
print(ret["message"])
return ret["message"] == "SUCCESS"
def autoFill(self, rows):
for item in rows:
index = 0
while index < len(item["fieldItems"]):
if item["fieldItems"][index]["isSelected"] == 1:index = index + 1
else:item["fieldItems"].pop(index)
#此函数通过表格的默认值自动填写,如果你们学校没有提供默认值的,需要手动编辑此函数。
#先print(rows),观察表格的形式,想选择哪一个选项,pop掉其他无关选项就行了。当然也可以直接将rows硬编码在代码里面。
#因为每个人的定位地址都不一样,有些学校的表格也不一定一样。
def autoComplete(self, address):
collectList = self.getCollectorList()
print(collectList)
for item in collectList:
detail = self.getCollectorDetail(item["wid"])
form = self.getCollectorFormFiled(detail["collector"]["formWid"], detail["collector"]["wid"])
formpath = "./formdb/{formwid}.json".format(formwid=detail["collector"]["formWid"])
if os.path.exists(formpath):
with open(formpath,"rb") as file:
form = json.loads(file.read().decode("utf-8"))
self.autoFill(form)
self.submitCollectorForm(detail["collector"]["formWid"], detail["collector"]["wid"], detail["collector"]["schoolTaskWid"], form, address)
else:
with open(formpath,"wb") as file:
file.write(json.dumps(form,ensure_ascii=False).encode("utf-8"))
print("请手动填写{formpath},之后重新运行脚本".format(formpath=formpath))
exit()
confirmList = self.getNoticeList()
print(confirmList)
for item in confirmList:self.confirmNotice(item["noticeWid"])
if __name__ == "__main__":
if len(sys.argv) != 5:
print("python3 DailyCp.py 学校全名 学号 密码 定位地址")
exit()
app = DailyCP(sys.argv[1])
if not app.login(sys.argv[2], sys.argv[3]):exit()
app.autoComplete(sys.argv[4])
-----关于自定义任意表单内容
表单的内容多种多样,也不可能共享给其他人使用,因此本脚本采用“一次编辑,永久使用”的思路。如果脚本发现未知的表单,会将表单保存到formdb文件夹下,用户必须手动填写好内容,再次运行脚本即可。
-----关于AuthServer的登录方式
目前已知IAP和AuthServer这两种登录方式,IAP是统一的,AuthServer每个学校都不一样。如果你发现脚本并不能正常运作,请根据输出的信息自行修改代码。(理论上所有学校都可以直接运行登入)