175 lines
7.2 KiB
Python
175 lines
7.2 KiB
Python
import os
|
||
import argparse
|
||
|
||
from playwright.sync_api import sync_playwright
|
||
|
||
|
||
def save_login_state(auth_file):
|
||
with sync_playwright() as p:
|
||
os.makedirs(os.path.dirname(auth_file), exist_ok=True)
|
||
# 1. 启动浏览器
|
||
# headless=False 表示显示浏览器窗口,方便用户扫码登录
|
||
browser = p.chromium.launch(headless=False)
|
||
|
||
# 2. 创建浏览器上下文(Context)
|
||
# Context 类似于一个独立的浏览器会话,包含 cookies、localStorage 等
|
||
context = browser.new_context()
|
||
|
||
# 3. 创建新页面(Tab页)
|
||
page = context.new_page()
|
||
|
||
# ---------- 核心:设置网络请求监听 ----------
|
||
# 标记是否已登录成功(用于跨函数修改)
|
||
login_success = False
|
||
|
||
def on_response(response):
|
||
"""
|
||
监听所有网络响应的回调函数
|
||
每当浏览器收到任何 HTTP 响应时,都会调用这个函数
|
||
|
||
Args:
|
||
response: Playwright 的 Response 对象,包含响应的所有信息
|
||
"""
|
||
# 使用 nonlocal 声明,允许修改外层函数的 login_success 变量
|
||
nonlocal login_success
|
||
|
||
# 检查响应URL是否包含登录相关的关键词
|
||
# response.url 是请求的完整地址,如 "https://mail.qq.com/cgi-bin/login"
|
||
if 'login' in response.url or 'auth' in response.url:
|
||
# response.status 是 HTTP 状态码,200 表示成功
|
||
# 其他常见状态码:301(重定向)、302(临时重定向)、400(错误请求)、401(未授权)、403(禁止)、404(未找到)、500(服务器错误)
|
||
if response.status == 200:
|
||
print(f"[网络监听] 检测到登录API响应成功")
|
||
print(f" - 请求地址: {response.url}")
|
||
print(f" - 状态码: {response.status}")
|
||
login_success = True
|
||
|
||
# 将回调函数注册到 page 对象
|
||
# 每当有网络响应时,Playwright 会自动调用 on_response 函数
|
||
page.on('response', on_response)
|
||
|
||
# 其他可用的事件:'request'(请求发送时)、'requestfailed'(请求失败时)、'requestfinished'(请求完成时)
|
||
|
||
# ---------- 可选:监听 URL 变化 ----------
|
||
def on_url_change(frame):
|
||
"""
|
||
监听页面导航(URL变化)的回调函数
|
||
每当页面跳转或刷新时都会调用
|
||
|
||
Args:
|
||
frame: 页面框架对象
|
||
"""
|
||
nonlocal login_success
|
||
|
||
# 获取当前页面的完整URL
|
||
current_url = page.url
|
||
|
||
# 检查URL是否已进入邮箱系统
|
||
# 登录前是 'https://mail.qq.com',登录后通常会变成包含其他路径的URL
|
||
# 例如:'https://mail.qq.com/cgi-bin/frame_html?sid=xxx'
|
||
if 'mail' in current_url and current_url != 'https://mail.qq.com':
|
||
print(f"[URL监听] 检测到页面已跳转到邮箱系统")
|
||
print(f" - 当前URL: {current_url}")
|
||
login_success = True
|
||
|
||
# 注册 URL 变化监听器
|
||
# 'framenavigated' 事件在页面导航(跳转)时触发
|
||
page.on('framenavigated', on_url_change)
|
||
|
||
# 4. 访问QQ邮箱首页
|
||
print("正在打开QQ邮箱登录页面...")
|
||
page.goto('https://mail.qq.com')
|
||
# goto() 会等待页面加载完成(直到网络空闲)
|
||
|
||
# 5. 提示用户扫码登录
|
||
print("=" * 50)
|
||
print("请使用手机QQ扫码登录")
|
||
print("系统将自动检测登录状态,无需手动操作")
|
||
print("等待登录中...")
|
||
print("=" * 50)
|
||
|
||
# 6. 等待登录成功或超时
|
||
try:
|
||
# wait_for_function 会持续检查 JavaScript 表达式是否为真
|
||
# 这里检查两个条件:
|
||
# 1. 页面是否出现"收件箱"文字(登录成功的典型标志)
|
||
# 2. 页面是否出现"inbox"文字(英文版)
|
||
# timeout=300000 表示最多等待 300 秒(5分钟)
|
||
# 如果5分钟内条件满足,则继续执行;否则抛出异常
|
||
page.wait_for_function(
|
||
'document.body.innerText.includes("收件箱") || document.body.innerText.includes("inbox")',
|
||
timeout=300000 # 毫秒单位,300000ms = 300秒 = 5分钟
|
||
)
|
||
# 如果 wait_for_function 正常返回,说明检测到了"收件箱"文字
|
||
print("[文本监听] 检测到'收件箱'文字,登录确认成功!")
|
||
|
||
except Exception as e:
|
||
# 超时或其他异常会执行这里
|
||
print(f"[警告] 未检测到'收件箱'文字,可能页面结构有变化或登录超时")
|
||
print(f" 错误信息: {e}")
|
||
|
||
# 7. 检查是否通过其他方式检测到登录成功
|
||
if login_success:
|
||
print("\n✅ 已通过多种方式确认登录成功!")
|
||
else:
|
||
print("\n⚠️ 未明确检测到登录成功标志,但仍将保存当前状态")
|
||
print(" 如果登录成功,状态应该是有效的")
|
||
|
||
# 8. 额外等待2秒,确保状态稳定
|
||
# 目的:等待所有异步请求完成、cookies 完全写入、页面状态稳定
|
||
print("等待2秒,确保状态稳定...")
|
||
page.wait_for_timeout(2000) # 2000毫秒 = 2秒
|
||
|
||
# 9. 保存登录状态(cookies、localStorage、sessionStorage 等)
|
||
# storage_state 会保存当前 context 的所有存储数据
|
||
# 保存后,下次可以直接用 storage_state 参数加载,无需重新登录
|
||
context.storage_state(path=auth_file)
|
||
print(f"✅ 登录状态已保存到: {auth_file}")
|
||
|
||
# 10. 关闭浏览器,释放资源
|
||
browser.close()
|
||
print("浏览器已关闭")
|
||
|
||
|
||
def crawl_with_saved_state(auth_file):
|
||
with sync_playwright() as p:
|
||
# 加载之前保存的登录状态
|
||
browser = p.chromium.launch(headless=False) # 可以无头模式了
|
||
context = browser.new_context(storage_state=auth_file)
|
||
page = context.new_page()
|
||
|
||
# 直接访问需要登录的页面
|
||
page.goto('https://mail.qq.com')
|
||
|
||
# 现在已经是登录状态了
|
||
print("当前URL:", page.url)
|
||
|
||
# 获取页面内容
|
||
content = page.content()
|
||
print("页面长度:", len(content))
|
||
|
||
# 可以使用 BeautifulSoup 解析
|
||
# from bs4 import BeautifulSoup
|
||
# soup = BeautifulSoup(content, 'html.parser')
|
||
page.wait_for_timeout(5000)
|
||
browser.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(description='爬虫脚本')
|
||
parser.add_argument('account', help='用户')
|
||
# parser.add_argument('-p', '--password', help='密码(可选)')
|
||
args = parser.parse_args()
|
||
account = args.account
|
||
print(f"用户名{account}")
|
||
if not account:
|
||
print("请输入用户名")
|
||
exit(1)
|
||
auth_file_path = "./auth/mail"
|
||
file_path = f"{auth_file_path}/{account}.json"
|
||
# 判断文件是否存在
|
||
if os.path.exists(file_path):
|
||
crawl_with_saved_state(file_path)
|
||
else:
|
||
save_login_state(file_path)
|