├── crawl ├── __init__.py ├── project_info.py ├── exam_base.py ├── utils.py └── exam_zujuan.py ├── ui ├── __init__.py ├── untitled.ui ├── mttkinter.py └── main_ui.py ├── README.md ├── .gitignore ├── config ├── cookies.ck └── project.json ├── qrcode ├── default.png └── wx_qrcode.png ├── testfun.py └── requirement.txt /crawl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exam_paper -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /config/cookies.ck: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaibush/tk-exam-paper/HEAD/config/cookies.ck -------------------------------------------------------------------------------- /qrcode/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaibush/tk-exam-paper/HEAD/qrcode/default.png -------------------------------------------------------------------------------- /qrcode/wx_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaibush/tk-exam-paper/HEAD/qrcode/wx_qrcode.png -------------------------------------------------------------------------------- /testfun.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | import os,time,random 3 | 4 | def run(name): 5 | print("子进程启动") 6 | 7 | 8 | if __name__ == "__main__": 9 | print("父进程启动") 10 | pp = Pool(2) 11 | for i in range(5): 12 | pp.apply_async(run,args = (i,)) 13 | pp.close() 14 | pp.join() 15 | print("父进程结束") -------------------------------------------------------------------------------- /config/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": { 3 | "login": "https://passport.zujuan.com/login", 4 | "qrcode": "https://passport.zujuan.com/connect/weixin-qrcode?iframe=1&width=220&height=220", 5 | "wxlogin": "https://passport.zujuan.com/connect/wxlogin", 6 | "issubscribe": "https://passport.zujuan.com/connect/issubscribe", 7 | "user": "https://www.zujuan.com/u/index", 8 | "zujuan": "https://www.zujuan.com/u/zujuan", 9 | "jump_url": "https://www.zujuan.com" 10 | }, 11 | "timeout": 600 12 | } 13 | -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.7.1 2 | bs4==0.0.1 3 | certifi==2019.3.9 4 | chardet==3.0.4 5 | Click==7.0 6 | dominate==2.3.5 7 | Flask==1.0.2 8 | Flask-Bootstrap==3.3.7.1 9 | Flask-Moment==0.7.0 10 | Flask-Script==2.0.6 11 | Flask-SQLAlchemy==2.3.2 12 | Flask-WTF==0.14.2 13 | idna==2.8 14 | itsdangerous==1.1.0 15 | Jinja2==2.10 16 | lxml==4.3.2 17 | MarkupSafe==1.1.1 18 | mysqlclient==1.4.2.post1 19 | Pillow==6.2.1 20 | PyMySQL==0.9.3 21 | PyQt5==5.13.0 22 | PyQt5-sip==12.7.0 23 | pyqt5-tools==5.13.0.1.5 24 | python-dotenv==0.10.3 25 | requests==2.21.0 26 | soupsieve==1.8 27 | SQLAlchemy==1.3.1 28 | tkimg==0.0.1 29 | ttkwidgets==0.10.0 30 | urllib3==1.24.1 31 | visitor==0.1.3 32 | Werkzeug==0.14.1 33 | WTForms==2.2.1 34 | -------------------------------------------------------------------------------- /crawl/project_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | class Project: 6 | root = os.path.dirname(os.path.dirname(__file__)) 7 | project_config = os.path.join(root, "config", "project.json") 8 | cookies = os.path.join(root, "config", "cookies.ck") 9 | try: 10 | with open(project_config) as fp: 11 | project_info = json.load(fp) 12 | urls = project_info["urls"] 13 | check_timeout = project_info["timeout"] 14 | except FileNotFoundError: 15 | urls = { 16 | "login": "https://passport.zujuan.com/login", 17 | "qrcode": "https://passport.zujuan.com/connect/weixin-qrcode?iframe=1&width=220&height=220", 18 | "wxlogin": "https://passport.zujuan.com/connect/wxlogin", 19 | "issubscribe": "https://passport.zujuan.com/connect/issubscribe", 20 | "user": "https://www.zujuan.com/u/index", 21 | "zujuan": "https://www.zujuan.com/u/zujuan" 22 | } 23 | check_timeout = 300 24 | qrcode = os.path.join(root, "qrcode", "wx_qrcode.png") 25 | default_png = os.path.join(root, "qrcode", "default.png") 26 | scan_flag = os.path.join(root, "flags", "scan.flag") 27 | 28 | -------------------------------------------------------------------------------- /crawl/exam_base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import bs4 4 | import requests 5 | 6 | from crawl.project_info import Project 7 | from crawl.utils import logger 8 | 9 | 10 | class URLs: 11 | login = Project.urls["login"] 12 | qrcode = Project.urls["qrcode"] 13 | wxlogin = Project.urls["wxlogin"] 14 | issubscribe = Project.urls["issubscribe"] 15 | user = Project.urls["user"] 16 | zujuan = Project.urls["zujuan"] 17 | jump_url = Project.urls["jump_url"] 18 | 19 | 20 | class ExamPaperBase: 21 | sess = requests.Session() 22 | headers = { 23 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/" 24 | "537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" 25 | } 26 | 27 | get = sess.get 28 | 29 | @logger 30 | def check_login_succ(self): 31 | resp = self.sess.get(URLs.user) 32 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 33 | login_fail = soup.find("div", attrs={"class": "mistack-content"}) 34 | if login_fail and login_fail.text.find("未登录") != -1: 35 | logging.info("未登录,请重新扫码登录") 36 | return False 37 | login_succ = soup.find("legend", attrs={"class": "form-title"}) 38 | if login_succ and login_succ.text.find("第三方账号绑定") != -1: 39 | logging.info("已检测,登录成功") 40 | return True 41 | return False 42 | 43 | @staticmethod 44 | def clear_text(text): 45 | return text.replace("\n", "") 46 | 47 | 48 | class LogoutError(Exception): 49 | pass 50 | -------------------------------------------------------------------------------- /crawl/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import multiprocessing as mp 3 | import pickle 4 | from functools import partial 5 | from functools import wraps 6 | from queue import Queue 7 | 8 | import requests 9 | from requests.cookies import RequestsCookieJar 10 | 11 | from crawl.project_info import Project 12 | 13 | 14 | def save_cookies(cookies): 15 | with open(Project.cookies, 'wb') as f: 16 | pickle.dump(cookies.get_dict(), f) 17 | 18 | 19 | def load_cookies(): 20 | try: 21 | with open(Project.cookies, 'rb') as f: 22 | cookies = requests.utils.cookiejar_from_dict( 23 | pickle.load(f) 24 | ) 25 | except: 26 | cookies = RequestsCookieJar() 27 | return cookies 28 | 29 | 30 | def logger(func): 31 | @wraps(func) 32 | def wrapper(*args, **kwargs): 33 | logging.info("-->running (%s)", func.__name__) 34 | try: 35 | return func(*args, **kwargs) 36 | except Exception as e: 37 | logging.error("running (%s) error", func.__name__) 38 | raise 39 | 40 | return wrapper 41 | 42 | 43 | class WorkProcess: 44 | workers = [] 45 | 46 | def put(self, callback, *args, **kwargs): 47 | self.workers.append(partial(callback, *args, **kwargs)) 48 | 49 | def clear(self): 50 | self.workers.clear() 51 | 52 | def stop_old_work(self): 53 | for p in self.workers: 54 | if p.is_alive(): 55 | logging.info("stop old worker: %s", p.pid) 56 | p.terminate() 57 | self.clear() 58 | 59 | def run(self): 60 | if self.workers: 61 | func = self.workers.pop() 62 | self.stop_old_work() 63 | 64 | p = mp.Process(target=func) 65 | p.start() 66 | logging.info("child pid: %s", p.pid) 67 | self.workers.append(p) 68 | logging.info("p.is_alive: %s", p.is_alive()) 69 | # p.join() 70 | 71 | 72 | logging.basicConfig(level=logging.INFO) 73 | 74 | 75 | if __name__ == "__main__": 76 | mp.freeze_support() 77 | result = mp.Manager().dict() 78 | w = WorkProcess() 79 | -------------------------------------------------------------------------------- /ui/untitled.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 522 10 | 410 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 260 22 | 201 23 | 31 24 | 25 | 26 | 27 | 28 | 29 | 30 | 刷新二维码 31 | 32 | 33 | 34 | 35 | 36 | 37 | 一键登录 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 11 47 | 11 48 | 84 49 | 16 50 | 51 | 52 | 53 | 扫描二维码登录 54 | 55 | 56 | 57 | 58 | true 59 | 60 | 61 | 62 | 11 63 | 29 64 | 220 65 | 220 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 0 74 | 0 75 | 522 76 | 23 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /crawl/exam_zujuan.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import random 5 | import threading 6 | import time 7 | import multiprocessing as mp 8 | from multiprocessing.pool import Pool 9 | from urllib import parse 10 | from collections import OrderedDict, namedtuple 11 | import bs4 12 | import requests 13 | 14 | from crawl.exam_base import ExamPaperBase, URLs, LogoutError 15 | from crawl.utils import save_cookies, load_cookies, logger 16 | from crawl.project_info import Project 17 | 18 | 19 | class ScanLogin(ExamPaperBase): 20 | def __init__(self): 21 | try: 22 | self.sess.get(URLs.login) 23 | except Exception: 24 | logging.exception("error") 25 | 26 | @staticmethod 27 | def remove_scan_flag(): 28 | if os.path.exists(Project.scan_flag): 29 | os.remove(Project.scan_flag) 30 | 31 | @staticmethod 32 | def generate_scan_flag(): 33 | with open(Project.scan_flag, "wb") as fp: 34 | fp.write(b"1") 35 | 36 | @staticmethod 37 | def get_scan_flag(): 38 | return os.path.exists(Project.scan_flag) 39 | 40 | @logger 41 | def get_qrcode_url(self): 42 | resp = self.sess.get(URLs.qrcode) 43 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 44 | wrp_code = soup.find("div", attrs={"class": "wrp_code"}) 45 | qrcode_url = wrp_code.contents[0].attrs["src"] 46 | return qrcode_url 47 | 48 | @staticmethod 49 | @logger 50 | def get_ticket(qrcode_url): 51 | query = dict( 52 | parse.parse_qsl( 53 | parse.urlsplit(qrcode_url).query 54 | ) 55 | ) 56 | return query["ticket"] 57 | 58 | @logger 59 | def save_qrcode_pic(self, qrcode_url): 60 | resp = self.sess.get(qrcode_url) 61 | with open(Project.qrcode, "wb") as fp: 62 | fp.write(resp.content) 63 | 64 | @logger 65 | def check_scan(self, ticket): 66 | logging.info("等待扫码: %s", ticket) 67 | self.remove_scan_flag() 68 | query = { 69 | "ticket": ticket, 70 | "jump_url": URLs.jump_url, 71 | "r": random.random() 72 | } 73 | check_login_url = URLs.issubscribe + "?" + parse.urlencode(query) 74 | check_cnt = 0 75 | scan_status = False 76 | while check_cnt < Project.check_timeout: 77 | logging.info("扫码...[%s]", threading.current_thread()) 78 | is_login_resp = self.sess.get(check_login_url) 79 | resp = json.loads(is_login_resp.text) 80 | # code = 0 等待扫码 81 | if resp["code"] == 1: 82 | scan_status = True 83 | self.generate_scan_flag() 84 | break 85 | time.sleep(1) 86 | check_cnt += 1 87 | return scan_status 88 | 89 | @logger 90 | def login_by_scan(self, ticket): 91 | wx_query = { 92 | "ticket": ticket, 93 | "jump_url": URLs.jump_url 94 | } 95 | resp = self.sess.get( 96 | URLs.wxlogin + '?' + parse.urlencode(wx_query), 97 | verify=False 98 | ) 99 | if resp.status_code == 200: 100 | logging.info("登录成功,保存cookies") 101 | save_cookies(self.sess.cookies) 102 | else: 103 | raise requests.HTTPError("wechat login failed") 104 | 105 | 106 | class CookiesLogin(ExamPaperBase): 107 | @logger 108 | def login_by_cookies(self): 109 | cookies = load_cookies() 110 | self.sess.cookies = cookies 111 | if not self.check_login_succ(): 112 | raise LogoutError("pls scan qrcode to login again") 113 | 114 | 115 | class ZuJuanView(ExamPaperBase): 116 | def get_username(self): 117 | resp = self.get(URLs.user) 118 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 119 | real_name = soup.find("div", attrs={"id": "J_realname"}) 120 | return real_name.text.replace("\n", "") if real_name else "未知用户名" 121 | 122 | def get_zujuan_view(self): 123 | ret = OrderedDict() 124 | resp = self.get(URLs.zujuan) 125 | soup = bs4.BeautifulSoup(resp.text, "html.parser") 126 | zujuan = soup.find("ul", attrs={"class": "f-cb"}) 127 | if zujuan: 128 | for li in zujuan.find_all("p", attrs={"class": "test-txt-p1"}): 129 | href = li.find("a") 130 | Record = namedtuple('Record', ['text', 'href']) 131 | info = Record(href.text, href["href"]) 132 | ret[href["pid"]] = info 133 | return ret 134 | 135 | 136 | class ZuJuanTasks(ExamPaperBase): 137 | 138 | def zujuan_task(self, task): 139 | print(task) 140 | time.sleep(10) 141 | print("end===========") 142 | return task 143 | 144 | def task_run(self, pool, args): 145 | for task_args in args: 146 | pool.apply_async(self.zujuan_task, (task_args,)) 147 | pool.close() 148 | logging.info("等待所有task执行完成...") 149 | pool.join() 150 | logging.info("所有task执行完成...") 151 | 152 | @staticmethod 153 | def task_shutdown(pool): 154 | if isinstance(pool, Pool): 155 | logging.info("终止所有任务") 156 | pool.terminate() 157 | 158 | 159 | if __name__ == "__main__": 160 | mp.freeze_support() 161 | wx_scan = ScanLogin() 162 | wx_scan.remove_scan_flag() 163 | 164 | pool = mp.Pool(processes=2) 165 | zujuan = ZuJuanTasks() 166 | zujuan.task_run( 167 | pool, 168 | [ 169 | ["组卷名称", "href====="], 170 | ["组卷名称2", "href=====2"], 171 | ] 172 | 173 | ) 174 | zujuan.task_shutdown(pool) 175 | # qrcode = wx_scan.get_qrcode_url() 176 | # wx_scan.save_qrcode_pic(qrcode) 177 | # ticket = wx_scan.get_ticket(qrcode) 178 | # print(ticket) 179 | # wx_scan.check_scan(ticket) 180 | # wx_scan.login_by_scan(ticket) 181 | # wx_scan.check_login_succ() 182 | # 183 | # cookies_login = CookiesLogin() 184 | # cookies_login.login_by_cookies() 185 | # 186 | # print(ZuJuanView().get_username()) 187 | # print(ZuJuanView().get_zujuan_view()) 188 | -------------------------------------------------------------------------------- /ui/mttkinter.py: -------------------------------------------------------------------------------- 1 | '''Thread-safe version of tkinter. 2 | Copyright (c) 2014, Andrew Barnert 3 | Based on mtTkinter (for Python 2.x), copyright (c) 2009, Allen B. Taylor 4 | This module is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Lesser Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Lesser Public License for more details. 12 | You should have received a copy of the GNU Lesser Public License 13 | along with this program. If not, see . 14 | Usage: 15 | import mttkinter as tkinter 16 | # Use "t." as usual. 17 | or 18 | from mtt import * 19 | # Use tkinter module definitions as usual. 20 | This module modifies the original tkinter module in memory, making all 21 | functionality thread-safe. It does this by wrapping the Tk class' tk 22 | instance with an object that diverts calls through an event queue when 23 | the call is issued from a thread other than the thread in which the Tk 24 | instance was created. The events are processed in the creation thread 25 | via an 'after' event. 26 | The modified Tk class accepts two additional keyword parameters on its 27 | __init__ method: 28 | mtDebug: 29 | 0 = No debug output (default) 30 | 1 = Minimal debug output 31 | ... 32 | 9 = Full debug output 33 | mtCheckPeriod: 34 | Amount of time in milliseconds (default 100) between checks for 35 | out-of-thread events when things are otherwise idle. Decreasing 36 | this value can improve GUI responsiveness, but at the expense of 37 | consuming more CPU cycles. 38 | Note that, because it modifies the original tkinter module (in memory), 39 | other modules that use tkinter (e.g., Pmw) reap the benefits automagically 40 | as long as mttkinter is imported at some point before extra threads are 41 | created. 42 | Author: Allen B. Taylor, a.b.taylor@gmail.com 43 | ''' 44 | 45 | from tkinter import * 46 | import threading 47 | import queue 48 | 49 | 50 | class _Tk(object): 51 | """ 52 | Wrapper for underlying attribute tk of class Tk. 53 | """ 54 | 55 | def __init__(self, tk, mtDebug=0, mtCheckPeriod=10): 56 | self._tk = tk 57 | 58 | # Create the incoming event queue. 59 | self._eventQueue = queue.Queue(1) 60 | 61 | # Identify the thread from which this object is being created so we can 62 | # tell later whether an event is coming from another thread. 63 | self._creationThread = threading.currentThread() 64 | 65 | # Store remaining values. 66 | self._debug = mtDebug 67 | self._checkPeriod = mtCheckPeriod 68 | 69 | def __getattr__(self, name): 70 | # Divert attribute accesses to a wrapper around the underlying tk 71 | # object. 72 | return _TkAttr(self, getattr(self._tk, name)) 73 | 74 | 75 | class _TkAttr(object): 76 | """ 77 | Thread-safe callable attribute wrapper. 78 | """ 79 | 80 | def __init__(self, tk, attr): 81 | self._tk = tk 82 | self._attr = attr 83 | 84 | def __call__(self, *args, **kwargs): 85 | """ 86 | Thread-safe method invocation. 87 | Diverts out-of-thread calls through the event queue. 88 | Forwards all other method calls to the underlying tk object directly. 89 | """ 90 | 91 | # Check if we're in the creation thread. 92 | if threading.currentThread() == self._tk._creationThread: 93 | # We're in the creation thread; just call the event directly. 94 | if self._tk._debug >= 8 or \ 95 | self._tk._debug >= 3 and self._attr.__name__ == 'call' and \ 96 | len(args) >= 1 and args[0] == 'after': 97 | print('Calling event directly: {} {} {}'.format( 98 | self._attr.__name__, args, kwargs)) 99 | return self._attr(*args, **kwargs) 100 | else: 101 | # We're in a different thread than the creation thread; enqueue 102 | # the event, and then wait for the response. 103 | responseQueue = queue.Queue(1) 104 | if self._tk._debug >= 1: 105 | print('Marshalling event: {} {} {}'.format( 106 | self._attr.__name__, args, kwargs)) 107 | self._tk._eventQueue.put((self._attr, args, kwargs, responseQueue)) 108 | isException, response = responseQueue.get() 109 | 110 | # Handle the response, whether it's a normal return value or 111 | # an exception. 112 | if isException: 113 | exType, exValue, exTb = response 114 | raise exType(exValue).with_traceback(exTb) 115 | else: 116 | return response 117 | 118 | 119 | # Define a hook for class Tk's __init__ method. 120 | def _Tk__init__(self, *args, **kwargs): 121 | # We support some new keyword arguments that the original __init__ method 122 | # doesn't expect, so separate those out before doing anything else. 123 | new_kwnames = ('mtCheckPeriod', 'mtDebug') 124 | new_kwargs = {} 125 | for name, value in kwargs.items(): 126 | if name in new_kwnames: 127 | new_kwargs[name] = value 128 | kwargs.clear() 129 | 130 | # Call the original __init__ method, creating the internal tk member. 131 | self.__original__init__mttkinter(*args, **kwargs) 132 | 133 | # Replace the internal tk member with a wrapper that handles calls from 134 | # other threads. 135 | self.tk = _Tk(self.tk, **new_kwargs) 136 | 137 | # Set up the first event to check for out-of-thread events. 138 | self.after_idle(_CheckEvents, self) 139 | 140 | 141 | # Replace Tk's original __init__ with the hook. 142 | Tk.__original__init__mttkinter = Tk.__init__ 143 | Tk.__init__ = _Tk__init__ 144 | 145 | 146 | def _CheckEvents(tk): 147 | "Event checker event." 148 | 149 | used = False 150 | try: 151 | # Process all enqueued events, then exit. 152 | while True: 153 | try: 154 | # Get an event request from the queue. 155 | method, args, kwargs, responseQueue = \ 156 | tk.tk._eventQueue.get_nowait() 157 | except: 158 | if tk.tk._debug >= 2: 159 | print('Event queue empty') 160 | # No more events to process. 161 | break 162 | else: 163 | # Call the event with the given arguments, and then return 164 | # the result back to the caller via the response queue. 165 | used = True 166 | if tk.tk._debug >= 2: 167 | print('Calling event from main thread: {} {} {}' 168 | .format(method.__name__, args, kwargs)) 169 | try: 170 | responseQueue.put((False, method(*args, **kwargs))) 171 | except SystemExit as ex: 172 | raise 173 | except Exception as ex: 174 | # Calling the event caused an exception; return the 175 | # exception back to the caller so that it can be raised 176 | # in the caller's thread. 177 | from sys import exc_info 178 | exType, exValue, exTb = exc_info() 179 | responseQueue.put((True, (exType, exValue, exTb))) 180 | finally: 181 | # Schedule to check again. If we just processed an event, check 182 | # immediately; if we didn't, check later. 183 | if used: 184 | tk.after_idle(_CheckEvents, tk) 185 | else: 186 | tk.after(tk.tk._checkPeriod, _CheckEvents, tk) 187 | 188 | 189 | # Test thread entry point. 190 | def _testThread(root): 191 | text = "This is Tcl/Tk version %s" % TclVersion 192 | if TclVersion >= 8.1: 193 | try: 194 | text = text + "\nThis should be a cedilla: \347" 195 | except NameError: 196 | pass # no unicode support 197 | try: 198 | if root.globalgetvar('tcl_platform(threaded)'): 199 | text = text + "\nTcl is built with thread support" 200 | else: 201 | raise RuntimeError 202 | except: 203 | text = text + "\nTcl is NOT built with thread support" 204 | text = text + "\nmttkinter works with or without Tcl thread support" 205 | label = Label(root, text=text) 206 | label.pack() 207 | button = Button(root, text="Click me!", 208 | command=lambda root=root: root.button.configure( 209 | text="[%s]" % root.button['text'])) 210 | button.pack() 211 | root.button = button 212 | quit = Button(root, text="QUIT", command=root.destroy) 213 | quit.pack() 214 | # The following three commands are needed so the window pops 215 | # up on top on Windows... 216 | root.iconify() 217 | root.update() 218 | root.deiconify() 219 | # Simulate button presses... 220 | button.invoke() 221 | root.after(1000, _pressOk, root, button) 222 | 223 | 224 | # Test button continuous press event. 225 | def _pressOk(root, button): 226 | button.invoke() 227 | try: 228 | root.after(1000, _pressOk, root, button) 229 | except: 230 | pass # Likely we're exiting 231 | 232 | 233 | # Test. Mostly borrowed from the tkinter module, but the important bits moved 234 | # into a separate thread. 235 | if __name__ == '__main__': 236 | import threading 237 | 238 | root = Tk(mtDebug=1) 239 | thread = threading.Thread(target=_testThread, args=(root,)) 240 | thread.start() 241 | root.mainloop() 242 | -------------------------------------------------------------------------------- /ui/main_ui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import threading 4 | from multiprocessing import freeze_support, Pool 5 | from tkinter import messagebox 6 | # from tkinter import * 7 | import requests 8 | 9 | from ui.mttkinter import * 10 | from tkinter.ttk import * 11 | 12 | from PIL import Image, ImageTk 13 | from ttkwidgets import * 14 | 15 | from crawl.exam_zujuan import ScanLogin, CookiesLogin, ZuJuanView, ZuJuanTasks 16 | from crawl.project_info import Project 17 | from crawl.utils import WorkProcess 18 | 19 | 20 | class UIWidget(dict): 21 | def __getattribute__(self, item): 22 | if item in self: 23 | return self[item] 24 | return None 25 | 26 | 27 | UI = UIWidget() 28 | worker = WorkProcess() 29 | 30 | 31 | class MyDebugWindow(DebugWindow): 32 | 33 | # def write(self, line): 34 | # self.text.insert(END, line) 35 | # self.text.see(END) 36 | 37 | def quit(self): 38 | super(MyDebugWindow, self).quit() 39 | log.removeHandler(UI.handler) 40 | del UI["debug"], UI["handler"] 41 | 42 | 43 | class MainUI: 44 | def __init__(self, root): 45 | self.root = root 46 | self.build_debug_ui() 47 | self.build_ui() 48 | 49 | def build_debug_ui(self): 50 | UI["debug"] = MyDebugWindow(self.root) 51 | UI.debug.geometry("400x400+0+0") 52 | stdout_handler = logging.StreamHandler(UI.debug) 53 | log.addHandler(stdout_handler) 54 | UI["handler"] = stdout_handler 55 | 56 | def build_ui(self): 57 | self.build_left_ui() 58 | self.build_right_ui() 59 | 60 | def resize_img(self, path): 61 | if hasattr(self.root, "img"): 62 | del self.root.img 63 | pil_image = Image.open(path).resize((220, 220)) 64 | tk_image = ImageTk.PhotoImage(image=pil_image) 65 | self.root.img = tk_image 66 | return tk_image 67 | 68 | def update_qrcode(self, path): 69 | UI.qrcode_display.delete(ALL) 70 | tk_image = self.resize_img(path) 71 | image_id = UI.qrcode_display.create_image(3, 3, anchor='nw', 72 | image=tk_image) 73 | 74 | def build_left_ui(self): 75 | login_frame = Labelframe(self.root, text="扫描二维码登录", labelanchor="n") 76 | login_frame.pack(side="left", anchor="nw") 77 | 78 | def make_canvas(): 79 | canvas_frame = Frame(login_frame) 80 | canvas_frame.pack(side="top", anchor="nw") 81 | qrcode_display = Canvas(canvas_frame, bg="grey", width=222, 82 | height=222) 83 | qrcode_display.pack(side="top", anchor="nw") 84 | UI["qrcode_display"] = qrcode_display 85 | self.update_qrcode(Project.qrcode) 86 | 87 | def make_buttons(): 88 | bt_frame = Frame(login_frame) 89 | bt_frame.pack(side="top", anchor="nw", fill="both") 90 | 91 | Button(bt_frame, text="一键登录", command=self.login_by_cookies).grid( 92 | row=0, column=0, ipadx=12.5 93 | ) 94 | Button(bt_frame, text="扫码登录", command=self.login_by_scan).grid( 95 | row=0, column=1, ipadx=12.5 96 | ) 97 | Button(bt_frame, text="生成", command=self.run_tasks).grid( 98 | row=1, column=0, ipadx=12.5 99 | ) 100 | Button(bt_frame, text="终止任务", command=self.stop_tasks).grid( 101 | row=1, column=1, ipadx=12.5 102 | ) 103 | 104 | make_canvas() 105 | make_buttons() 106 | 107 | def build_right_ui(self): 108 | user_frame = LabelFrame(self.root, text="用户信息", labelanchor="n") 109 | user_frame.pack(side="left", anchor="nw") 110 | 111 | def make_user_info(): 112 | UI["user"] = Label(user_frame, text="昵称: 未登录") 113 | UI.user.pack(side="top", anchor="nw") 114 | 115 | def make_paper_records(): 116 | Label(user_frame, text="组卷记录").pack(side="top", anchor="nw") 117 | box = ScrolledListbox(user_frame, width=80, height=12) 118 | UI["box"] = box.listbox 119 | box.pack(side="top", anchor="nw") 120 | 121 | def make_tasks(): 122 | Label(user_frame, text="添加任务").pack(side="top", anchor="nw") 123 | box = ScrolledListbox(user_frame, width=80, height=6) 124 | UI["task"] = box.listbox 125 | box.pack(side="top", anchor="nw") 126 | 127 | make_user_info() 128 | make_paper_records() 129 | make_tasks() 130 | 131 | def show_msg(self): 132 | top = Toplevel(self.root) 133 | Label(top, text="等待task完成").pack() 134 | 135 | def login_by_cookies(self): 136 | pass 137 | 138 | def login_by_scan(self): 139 | pass 140 | 141 | def run_tasks(self): 142 | pass 143 | 144 | def stop_tasks(self): 145 | pass 146 | 147 | 148 | class LoginUI(MainUI): 149 | def __init__(self, root): 150 | self.pool = None 151 | self.login_scan_ids = [] 152 | self.check_scan_ids = [] 153 | super(LoginUI, self).__init__(root) 154 | self.build_pop_menu() 155 | self.init_login() 156 | UI.box.bind('', self.pop_menu_event) 157 | 158 | def build_pop_menu(self): 159 | self.menubar = Menu(self.root, tearoff=False) # 制作一个菜单实例 160 | for menu in [ 161 | ("添加任务", self.make_task), 162 | ]: 163 | self.menubar.add_command(label=menu[0], command=menu[1]) 164 | 165 | def pop_menu_event(self, event): 166 | self.menubar.post(event.x_root, event.y_root) 167 | 168 | def init_login(self): 169 | self.wx_scan = ScanLogin() 170 | self.cookies_login = CookiesLogin() 171 | self.zujuan = ZuJuanView() 172 | self.get = self.wx_scan.get 173 | 174 | def check_scan(self, ticket): 175 | status = 0 176 | logging.info("等待扫码...") 177 | if worker.workers: 178 | p = worker.workers[-1] 179 | if not p.is_alive() and self.wx_scan.get_scan_flag(): 180 | status = 1 181 | logging.info("扫码完成,正在跳转...") 182 | threading.Thread( 183 | target=self.update_by_thread, args=(ticket,) 184 | ).start() 185 | 186 | if not status: 187 | check_scan_id = self.root.after(1000, lambda: self.check_scan(ticket)) 188 | self.check_scan_ids.append(check_scan_id) 189 | 190 | def login_by_scan(self): 191 | if not UI.debug: 192 | self.build_debug_ui() 193 | self.clear_view() 194 | qrcode = self.wx_scan.get_qrcode_url() 195 | self.wx_scan.save_qrcode_pic(qrcode) 196 | self.update_qrcode(Project.qrcode) 197 | ticket = self.wx_scan.get_ticket(qrcode) 198 | worker.put(self.wx_scan.check_scan, ticket) 199 | worker.run() 200 | self.cancel_before_scan() 201 | login_scan_id = self.root.after( 202 | 1000, lambda: self.check_scan(ticket) 203 | ) 204 | self.login_scan_ids.append(login_scan_id) 205 | 206 | def login_by_cookies(self): 207 | if not UI.debug: 208 | self.build_debug_ui() 209 | self.clear_view() 210 | self.cancel_before_scan() 211 | self.cookies_login.login_by_cookies() 212 | self.update_user() 213 | self.update_exam_view() 214 | 215 | def cancel_before_scan(self): 216 | logging.info("清除登录历史") 217 | for _id in self.login_scan_ids: 218 | self.root.after_cancel(_id) 219 | for _id in self.check_scan_ids: 220 | self.root.after_cancel(_id) 221 | self.login_scan_ids.clear() 222 | self.check_scan_ids.clear() 223 | worker.stop_old_work() 224 | 225 | def update_user(self): 226 | username = self.zujuan.get_username() 227 | UI.user.config(text=username) 228 | 229 | def clear_view(self): 230 | logging.info("清除视图") 231 | UI.box.delete(0, END) 232 | UI.task.delete(0, END) 233 | 234 | def update_exam_view(self): 235 | if UI.records: 236 | del UI["records"] 237 | records = self.zujuan.get_zujuan_view() 238 | UI["records"] = records 239 | for record_pid in records: 240 | UI.box.insert(END, records[record_pid].text + "-%s" % record_pid) 241 | # self.wx_scan.login_by_scan(ticket) 242 | # self.wx_scan.check_login_succ() 243 | 244 | def update_by_thread(self, ticket): 245 | self.wx_scan.login_by_scan(ticket) 246 | logging.info("更新界面记录") 247 | self.update_user() 248 | self.update_exam_view() 249 | 250 | def parse_record(self, record): 251 | text, pid = record.split('-') 252 | return text, pid 253 | 254 | def is_add_task(self, add): 255 | tasks = UI.task.get(0, END) 256 | for task in tasks: 257 | if task == add: 258 | return True 259 | return False 260 | 261 | def make_task(self): 262 | sel = UI.box.curselection() 263 | if sel: 264 | sel_text = UI.box.get(sel) 265 | if self.is_add_task(sel_text): 266 | return messagebox.showinfo("提示", "任务已存在") 267 | UI.task.insert(END, sel_text) 268 | 269 | def all_tasks_pending(self): 270 | tasks = UI.task.get(0, END) 271 | href_pool = [] 272 | if UI.records: 273 | for task in tasks: 274 | text, pid = self.parse_record(task) 275 | href_pool.append( 276 | [task, UI.records[pid].href] 277 | ) 278 | return href_pool 279 | 280 | def run_tasks(self): 281 | threading.Thread(target=self._run_tasks).start() 282 | 283 | def _run_tasks(self): 284 | logging.info("执行组卷任务") 285 | yes = messagebox.askyesno("提示", "组卷tasks启动?") 286 | if yes: 287 | self.show_msg() 288 | self.pool = Pool() 289 | ZuJuanTasks().task_run( 290 | self.pool, self.all_tasks_pending() 291 | ) 292 | messagebox.showinfo("提示", "组卷tasks已结束") 293 | 294 | def stop_tasks(self): 295 | ZuJuanTasks().task_shutdown(self.pool) 296 | 297 | 298 | def network_heart(root): 299 | root.withdraw() 300 | try: 301 | requests.get("http://www.baidu.com") 302 | except: 303 | messagebox.showerror("网络错误", "无法访问互联网,退出程序", parent=root) 304 | os._exit(-1) 305 | else: 306 | root.deiconify() 307 | 308 | 309 | if __name__ == "__main__": 310 | freeze_support() 311 | tk = Tk() 312 | tk.title("zujuan") 313 | log = logging.getLogger() 314 | 315 | threading.Thread(target=network_heart, args=(tk,)).start() 316 | gui_thead = threading.Timer(0.5, LoginUI, args=(tk,)) 317 | gui_thead.start() 318 | 319 | tk.mainloop() 320 | --------------------------------------------------------------------------------