├── .gitignore ├── README.md ├── __init__.py ├── config.py ├── db.py ├── driver.py ├── image.py ├── imgs.db ├── imgs ├── 1a5173eded00b83fa304de821abcdb1c.png ├── 247d9b958f3503160cb0484478e72c46.png ├── 3681ca0781da7f1198bab0bcf366503e.png ├── 5763e2b8cded903550c7d1aac2d29880.png ├── 582ba9fec8f729dbf8b11412805fc6a2.png ├── 74e49314ea7c5d1eddd34d0e57d1acad.png ├── 75614b89a6abad6ac49461f94c1b0098.png ├── 829603b3989fc9e8a1a86fa4ad45309a.png ├── d716893d7f139cda3e651877009c47fa.png └── d992bd2b3b787d2c0f0bc06be30326e3.png ├── logger.py ├── logs └── .gitconfig ├── main.py ├── requirements.txt ├── tools.py └── wechat.py /.gitignore: -------------------------------------------------------------------------------- 1 | /DefaultAppData 2 | /chrome 3 | /logs 4 | /.idea 5 | tmp_*.py 6 | *_tmp.py 7 | *.bat 8 | *.vbs 9 | *.sh 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UESTC电子科技大学(研究生)每日健康打卡 2 | - 模拟手动登录、过滑动验证码等步骤。 3 | ## 严正声明 4 | 各组织机构的健康打卡制度是国家疫情防控的重要一环,违反疫情防控有关规定需付刑事责任.此项目仅供学习交流,不可用于违法违规用途.使用本项目造成的任何后果使用者自行承担. 5 | ## 快速使用示例 6 | - chrome+chromedriver软件包下载地址:https://www.mediafire.com/folder/ty2a7wlx1kl3h/AutoCheckInUESTC 7 | - Windows x64 or Ubuntu x64 8 | ``` 9 | # 1. 下载 AutoCheckInUESTC所有代码 10 | 11 | # 2.1 For Ubuntu: 下载chrome97_ubuntu64.tar.gz并解压到对应目录 12 | tar -zxvf chrome97_ubuntu64.tar.gz -C AutoCheckInUESTC/chrome 13 | 14 | # 2.2 For Windows: 下载chrome97_win10_64.zip并解压到目录 AutoCheckInUESTC/chrome/ 15 | 16 | # 3. python环境安装示例: 17 | # 使用conda创建名为`auto`的虚拟环境.(需安装Anaconda或Miniconda) 18 | conda create -n auto python=3.7 numpy opencv -y 19 | # 激活并进入该环境 20 | conda activate auto 21 | # 使用pip安装指定版本的依赖 22 | pip install undetected_chromedriver==3.0.3 23 | pip install selenium==4.0.0b4 24 | 25 | # 4. 配置打卡账户 26 | # 把账号密码写进`config.py`的`user_list`里,支持多用户一起签到 27 | # 例如 28 | user_list = [ 29 | User('1234567890', 'Mima12345?789', ''), 30 | ] 31 | 32 | # 5. 运行 33 | cd AutoCheckInUESTC 34 | conda activate auto 35 | python main.py 36 | 37 | ``` 38 | 39 | ## 软件依赖 40 | 需要安装 `chrome 浏览器`, `chromedriver`. 以下方式任选其一 : 41 | 42 | 1. 直接下载打包好的chrome+chromedriver软件包[win10_x64 or ubuntu_x64], 下载地址: https://www.mediafire.com/folder/ty2a7wlx1kl3h/AutoCheckInUESTC , 解压到项目内的`chrome`文件夹下. 43 | linux 解压可以用这条命令`tar -zxvf chrome97_ubuntu64.tar.gz -C AutoCheckInUESTC/chrome/` 44 | 2. 或者, 如果你已经安装了最新的chrome, `undetected_chromedriver`程序将自动下载并缓存最新的chromedriver. 当你升级chrome时要手动清除项目目录下的 `chromedriver.exe` or `chromedriver` 缓存. 45 | 3. 或者, 你希望使用特定版本的chrome和chromedriver,可以在这里下载:https://chromedriver.chromium.org/downloads , 下载后放到`chrome`文件夹下. 保证该目录下有`chromedriver.exe`和`chrome.exe`(linux 下为`chromedriver`和`chrome`)这两个可执行文件. 46 | 47 | ## 环境依赖 48 | - windows 10 64位请安装以下环境 49 | 50 | ```python 51 | # 手动配置请安装以下python包: 52 | python >= 3.7 # 版本无严格要求, 但是尽量使用3.7版本避免包依赖问题 53 | numpy # 版本无严格要求 54 | selenium==4.0.0b4 # 请使用 pip 安装 55 | opencv # 版本无严格要求,这个包在pip安装的时候名字叫`opencv-python`,conda 安装的时候叫做`opencv` or `py-opencv` 56 | undetected_chromedriver == 3.0.3 # 这个包用pip安装,win10下版本3.0.3通过测试,3.1.3未通过测试 57 | 58 | # 如需开发调试,还要安装以下包: 59 | matplotlib # 版本无严格要求 60 | ``` 61 | 62 | ## 配置打卡账户 63 | - 把账号密码写进`config.py`的`user_list`里,支持多用户一起签到 64 | 65 | ```python 66 | # 把账号密码写进`config.py`的`user_list`里,支持多用户一起签到 67 | # 例如 68 | user_list = [ 69 | User('1234567890', 'Mima12345?789', ''), 70 | User('0987654321', 'ababxyxyawsd9', ''), 71 | ] 72 | ``` 73 | - 推荐结合windows/Linux计划任务功能,实现每天运行一次自动打卡. 74 | ## 开启微信通知 75 | 套娃是吧??? 76 | 77 | 这个比较麻烦,而且不是很必要. 78 | 运行这个程序初心就是最小化被通知消息打扰的可能性,避免错过其他重要信息.现在通知群管你打不打卡一天都是七八条打卡通知.(我...) 79 | 累了累了(大家都不容易). 80 | 如果您还需要该打卡程序通过微信告知你程序运行结果的话,请按照以下步骤配置. 81 | - 注册微信公众测试号. 82 | 83 | 打开以下网址:`https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login` ,点击登录后用微信扫描登陆.然后就能看到`appID`和`appsecret`,先不着急复制. 84 | 85 | - 关注 86 | 87 | 用自己的微信扫码(刚刚那个页面下面有测试号二维码)关注这个测试号,刷新一下网页就会发现用户列表里多了一个用户的昵称和`微信号`等信息,不着急复制,待会会用. 88 | 89 | - 新增模板消息 90 | 91 | 模板消息接口板块有个绿色的新增测试模板按钮,新增两个模板,标题和内容如下: 92 | ```angular2html 93 | # 模板1 94 | 模板标题:打卡成功 95 | 模板内容:时间:{{datetime.DATA}} 用户:{{user.DATA}} {{text.DATA}} 96 | 97 | # 模板2 98 | 模板标题:打卡失败 99 | 模板内容:时间:{{datetime.DATA}} 用户:{{user.DATA}} {{text.DATA}} 100 | ``` 101 | 填好后每个模板会有一个唯一ID,就是页面上`模板ID(用于接口调用)`这一列的内容,待会会用. 102 | - 配置编辑`wechat.py` 103 | ```angular2html 104 | wechat_options = { 105 | "appID": "zdfgbnstu86e75jsrte4s56uj", # 页面上的 appID 106 | "appsecret": "4w5u6eo76kjahestgjassrxfnzdrhtrjsm", # 页面上的 appsecret 107 | "ok_tpl_id": "aewhrzdnjrkystduhtgrctymjtjhgkxmf", # 这个是标题为'打卡成功'模板的模板id,请替换成自己的 108 | "failed_tpl_id": "awersfnjndfznrysjrgeazdrsgerjterhjnzrtgj", # 这个是标题为'打卡失败'模板的模板id,请替换成自己的 109 | } 110 | ``` 111 | - 务必别忘了配置微信号(open_id) 112 | ```angular2html 113 | # 例如 114 | user_list = [ 115 | # 各式:User(user_name, passwd, wechat_openid) 116 | # 第三项wechat_openid是微信测试号页面上[用户列表]栏里[微信号]的值,是关注测试微信号用户的open_id 117 | # 不启用WeChat通知功能的话该项留空,填None 或者 '' 都可 118 | # 例如: User('1234567890', '123456Abc@#$', ''), 119 | User('1234567890', 'Mima12345?789', 'awegretjsukjyhaegrtsyuykeragvf'), 120 | ] 121 | ``` 122 | 123 | - 测试一下能不能收到消息 124 | ```angular2html 125 | # wechat.py 结尾的 __mian__ 代码部分填上要接受消息的微信号(微信测试号页面用户列表里的微信号wechat_openid) 126 | wechat_user_id = 'abcabcabcabcabcabcabcabcabc' 127 | # 运行 128 | python wechat.py 129 | # 然后关注的测试公众号应该会给你发两条模板消息.秒发,几乎没有延迟. 130 | ``` -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b71db892/AutoCheckInUESTC/4ea4d18e639ff6c16edd01186bc0b96add7d0724/__init__.py -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | User = namedtuple('User', ['user_name', 'passwd', 'wechat_openid']) 4 | 5 | # 需要打卡的用户列表 6 | user_list = [ 7 | # openid是页面上 用户列表 栏里 微信号 的值,是关注测试微信号用户的open_id 8 | # 不启用WeChat通知功能的话该项留空, 填None 或者 '' 都可 9 | # 例如: User('1234567890', '123456Abc@#$', ''), 10 | User('1234567890', '123456Abc@#$', 'abcabcabcabc'), 11 | ] 12 | 13 | # 微信推送api配置,不使用WeChat推送的话该项可忽略,配置流程在README.MD里有说明 14 | wechat_options = { 15 | "appID": "abcabcabcabcabc", # 页面上的 appID 16 | "appsecret": "abcabcabcabcabcabcabcabcabcabc", # 页面上的 appsecret 17 | "ok_tpl_id": "abcabcabcabcabcabcabcabcabcabcabcabcabc", # 这个是标题为'打卡成功'模板的模板id,请替换成自己的 18 | "failed_tpl_id": "abcabcabcabcabcabcabcabcabcabcabcabc", # 这个是标题为'打卡失败'模板的模板id,请替换成自己的 19 | "push_url": "https://api.weixin.qq.com/cgi-bin/message/template/send?", # 这行应该不用动 20 | "token_url": "https://api.weixin.qq.com/cgi-bin/token?" 21 | } 22 | 23 | # 使用指定版本的chrome: 24 | # 指定存放chrome.exe 和 chromedriver.exe的文件夹, 可使用绝对路径或者相对于 main.py 文件的相对路径 25 | chrome_path = './chrome' 26 | # chrome_path = 'C:\\Users\\ROOT\\chrome_dir' 27 | # chrome_path = '/root/chrome_dir/' 28 | 29 | if __name__ == '__main__': 30 | pass 31 | -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from logger import logger 2 | import sqlite3 3 | import os 4 | 5 | 6 | class DB: 7 | ''' 8 | 这是一个存储滑动验证码图片的数据库。所有图片以base64字符串形式存储。 9 | 由于学工系统验证码图片数量很有限,该库已经收集了所有完整的验证图片。 10 | 图片验证算法会自动把没见过的图片加入数据库。 11 | 12 | 图片有三种: 13 | RAW_IMGS :原始验证图片,一般尺寸:高*宽=360*590左右,大小不固定 14 | BLOCK_IMGS :原始滑块图片,一般尺寸:高*宽=360*93左右,大小不固定 15 | COOKED_IMGS :经过算法拼合的验证图片,一般尺寸:高*宽=360*590左右,大小不固定 16 | 拼合算法: 17 | 很无脑,类似于不断试错。 18 | ''' 19 | 20 | def __init__(self, db=os.path.join(os.path.split(os.path.realpath(__file__))[0], 'imgs.db')): 21 | # 连接到SQlite数据库 22 | # 数据库文件是test.db,不存在,则自动创建 23 | logger.info(f'DataBase 路径:{db}') 24 | self._conn = sqlite3.connect(db) 25 | self._conn.execute('pragma foreign_keys=on') 26 | self._cursor = self._conn.cursor() 27 | self._init_db() 28 | 29 | def _init_db(self): 30 | # 查询已有的表 31 | self._cursor.execute("select name from sqlite_master where type='table' order by name") 32 | r = self._cursor.fetchall() 33 | logger.info(f'DataBase Tables:{r}') 34 | # 此表存储了所有经过拼合的验证图片(不一定完整,一般需要若干张有缺口的图片才能拼合成一张完整的) 35 | # (此表是GroundTruth答案集合,请不要删除) 36 | COOKED_IMGS = '''CREATE TABLE COOKED_IMGS 37 | (ID INTEGER PRIMARY KEY AUTOINCREMENT, 38 | MD5 CHAR(50) NOT NULL UNIQUE, 39 | BASE64 TEXT NOT NULL, 40 | WIN_START INTEGER NOT NULL, 41 | WIN_END INTEGER NOT NULL);''' 42 | 43 | # 此表存储了所有没有经过拼合的原始验证图片(若数据库占用空间过大,可删除此表) 44 | RAW_IMGS = '''CREATE TABLE RAW_IMGS 45 | (ID INTEGER PRIMARY KEY AUTOINCREMENT, 46 | MD5 CHAR(50) NOT NULL UNIQUE, 47 | BASE64 TEXT NOT NULL, 48 | WIN_START INTEGER NOT NULL, 49 | WIN_END INTEGER NOT NULL, 50 | COOKED_IMG_ID INTEGER REFERENCES COOKED_IMGS(ID) 51 | ON UPDATE CASCADE 52 | ON DELETE SET NULL);''' 53 | 54 | # 此表存储了所有滑动块的原始图片(若数据库占用空间过大,可删除此表) 55 | BLOCK_IMGS = '''CREATE TABLE BLOCK_IMGS 56 | (ID INTEGER PRIMARY KEY AUTOINCREMENT, 57 | MD5 CHAR(50) NOT NULL UNIQUE, 58 | BASE64 TEXT NOT NULL, 59 | RAW_IMG_ID INTEGER REFERENCES RAW_IMGS(ID) 60 | ON UPDATE CASCADE 61 | ON DELETE SET NULL);''' 62 | 63 | def check_table(table_name, comm=None): 64 | if tuple([table_name, ]) in r: 65 | logger.info(f"{table_name} exists.") 66 | else: 67 | logger.info(f"create table {table_name}.") 68 | self._cursor.execute(comm) 69 | 70 | check_table('COOKED_IMGS', comm=COOKED_IMGS) 71 | check_table('RAW_IMGS', comm=RAW_IMGS) 72 | check_table('BLOCK_IMGS', comm=BLOCK_IMGS) 73 | 74 | def _select(self, table, columns: list): 75 | comm = f'SELECT {", ".join(["`" + c + "`" for c in columns])} FROM {table};' 76 | # print(comm) 77 | self._cursor.execute(comm) 78 | result = self._cursor.fetchall() 79 | # print(f'select {len(result)} imgs.') 80 | return result 81 | 82 | def save_raw_img(self, md5_str: str, base64_src: str, win: list, cooked_id: int): 83 | win_start, win_end = win 84 | if tuple([md5_str, ]) in self._select('RAW_IMGS', ['MD5']): 85 | print(f' duplicate!') 86 | return 87 | comm = f'insert into RAW_IMGS (MD5, BASE64, WIN_START, WIN_END, COOKED_IMG_ID)' \ 88 | f' values ("{str(md5_str)}", "{str(base64_src)}", {int(win_start)}, {int(win_end)}, {int(cooked_id)})' 89 | # print(comm) 90 | self._cursor.execute(comm) 91 | self._conn.commit() 92 | 93 | def save_cooked_img(self, md5_str: str, base64_src: str, win: list, ): 94 | win_start, win_end = win 95 | if tuple([md5_str, ]) in self._select('COOKED_IMGS', ['MD5']): 96 | print(f' duplicate!') 97 | return 98 | comm = f'insert into COOKED_IMGS (MD5, BASE64, WIN_START, WIN_END)' \ 99 | f' values ("{str(md5_str)}", "{str(base64_src)}", {int(win_start)}, {int(win_end)})' 100 | # print(comm) 101 | self._cursor.execute(comm) 102 | self._conn.commit() 103 | 104 | def save_block_img(self, md5_str: str, base64_src: str, raw_id: int): 105 | if tuple([md5_str, ]) in self._select('BLOCK_IMGS', ['MD5']): 106 | print(f' duplicate!') 107 | return 108 | comm = f'insert into BLOCK_IMGS (MD5, BASE64, RAW_IMG_ID) ' \ 109 | f'values ("{str(md5_str)}", "{str(base64_src)}", {int(raw_id)})' 110 | # print(comm) 111 | self._cursor.execute(comm) 112 | self._conn.commit() 113 | 114 | def update_cooked_img(self, id, md5_str, base64_src, win: list): 115 | # 如果算法拼合出一张【新的】验证图片(不一定完整,但比旧的图片更接近完整),需要存进数据库。 116 | comm = f'UPDATE COOKED_IMGS SET `MD5` = "{str(md5_str)}", `BASE64` = "{str(base64_src)}", ' \ 117 | f'`WIN_START` = {int(win[0])}, `WIN_END` = {str(win[1])} WHERE `ID` = {int(id)};' 118 | # print(comm) 119 | self._cursor.execute(comm) 120 | print(f'update {self._cursor.rowcount} imgs') 121 | self._conn.commit() 122 | 123 | def close(self): 124 | try: 125 | # # 关闭Cursor: 126 | # self._cursor.close() 127 | # 提交事务: 128 | self._conn.commit() 129 | # 关闭connection: 130 | self._conn.close() 131 | except Exception as e: 132 | print("error when closing db.") 133 | 134 | def get_cooked_img_id(self, md5): 135 | return self._get_img_id('COOKED_IMGS', md5) 136 | 137 | def get_raw_img_id(self, md5): 138 | return self._get_img_id('RAW_IMGS', md5) 139 | 140 | def _get_img_id(self, table, md5): 141 | comm = f'SELECT ID FROM {table} WHERE MD5 = "{md5}";' 142 | # print(comm) 143 | self._cursor.execute(comm) 144 | result = self._cursor.fetchone()[0] 145 | # print(f' img id : {result}') 146 | return result 147 | 148 | def get_cooked_imgs(self): 149 | return self._select('COOKED_IMGS', ['ID', 'MD5', 'BASE64', 'WIN_START', 'WIN_END']) 150 | 151 | def get_raw_imgs(self): 152 | return self._select('RAW_IMGS', ['ID', 'MD5', 'BASE64', 'WIN_START', 'WIN_END', 'COOKED_IMG_ID']) 153 | 154 | def __del__(self): 155 | self.close() 156 | -------------------------------------------------------------------------------- /driver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | import datetime 6 | from pathlib import Path 7 | 8 | from selenium import webdriver 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.support.ui import WebDriverWait 11 | from selenium.webdriver.support import expected_conditions as EC 12 | import undetected_chromedriver as uc 13 | 14 | import tools 15 | from db import DB 16 | from image import IMG 17 | from logger import logger 18 | from config import chrome_path 19 | 20 | 21 | class CheckIn: 22 | """ 23 | 签到逻辑 24 | 使用的网址是移动设备的签到,和企业微信的签到打卡是同一个网址. 25 | """ 26 | 27 | # sleep 比较多,尽量防止网速或者cpu拖慢页面加载 28 | 29 | def __init__(self, user_name, passwd, mobile=False, headless=False): 30 | 31 | self.user_name = user_name 32 | self.passwd = passwd 33 | self.mobile = mobile 34 | 35 | self.db = DB() 36 | self.checkin_name = '李华' 37 | self.checkin_date = None 38 | self.checkin_time = None 39 | 40 | self.driver = self.get_driver(headless, mobile) 41 | 42 | if not mobile: 43 | self.driver.maximize_window() 44 | self.wait = WebDriverWait(self.driver, 15, 0.5) 45 | logger.info(f'初始化完成...') 46 | 47 | @staticmethod 48 | def get_driver(headless, mobile): 49 | 50 | options = webdriver.ChromeOptions() 51 | platform = sys.platform 52 | project_dir = os.path.abspath(Path(__file__).parent) 53 | chrome_dir = os.path.abspath(chrome_path) 54 | if platform.endswith("win32"): 55 | chrome_executable_path = os.path.join(chrome_dir, 'chrome.exe') 56 | chrome_driver_path = os.path.join(chrome_dir, 'chromedriver.exe') 57 | elif platform.startswith("linux"): 58 | chrome_executable_path = os.path.join(chrome_dir, 'chrome') 59 | chrome_crashpad_handler_path = os.path.join(chrome_dir, 'chrome_crashpad_handler') 60 | chrome_driver_path = os.path.join(chrome_dir, 'chromedriver') 61 | # 直接copy过来的chrome可能会有权限问题, windows server也可能遇到.exe锁定问题,需要在资源管理器右键文件属性解锁 62 | os.system(f'chmod a+x {chrome_executable_path}') 63 | os.system(f'chmod a+x {chrome_crashpad_handler_path}') 64 | os.system(f'chmod a+x {chrome_driver_path}') 65 | elif platform.endswith("darwin"): # 暂不支持 66 | logger.warning(f'Unsupported Platform `darwin`.') 67 | raise Exception(f"Unsupported Platform `darwin`.") 68 | else: 69 | raise Exception(f"What's your platform?") 70 | 71 | if mobile: 72 | options.add_experimental_option("mobileEmulation", {"deviceName": "iPhone X"}) 73 | if headless: # 是否使用无头浏览器(不显示UI界面的浏览器,适合部署在服务器) 74 | # options.headless = True 75 | options.add_argument('--headless') 76 | options.add_argument('--mute-audio') # 关闭声音 77 | options.add_argument('--disable-extensions') 78 | options.add_argument('--disable-gpu') 79 | options.add_argument("--no-sandbox") 80 | options.add_argument(f'--user-data-dir={os.path.join(project_dir, "DefaultAppData")}') # 设置成用户自己的数据目录 81 | options.add_experimental_option('excludeSwitches', ['enable-automation']) # 绕过js检测 82 | # 在chrome79版本之后,上面的实验选项已经不能屏蔽webdriver特征了 83 | # 屏蔽webdriver特征 84 | options.add_argument("--disable-blink-features") 85 | options.add_argument("--disable-blink-features=AutomationControlled") 86 | # ==================== 寻找 chrome ==================== 87 | if os.path.exists(chrome_executable_path): 88 | options.binary_location = chrome_executable_path 89 | logger.info(f'可找到 "{chrome_executable_path}"') 90 | else: 91 | logger.info(f'chrome目录不存在:"{chrome_executable_path}"') 92 | logger.info(f'使用系统默认chrome位置') 93 | # ==================== 寻找 chromedriver ==================== 94 | # 新版undetected_chromedriver改动较大且不支持chrome模拟手机 95 | if os.path.exists(chrome_driver_path): 96 | logger.info(f'可找到 "{chrome_driver_path}"') 97 | else: 98 | # 自动下载chromedriver默认仅支持最新版的chrome, 如果使用其他版chrome要手动指定版本 99 | # chromedriver会被undetected_chromedriver缓存在项目目录下, 如果修改了chrome版本要手动删除旧的chromedriver.exe文件 100 | logger.info(f'chromedriver目录不存在:"{chrome_driver_path}"') 101 | logger.info(f'使用undetected_chromedriver默认chromedriver版本') 102 | chrome_driver_path = None 103 | # uc.TARGET_VERSION = 97 # 可以手动指定目标chrome版本 104 | logger.info(f'启用 undetected_chromedriver, chromedriver版本:{chrome_driver_path or uc.TARGET_VERSION}') 105 | return uc.Chrome(executable_path=chrome_driver_path, options=options) # version = 97 106 | 107 | def run(self): 108 | self.mobile_open_website() 109 | self.mobile_pic_confirm() 110 | self.mobile_check_in_once() 111 | 112 | def mobile_open_website(self): 113 | # 打开网址 自动跳转 114 | logger.info(f'open website...') 115 | self.driver.get('https://eportal.uestc.edu.cn/jkdkapp/sys/lwReportEpidemicStu/*default/index.do#/dailyReport') 116 | # 使用js方式输入信息(比chromedriver原生方式快多了) 117 | self.wait.until(EC.presence_of_element_located((By.ID, "mobileUsername"))) 118 | self.wait.until(EC.presence_of_element_located((By.ID, "mobilePassword"))) 119 | self.wait.until(EC.presence_of_element_located((By.ID, "load"))) 120 | time.sleep(2) 121 | logger.info(f'input username ...') 122 | js_comm = f'document.getElementById("mobileUsername").value="{tools.base64_decode(self.user_name)}"' 123 | self.driver.execute_script(js_comm) 124 | time.sleep(2) 125 | logger.info(f'input password ...') 126 | js_comm = f'document.getElementById("mobilePassword").value="{tools.base64_decode(self.passwd)}"' 127 | self.driver.execute_script(js_comm) 128 | time.sleep(2) 129 | logger.info(f'login...') 130 | self.wait.until(EC.presence_of_element_located((By.ID, "load"))) 131 | js_comm = 'document.getElementById("load").click()' 132 | self.driver.execute_script(js_comm) 133 | time.sleep(2) 134 | 135 | def init_db(self): 136 | # PC端收集验证图片(无用函数) 137 | # 无缺口的验证图片使用暴力枚举拼合而成,所以需要一个刷题拼合答案的过程. 138 | # 在调试期间代码附带的数据库已经初始化好了,此函数一般没啥用. 139 | for i in range(100): 140 | # 刷新验证问题 141 | self.wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="captcha"]/i[1]'))) 142 | refresh_btn = self.driver.find_element(By.XPATH, '//*[@id="captcha"]/i[1]') 143 | refresh_btn.click() 144 | # time.sleep(1) 145 | # 图片 146 | self.wait.until(EC.presence_of_element_located((By.ID, "img1"))) 147 | self.wait.until(EC.presence_of_element_located((By.ID, "img2"))) 148 | time.sleep(1) 149 | img1_base64 = self.driver.find_element(By.ID, 'img1').get_attribute("src") 150 | img2_base64 = self.driver.find_element(By.ID, 'img2').get_attribute("src") 151 | # 真实显示框大小 152 | self.wait.until(EC.presence_of_element_located((By.ID, "captcha"))) 153 | canvas = self.driver.find_element(By.ID, "captcha") 154 | canvas_width = canvas.size['width'] 155 | # 滑动条 156 | self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, "slider"))) 157 | slider = self.driver.find_element(By.CLASS_NAME, "slider") 158 | 159 | img1 = IMG(base64_src=img1_base64) 160 | img2 = IMG(base64_src=img2_base64) 161 | 162 | dis = tools.get_distance(img1, img2, canvas_width, self.db) 163 | dis = int(dis - slider.size['width'] * 0.5) 164 | print(dis) 165 | 166 | def mobile_pic_confirm(self, ): 167 | # 移动端过滑动验证码 168 | 169 | # 11.13更新,今天登陆页面临时取消了滑动图片验证码,导致图片验证环节出错. 170 | # 更新后若直接登陆成功则自动跳过图片获取验证环节 171 | # 是否免验证 172 | try: 173 | logger.info(f'Check Picture Test') 174 | tmp_wait = WebDriverWait(self.driver, 5, 0.5) 175 | tmp_wait.until(EC.presence_of_element_located((By.XPATH, "//div[text()='调整搜索范围']"))) 176 | logger.info(f"We don't need calculate Picture Test Today! Happy Happy EZ Way!") 177 | return 178 | except: 179 | logger.info(f"Calculate Picture Test.") 180 | # 获取验证图片和滑块图片 181 | self.wait.until(EC.presence_of_element_located((By.ID, "img1"))) 182 | self.wait.until(EC.presence_of_element_located((By.ID, "img2"))) 183 | time.sleep(5) 184 | img1_base64 = self.driver.find_element(By.ID, 'img1').get_attribute("src") 185 | img2_base64 = self.driver.find_element(By.ID, 'img2').get_attribute("src") 186 | # 获取验证图片真实显示框大小,用于计算真实滑动距离 187 | self.wait.until(EC.presence_of_element_located((By.ID, "captcha"))) 188 | canvas = self.driver.find_element(By.ID, "captcha") 189 | canvas_width = canvas.size['width'] 190 | # 滑动条 191 | time.sleep(2) 192 | self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, "slider"))) 193 | slider = self.driver.find_elements(By.CLASS_NAME, "slider")[0] 194 | # 计算滑动距离 195 | img1 = IMG(base64_src=img1_base64) 196 | img2 = IMG(base64_src=img2_base64) 197 | dis = tools.get_distance(img1, img2, canvas_width, self.db) 198 | logger.info(f"实际距离{dis}") 199 | # 下面这2行代码不要动,虽然无法解释,但是现在它能用. 200 | # 理论上滑动距离要减去滑块宽度的一半才对,但是实际debug时发现不用减就能过验证,实在是无法理解,可能是我距离逻辑写的不对. 201 | # 所以不要动下面这2行代码 202 | # dis = int(dis - slider.size['width'] * 0.5) 203 | logger.info(f"减去半个滑块实际距离{dis}") 204 | track = tools.get_track(dis) 205 | # 执行向右滑动的动作.移动设备触屏命令不方便使用drive驱动,所以使用js的方式模拟出发滑动事件 206 | movejs = """ 207 | function sendTouchEvent(x, y, element, eventType) { 208 | const touchObj = new Touch({ 209 | identifier: Date.now(), 210 | target: element, 211 | clientX: x, 212 | clientY: y, 213 | pageX: x, 214 | pageY: y, 215 | radiusX: 2.5, 216 | radiusY: 2.5, 217 | rotationAngle: 10, 218 | force: 0.5, 219 | }); 220 | 221 | const touchEvent = new TouchEvent(eventType, { 222 | cancelable: true, 223 | bubbles: true, 224 | touches: [touchObj], 225 | targetTouches: [], 226 | changedTouches: [touchObj], 227 | shiftKey: true, 228 | }); 229 | 230 | element.dispatchEvent(touchEvent); 231 | } 232 | 233 | const myElement = document.getElementsByClassName('slider')[0] 234 | function move(ele, x, y){ 235 | rect = ele.getBoundingClientRect() 236 | sendTouchEvent((rect.left + rect.right)/2, (rect.top + rect.bottom)/2, ele, 'touchstart'); 237 | sendTouchEvent((rect.left + rect.right)/2 + x, (rect.top + rect.bottom)/2 + y, ele, 'touchmove'); 238 | sendTouchEvent((rect.left + rect.right)/2 + x, (rect.top + rect.bottom)/2 + y, ele, 'touchend'); 239 | } 240 | 241 | """ 242 | js_comm = movejs + "move(myElement, {},0);".format(sum(track)) 243 | self.driver.execute_script(js_comm) 244 | time.sleep(10) 245 | 246 | def mobile_check_in_once(self): 247 | # 进行一次签到 248 | if self.is_finished(): 249 | logger.info(f'填完了') 250 | return 251 | else: 252 | logger.info(f'没填') 253 | logger.info('新增签到记录') 254 | js_comm = 'document.querySelector("#app > div > div.mint-layout-container.pjcse52gj > ' \ 255 | 'div.mint-fixed-button.mt-color-white.sjarvhx43.mint-fixed-button--bottom-right.' \ 256 | 'mint-fixed-button--primary.mt-bg-primary").click()' 257 | self.driver.execute_script(js_comm) 258 | self.wait.until(EC.presence_of_element_located((By.XPATH, "//div[text()='基本信息']"))) 259 | time.sleep(2) 260 | logger.info('提交') 261 | js_comm = 'document.querySelector("#app > div > div > div.mint-layout-container.OPjctwlgzsl > button").click()' 262 | self.driver.execute_script(js_comm) 263 | 264 | # 11.7日更新: 265 | # 已经提交过一次的内容默认在下次不用重复填写可直接提交.11.7日新增了疫苗接种情况一栏,程序未设置填写功能,因此打卡出错 266 | # 不妨每次都检测一次是否有未填写项目的提示.有新提示的话本次打卡失败 267 | have_tip, middle_tip = False, '' 268 | try: 269 | self.wait.until( 270 | EC.presence_of_all_elements_located((By.CSS_SELECTOR, "body > div.mint-toast.is-placemiddle"))) 271 | have_tip = True 272 | time.sleep(0.5) 273 | middle_tip = self.driver.find_elements(By.CSS_SELECTOR, "body > div.mint-toast.is-placemiddle")[0].text 274 | except: 275 | pass 276 | if have_tip: 277 | e = f"have_tip:{have_tip}, tips:{middle_tip}" 278 | logger.error(f"Some tips on the web pages:{e}") 279 | raise Exception(e) 280 | 281 | logger.info('确定') 282 | js_comm = 'document.querySelector("body > div.mint-msgbox-wrapper > div > div.mint-msgbox-btns > ' \ 283 | 'button.mint-msgbox-btn.mint-msgbox-confirm.mt-btn-primary").click()' 284 | self.driver.execute_script(js_comm) 285 | time.sleep(2) 286 | if self.is_finished(): 287 | logger.info(f'填完了') 288 | else: 289 | logger.warning(f'WTF? 见鬼了?填完了但是没填?') 290 | raise Exception('Unknown ERROR') 291 | self.close() 292 | 293 | def is_finished(self): 294 | # 检查是否签到完成 295 | try: 296 | # 11.1日更新:每月第一天获取不到历史打卡记录,所以会出错 297 | selector = '#app > div > div.mint-layout-container.pjcse52gj > div.cjataj7ar > div > div > div:nth-child(2)' 298 | self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector))) 299 | time.sleep(2) 300 | last_checkin = self.driver.find_elements(By.CSS_SELECTOR, selector)[0] 301 | last_checkin_text = last_checkin.text 302 | logger.info(f"last checkin text:{last_checkin_text}") 303 | rule = r'.*-(?P.*)\n.*\n.*\n.*\n(?P\d{4}-\d{1,2}-\d{1,2})\s(?P