├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .vscode └── settings.json ├── Donate-alipay.jpg ├── Donate-wechat.jpg ├── LICENSE ├── README.md ├── config └── default.ini ├── data.json ├── requirements.txt ├── setup.cmd ├── start.cmd └── xuexi ├── __init__.py ├── __main__.py ├── model.py ├── secureRandom.py └── unit.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 项目依赖/ 106 | *.log 107 | *.xml 108 | *.png 109 | config/custom.ini 110 | start_for*.cmd 111 | 使用帮助.html -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "c:\\Users\\vince\\Projects\\AutoXue\\venv\\Scripts\\python.exe" 3 | } -------------------------------------------------------------------------------- /Donate-alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechXueXi/AutoXue/4e98d840e042f41ab144e4e7f2aee706c578fa7e/Donate-alipay.jpg -------------------------------------------------------------------------------- /Donate-wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechXueXi/AutoXue/4e98d840e042f41ab144e4e7f2aee706c578fa7e/Donate-wechat.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 这是从https://github.com/kessil/AutoXue 搬运来的项目 2 | 3 | 现支持挑战答题、每日答题、新闻阅读、视听学习。支持较全,37分/天 4 | 5 | 缺点:对电脑配置要求较高,且安装运行环境很麻烦。 6 | 7 | **使用更方便可达 46分/天的TechXueXi:https://github.com/TechXueXi/** 8 | 9 | 如果您现在前往https://github.com/kessil/AutoXue ,请注意使用dev分支。 10 | 11 | # 挑战答题 12 | 13 | ## 免责申明 14 | `AutoXue`为本人`Python`学习交流的开源非营利项目,仅作为`Python`学习交流之用,使用需严格遵守开源许可协议。严禁用于商业用途,禁止使用`AutoXue`进行任何盈利活动。对一切非法使用所产生的后果,本人概不负责。 15 | 16 | ## 环境准备[下载](http://49.235.90.76:5000/downloads) 17 | 0. 如果之前添加过环境变量`ADB1.0.40`请确保删除之 18 | 1. 安装`JDK`,本文使用JDK1.8 19 | + 在环境变量中新建`JAVA_HOME`变量,值为JDK安装路径,如`C:\Program Files\Java\jdk1.8.0_05` 20 | + 新建`CLASSPATH`变量,值为`.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;` 21 | + `Path`变量中添加:`%JAVA_HOME%\bin和%JAVA_HOME%\jre\bin` 22 | 2. 安装`SDK`,本文使用SDK r24.4.1 23 | + 在环境变量中新建`ANDROID_HOME`,值为SDK安装路径,如`C:\Program Files (x86)\Android\android-sdk` 24 | + 在Path变量中添加项:`%ANDROID_HOME%\platform-tools` 和 `%ANDROID_HOME%\tools` 25 | + 打开`SDK Manager.exe` 安装对应的工具和包 26 | 27 | 3. 安装`Appium-desktop`,为了使用`UiAutomator2`,请将`Appium`设为以管理员权限启动,并赋予JDK和SDK所有权限 28 | 4. 安装一个模拟器,就选夜神Nox吧,如用其他模拟器或真机出现问题请自救。 29 | 5. 安装`Python`,请至少使用3.7+版本,推荐3.8 30 | 31 | ## 使用方法(windows) 32 | 0. 克隆项目 `git clone https://github.com/kessil/AutoXue.git --depth 1` 33 | 1. 双击运行`setup.cmd` 34 | 2. 启动 `Appium` 和 `Nox` 35 | 3. 双击运行 `start.cmd` 36 | 37 | ## 写在最后 38 | + 在[这里](http://49.235.90.76:5000/downloads)可能有您需要的安装包,你可以官方网站下载使用最新版本,也可在[这里](http://49.235.90.76:5000/downloads)下载(未必最新版) 39 | + 强烈建议需要自定义配置文件的用户下载使用vscode编辑器,[why vscode?](https://hacpai.com/article/1569745141957),请一定不要使用系统自带记事本修改配置文件。 40 | 41 | -------------------------------------------------------------------------------- /config/default.ini: -------------------------------------------------------------------------------- 1 | ; default.ini 2 | ; Do not Modify! 3 | 4 | [capability] 5 | ; Appium Capability Setting 6 | platformname = Android 7 | automationname = UiAutomator2 8 | unicodekeyboard = true 9 | resetkeyboard = true 10 | noreset = true 11 | apppackage = cn.xuexi.android 12 | appactivity = com.alibaba.android.rimet.biz.SplashActivity 13 | 14 | ; 下面3条配置可能需要修改 15 | ; 1. 在./xuexi目录下新建custom.ini配置文件 16 | ; 2. 在custom.ini中写入以下内容(打开模拟器后在cmd输入adb devices -l 查看需要替换的具体值) 17 | ; [capability] 18 | ; platformversion = 你的安卓版本 19 | ; devicename = 你的设备名称 20 | ; uuid = 你的uuid 21 | ; ---------------------------------- 22 | platformversion = 5.1.1 23 | devicename = MI_6 24 | uuid = 127.0.0.1:62001 25 | ; ---------------------------------- 26 | [api] 27 | ; Resources URL 28 | ; remote api url 29 | url = http://49.235.90.76:5000/api/questions 30 | 31 | [prefers] 32 | ; 控制台输出日志等级 33 | console_levelname = INFO 34 | logging_path = ./logs 35 | ; ; 学习方案:积分-score 点点通-bonus 36 | ; plan_of_xuexi = score 37 | ; # 挑战答题题数上下限 38 | challenge_count_min = 10 39 | challenge_count_max = 15 40 | 41 | ; # 挑战答题提交延时上下限 42 | challenge_delay_min = 2 43 | challenge_delay_max = 6 44 | 45 | ; # 每日答题题间延时上下限 46 | daily_delay_min = 3 47 | daily_delay_max = 5 48 | 49 | ; # 每日答题组间延时上下限 50 | daily_group_delay_min = 5 51 | daily_group_delay_max = 10 52 | 53 | ; 每日答题单组题数 54 | daily_count_each_group = 10 55 | 56 | ; # 试听学习观看视频数量上下限 57 | video_count_min = 12 58 | video_count_max = 15 59 | 60 | ; # 试听学习每则视频观看时间上下限 61 | ; video_delay_min = 25 62 | ; video_delay_max = 30 63 | 64 | ; # 收听广播栏目 65 | radio_chanel = 音乐之声 66 | ; 广播开关 67 | ; -default 根据视听学习时长情况自主选择 68 | ; -enable 开启 69 | ; -disable 关闭 70 | radio_switch = default 71 | 72 | ; # 新闻学习栏目 73 | article_volumn_title = 订阅 74 | 75 | ; # 新闻学习数量上下限 76 | article_count_min = 12 77 | article_count_max = 15 78 | 79 | ; # 新闻学习阅读组间时延上下限 80 | article_delay_min = 3 81 | article_delay_max = 5 82 | 83 | ; # 收藏分享数量 84 | star_share_comments_count = 2 85 | 86 | ; 自动注销 bool 87 | keep_alive = true 88 | 89 | ; 执行每周答题、专项答题的日子 90 | ; ------------------------------------------------ 91 | ; 字符串形式:1~7表示周一至周日 0表示跳过 92 | workdays = 0 93 | 94 | [rules] 95 | ; Xpath rules 96 | login_username = //android.widget.EditText[@resource-id="cn.xuexi.android:id/et_phone_input"] 97 | login_password = //android.widget.EditText[@resource-id="cn.xuexi.android:id/et_pwd_login"] 98 | login_submit = //android.widget.Button[@resource-id="cn.xuexi.android:id/btn_next"] 99 | login_confirm = //android.view.View[@text="同意"] 100 | 101 | setting_submit = //android.widget.TextView[@text="我的"]/following-sibling::android.widget.TextView[1] 102 | logout_submit = //android.widget.TextView[@text="退出登录"] 103 | logout_confirm = //android.widget.Button[@text="确认"] 104 | ; logout_confirm = //android.widget.Button[@resource-id="android:id/button1"] 105 | 106 | home_entry = //*[@resource-id="cn.xuexi.android:id/home_bottom_tab_button_work"] 107 | bailing_enter = //*[@resource-id="cn.xuexi.android:id/home_bottom_tab_button_ding"] 108 | mine_entry = //*[@resource-id="cn.xuexi.android:id/comm_head_xuexi_mine"] 109 | video_first = //android.widget.ListView/android.widget.FrameLayout[1] 110 | 111 | score_entry = //android.widget.TextView[@resource-id="cn.xuexi.android:id/comm_head_xuexi_score"] 112 | score_list = //android.view.View[starts-with(@text, "已获")] 113 | 114 | quiz_entry = //*[@text="我要答题"] 115 | challenge_entry = //*[@text="挑战答题"] 116 | challenge_content = //android.widget.ListView/preceding-sibling::android.view.View[1] 117 | challenge_options = //android.widget.ListView//android.widget.RadioButton/following-sibling::android.view.View[1] 118 | challenge_revival = //android.view.View[@text="分享就能复活" or @text="再来一局"] 119 | 120 | daily_entry = //*[@text="每日答题"] 121 | daily_category = //*[@text="填空题" or @text="单选题" or @text="多选题"] 122 | daily_submit = //*[@text="确定" or @text="下一题" or @text="完成"] 123 | daily_tips_open = //android.view.View[@text="查看提示"] 124 | daily_tips_close = //android.view.View[@text="提示"]/following-sibling::android.view.View[1] 125 | daily_tips = //android.view.View[@text="提示"]/../following-sibling::android.view.View[1]/android.view.View 126 | 127 | daily_blank_content = //android.widget.EditText/../../android.view.View 128 | daily_blank_container = //android.widget.EditText/../android.view.View 129 | daily_blank_edits = //android.widget.EditText 130 | 131 | daily_content = //android.widget.ListView/preceding-sibling::android.view.View[1] 132 | daily_options = //android.widget.ListView/android.view.View/android.view.View/android.view.View[@index="2"] 133 | 134 | daily_wrong_or_not = //android.view.View[@text="答案解析"] 135 | daily_answer = //android.view.View[starts-with(@text, "正确答案: ")] 136 | daily_notes = //android.view.View[starts-with(@text, "正确答案: ")]/following-sibling::android.view.View[1] 137 | daily_score = //android.view.View[starts-with(@text, "+")] 138 | daily_again = //*[@text="再来一组"] 139 | daily_back_confirm = //*[@text="退出"] 140 | 141 | article_volumn = //android.support.v4.view.ViewPager/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.view.View/android.widget.LinearLayout/android.widget.TextView 142 | article_list = //*[@resource-id="cn.xuexi.android:id/general_card_title_id"] 143 | article_comments = //android.widget.TextView[@text="欢迎发表你的观点"] 144 | article_comments_list = //android.widget.TextView[@text="欢迎发表你的观点"]/following-sibling::android.widget.FrameLayout 145 | article_stars = //android.widget.TextView[@text="欢迎发表你的观点"]/following-sibling::android.widget.ImageView[1] 146 | article_share = //android.widget.TextView[@text="欢迎发表你的观点"]/following-sibling::android.widget.ImageView[2] 147 | article_share_xuexi = //android.widget.TextView[@text="分享到学习强国"] 148 | article_comments_edit = //android.widget.EditText 149 | article_comments_publish = //android.widget.TextView[@text="发布"] 150 | article_comments_delete = //android.widget.TextView[@text="删除"] 151 | article_comments_delete_confirm = //android.widget.Button[@text="确认"] 152 | article_thumb_up = //*[@resource-id="like-wrapper"] 153 | article_kaleidoscope = //android.support.v7.widget.RecyclerView/android.widget.LinearLayout[1]/android.widget.TextView 154 | 155 | weekly_entry = //*[@text="每周答题"] 156 | ; 仅尝试本周,其他请手动 157 | weekly_current = //android.widget.ListView[1]/android.view.View[1]/android.view.View[2] 158 | weekly_titles = //android.widget.ListView/android.view.View/android.view.View[1] 159 | weekly_states = //android.widget.ListView/android.view.View/android.view.View[2] 160 | weekly_submit = //*[@text="确定"] 161 | weekly_back_confirm = //*[@text="确定"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechXueXi/AutoXue/4e98d840e042f41ab144e4e7f2aee706c578fa7e/requirements.txt -------------------------------------------------------------------------------- /setup.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd /d %~dp0 3 | echo %cd% 4 | echo first, confirm python3.7+ installed? 5 | echo install venv... 6 | python -m venv venv 7 | echo venv installed OK. 8 | echo install packages... 9 | REM venv\scripts\python -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 10 | venv\scripts\python -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple 11 | venv\scripts\python -m pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple 12 | venv\scripts\python -m pip install Appium-Python-Client -i https://pypi.tuna.tsinghua.edu.cn/simple 13 | echo packages installed OK. 14 | pause -------------------------------------------------------------------------------- /start.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd /d %~dp0 3 | echo %cd% 4 | 5 | tasklist /nh|find /i "Appium.exe" 6 | if errorlevel 1 ( 7 | echo could not start while Appium not running 8 | goto breakout 9 | ) else ( 10 | REM echo Appium is running 11 | ) 12 | 13 | tasklist /nh|find /i "Nox.exe" 14 | if errorlevel 1 ( 15 | echo could not start while Nox not running 16 | goto breakout 17 | ) else ( 18 | REM echo Nox is running 19 | goto run 20 | ) 21 | 22 | 23 | :run 24 | REM echo venv\scripts\python -m xuexi 25 | venv\scripts\python -m xuexi 26 | pause 27 | exit 28 | 29 | :breakout 30 | echo please start Appium and Nox 31 | pause 32 | exit -------------------------------------------------------------------------------- /xuexi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | ''' 4 | @project: AutoXue 5 | @file: __init__.py 6 | @author: kessil 7 | @contact: https://github.com/kessil/AutoXue/ 8 | @time: 2019-10-26(星期六) 09:03 9 | @Copyright © 2019. All rights reserved. 10 | ''' 11 | import re 12 | import time 13 | import requests 14 | import string 15 | import subprocess 16 | from datetime import datetime 17 | from urllib.parse import quote 18 | from itertools import accumulate 19 | from collections import defaultdict 20 | from appium import webdriver 21 | from selenium.common.exceptions import NoSuchElementException 22 | from selenium.webdriver.support.ui import WebDriverWait 23 | from selenium.webdriver.support import expected_conditions as EC 24 | from selenium.webdriver.common.by import By 25 | from .unit import Timer, logger, caps, rules, cfg 26 | from .model import BankQuery 27 | from .secureRandom import SecureRandom as random 28 | 29 | class Automation(): 30 | # 初始化 Appium 基本参数 31 | def __init__(self): 32 | self.connect() 33 | self.desired_caps = { 34 | "platformName": caps["platformname"], 35 | "platformVersion": caps["platformversion"], 36 | "automationName": caps["automationname"], 37 | "unicodeKeyboard": caps["unicodekeyboard"], 38 | "resetKeyboard": caps["resetkeyboard"], 39 | "noReset": caps["noreset"], 40 | 'newCommandTimeout': 800, 41 | "deviceName": caps["devicename"], 42 | "uuid": caps["uuid"], 43 | "appPackage": caps["apppackage"], 44 | "appActivity": caps["appactivity"] 45 | } 46 | logger.info('打开 appium 服务,正在配置...') 47 | self.driver = webdriver.Remote('http://localhost:4723/wd/hub', self.desired_caps) 48 | self.wait = WebDriverWait(self.driver, 15) 49 | self.size = self.driver.get_window_size() 50 | 51 | def connect(self): 52 | logger.info(f'正在连接模拟器 {caps["uuid"]},请稍候...') 53 | if 0 == subprocess.check_call(f'adb connect {caps["uuid"]}', shell=True, stdout=subprocess.PIPE): 54 | logger.info(f'模拟器 {caps["uuid"]} 连接成功') 55 | else: 56 | logger.info(f'模拟器 {caps["uuid"]} 连接失败') 57 | 58 | def disconnect(self): 59 | logger.info(f'正在断开模拟器 {caps["uuid"]},请稍候...') 60 | if 0 == subprocess.check_call(f'adb disconnect {caps["uuid"]}', shell=True, stdout=subprocess.PIPE): 61 | logger.info(f'模拟器 {caps["uuid"]} 断开成功') 62 | else: 63 | logger.info(f'模拟器 {caps["uuid"]} 断开失败') 64 | 65 | # 屏幕方法 66 | def swipe_up(self): 67 | # 向上滑动屏幕 68 | self.driver.swipe(self.size['width'] * random.uniform(0.55, 0.65), 69 | self.size['height'] * random.uniform(0.65, 0.75), 70 | self.size['width'] * random.uniform(0.55, 0.65), 71 | self.size['height'] * random.uniform(0.25, 0.35), random.uniform(800, 1200)) 72 | logger.debug('向上滑动屏幕') 73 | 74 | def swipe_down(self): 75 | # 向下滑动屏幕 76 | self.driver.swipe(self.size['width'] * random.uniform(0.55, 0.65), 77 | self.size['height'] * random.uniform(0.25, 0.35), 78 | self.size['width'] * random.uniform(0.55, 0.65), 79 | self.size['height'] * random.uniform(0.65, 0.75), random.uniform(800, 1200)) 80 | logger.debug('向下滑动屏幕') 81 | 82 | def swipe_right(self): 83 | # 向右滑动屏幕 84 | self.driver.swipe(self.size['width'] * random.uniform(0.01, 0.11), 85 | self.size['height'] * random.uniform(0.75, 0.89), 86 | self.size['width'] * random.uniform(0.89, 0.98), 87 | self.size['height'] * random.uniform(0.75, 0.89), random.uniform(800, 1200)) 88 | logger.debug('向右滑动屏幕') 89 | def swipe_left(self): 90 | # 向右滑动屏幕 91 | self.driver.swipe(self.size['width'] * random.uniform(0.89, 0.98), 92 | self.size['height'] * random.uniform(0.75, 0.89), 93 | self.size['width'] * random.uniform(0.01, 0.11), 94 | self.size['height'] * random.uniform(0.75, 0.89), random.uniform(800, 1200)) 95 | logger.debug('向左滑动屏幕') 96 | 97 | def find_element(self, ele:str): 98 | logger.debug(f'find elements by xpath: {ele}') 99 | try: 100 | element = self.driver.find_element_by_xpath(ele) 101 | except NoSuchElementException as e: 102 | logger.error(f'找不到元素: {ele}') 103 | raise e 104 | return element 105 | 106 | def find_elements(self, ele:str): 107 | logger.debug(f'find elements by xpath: {ele}') 108 | try: 109 | elements = self.driver.find_elements_by_xpath(ele) 110 | except NoSuchElementException as e: 111 | logger.error(f'找不到元素: {ele}') 112 | raise e 113 | return elements 114 | 115 | # 返回事件 116 | def safe_back(self, msg='default msg'): 117 | logger.debug(msg) 118 | self.driver.keyevent(4) 119 | time.sleep(1) 120 | 121 | def safe_click(self, ele:str): 122 | logger.debug(f'safe click {ele}') 123 | button = self.wait.until(EC.presence_of_element_located((By.XPATH, ele))) 124 | # button = self.find_element(ele) 125 | button.click() 126 | time.sleep(1) 127 | 128 | def __del__(self): 129 | self.driver.close_app() 130 | self.driver.quit() 131 | 132 | 133 | class App(Automation): 134 | def __init__(self, username="", password=""): 135 | self.username = username 136 | self.password = password 137 | self.headers = { 138 | 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" 139 | } 140 | self.query = BankQuery() 141 | self.bank = None 142 | self.score = defaultdict(tuple) 143 | 144 | super().__init__() 145 | 146 | self.login_or_not() 147 | self.driver.wait_activity('com.alibaba.android.rimet.biz.home.activity.HomeActivity', 20, 3) 148 | self.view_score() 149 | self._read_init() 150 | self._view_init() 151 | self._daily_init() 152 | self._challenge_init() 153 | self._weekly_init() 154 | 155 | def login_or_not(self): 156 | # com.alibaba.android.user.login.SignUpWithPwdActivity 157 | time.sleep(10) 158 | try: 159 | home = self.driver.find_element_by_xpath(rules["home_entry"]) 160 | logger.debug(f'不需要登录') 161 | return 162 | except NoSuchElementException as e: 163 | logger.debug(self.driver.current_activity) 164 | logger.debug(f"非首页,先进行登录") 165 | 166 | if not self.username or not self.password: 167 | logger.error(f'未提供有效的username和password') 168 | logger.info(f'也许你可以通过下面的命令重新启动:') 169 | logger.info(f'\tpython -m xuexi -u "your_username" -p "your_password"') 170 | raise ValueError('需要提供登录的用户名和密钥,或者提前在App登录账号后运行本程序') 171 | 172 | username = self.wait.until(EC.presence_of_element_located(( 173 | By.XPATH, rules["login_username"] 174 | ))) 175 | password = self.wait.until(EC.presence_of_element_located(( 176 | By.XPATH, rules["login_password"] 177 | ))) 178 | username.send_keys(self.username) 179 | password.send_keys(self.password) 180 | self.safe_click(rules["login_submit"]) 181 | time.sleep(8) 182 | try: 183 | home = self.driver.find_element_by_xpath(rules["home_entry"]) 184 | logger.debug(f'无需点击同意条款按钮') 185 | return 186 | except NoSuchElementException as e: 187 | logger.debug(self.driver.current_activity) 188 | logger.debug(f"需要点击同意条款按钮") 189 | self.safe_click(rules["login_confirm"]) 190 | time.sleep(3) 191 | 192 | def logout_or_not(self): 193 | if cfg.getboolean("prefers", "keep_alive"): 194 | logger.debug("无需自动注销账号") 195 | return 196 | self.safe_click(rules["mine_entry"]) 197 | self.safe_click(rules["setting_submit"]) 198 | self.safe_click(rules["logout_submit"]) 199 | self.safe_click(rules["logout_confirm"]) 200 | logger.info("已注销") 201 | 202 | 203 | def view_score(self): 204 | self.safe_click(rules['score_entry']) 205 | titles = ["登录", "阅读文章", "视听学习", "文章学习时长", 206 | "视听学习时长", "每日答题", "每周答题", "专项答题", 207 | "挑战答题", "订阅", "收藏", "分享", "发表观点", "本地频道"] 208 | score_list = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules['score_list']))) 209 | # score_list = self.find_elements(rules["score_list"]) 210 | for t, score in zip(titles, score_list): 211 | s = score.get_attribute("name") 212 | self.score[t] = tuple([int(x) for x in re.findall(r'\d+', s)]) 213 | 214 | # print(self.score) 215 | for i in self.score: 216 | logger.debug(f'{i}, {self.score[i]}') 217 | self.safe_back('score -> home') 218 | 219 | def back_or_not(self, title): 220 | # return False 221 | g, t = self.score[title] 222 | if g == t: 223 | logger.debug(f'{title} 积分已达成,无需重复获取积分') 224 | return True 225 | return False 226 | 227 | def _search(self, content, options, exclude=''): 228 | # 职责 网上搜索 229 | logger.debug(f'搜索 {content} ') 230 | logger.info(f"选项 {options}") 231 | content = re.sub(r'[\((]出题单位.*', "", content) 232 | if options[-1].startswith("以上") and chr(len(options)+64) not in exclude: 233 | logger.info(f'根据经验: {chr(len(options)+64)} 很可能是正确答案') 234 | return chr(len(options)+64) 235 | # url = quote('https://www.baidu.com/s?wd=' + content, safe=string.printable) 236 | url = quote("https://www.sogou.com/web?query=" + content, safe=string.printable) 237 | response = requests.get(url, headers=self.headers).text 238 | counts = [] 239 | for i, option in zip(['A', 'B', 'C', 'D', 'E', 'F'], options): 240 | count = response.count(option) 241 | counts.append((count, i)) 242 | logger.info(f'{i}. {option}: {count} 次') 243 | counts = sorted(counts, key=lambda x:x[0], reverse=True) 244 | counts = [x for x in counts if x[1] not in exclude] 245 | c, i = counts[0] 246 | if 0 == c: 247 | # 替换了百度引擎为搜狗引擎,结果全为零的机会应该会大幅降低 248 | _, i = random.choice(counts) 249 | logger.info(f'搜索结果全0,随机一个 {i}') 250 | logger.info(f'根据搜索结果: {i} 很可能是正确答案') 251 | return i 252 | 253 | def _verify(self, category, content, options): 254 | # 职责: 检索题库 查看提示 255 | letters = list("ABCDEFGHIJKLMN") 256 | self.bank = self.query.get({ 257 | "category": category, 258 | "content": content, 259 | "options": options 260 | }) 261 | if self.bank and self.bank["answer"]: 262 | logger.info(f'已知的正确答案: {self.bank["answer"]}') 263 | return self.bank["answer"] 264 | excludes = self.bank["excludes"] if self.bank else "" 265 | tips = self._view_tips() 266 | if not tips: 267 | logger.debug("本题没有提示") 268 | if "填空题" == category: 269 | return None 270 | elif "多选题" == category: 271 | return "ABCDEFG"[:len(options)] 272 | elif "单选题" == category: 273 | return self._search(content, options, excludes) 274 | else: 275 | logger.debug("题目类型非法") 276 | else: 277 | if "填空题" == category: 278 | dest = re.findall(r'.{0,2}\s+.{0,2}', content) 279 | logger.debug(f'dest: {dest}') 280 | if 1 == len(dest): 281 | dest = dest[0] 282 | logger.debug(f'单处填空题可以尝试正则匹配') 283 | pattern = re.sub(r'\s+', '(.+?)', dest) 284 | logger.debug(f'匹配模式 {pattern}') 285 | res = re.findall(pattern, tips) 286 | if 1 == len(res): 287 | return res[0] 288 | logger.debug(f'多处填空题难以预料结果,索性不处理') 289 | return None 290 | 291 | elif "多选题" == category: 292 | check_res = [letter for letter, option in zip(letters, options) if option in tips] 293 | if len(check_res) > 1: 294 | logger.debug(f'根据提示,可选项有: {check_res}') 295 | return "".join(check_res) 296 | return "ABCDEFG"[:len(options)] 297 | elif "单选题" == category: 298 | radio_in_tips, radio_out_tips = "", "" 299 | for letter, option in zip(letters, options): 300 | if option in tips: 301 | logger.debug(f'{option} in tips') 302 | radio_in_tips += letter 303 | else: 304 | logger.debug(f'{option} out tips') 305 | radio_out_tips += letter 306 | 307 | logger.debug(f'含 {radio_in_tips} 不含 {radio_out_tips}') 308 | if 1 == len(radio_in_tips) and radio_in_tips not in excludes: 309 | logger.debug(f'根据提示 {radio_in_tips}') 310 | return radio_in_tips 311 | if 1 == len(radio_out_tips) and radio_out_tips not in excludes: 312 | logger.debug(f'根据提示 {radio_out_tips}') 313 | return radio_out_tips 314 | return self._search(content, options, excludes) 315 | else: 316 | logger.debug("题目类型非法") 317 | 318 | 319 | def _update_bank(self, item): 320 | if not self.bank or not self.bank["answer"]: 321 | self.query.put(item) 322 | 323 | # 挑战答题模块 324 | # class Challenge(App): 325 | def _challenge_init(self): 326 | # super().__init__() 327 | try: 328 | self.challenge_count = cfg.getint('prefers', 'challenge_count') 329 | except: 330 | g, t = self.score["挑战答题"] 331 | if t == g: 332 | self.challenge_count = 0 333 | else: 334 | self.challenge_count = random.randint( 335 | cfg.getint('prefers', 'challenge_count_min'), 336 | cfg.getint('prefers', 'challenge_count_max')) 337 | 338 | self.challenge_delay_bot = cfg.getint('prefers', 'challenge_delay_min') 339 | self.challenge_delay_top = cfg.getint('prefers', 'challenge_delay_max') 340 | 341 | def _challenge_cycle(self, num): 342 | self.safe_click(rules['challenge_entry']) 343 | offset = 0 # 自动答错的偏移开关 344 | while num>-1: 345 | content = self.wait.until(EC.presence_of_element_located( 346 | (By.XPATH, rules['challenge_content']))).get_attribute("name") 347 | # content = self.find_element(rules["challenge_content"]).get_attribute("name") 348 | option_elements = self.wait.until(EC.presence_of_all_elements_located( 349 | (By.XPATH, rules['challenge_options']))) 350 | # option_elements = self.find_elements(rules['challenge_options']) 351 | options = [x.get_attribute("name") for x in option_elements] 352 | length_of_options = len(options) 353 | logger.info(f'<{num}> {content}') 354 | answer = self._verify(category='单选题', content=content, options=options) 355 | delay_time = random.randint(self.challenge_delay_bot, self.challenge_delay_top) 356 | if 0 == num: 357 | offset = random.randint(1, length_of_options) 358 | logger.info(f'已完成指定题量,设置提交选项偏移 -{offset}') 359 | logger.info(f'随机延时 {delay_time} 秒提交答案: {answer}-{offset}') 360 | else: 361 | logger.info(f'随机延时 {delay_time} 秒提交答案: {answer}') 362 | time.sleep(delay_time) 363 | # 利用python切片的特性,即使索引值为-offset,可以正确取值 364 | option_elements[ord(answer)-65 - offset].click() 365 | try: 366 | time.sleep(2) 367 | wrong = self.driver.find_element_by_xpath(rules['challenge_revival']) 368 | logger.debug(f'很遗憾本题回答错误') 369 | self._update_bank({ 370 | "category": "单选题", 371 | "content": content, 372 | "options": options, 373 | "answer": "", 374 | "excludes": answer, 375 | "notes": "" 376 | }) 377 | break 378 | except: 379 | logger.debug(f'恭喜本题回答正确') 380 | num -= 1 381 | self._update_bank({ 382 | "category": "单选题", 383 | "content": content, 384 | "options": options, 385 | "answer": answer, 386 | "excludes": "", 387 | "notes": "" 388 | }) 389 | # else: 390 | # logger.info(f'已完成指定题量, 本题故意答错后自动退出,否则延时30秒等待死亡') 391 | # content = self.wait.until(EC.presence_of_element_located( 392 | # (By.XPATH, rules['challenge_content']))).get_attribute("name") 393 | # # content = self.find_elements(rules['challenge_content']).get_attribute("name") 394 | # option_elements = self.wait.until(EC.presence_of_all_elements_located( 395 | # (By.XPATH, rules['challenge_options']))) 396 | # # option_elements = self.find_elements(rules['challenge_options']) 397 | # options = [x.get_attribute("name") for x in option_elements] 398 | # length_of_options = len(options) 399 | # logger.info(f'<{num}> {content}') 400 | # answer = self._verify(category='单选题', content=content, options=options) 401 | # final_choose = ((ord(answer)-65)+random.randint(1,length_of_options))%length_of_options 402 | # delay_time = random.randint(self.challenge_delay_bot, self.challenge_delay_top) 403 | # logger.info(f'随机延时 {delay_time} 秒提交答案: {chr(final_choose+65)}') 404 | # time.sleep(delay_time) 405 | # option_elements[final_choose].click() 406 | # time.sleep(2) 407 | # try: 408 | # wrong = self.driver.find_element_by_xpath(rules['challenge_revival']) 409 | # logger.debug(f'恭喜回答错误') 410 | # except: 411 | # logger.debug('抱歉回答正确') 412 | # time.sleep(30) 413 | self.safe_back('challenge -> share_page') 414 | # 更新后挑战答题需要增加一次返回 415 | self.safe_back('share_page -> quiz') 416 | return num 417 | 418 | 419 | def _challenge(self): 420 | logger.info(f'挑战答题 目标 {self.challenge_count} 题, Go!') 421 | while True: 422 | result = self._challenge_cycle(self.challenge_count) 423 | if 0 >= result: 424 | logger.info(f'已成功挑战 {self.challenge_count} 题,正在返回') 425 | break 426 | else: 427 | delay_time = random.randint(5,10) 428 | logger.info(f'本次挑战 {self.challenge_count - result} 题,{delay_time} 秒后再来一组') 429 | time.sleep(delay_time) 430 | continue 431 | 432 | 433 | 434 | def challenge(self): 435 | if 0 == self.challenge_count: 436 | logger.info(f'挑战答题积分已达成,无需重复挑战') 437 | return 438 | self.safe_click(rules['mine_entry']) 439 | self.safe_click(rules['quiz_entry']) 440 | time.sleep(3) 441 | self._challenge() 442 | self.safe_back('quiz -> mine') 443 | self.safe_back('mine -> home') 444 | 445 | # 每日答题模块 446 | # class Daily(App): 447 | def _daily_init(self): 448 | # super().__init__() 449 | self.g, self.t = 0, 6 450 | self.count_of_each_group = cfg.getint('prefers', 'daily_count_each_group') 451 | try: 452 | self.daily_count = cfg.getint('prefers', 'daily_count') 453 | self.daily_force = self.daily_count > 0 454 | except: 455 | self.g, self.t = self.score["每日答题"] 456 | self.daily_count = self.t - self.g 457 | self.daily_force = False 458 | 459 | self.daily_delay_bot = cfg.getint('prefers', 'daily_delay_min') 460 | self.daily_delay_top = cfg.getint('prefers', 'daily_delay_max') 461 | 462 | self.delay_group_bot = cfg.getint('prefers', 'daily_group_delay_min') 463 | self.delay_group_top = cfg.getint('prefers', 'daily_group_delay_max') 464 | 465 | def _submit(self, delay=None): 466 | if not delay: 467 | delay = random.randint(self.daily_delay_bot, self.daily_delay_top) 468 | logger.info(f'随机延时 {delay} 秒...') 469 | time.sleep(delay) 470 | self.safe_click(rules["daily_submit"]) 471 | time.sleep(random.randint(1,3)) 472 | 473 | def _view_tips(self): 474 | content = "" 475 | try: 476 | tips_open = self.driver.find_element_by_xpath(rules["daily_tips_open"]) 477 | tips_open.click() 478 | except NoSuchElementException as e: 479 | logger.debug("没有可点击的【查看提示】按钮") 480 | return "" 481 | time.sleep(2) 482 | try: 483 | tips = self.wait.until(EC.presence_of_element_located(( 484 | By.XPATH, rules["daily_tips"] 485 | ))) 486 | content = tips.get_attribute("name") 487 | logger.debug(f'提示 {content}') 488 | except NoSuchElementException as e: 489 | logger.error(f'无法查看提示内容') 490 | return "" 491 | time.sleep(2) 492 | try: 493 | tips_close = self.driver.find_element_by_xpath(rules["daily_tips_close"]) 494 | tips_close.click() 495 | 496 | except NoSuchElementException as e: 497 | logger.debug("没有可点击的【X】按钮") 498 | time.sleep(2) 499 | return content 500 | 501 | def _blank_answer_divide(self, ans:str, arr:list): 502 | accu_revr = [x for x in accumulate(arr)] 503 | print(accu_revr) 504 | temp = list(ans) 505 | for c in accu_revr[-2::-1]: 506 | temp.insert(c, " ") 507 | return "".join(temp) 508 | 509 | def _blank(self): 510 | contents = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules["daily_blank_content"]))) 511 | # contents = self.find_elements(rules["daily_blank_content"]) 512 | # content = " ".join([x.get_attribute("name") for x in contents]) 513 | logger.debug(f'len of blank contents is {len(contents)}') 514 | if 1 < len(contents): 515 | # 针对作妖的UI布局某一版 516 | content, spaces = "", [] 517 | for item in contents: 518 | content_text = item.get_attribute("name") 519 | if "" != content_text: 520 | content += content_text 521 | else: 522 | length_of_spaces = len(item.find_elements(By.CLASS_NAME, "android.view.View"))-1 523 | print(f'空格数 {length_of_spaces}') 524 | spaces.append(length_of_spaces) 525 | content += " " * (length_of_spaces) 526 | 527 | 528 | else: 529 | # 针对作妖的UI布局某一版 530 | contents = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules["daily_blank_container"]))) 531 | content, spaces, _spaces = "", [], 0 532 | for item in contents: 533 | content_text = item.get_attribute("name") 534 | if "" != content_text: 535 | content += content_text 536 | if _spaces: 537 | spaces.append(_spaces) 538 | _spaces = 0 539 | else: 540 | content += " " 541 | _spaces += 1 542 | else: # for...else... 543 | # 如果填空处在最后,需要加一个判断 544 | if _spaces: 545 | spaces.append(_spaces) 546 | logger.debug(f'[填空题] {content} [{" ".join([str(x) for x in spaces])}]') 547 | 548 | blank_edits = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules["daily_blank_edits"]))) 549 | # blank_edits = self.find_elements(rules["daily_blank_edits"]) 550 | length_of_edits = len(blank_edits) 551 | logger.info(f'填空题 {content}') 552 | answer = self._verify("填空题", content, "") # 553 | if not answer: 554 | words = (''.join(random.sample(string.ascii_letters + string.digits, 8)) for i in range(length_of_edits)) 555 | else: 556 | words = answer.split(" ") 557 | logger.debug(f'提交答案 {words}') 558 | for k,v in zip(blank_edits, words): 559 | k.send_keys(v) 560 | time.sleep(1) 561 | 562 | self._submit() 563 | try: 564 | wrong_or_not = self.driver.find_element_by_xpath(rules["daily_wrong_or_not"]) 565 | right_answer = self.driver.find_element_by_xpath(rules["daily_answer"]).get_attribute("name") 566 | answer = re.sub(r'正确答案: ', '', right_answer) 567 | logger.info(f"答案 {answer}") 568 | notes = self.driver.find_element_by_xpath(rules["daily_notes"]).get_attribute("name") 569 | logger.debug(f"解析 {notes}") 570 | self._submit(2) 571 | if 1 == length_of_edits: 572 | self._update_bank({ 573 | "category": "填空题", 574 | "content": content, 575 | "options": [""], 576 | "answer": answer, 577 | "excludes": "", 578 | "notes": notes 579 | }) 580 | else: 581 | logger.error("多位置的填空题待验证正确性") 582 | self._update_bank({ 583 | "category": "填空题", 584 | "content": content, 585 | "options": [""], 586 | "answer": self._blank_answer_divide(answer, spaces), 587 | "excludes": "", 588 | "notes": notes 589 | }) 590 | except: 591 | logger.debug("填空题回答正确") 592 | 593 | 594 | def _radio(self): 595 | content = self.wait.until(EC.presence_of_element_located((By.XPATH, rules["daily_content"]))).get_attribute("name") 596 | # content = self.find_element(rules["daily_content"]).get_attribute("name") 597 | option_elements = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules["daily_options"]))) 598 | # option_elements = self.driver.find_elements(rules["daily_options"]) 599 | options = [x.get_attribute("name") for x in option_elements] 600 | length_of_options = len(options) 601 | logger.info(f"单选题 {content}") 602 | logger.info(f"选项 {options}") 603 | answer = self._verify("单选题", content, options) 604 | choose_index = ord(answer) - 65 605 | logger.info(f"提交答案 {answer}") 606 | option_elements[choose_index].click() 607 | # 提交答案 608 | self._submit() 609 | try: 610 | wrong_or_not = self.driver.find_element_by_xpath(rules["daily_wrong_or_not"]) 611 | right_answer = self.driver.find_element_by_xpath(rules["daily_answer"]).get_attribute("name") 612 | right_answer = re.sub(r'正确答案: ', '', right_answer) 613 | logger.info(f"答案 {right_answer}") 614 | # notes = self.driver.find_element_by_xpath(rules["daily_notes"]).get_attribute("name") 615 | # logger.debug(f"解析 {notes}") 616 | self._submit(2) 617 | self._update_bank({ 618 | "category": "单选题", 619 | "content": content, 620 | "options": options, 621 | "answer": right_answer, 622 | "excludes": "", 623 | "notes": "" 624 | }) 625 | except: 626 | self._update_bank({ 627 | "category": "单选题", 628 | "content": content, 629 | "options": options, 630 | "answer": answer, 631 | "excludes": "", 632 | "notes": "" 633 | }) 634 | 635 | def _check(self): 636 | content = self.wait.until(EC.presence_of_element_located((By.XPATH, rules["daily_content"]))).get_attribute("name") 637 | # content = self.find_element(rules["daily_content"]).get_attribute("name") 638 | option_elements = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules["daily_options"]))) 639 | # option_elements = self.driver.find_elements(rules["daily_options"]) 640 | options = [x.get_attribute("name") for x in option_elements] 641 | length_of_options = len(options) 642 | logger.info(f"多选题 {content}\n{options}") 643 | answer = self._verify("多选题", content, options) 644 | logger.debug(f'提交答案 {answer}') 645 | for k, option in zip(list("ABCDEFG"), option_elements): 646 | if k in answer: 647 | option.click() 648 | time.sleep(1) 649 | else: 650 | continue 651 | # 提交答案 652 | self._submit() 653 | try: 654 | wrong_or_not = self.driver.find_element_by_xpath(rules["daily_wrong_or_not"]) 655 | right_answer = self.driver.find_element_by_xpath(rules["daily_answer"]).get_attribute("name") 656 | right_answer = re.sub(r'正确答案: ', '', right_answer) 657 | logger.info(f"答案 {right_answer}") 658 | # notes = self.driver.find_element_by_xpath(rules["daily_notes"]).get_attribute("name") 659 | # logger.debug(f"解析 {notes}") 660 | self._submit(2) 661 | self._update_bank({ 662 | "category": "多选题", 663 | "content": content, 664 | "options": options, 665 | "answer": right_answer, 666 | "excludes": "", 667 | "notes": "" 668 | }) 669 | except: 670 | self._update_bank({ 671 | "category": "多选题", 672 | "content": content, 673 | "options": options, 674 | "answer": answer, 675 | "excludes": "", 676 | "notes": "" 677 | }) 678 | 679 | def _dispatch(self, count_of_each_group): 680 | for i in range(count_of_each_group): 681 | logger.debug(f'每日答题 第 {count_of_each_group-1-i} 题') 682 | try: 683 | category = self.driver.find_element_by_xpath(rules["daily_category"]).get_attribute("name") 684 | except NoSuchElementException as e: 685 | logger.error(f'无法获取题目类型') 686 | raise e 687 | if "填空题" == category: 688 | self._blank() 689 | elif "单选题" == category: 690 | self._radio() 691 | elif "多选题" == category: 692 | self._check() 693 | else: 694 | logger.error(f"未知的题目类型: {category}") 695 | 696 | 697 | def _daily(self, num): 698 | self.safe_click(rules["daily_entry"]) 699 | while num: 700 | num -= 1 701 | logger.info(f'每日答题 第 {num}# 组') 702 | self._dispatch(self.count_of_each_group) 703 | if not self.daily_force: 704 | score = self.wait.until(EC.presence_of_element_located((By.XPATH, rules["daily_score"]))).get_attribute("name") 705 | # score = self.find_element(rules["daily_score"]).get_attribute("name") 706 | try: 707 | score = int(score) 708 | except: 709 | raise TypeError('integer required') 710 | self.g += score 711 | if self.g == self.t: 712 | logger.info(f"今日答题已完成,返回") 713 | break 714 | if num == 0: 715 | logger.debug(f'今日循环结束 <{self.g} / {self.t}>') 716 | break 717 | delay = random.randint(self.delay_group_bot, self.delay_group_top) 718 | logger.info(f'每日答题未完成 <{self.g} / {self.t}> {delay} 秒后再来一组') 719 | time.sleep(delay) 720 | self.safe_click(rules['daily_again']) 721 | continue 722 | else: 723 | logger.debug("应该不会执行本行代码") 724 | 725 | self.safe_back('daily -> quiz') 726 | try: 727 | back_confirm = self.driver.find_element_by_xpath(rules["daily_back_confirm"]) 728 | back_confirm.click() 729 | except: 730 | logger.debug(f"无需点击确认退出") 731 | 732 | def daily(self): 733 | if 0 == self.daily_count: 734 | logger.info(f'每日答题积分已达成,无需重复答题') 735 | return 736 | self.safe_click(rules['mine_entry']) 737 | self.safe_click(rules['quiz_entry']) 738 | time.sleep(3) 739 | self._daily(self.daily_count) 740 | self.safe_back('quiz -> mine') 741 | self.safe_back('mine -> home') 742 | 743 | # 新闻阅读模块 744 | # class Read(App): 745 | def _read_init(self): 746 | # super().__init__() 747 | self.read_time = 720 748 | self.volumn_title = cfg.get("prefers", "article_volumn_title") 749 | self.star_share_comments_count = cfg.getint("prefers", "star_share_comments_count") 750 | self.titles = list() 751 | try: 752 | self.read_count = cfg.getint("prefers", "article_count") 753 | self.read_delay = 30 754 | except: 755 | g, t = self.score["阅读文章"] 756 | if t == g: 757 | self.read_count = 0 758 | self.read_delay = random.randint(45, 60) 759 | else: 760 | self.read_count = random.randint( 761 | cfg.getint('prefers', 'article_count_min'), 762 | cfg.getint('prefers', 'article_count_max')) 763 | self.read_delay = self.read_time // self.read_count + 1 764 | 765 | def _star_once(self): 766 | if self.back_or_not("收藏"): 767 | return 768 | logger.debug(f'这篇文章真是妙笔生花呀!收藏啦!') 769 | self.safe_click(rules['article_stars']) 770 | # self.safe_click(rules['article_stars']) # 取消收藏 771 | 772 | def _comments_once(self, title="好好学习,天天强国"): 773 | # return # 拒绝留言 774 | if self.back_or_not("发表观点"): 775 | return 776 | logger.debug(f'哇塞,这么精彩的文章必须留个言再走!') 777 | self.safe_click(rules['article_comments']) 778 | edit_area = self.wait.until(EC.presence_of_element_located((By.XPATH, rules['article_comments_edit']))) 779 | # edit_area = self.find_element(rules['article_comments_edit']) 780 | edit_area.send_keys(title) 781 | self.safe_click(rules['article_comments_publish']) 782 | time.sleep(2) 783 | self.safe_click(rules['article_comments_list']) 784 | self.safe_click(rules['article_comments_delete']) 785 | self.safe_click(rules['article_comments_delete_confirm']) 786 | 787 | def _share_once(self): 788 | if self.back_or_not("分享"): 789 | return 790 | logger.debug(f'好东西必须和好基友分享,走起,转起!') 791 | self.safe_click(rules['article_share']) 792 | self.safe_click(rules['article_share_xuexi']) 793 | time.sleep(3) 794 | self.safe_back('share -> article') 795 | 796 | def _star_share_comments(self, title): 797 | logger.debug(f'哟哟,切克闹,收藏转发来一套') 798 | if random.random() < 0.33: 799 | self._comments_once(title) 800 | if random.random() < 0.5: 801 | self._star_once() 802 | self._share_once() 803 | else: 804 | self._share_once() 805 | self._star_once() 806 | else: 807 | if random.random() < 0.5: 808 | self._star_once() 809 | self._share_once() 810 | else: 811 | self._share_once() 812 | self._star_once() 813 | self._comments_once(title) 814 | 815 | def _read(self, num, ssc_count): 816 | logger.info(f'预计阅读新闻 {num} 则') 817 | while num > 0: # or ssc_count: 818 | try: 819 | articles = self.driver.find_elements_by_xpath(rules['article_list']) 820 | except: 821 | logger.debug(f'真是遗憾,一屏都没有可点击的新闻') 822 | articles = [] 823 | for article in articles: 824 | title = article.get_attribute("name") 825 | if title in self.titles: 826 | continue 827 | try: 828 | pic_num = article.parent.find_element_by_id("cn.xuexi.android:id/st_feeds_card_mask_pic_num") 829 | logger.debug(f'这绝对是摄影集,直接下一篇') 830 | continue 831 | except: 832 | logger.debug(f'这篇文章应该不是摄影集了吧') 833 | article.click() 834 | num -= 1 835 | logger.info(f'<{num}> 当前篇目 {title}') 836 | article_delay = random.randint(self.read_delay, self.read_delay+min(10, self.read_count)) 837 | logger.info(f'阅读时间估计 {article_delay} 秒...') 838 | while article_delay > 0: 839 | if article_delay < 20: 840 | delay = article_delay 841 | else: 842 | delay = random.randint(min(10, article_delay), min(20, article_delay)) 843 | logger.debug(f'延时 {delay} 秒...') 844 | time.sleep(delay) 845 | article_delay -= delay 846 | self.swipe_up() 847 | else: 848 | logger.debug(f'完成阅读 {title}') 849 | 850 | if ssc_count > 0: 851 | try: 852 | comment_area = self.driver.find_element_by_xpath(rules['article_comments']) 853 | self._star_share_comments(title) 854 | ssc_count -= 1 855 | except: 856 | logger.debug('这是一篇关闭评论的文章,收藏分享留言过程出现错误') 857 | 858 | self.titles.append(title) 859 | self.safe_back('article -> list') 860 | if 0 >= num: 861 | break 862 | else: 863 | self.swipe_up() 864 | 865 | def _kaleidoscope(self): 866 | ''' 本地频道积分 +1 ''' 867 | if self.back_or_not("本地频道"): 868 | return 869 | volumns = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules['article_volumn']))) 870 | volumns[3].click() 871 | time.sleep(10) 872 | # self.safe_click(rules['article_kaleidoscope']) 873 | target = None 874 | try: 875 | target = self.driver.find_element_by_xpath(rules['article_kaleidoscope']) 876 | except NoSuchElementException as e: 877 | logger.error(f'没有找到城市万花筒入口') 878 | 879 | if target: 880 | target.click() 881 | time.sleep(3) 882 | delay = random.randint(5, 15) 883 | logger.info(f"在本地学习平台驻足 {delay} 秒") 884 | time.sleep(delay) 885 | self.safe_back('学习平台 -> 文章列表') 886 | 887 | 888 | 889 | 890 | def read(self): 891 | if 0 == self.read_count: 892 | logger.info(f'新闻阅读已达成,无需重复阅读') 893 | return 894 | logger.debug(f'正在进行新闻学习...') 895 | self._kaleidoscope() 896 | vol_not_found = True 897 | while vol_not_found: 898 | volumns = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, rules['article_volumn']))) 899 | # volumns = self.find_elements(rules['article_volumn']) 900 | first_vol = volumns[1] 901 | for vol in volumns: 902 | title = vol.get_attribute("name") 903 | logger.debug(title) 904 | if self.volumn_title == title: 905 | vol.click() 906 | vol_not_found = False 907 | break 908 | else: 909 | logger.debug(f'未找到 {self.volumn_title},左滑一屏') 910 | self.driver.scroll(vol, first_vol, duration=500) 911 | 912 | self._read(self.read_count, self.star_share_comments_count) 913 | 914 | # 视听学习模块 915 | # class View(App): 916 | def _view_init(self): 917 | # super().__init__() 918 | self.has_bgm = cfg.get("prefers", "radio_switch") 919 | if "disable" == self.has_bgm: 920 | self.view_time = 1080 921 | else: 922 | self.view_time = 360 923 | self.radio_chanel = cfg.get("prefers", "radio_chanel") 924 | try: 925 | self.video_count = cfg.getint("prefers", "video_count") 926 | self.view_delay = 15 927 | except: 928 | g, t = self.score["视听学习"] 929 | if t == g: 930 | self.video_count = 0 931 | self.view_delay = random.randint(15, 30) 932 | else: 933 | self.video_count = random.randint( 934 | cfg.getint('prefers', 'video_count_min'), 935 | cfg.getint('prefers', 'video_count_max')) 936 | self.view_delay = self.view_time // self.video_count + 1 937 | 938 | def music(self): 939 | if "disable" == self.has_bgm: 940 | logger.debug(f'广播开关 关闭') 941 | elif "enable" == self.has_bgm: 942 | logger.debug(f'广播开关 开启') 943 | self._music() 944 | else: 945 | logger.debug(f'广播开关 默认') 946 | g, t = self.score["视听学习时长"] 947 | if g == t: 948 | logger.debug(f'视听学习时长积分已达成,无需重复收听') 949 | return 950 | else: 951 | self._music() 952 | 953 | def _music(self): 954 | logger.debug(f'正在打开《{self.radio_chanel}》...') 955 | self.safe_click('//*[@resource-id="cn.xuexi.android:id/home_bottom_tab_button_mine"]') 956 | self.safe_click('//*[@text="听新闻广播"]') 957 | self.safe_click(f'//*[@text="{self.radio_chanel}"]') 958 | self.safe_click(rules['home_entry']) 959 | 960 | def _watch(self, video_count=None): 961 | if not video_count: 962 | logger.info('视听学习积分已达成,无须重复视听') 963 | return 964 | logger.info("开始浏览百灵视频...") 965 | self.safe_click(rules['bailing_enter']) 966 | self.safe_click(rules['bailing_enter']) # 再点一次刷新短视频列表 967 | self.safe_click(rules['video_first']) 968 | logger.info(f'预计观看视频 {video_count} 则') 969 | while video_count: 970 | video_count -= 1 971 | video_delay = random.randint(self.view_delay, self.view_delay + min(10, self.video_count)) 972 | logger.info(f'正在观看视频 <{video_count}#> {video_delay} 秒进入下一则...') 973 | time.sleep(video_delay) 974 | self.swipe_up() 975 | else: 976 | logger.info(f'视听学习完毕,正在返回...') 977 | self.safe_back('video -> bailing') 978 | logger.debug(f'正在返回首页...') 979 | self.safe_click(rules['home_entry']) 980 | 981 | def watch(self): 982 | self._watch(self.video_count) 983 | 984 | # class Weekly(App): 985 | def _weekly_init(self): 986 | self.workdays = cfg.get("prefers", "workdays") 987 | 988 | def _weekly(self): 989 | self.safe_click(rules["weekly_entry"]) 990 | titles= self.wait.until( 991 | EC.presence_of_all_elements_located((By.XPATH, rules["weekly_titles"]))) 992 | 993 | states= self.wait.until( 994 | EC.presence_of_all_elements_located((By.XPATH, rules["weekly_states"]))) 995 | 996 | # first, last = None, None 997 | for title, state in zip(titles, states): 998 | # if not first and title.location_in_view["y"]>0: 999 | # first = title 1000 | if self.size["height"] - title.location_in_view["y"] < 10: 1001 | logger.debug(f'屏幕内没有未作答试卷') 1002 | break 1003 | logger.debug(f'{title.get_attribute("name")} {state.get_attribute("name")}') 1004 | if "未作答" == state.get_attribute("name"): 1005 | logger.info(f'{title.get_attribute("name")}, 开始!') 1006 | state.click() 1007 | time.sleep(random.randint(5,9)) 1008 | self._dispatch(5) # 这里直接采用每日答题 1009 | break 1010 | self.safe_back('weekly report -> weekly list') 1011 | self.safe_back('weekly list -> quiz') 1012 | 1013 | 1014 | 1015 | 1016 | def weekly(self): 1017 | ''' 每周答题 1018 | 复用每日答题的方法,无法保证每次得满分,如不能接受,请将配置workdays设为0 1019 | ''' 1020 | day_of_week = datetime.now().isoweekday() 1021 | if str(day_of_week) not in self.workdays: 1022 | logger.debug(f'今日不宜每周答题 {day_of_week} / {self.workdays}') 1023 | return 1024 | if self.back_or_not("每周答题"): 1025 | return 1026 | 1027 | self.safe_click(rules['mine_entry']) 1028 | self.safe_click(rules['quiz_entry']) 1029 | time.sleep(3) 1030 | self._weekly() 1031 | self.safe_back('quiz -> mine') 1032 | self.safe_back('mine -> home') 1033 | 1034 | -------------------------------------------------------------------------------- /xuexi/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | ''' 4 | @project: AutoXue 5 | @file: __main__.py 6 | @author: kessil 7 | @contact: https://github.com/kessil/AutoXue/ 8 | @time: 2019-10-26(星期六) 10:22 9 | @Copyright © 2019. All rights reserved. 10 | ''' 11 | from argparse import ArgumentParser 12 | import time 13 | from . import App 14 | from .unit import logger 15 | from .secureRandom import SecureRandom as random 16 | import sys 17 | 18 | parse = ArgumentParser(description="Accept username and password if necessary!") 19 | 20 | parse.add_argument("-u", "--username", metavar="username", type=str, default='', help='User Name') 21 | parse.add_argument("-p", "--password", metavar="password", type=str, default='', help='Pass Word') 22 | args = parse.parse_args() 23 | app = App(args.username, args.password) 24 | 25 | def shuffle(funcs): 26 | random.shuffle(funcs) 27 | for func in funcs: 28 | func() 29 | time.sleep(5) 30 | 31 | def start(): 32 | if random.random() > 0.5: 33 | logger.debug(f'视听学习优先') 34 | app.watch() 35 | app.music() 36 | shuffle([app.read, app.daily, app.challenge, app.weekly]) 37 | else: 38 | logger.debug(f'视听学习置后') 39 | app.music() 40 | shuffle([app.read, app.daily, app.challenge, app.weekly]) 41 | app.watch() 42 | app.logout_or_not() 43 | 44 | sys.exit(0) 45 | 46 | def test(): 47 | app.weekly() 48 | logger.info(f'测试完毕') 49 | 50 | if __name__ == "__main__": 51 | start() 52 | # test() -------------------------------------------------------------------------------- /xuexi/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | ''' 4 | @project: AutoXue 5 | @file: model.py 6 | @author: kessil 7 | @contact: https://github.com/kessil/AutoXue/ 8 | @time: 2019-10-27(星期天) 10:43 9 | @Copyright © 2019. All rights reserved. 10 | ''' 11 | import json 12 | import requests 13 | from .unit import cfg, logger 14 | 15 | class Structure: 16 | _fields = [] 17 | 18 | def __init__(self, *args, **kwargs): 19 | if len(args) > len(self._fields): 20 | raise TypeError('Expected {} arguments'.format(len(self._fields))) 21 | 22 | # Set all of the positional arguments 23 | for name, value in zip(self._fields, args): 24 | setattr(self, name, value) 25 | 26 | # Set the remaining keyword arguments 27 | for name in self._fields[len(args):]: 28 | setattr(self, name, kwargs.pop(name)) 29 | 30 | # Check for any remaining unknown arguments 31 | if kwargs: 32 | raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs))) 33 | 34 | class Bank(Structure): 35 | _fields = ['id', 'category', 'content', 'options', 'answer', 'excludes', 'description'] 36 | 37 | def __repr__(self): 38 | return f'{self.content}' 39 | 40 | def to_json(self): 41 | pass 42 | 43 | @classmethod 44 | def from_json(self, data): 45 | pass 46 | 47 | class BankQuery: 48 | def __init__(self): 49 | self.url = cfg.get('api', 'url') 50 | self.headers = { 51 | 'Content-Type': 'application/json;charset=UTF-8', 52 | 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" 53 | } 54 | 55 | def post(self, item, url=None): 56 | if not url: 57 | url = self.url 58 | if "" == item["content"]: 59 | logger.debug(f'content is empty') 60 | return False 61 | logger.debug(f'POST {item["content"]} {item["options"]} {item["answer"]} {item["excludes"]}...') 62 | 63 | try: 64 | res = requests.post(url=url, headers=self.headers, json=item) 65 | if 201 == res.status_code: 66 | return True 67 | except: 68 | return False 69 | 70 | def put(self, item, url=None): 71 | if not url: 72 | url = self.url 73 | if "" == item["content"]: 74 | logger.debug(f'content is empty') 75 | return False 76 | logger.debug(f'PUT {item["content"]} {item["options"]} {item["answer"]} {item["excludes"]}...') 77 | try: 78 | res = requests.put(url=url, headers=self.headers, json=item) 79 | if 201 == res.status_code: 80 | logger.info('添加新记录') 81 | return True 82 | elif 200 == res.status_code: 83 | logger.info('更新记录') 84 | return True 85 | else: 86 | logger.debug("PUT do nothing") 87 | return False 88 | except: 89 | return False 90 | 91 | def get(self, item, url=None): 92 | if not url: 93 | url = self.url 94 | if "" == item["content"]: 95 | logger.debug(f'content is empty') 96 | return None 97 | logger.debug(f'GET {item["content"]}...') 98 | try: 99 | res = requests.post(url=url, headers=self.headers, json=item) 100 | if 200 == res.status_code: 101 | logger.debug(f'GET item success') 102 | # logger.debug(res.text) 103 | # logger.debug(json.loads(res.text)) 104 | return json.loads(res.text) 105 | else: 106 | logger.debug(f'GET item failure') 107 | return None 108 | except: 109 | logger.debug('request faild') 110 | return None 111 | -------------------------------------------------------------------------------- /xuexi/secureRandom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | ''' 4 | @project: AutoXue 5 | @file: secureRandom.py 6 | @author: wongsyrone 7 | @Copyright © 2019. All rights reserved. 8 | ''' 9 | 10 | import secrets 11 | 12 | # 从 secrets 模块获取 SystemRandom 实例 13 | _inst = secrets.SystemRandom() 14 | 15 | class SecureRandom: 16 | seed = _inst.seed 17 | random = _inst.random 18 | uniform = _inst.uniform 19 | triangular = _inst.triangular 20 | randint = _inst.randint 21 | choice = _inst.choice 22 | randrange = _inst.randrange 23 | sample = _inst.sample 24 | shuffle = _inst.shuffle 25 | normalvariate = _inst.normalvariate 26 | lognormvariate = _inst.lognormvariate 27 | expovariate = _inst.expovariate 28 | vonmisesvariate = _inst.vonmisesvariate 29 | gammavariate = _inst.gammavariate 30 | gauss = _inst.gauss 31 | betavariate = _inst.betavariate 32 | paretovariate = _inst.paretovariate 33 | weibullvariate = _inst.weibullvariate 34 | getstate = _inst.getstate 35 | setstate = _inst.setstate 36 | getrandbits = _inst.getrandbits 37 | 38 | def notice(): 39 | raise NotImplementedError('The library does not support execution. Please import to another py file') 40 | 41 | if __name__ == '__main__': 42 | notice() -------------------------------------------------------------------------------- /xuexi/unit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | ''' 4 | @project: AutoXue 5 | @file: unit.py 6 | @author: kessil 7 | @contact: https://github.com/kessil/AutoXue/ 8 | @time: 2019-10-25(星期五) 21:44 9 | @Copyright © 2019. All rights reserved. 10 | ''' 11 | import time 12 | import logging 13 | from pathlib import Path 14 | from configparser import ConfigParser 15 | 16 | class Timer: 17 | ''' 简易计时器 18 | 使用形如: 19 | with Timer() as t: 20 | pass... 21 | print(t.elapsed) 22 | ''' 23 | def __init__(self, func=time.perf_counter): 24 | self.elapsed = 0.0 25 | self._func = func 26 | self._start = None 27 | 28 | def start(self): 29 | if self._start is not None: 30 | raise RuntimeError('Already started') 31 | self._start = self._func() 32 | 33 | def stop(self): 34 | if self._start is None: 35 | raise RuntimeError('Not started') 36 | end = self._func() 37 | self.elapsed += end - self._start 38 | self._start = None 39 | 40 | def reset(self): 41 | self.elapsed = 0.0 42 | 43 | @property 44 | def running(self): 45 | return self._start is not None 46 | 47 | def __enter__(self): 48 | self.start() 49 | return self 50 | 51 | def __exit__(self, *args): 52 | self.stop() 53 | 54 | 55 | cfg = ConfigParser() 56 | cfg.read('./config/default.ini', encoding='utf-8') 57 | cfg.read('./config/custom.ini', encoding='utf-8') 58 | 59 | 60 | def create_logger(loggername:str='logger', levelname:str='DEBUG', console_levelname='INFO'): 61 | levels = { 62 | 'DEBUG': logging.DEBUG, 63 | 'INFO': logging.INFO, 64 | 'WARNING': logging.WARNING, 65 | 'ERROR': logging.ERROR, 66 | 'CRITICAL': logging.CRITICAL 67 | } 68 | 69 | logger = logging.getLogger(loggername) 70 | logger.setLevel(levels[levelname]) 71 | 72 | logger_format = logging.Formatter("[%(asctime)s][%(levelname)s][%(filename)s][%(funcName)s][%(lineno)03s]: %(message)s") 73 | console_format = logging.Formatter("[%(levelname)s] %(message)s") 74 | 75 | handler_console = logging.StreamHandler() 76 | handler_console.setFormatter(console_format) 77 | handler_console.setLevel(levels[console_levelname]) 78 | 79 | # path = Path(__file__).parent/'logs' # 日志目录 80 | path = Path(cfg.get('prefers', 'logging_path')) 81 | path.mkdir(parents=True, exist_ok=True) 82 | today = time.strftime("%Y-%m-%d") # 日志文件名 83 | common_filename = path / f'{today}.log' 84 | handler_common = logging.FileHandler(common_filename , mode='a+', encoding='utf-8') 85 | handler_common.setLevel(levels[levelname]) 86 | handler_common.setFormatter(logger_format) 87 | 88 | logger.addHandler(handler_console) 89 | logger.addHandler(handler_common) 90 | 91 | return logger 92 | 93 | 94 | configs = dict(cfg._sections) 95 | caps = dict(configs['capability']) 96 | for key, value in caps.items(): 97 | if "true" == value.lower(): 98 | caps[key] = True 99 | elif 'false' == value.lower(): 100 | caps[key] = False 101 | else: 102 | pass 103 | 104 | # prefers = dict(configs['prefers']) 105 | # for key, value in prefers.items(): 106 | # if "true" == value.lower(): 107 | # prefers[key] = True 108 | # elif 'false' == value.lower(): 109 | # prefers[key] = False 110 | # else: 111 | # pass 112 | 113 | rules = dict(configs['rules']) 114 | logger = create_logger('xuexi', console_levelname=cfg.get("prefers", "console_levelname")) 115 | 116 | 117 | if __name__ == "__main__": 118 | for k,v in caps.items(): 119 | print(k,v) --------------------------------------------------------------------------------