开发环境
- python3.0
-----自动填表/打卡/上报原理
其实也就是模拟一个数据包,调用今日校园的api;实现登入功能;然后查找未填写的表;倘若表的序列号已经存在你的代码包里面,就会自动将包数据发到职教云服务器上面去,倘若没有,将会把表自动打包程json;然后你只需要编辑下json数据,以后这个序列的包就可以自动提交了
-----使用说明
理论上支持大部分学校(IAP登录方式,自己先试试能用不)和任意表单内容的自定义。目前已知两种学工号登录方式,均已实现。运行以下命令即可
pip install -r requirements.txt python3 DailyCP.py 学校全名 学号 密码 定位地址 12
注意系统时间是否设置正确
Linux下可以使用corntab -e设置定时任务让程序每天自动打卡(下面的是设置0-9点触发的)
0 9 * * * python3 DailyCP.py 学校全名 学号 密码 定位地址 1
- 部分代码(主要函数)
- 获取全部的项目源码请到公众号【sky趣完团】
或者加入qq群:749717825
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'''<input type="hidden" name="(.*?)" value="(.*?)"''',ret.text)) salt = dict(re.findall(r'''<input type="hidden" id="(.*?)" value="(.*?)"''',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每个学校都不一样。如果你发现脚本并不能正常运作,请根据输出的信息自行修改代码。(理论上所有学校都可以直接运行登入)
完整项目代码获取点这里即可