-
Notifications
You must be signed in to change notification settings - Fork 87
/
qq_login.py
2153 lines (1804 loc) · 98.6 KB
/
qq_login.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
# Generated by Selenium IDE
import datetime
import json
import logging
import os
import shutil
import threading
import time
from collections import Counter
from urllib.parse import quote_plus, unquote_plus
from selenium import webdriver
from selenium.common.exceptions import (
InvalidArgumentException,
NoSuchWindowException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait
from alist import download_from_alist
from compress import decompress_dir_with_bandizip
from config import AccountConfig, CommonConfig
from config import config as get_config
from config import load_config
from config_cloud import config_cloud
from dao import GuanJiaUserInfo
from data_struct import ConfigInterface
from db import CaptchaDB, LoginRetryDB
from download import download_latest_github_release
from exceptions_def import (
GithubActionLoginException,
RequireVerifyMessageButInHeadlessMode,
SameAccountTryLoginAtMultipleThreadsException,
)
from first_run import is_first_run_in
from log import color, logger
from urls import get_act_url
from util import (
MiB,
async_message_box,
count_down,
download_chrome_driver,
get_file_or_directory_size,
get_screen_size,
is_run_in_github_action,
is_windows,
pause_and_exit,
range_from_one,
show_head_line,
truncate,
try_except,
use_by_myself,
)
from version import now_version, ver_time
if is_windows():
import win32con
class LoginResult(ConfigInterface):
def __init__(
self,
uin="",
skey="",
openid="",
p_skey="",
vuserid="",
qc_openid="",
qc_k="",
apps_p_skey="",
xinyue_openid="",
xinyue_access_token="",
qc_access_token="",
qc_nickname="",
iwan_openid="",
iwan_access_token="",
common_openid="",
common_access_token="",
):
# 使用炎炎夏日活动界面得到
self.uin = uin
self.skey = skey
# 登录QQ空间得到
self.p_skey = p_skey
# 使用心悦活动界面得到
self.openid = openid
# 使用腾讯视频相关页面得到
self.vuserid = vuserid
# 登录电脑管家页面得到
self.qc_openid = qc_openid
self.qc_k = qc_k
self.qc_access_token = qc_access_token
self.qc_nickname = qc_nickname
# 分享用p_skey
self.apps_p_skey = apps_p_skey
# 心悦相关信息
self.xinyue_openid = xinyue_openid
self.xinyue_access_token = xinyue_access_token
# 爱玩相关
self.iwan_openid = iwan_openid
self.iwan_access_token = iwan_access_token
# 后续需要登录特定网页获取的openid和access-token都放在这里
self.common_openid = common_openid
self.common_access_token = common_access_token
self.guanjia_skey_version = 0
class QQLogin:
login_type_auto_login = "账密自动登录"
login_type_qr_login = "扫码登录"
login_mode_normal = "normal"
login_mode_xinyue = "xinyue"
login_mode_qzone = "qzone"
login_mode_guanjia = "guanjia"
login_mode_wegame = "wegame"
login_mode_club_vip = "club_vip"
login_mode_iwan = "iwan"
login_mode_supercore = "supercore"
login_mode_djc = "djc"
login_mode_to_description = {
login_mode_normal: "普通",
login_mode_xinyue: "心悦",
login_mode_qzone: "QQ空间",
login_mode_guanjia: "电脑管家",
login_mode_wegame: "Wegame",
login_mode_club_vip: "club.vip",
login_mode_iwan: "爱玩",
login_mode_supercore: "超享玩",
login_mode_djc: "道聚城",
}
bandizip_executable_path = os.path.realpath("./utils/bandizip_portable/bz.exe")
# re: chrome版本一键升级流程
# 0. 使用 _update_chrome.py 脚本,按照提示操作即可获取最新稳定版本chrome的便携版、driver、安装包等
# .
# note: chrome版本手动升级流程
# 1. 下载新版本chrome driver => chromedriver_{ver}.exe
# 1.1 https://sites.google.com/chromium.org/driver/downloads
# 2. 制作新版本便携版压缩包 => chrome_portable_{ver}.7z
# 2.1 获取安装包
# 2.1.1 找到系统安装的chrome的安装包
# 2.1.1.1 %PROGRAMFILES%\Google\Chrome\Application
# 2.1.1.2 %PROGRAMFILES(X86)%\Google\Chrome\Application
# 2.1.1.3 在这个目录中找到 90.0.4430.93\Installer\chrome.7z
# 2.1.1.4 90.0.4430.93可替换为最新版本的版本号
# 2.1.2 也可以从网上下载离线版安装包
# 2.1.2.1 下载地址
# 2.1.2.1.1 https://www.iplaysoft.com/tools/chrome/
# 2.1.2.2 下载内容形如90.0.4430.93_chrome_installer.exe,使用bandizip打开然后解压得到chrome.7z,即可进行下一步
# 2.2 将chrome.7z解压然后重新压缩,得到 chrome_portable_90.7z
# 2.2.1 确保chrome_portable_90.7z压缩包的首层目录形如(89.0.4389.72、chrome.exe、chrome_proxy.exe)
# 3. 替换chromedriver_{ver}.exe和chrome_portable_{ver}.7z到小助手 utils 目录下
# 3.1 修改版本号为 {ver} 后,测试下登录流程
# todo:
# 4. 下载新版本安装包 => Chrome_92.0.4515.131_普通安装包_非便携版.exe
# 4.1 https://www.iplaysoft.com/tools/chrome/
# 5. 上传以下内容到网盘的工具目录
# 5.1 chromedriver_{ver}.exe
# 5.1 chrome_portable_{ver}.7z
# 5.1 Chrome_92.0.4515.131_普通安装包_非便携版.exe
# undone:
# 6. 更新linux版的路径
# 6.1 _ubuntu_download_chrome_and_driver.sh
# 6.2 _centos_download_and_install_chrome_and_driver.sh
# re:
# 7. 入库以下文件
# qq_login.py
# chromedriver_{ver}.exe
# chrome_portable_{ver}.7z
# _centos_download_and_install_chrome_and_driver.sh
# _ubuntu_download_chrome_and_driver.sh
chrome_major_version = 107
chrome_driver_version = "107.0.5304.62"
default_window_width = 390
default_window_height = 360
mobile_emulation_qq = {
"deviceMetrics": {"width": 420, "height": 780},
"userAgent": "Mozilla/5.0 (Linux; U; Android 5.0.2; zh-cn; X900 Build/CBXCNOP5500912251S) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.4 TBS/025489 Mobile Safari/533.1 V1_AND_SQ_6.0.0_300_YYB_D QQ/6.0.0.2605 NetType/WIFI WebP/0.3.0 Pixel/1440",
}
def __init__(self, common_config, window_index=1):
self.cfg: CommonConfig = common_config
self.driver: WebDriver | None = None
self.window_title = ""
self.time_start_login = datetime.datetime.now()
self.screen_width, self.screen_height = get_screen_size()
col_size, row_size = (
round(self.screen_width / self.default_window_width),
round(self.screen_height / self.default_window_height),
)
self.window_position_x = self.default_window_width * ((window_index - 1) % col_size)
self.window_position_y = self.default_window_height * (((window_index - 1) // col_size) % row_size)
def prepare_chrome(self, ctx: str, login_type: str, login_url: str):
logger.info(
color("fg_bold_cyan")
+ f"{self.name} 正在初始化chrome driver(版本为{self.get_chrome_major_version()}),用以进行【{ctx}】相关操作。"
f"浏览器坐标:({self.window_position_x}, {self.window_position_y})@{self.default_window_width}*{self.default_window_height}({self.screen_width}*{self.screen_height})"
)
self.login_url = login_url
logger.info(color("bold_green") + f"{self.name} {ctx} 登录链接为: {login_url}")
if is_windows():
self.prepare_chrome_windows(login_type, login_url)
else:
self.prepare_chrome_linux(login_type, login_url)
self.cookies = self.driver.get_cookies()
def prepare_chrome_windows(self, login_type: str, login_url: str):
inited = False
try:
if not self.cfg.force_use_portable_chrome:
# 如果未强制使用便携版chrome,则首先尝试使用系统安装的chrome
options = self.new_options()
self.append_common_options(options, login_type, login_url)
self.driver = webdriver.Chrome(
service=Service(executable_path=self.chrome_driver_executable_path()),
options=options,
)
logger.info(color("bold_yellow") + f"{self.name} 使用自带chrome")
inited = True
except Exception:
pass
if not inited:
# 如果找不到,则尝试使用打包的便携版chrome
zip_name = os.path.basename(self.chrome_binary_7z())
# 判定本地是否有便携版压缩包,若无则说明自动下载失败,提示去网盘手动下载
if not os.path.isfile(self.chrome_binary_7z()):
zip_name = zip_name
installer_name = self.chrome_installer_name()
version = self.get_chrome_major_version()
chrome_root_directory = self.chrome_root_directory()
msg = (
"================ 这一段是问题描述 ================\n"
f"当前电脑未发现{version}版本的chrome浏览器,且{chrome_root_directory}目录下无便携版chrome浏览器的压缩包({zip_name})\n"
"\n"
"================ 这一段是解决方法 ================\n"
f"如果不想影响系统浏览器,请在稍后打开的网盘页面中下载[{zip_name}],并放到{chrome_root_directory}目录里(注意:是把这个压缩包原原本本地放到这个目录里,而不是解压后再放过来!!!),然后重新打开程序~\n"
"\n"
f"如果愿意装一个浏览器,请在稍后打开的网盘页面中下载{installer_name},下载完成后双击安装即可\n"
"\n"
"(一定要看清版本,如果发现网盘里的便携版和安装版版本都比提示里的高(比如这里提示87,网盘里显示89),建议直接下个最新的小助手压缩包,解压后把配置文件复制过去~)\n"
"\n"
"================ 这一段是补充说明 ================\n"
"1. 如果之前版本已经下载过这个文件,可以直接去之前版本复制过来~不需要再下载一次~\n"
"2. 如果之前一直都运行的好好的,今天突然不行了,可能是以下原因\n"
"2.1 系统安装的chrome自动升级到新版本了,当前小助手使用的驱动不支持该版本。解决办法:下载当前版本小助手对应版本的便携版chrome\n"
"2.2 新版小助手升级了驱动,当前系统安装的chrome或便携版chrome的版本太低了。解决办法:升级新版本chrome或下载新版本的便携版chrome\n"
"\n"
"------- 已经说得如此明白,如果还有人进群问,将直接踢出群聊 -------\n"
"------- 已经说得如此明白,如果还有人进群问,将直接踢出群聊 -------\n"
"------- 已经说得如此明白,如果还有人进群问,将直接踢出群聊 -------\n"
)
async_message_box(
msg,
f"你没有{self.get_chrome_major_version()}版本的chrome浏览器,需要安装完整版或下载便携版",
icon=win32con.MB_ICONERROR,
open_url="http://101.43.54.94:5244/%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%81chrome%E6%B5%8F%E8%A7%88%E5%99%A8%E3%80%81autojs%E3%80%81HttpCanary%E7%AD%89%E5%B0%8F%E5%B7%A5%E5%85%B7",
)
pause_and_exit(-1)
# 然后使用本地的chrome来初始化driver对象
options = self.new_options()
options.binary_location = self.chrome_binary_location()
options.add_argument("--no-sandbox")
options.add_argument("--no-default-browser-check")
options.add_argument("--no-first-run")
self.append_common_options(options, login_type, login_url)
self.driver = webdriver.Chrome(
service=Service(executable_path=self.chrome_driver_executable_path()),
options=options,
)
logger.info(color("bold_yellow") + f"{self.name} 使用便携版chrome")
def prepare_chrome_linux(self, login_type: str, login_url: str):
# linux下只尝试使用系统安装的chrome
options = self.new_options()
options.binary_location = self.chrome_binary_location_linux()
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-default-browser-check")
options.add_argument("--no-first-run")
self.append_common_options(options, login_type, login_url)
self.driver = webdriver.Chrome(
service=Service(executable_path=self.chrome_driver_executable_path_linux()),
options=options,
)
logger.info(color("bold_yellow") + f"{self.name} Linux环境下使用自带chrome")
def new_options(self) -> Options:
options = Options()
caps = DesiredCapabilities().CHROME
# caps["pageLoadStrategy"] = "normal" # Waits for full page load
caps["pageLoadStrategy"] = "none" # Do not wait for full page load
for k, v in caps.items():
options.set_capability(k, v)
return options
def append_common_options(self, options: Options, login_type: str, login_url: str):
options.add_argument(f"window-position={self.window_position_x},{self.window_position_y}")
options.add_argument(f"window-size={self.default_window_width},{self.default_window_height}")
options.add_argument(f"app={login_url}")
# 设置静音
options.add_argument("--mute-audio")
exclude_switches = []
if not self.cfg._debug_show_chrome_logs:
exclude_switches.append("enable-logging")
selenium_logger = logging.getLogger("selenium.webdriver.remote.remote_connection")
selenium_logger.setLevel(logging.WARNING)
# 使用Selenium期间将urllib的日志关闭
urllib_logger = logging.getLogger("urllib3.connectionpool")
urllib_logger.setLevel(logging.WARNING)
if self.cfg.run_in_headless_mode:
if login_type == self.login_type_auto_login:
logger.warning(f"{self.name} 已配置在自动登录模式时使用headless模式运行chrome")
options.add_argument("--headless")
else:
logger.warning(f"{self.name} 扫码登录模式不使用headless模式")
# 特殊处理linux环境
if not is_windows():
options.add_argument("--headless")
logger.warning(f"{self.name} 在linux环境下强制使用headless模式运行chrome")
# 隐藏提示:Chrome 正收到自动测试软件的控制。
exclude_switches.append("enable-automation")
if len(exclude_switches) != 0:
options.add_experimental_option("excludeSwitches", exclude_switches)
# 隐藏保存密码的提示窗
options.add_experimental_option(
"prefs", {"credentials_enable_service": False, "profile": {"password_manager_enabled": False}}
)
if self.login_mode == self.login_mode_supercore:
logger.info("当前是超享玩登录,将设置设备信息为手机qq,否则不能正常登录")
options.add_experimental_option("mobileEmulation", self.mobile_emulation_qq)
def destroy_chrome(self):
logger.info(f"{self.name} 释放chrome实例")
if self.driver is not None:
# 最小化网页
if is_windows():
self.driver.minimize_window()
threading.Thread(target=self.driver.quit, daemon=True).start()
# 使用Selenium结束将日志级别改回去
urllib_logger = logging.getLogger("urllib3.connectionpool")
urllib_logger.setLevel(logger.level)
@try_except(extra_msg="自动下载缺失的dlc失败,请根据上面打印的提示日志去操作~")
def check_and_download_chrome_ahead(self):
"""
尝试预先下载解压缩chrome的driver和便携版
主要用于处理多进程模式下,可能多个进程同时尝试该操作导致的问题
:return:
"""
logger.info("检查chrome相关内容是否ok")
if is_windows():
self.check_and_download_chrome_ahead_windows()
else:
self.check_and_download_chrome_ahead_linux()
def check_and_download_chrome_ahead_windows(self):
logger.info(
color("bold_yellow")
+ "如果自动下载失败,可能是网络问题,请根据提示下载的内容,自行去备用网盘下载该内容到utils目录下 https://docs.qq.com/doc/DYmdpaUthQnp4Rnpy"
)
chrome_driver_exe_name = os.path.basename(self.chrome_driver_executable_path())
zip_name = os.path.basename(self.chrome_binary_7z())
chrome_root_directory = self.chrome_root_directory()
logger.info("检查driver是否存在")
if not self.is_valid_chrome_file(self.chrome_driver_executable_path()):
logger.info(color("bold_yellow") + f"未在小助手utils目录里发现 {chrome_driver_exe_name} ,将尝试从网盘下载")
logger.info(
color("bold_cyan")
+ f"如果速度实在太慢,可以去QQ群文件里面下载 {chrome_driver_exe_name},然后原样放到小助手的 utils 目录中,再重新启动即可"
)
self.download_chrome_driver(chrome_driver_exe_name)
options = self.new_options()
options.add_argument("--headless")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
if not self.cfg.force_use_portable_chrome:
try:
logger.info(
color("bold_green")
+ "检查系统自带的chrome是否可用,如果一直卡在这里,请试试打开【配置工具/公共配置/登录/强制使用便携版chrome】开关后,再次运行~。如果安装了【360浏览器/qq浏览器】,可以先试试把chrome修改为默认浏览器,或者卸载掉他们,然后重启电脑后再运行小助手试试。"
)
# note: 调用chrome_driver创建新session时,chrome_driver会尝试添加en-us的键盘布局-。-这个目前没法修,因为必须依赖这个
self.driver = webdriver.Chrome(
service=Service(executable_path=self.chrome_driver_executable_path()), options=options
)
self.driver.quit()
return
except Exception:
logger.info("走到这里说明系统自带的chrome不可用")
else:
logger.info("当前配置为强制使用便携版chrome")
# 尝试从网盘下载合适版本的便携版chrome
if not self.is_valid_chrome_file(self.chrome_binary_7z()):
logger.info(
color("bold_yellow")
+ f"未在小助手utils目录里发现 便携版chrome 的压缩包,尝试自动从网盘下载 {zip_name},需要下载大概80MB的压缩包,请耐心等候"
)
logger.info(
color("bold_cyan")
+ f"如果速度实在太慢,可以去QQ群文件里面下载 {zip_name},然后原样放到小助手的 utils 目录中,再重新启动即可"
)
self.download_chrome_file(zip_name)
# 尝试解压
if not os.path.isdir(self.chrome_binary_directory()):
logger.info("自动解压便携版chrome到当前目录")
decompress_dir_with_bandizip(self.chrome_binary_7z(), dst_parent_folder=chrome_root_directory)
logger.info("检查便携版chrome是否有效")
try:
options.binary_location = self.chrome_binary_location()
# you may need some other options
options.add_argument("--no-sandbox")
options.add_argument("--no-default-browser-check")
options.add_argument("--no-first-run")
self.driver = webdriver.Chrome(
service=Service(executable_path=self.chrome_driver_executable_path()), options=options
)
self.driver.quit()
return
except Exception:
pass
# 走到这里,大概率是多线程并行下载导致文件出错了,尝试重新下载
logger.info(color("bold_yellow") + "似乎chrome相关文件损坏了,尝试重新下载并解压")
logger.info(
color("bold_cyan")
+ f"如果速度实在太慢,可以去QQ群文件里面下载 {zip_name} 和 {chrome_driver_exe_name},然后原样放到小助手的 utils 目录中,再重新启动即可"
)
self.download_chrome_driver(chrome_driver_exe_name)
self.download_chrome_file(zip_name)
shutil.rmtree(self.chrome_binary_directory(), ignore_errors=True)
decompress_dir_with_bandizip(self.chrome_binary_7z(), dst_parent_folder=chrome_root_directory)
def check_and_download_chrome_ahead_linux(self):
ok = True
if not os.path.exists(self.chrome_binary_location_linux()):
ok = False
logger.info(
color("bold_red")
+ (
f"未在发现chrome,请按照下面这个推文的步骤去下载安装最新稳定版本chrome到该位置\n"
f"预期位置: {self.chrome_binary_location_linux()}\n"
f"安装教程: https://blog.csdn.net/weixin_42649856/article/details/103275162 \n"
)
)
if not os.path.exists(self.chrome_driver_executable_path_linux()):
ok = False
logger.info(
color("bold_red")
+ (
f"未在发现chromedriver,请按照下面这个推文的步骤去下载安装最新稳定版本chromedriver到该位置(需要确保与安装的chrome版本匹配)\n"
f"预期位置: {self.chrome_driver_executable_path_linux()}\n"
f"安装教程: https://blog.csdn.net/weixin_42649856/article/details/103275162 \n"
)
)
if not ok:
logger.info(
color("bold_yellow")
+ (
"当前运行在非windows的环境,检测到chrome和对应driver未全部安装,请按照上述提示完成安装后重新运行~\n"
"或者根据你的系统直接使用或参考以下脚本之一来完成一键下载安装\n"
"1. _ubuntu_download_and_install_chrome_and_driver.sh \n"
"2. _centos_download_and_install_chrome_and_driver.sh \n"
)
)
pause_and_exit(-1)
def download_chrome_file(self, filename: str) -> str:
def download_by_github() -> str:
logger.warning("尝试通过github下载")
return download_latest_github_release(
"utils",
filename,
"fzls",
"djc_helper_chrome",
)
def download_by_alist() -> str:
logger.warning("尝试通过alist下载")
return download_from_alist(self.get_path_in_netdisk(filename), self.chrome_root_directory())
download_functions = [
download_by_github,
download_by_alist,
]
for download_function in download_functions:
try:
return download_function()
except Exception:
logger.info("下载失败了,尝试下一个下载方式")
raise Exception("所有下载方式都失败了")
def download_chrome_driver(self, chrome_driver_exe_name: str) -> str:
try:
return download_chrome_driver(self.chrome_driver_version, "utils", ".")
except Exception as e:
logger.error("从chrome官网下载driver失败,尝试从网盘下载", exc_info=e)
return self.download_chrome_file(chrome_driver_exe_name)
def download_from_github(self, filename: str) -> str:
return download_latest_github_release(
"utils",
filename,
"fzls",
"djc_helper_chrome",
)
def get_path_in_netdisk(self, filename: str) -> str:
return f"/文本编辑器、chrome浏览器、autojs、HttpCanary等小工具/{filename}"
def chrome_driver_executable_path(self):
# re: 这里chromedriver可以使用另一个库来自动为维护,后面可以看看有没有必要调整
# https://pypi.org/project/webdriver-manager/
return os.path.realpath(f"{self.chrome_root_directory()}/chromedriver_{self.get_chrome_major_version()}.exe")
def chrome_binary_7z(self):
return os.path.realpath(f"{self.chrome_root_directory()}/chrome_portable_{self.get_chrome_major_version()}.7z")
def chrome_binary_directory(self):
return os.path.realpath(f"{self.chrome_root_directory()}/chrome_portable_{self.get_chrome_major_version()}")
def chrome_binary_location(self):
return os.path.realpath(
f"{self.chrome_root_directory()}/chrome_portable_{self.get_chrome_major_version()}/chrome.exe"
)
def chrome_installer_name(self):
return f"Chrome_{self.get_chrome_major_version()}.(小版本号)_普通安装包_非便携版.exe"
def chrome_driver_executable_path_linux(self):
return "/usr/local/bin/chromedriver"
def chrome_binary_location_linux(self):
return "/usr/bin/google-chrome"
def chrome_root_directory(self):
return os.path.realpath("./utils")
def is_valid_chrome_file(self, chrome_filepath) -> bool:
if not os.path.isfile(chrome_filepath):
# 文件不存在
return False
invalid_filesize = 1 * MiB
if get_file_or_directory_size(chrome_filepath) <= invalid_filesize:
# 文件大小太小了,很有可能是没下载完全,或者下载报错了
return False
return True
def get_chrome_major_version(self) -> int:
version = self.chrome_major_version
if self.cfg is None or self.cfg.force_use_chrome_major_version == 0:
rule = config_cloud().chrome_version_replace_rule
if self.chrome_major_version in rule.troublesome_major_version_list and rule.valid_chrome_version != 0:
# 当前chrome版本在部分系统可能无法正常使用,替换为远程配置的可用版本
version = rule.valid_chrome_version
else:
# 使用默认的版本
version = self.chrome_major_version
else:
# 使用本地配置的版本
version = self.cfg.force_use_chrome_major_version
return version
def login(self, account, password, login_mode: str, name=""):
"""
自动登录指定账号,并返回登陆后的cookie中包含的uin、skey数据
:param account: 账号
:param password: 密码
:rtype: LoginResult
"""
self.name = name
self.account = account
self.password = password
self.window_title = f"将登录 {name}({account}) - {login_mode}"
logger.info(f"{name} 即将开始自动登录,无需任何手动操作,等待其完成即可")
logger.info(f"{name} 如果出现报错,可以尝试调高相关超时时间然后重新执行脚本")
def login_with_account_and_password():
logger.info(
color("bold_green") + f"{name} 当前为自动登录模式,请不要手动操作网页,否则可能会导致登录流程失败"
)
# 等待页面加载
self.wait_for_login_page_loaded()
logger.info("由于账号密码登录有可能会触发短信验证,因此优先尝试点击头像来登录~")
login_by_click_avatar_success = self.try_auto_click_avatar(account, name, self.login_type_auto_login)
if login_by_click_avatar_success:
logger.info("使用头像点击登录成功")
else:
logger.warning("点击头像登录失败,尝试输入账号密码来进行登录")
# 选择密码登录
self.driver.find_element(By.ID, "switcher_plogin").click()
# 输入账号
self.driver.find_element(By.ID, "u").clear()
self.driver.find_element(By.ID, "u").send_keys(account)
# 输入密码
self.driver.find_element(By.ID, "p").clear()
self.driver.find_element(By.ID, "p").send_keys(password)
logger.info(f"{name} 等待一会,确保登录键可以点击")
time.sleep(3)
# 发送登录请求
self.driver.find_element(By.ID, "login_button").click()
# 尝试自动处理验证码
self.try_auto_resolve_captcha()
return self._login(
self.login_type_auto_login, login_action_fn=login_with_account_and_password, login_mode=login_mode
)
def qr_login(self, login_mode: str, name="", account=""):
"""
二维码登录,并返回登陆后的cookie中包含的uin、skey数据
:rtype: LoginResult
"""
logger.info("即将开始扫码登录,请在弹出的网页中扫码登录~")
self.name = name
self.account = ""
self.password = ""
self.window_title = f"请扫码 {name} - {login_mode}"
def replace_qr_code_tip():
qr_js_wait_time = 1
try:
# 扫码登录
tip_class_name = "qr_safe_tips"
tip = f"请扫码 {name}"
logger.info(color("bold_green") + f"准备修改二维码上方 扫码提示文字 为 {tip}")
WebDriverWait(self.driver, qr_js_wait_time).until(
expected_conditions.visibility_of_element_located((By.CLASS_NAME, tip_class_name))
)
self.driver.execute_script(
f"document.getElementsByClassName('{tip_class_name}')[0].innerText = '{tip}'; "
)
except Exception as e:
logger.warning("替换扫码提示文字出错了(不影响登录流程)")
logger.debug("", exc_info=e)
try:
# 提示点击头像登录
tip_id = "qlogin_tips"
tip = f"请点击头像授权登录 {name} - 多于两个账号可以点击两侧箭头切换"
logger.info(color("bold_green") + f"准备修改二维码上方 点击头像提示文字 为 {tip}")
WebDriverWait(self.driver, qr_js_wait_time).until(
expected_conditions.visibility_of_element_located((By.ID, tip_id))
)
self.driver.execute_script(f"document.getElementById('{tip_id}').innerText = '{tip}'; ")
except Exception as e:
logger.warning("替换扫码提示文字出错了(不影响登录流程)")
logger.debug("", exc_info=e)
try:
# 调整箭头
logger.info(color("bold_green") + "准备修改两侧箭头为可见的 ⬅️和 ➡️")
self.driver.execute_script(
"""
function setArrow(elementId = "", arrow="") {
let target = document.getElementById(elementId)
// 需要修改 display 为 block 才能获取高度
let oldDisplay = target.style.display
target.style.display = "block"
let desiredHeight = parseInt((target.clientHeight || 120) * 1.5)
target.style.display = oldDisplay
// 修改为位于头像左右的箭头
target.innerText = arrow
target.style.lineHeight = desiredHeight + "px"
}
setArrow("prePage", "⬅️")
setArrow("nextPage", "➡️")
"""
)
except Exception as e:
logger.warning("修改箭头失败了(不影响登录流程)")
logger.debug("", exc_info=e)
def login_with_qr_code():
logger.info(
color("bold_yellow")
+ f"{name} 当前为扫码登录模式,请在{self.get_login_timeout(True)}s内完成扫码登录操作或快捷登录操作"
)
self.wait_for_login_page_loaded()
replace_qr_code_tip()
logger.info(color("bold_green") + f"{name} 尝试自动点击头像进行登录")
self.try_auto_click_avatar(account, name, self.login_type_qr_login)
return self._login(self.login_type_qr_login, login_action_fn=login_with_qr_code, login_mode=login_mode)
def wait_for_login_page_loaded(self):
logger.info(f"{self.name} 等待页面加载")
time.sleep(self.cfg.login.open_url_wait_time)
if self.login_type == self.login_type_auto_login:
# 仅自动登录模式需要检测窗口是否已经弹出来
logger.info(f"{self.name} 等待 登录框#switcher_plogin 加载完毕")
WebDriverWait(self.driver, self.cfg.login.load_login_iframe_timeout).until(
expected_conditions.visibility_of_element_located((By.ID, "switcher_plogin"))
)
def try_auto_click_avatar(self, account: str, name: str, login_type: str) -> bool:
# 检测功能开关
if login_type == self.login_type_auto_login:
enable = self.cfg.login.enable_auto_click_avatar_in_auto_login
else:
enable = self.cfg.login.enable_auto_click_avatar_in_qr_login
if not enable:
logger.warning(
f"当前未开启【{login_type} 模式下尝试点击头像来登录】,请自行操作~。若需要该功能,可在配置工具【公共配置/登录】中开启本功能"
)
return False
logger.info(
f"当前已开启【{login_type} 模式下尝试点击头像来登录】。如该功能有异常,导致登录流程无法正常进行,可在配置工具【公共配置/登录】中关闭本功能"
)
# 实际登录流程
login_success = False
ctx = f"【{name}({account})】"
try:
# 尝试自动点击头像登录
if account != "":
selector = f"#qlogin_list > a[uin='{account}']"
logger.info(color("bold_green") + f"{ctx} 尝试点击头像来登录")
logger.info("检查对应头像是否存在")
time.sleep(1)
self.driver.find_element(By.CSS_SELECTOR, selector)
logger.info("开始点击对应头像")
self.driver.execute_script(
f"""
document.querySelector("{selector}").click()
"""
)
# 由于有时候这个登录框可能会迟一点消失,这里改为多次尝试,减小误判的情况
logger.info(
f"判断点击头像登录是否成功,最大等待 {self.cfg.login.login_by_click_avatar_finished_timeout} 秒"
)
login_success = False
try:
WebDriverWait(self.driver, self.cfg.login.login_by_click_avatar_finished_timeout).until(
expected_conditions.invisibility_of_element((By.ID, "switcher_plogin"))
)
login_success = True
except InvalidArgumentException as e:
# 如果已经刷新到新页面,则会报这个异常,说明也是成功了
logger.info(
color("bold_yellow") + f"{ctx} 跳转到新的页面了,导致无法定位到登录按钮,这说明登录也成功了"
)
login_success = True
logger.debug("保存下异常信息", exc_info=e)
except Exception as e:
login_success = False
logger.debug("头像登录出错了", exc_info=e)
logger.info(color("bold_cyan") + f"{ctx} 点击头像登录的结果为: {'成功' if login_success else '失败'}")
elif login_type == self.login_type_qr_login:
async_message_box(
"现已支持扫码模式下自动点击头像进行登录,不过需要填写QQ号码,可使用配置工具填写QQ号码即可体验本功能",
"扫码自动点击头像功能提示",
show_once=True,
)
except Exception as e:
logger.warning(f"{ctx} 尝试自动点击头像登录失败了,请自行操作~")
logger.debug("", exc_info=e)
return login_success
def _login(self, login_type, login_action_fn=None, login_mode="normal"):
if not is_first_run_in(f"login_locker_{login_mode}_{self.name}", duration=datetime.timedelta(seconds=10)):
raise SameAccountTryLoginAtMultipleThreadsException
login_retry_key = "login_retry_key"
login_retry_data, retry_timeouts = self.get_retry_data(
login_retry_key, self.cfg.login.max_retry_count - 1, self.cfg.login.retry_wait_time
)
self.login_slow_retry_max_count = self.cfg.login.max_retry_count
for idx in range_from_one(self.login_slow_retry_max_count):
self.login_slow_retry_index = idx
logger.info(
color("bold_green")
+ f"[慢速重试阶段] [{idx}/{self.login_slow_retry_max_count}] {self.name} 开始本轮登录流程"
)
self.login_type = login_type
self.login_mode = login_mode
# note: 如果get_login_url的surl变更,代码中确认登录完成的地方也要一起改
login_fn, suffix, login_url = {
self.login_mode_normal: (
self._login_real,
"",
self.get_login_url(21000127, 8, "https://dnf.qq.com/"),
),
self.login_mode_xinyue: (
self._login_xinyue,
"心悦",
get_act_url("DNF地下城与勇士心悦特权专区"),
),
self.login_mode_qzone: (
self._login_qzone,
"QQ空间业务(如抽卡等需要用到)(不启用QQ空间系活动就不会触发本类型的登录,完整列表参见示例配置)",
self.get_login_url(15000103, 5, "https://act.qzone.qq.com/"),
),
self.login_mode_guanjia: (
self._login_guanjia,
"电脑管家(如电脑管家蚊子腿需要用到,完整列表参见示例配置)",
get_act_url("管家蚊子腿"),
),
self.login_mode_wegame: (
self._login_wegame,
"wegame(获取wegame相关api需要用到)",
self.get_login_url(1600001063, 733, "https://www.wegame.com.cn/"),
),
self.login_mode_club_vip: (
self._login_club_vip,
"club.vip.qq.com",
self.get_login_url(8000212, 18, "https://club.vip.qq.com/qqvip/acts2021/dnf"),
),
self.login_mode_iwan: (
self._login_iwan,
"爱玩",
"https://iwan.qq.com/g/gift",
),
self.login_mode_supercore: (
self._login_supercore,
"超享玩",
"https://act.supercore.qq.com/supercore/act/ac2cb66d798da4d71bd33c7a2ec1a7efb/index.html",
),
self.login_mode_djc: (
self._login_djc,
"道聚城",
get_act_url("道聚城"),
),
}[login_mode]
ctx = f"{login_type}-{suffix}"
login_exception = None
try:
if idx > 1:
logger.info(
color("bold_cyan")
+ f"已经是第{idx}次登陆,说明可能出现某些问题,将关闭隐藏浏览器选项,方便观察出现什么问题"
)
self.cfg.run_in_headless_mode = False
self.prepare_chrome(ctx, login_type, login_url)
lr = login_fn(ctx, login_action_fn=login_action_fn)
logger.debug(f"{self.name} 登录结果为 {lr}")
return lr
except Exception as e:
login_exception = e
finally:
login_result = color("bold_green") + "登录成功"
if login_exception is not None:
login_result = color("bold_cyan") + "登录失败"
current_url = ""
if self.driver is not None:
current_url = self.driver.current_url
used_time = datetime.datetime.now() - self.time_start_login
logger.info("")
logger.info(
f"[{login_result}] "
+ color("bold_yellow")
+ f"{self.name} [慢速重试阶段] 第{idx}/{self.login_slow_retry_max_count}次 {ctx} 共耗时为 {used_time}"
)
logger.info("")
self.destroy_chrome()
if login_exception is not None:
# 登陆失败
msg = f"[慢速重试阶段] {self.name} 第{self.login_slow_retry_index}/{self.login_slow_retry_max_count}次尝试登录出错"
if idx < self.login_slow_retry_max_count:
# 每次等待时长线性递增
wait_time = retry_timeouts[idx - 1]
msg += f"将等待较长一段时间后再重试,也就是 {wait_time:.2f}秒后重试(v{now_version} {ver_time})"
msg += f"\n\t当前登录重试等待时间序列:{retry_timeouts}"
msg += f"\n\t根据历史数据得出的推荐重试等待时间:{login_retry_data.recommended_first_retry_timeout}"
if use_by_myself():
msg += (
f"\n\t(仅我可见)历史重试成功等待时间列表:{login_retry_data.history_success_timeouts}"
)
msg += f"\n\t当前网址为 {current_url}"
logger.exception(msg, exc_info=login_exception)
if type(login_exception) is RequireVerifyMessageButInHeadlessMode:
logger.info(
color("bold_yellow")
+ f"检测到需要手动验证流程({login_exception}),将立即开始第二次慢速重试,且显示浏览器界面"
)
wait_time = 1
count_down(f"{truncate(self.name, 20):20s} 重试", wait_time)
else:
logger.exception(msg, exc_info=login_exception)
else:
# 登陆成功
if idx > 1:
# 第idx-1次的重试成功了,尝试更新历史数据
self.update_retry_data(
login_retry_key,
retry_timeouts[idx - 2],
self.cfg.login.recommended_retry_wait_time_change_rate,
self.name,
)
# 能走到这里说明登录失败了,大概率是网络不行
logger.warning(
color("bold_yellow")
+ (
f"已经尝试登录 {self.name} {self.cfg.login.max_retry_count}次,均已失败,大概率是网络有问题(v{now_version})\n"
"建议依次尝试下列措施\n"
"1. 重新打开程序\n"
"2. 重启电脑\n"
"3. 切换旧版本chrome(如果之前都是正常的) - 配置工具/公共配置/登录/chrome版本,修改为94或者更早的版本,并开启 强制使用便携版 开关\n"
"4. 更换dns,如谷歌、阿里、腾讯、百度的dns,具体更换方法请百度\n"
"5. 重装网卡驱动\n"
"6. 换个网络环境\n"
"7. 换台电脑\n"
)
)
if login_mode == self.login_mode_guanjia:
logger.warning(