| 1 | # -*- coding: utf-8 -*- |
| 2 | """百家号(headless)扫码登录 → 保存 cookie。 |
| 3 | |
| 4 | 说明: |
| 5 | - 无头浏览器打开百家号登录页并提取登录二维码,放大截图后保存为 png, |
| 6 | 并用终端 ASCII 二维码展示;你手机/百度APP扫码登录。 |
| 7 | - 扫码成功后自动把登录态写入 cookies/baijiahao_uploader/account.json。 |
| 8 | |
| 9 | 用法: |
| 10 | python examples/get_baijiahao_cookie_headless.py |
| 11 | """ |
| 12 | from pathlib import Path |
| 13 | |
| 14 | from playwright.async_api import async_playwright |
| 15 | from conf import BASE_DIR |
| 16 | from utils.login_qrcode import ( |
| 17 | build_login_qrcode_path, |
| 18 | print_terminal_qrcode, |
| 19 | ) |
| 20 | from uploader.baijiahao_uploader.main import cookie_auth, baijiahao_logger |
| 21 | |
| 22 | |
| 23 | QR_SELECTOR = 'img[src^="https://passport.baidu.com/v2/api/qrcode"]' |
| 24 | LOGIN_BTN_TEXT = "登录" |
| 25 | LOGIN_URL = "https://baijiahao.baidu.com/builder/theme/bjh/login" |
| 26 | |
| 27 | |
| 28 | async def _grab_qr(page, qrcode_path: Path) -> str: |
| 29 | qr = page.locator(QR_SELECTOR).first |
| 30 | await qr.wait_for(state="attached", timeout=60000) |
| 31 | src = await qr.get_attribute("src") |
| 32 | if src and src.startswith("https://"): |
| 33 | resp = await page.context.request.get(src) |
| 34 | qrcode_path.parent.mkdir(parents=True, exist_ok=True) |
| 35 | qrcode_path.write_bytes(await resp.body()) |
| 36 | else: |
| 37 | await qr.screenshot(path=str(qrcode_path)) |
| 38 | |
| 39 | qrcode_content = "" |
| 40 | try: |
| 41 | import cv2 |
| 42 | img = cv2.imread(str(qrcode_path)) |
| 43 | if img is not None: |
| 44 | h, w = img.shape[:2] |
| 45 | up = cv2.resize(img, (w * 3, h * 3), interpolation=cv2.INTER_CUBIC) |
| 46 | qrcode_content = cv2.QRCodeDetector().detectAndDecode(up)[0] or "" |
| 47 | except Exception: |
| 48 | pass |
| 49 | return qrcode_content or "" |
| 50 | |
| 51 | |
| 52 | async def _wait_login(page, max_checks: int = 120, interval: int = 3) -> bool: |
| 53 | import asyncio |
| 54 | async def _logged_in(): |
| 55 | if "login" in page.url: |
| 56 | return False |
| 57 | ctx = page.context |
| 58 | cookies = await ctx.cookies() |
| 59 | if any(c.get("name") in ("BDUSS", "STOKEN") for c in cookies): |
| 60 | return True |
| 61 | return False |
| 62 | |
| 63 | for _ in range(max_checks): |
| 64 | if await _logged_in(): |
| 65 | return True |
| 66 | await asyncio.sleep(interval) |
| 67 | return False |
| 68 | |
| 69 | |
| 70 | async def main(): |
| 71 | account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json") |
| 72 | account_file.parent.mkdir(parents=True, exist_ok=True) |
| 73 | |
| 74 | import os |
| 75 | if os.path.exists(account_file) and await cookie_auth(str(account_file)): |
| 76 | baijiahao_logger.success("[+] cookie 已有效,无需重新登录") |
| 77 | return |
| 78 | |
| 79 | qrcode_path = build_login_qrcode_path(str(account_file)) |
| 80 | async with async_playwright() as playwright: |
| 81 | browser = await playwright.chromium.launch( |
| 82 | headless=True, args=["--no-sandbox", "--disable-blink-features=AutomationControlled"] |
| 83 | ) |
| 84 | context = await browser.new_context() |
| 85 | page = await context.new_page() |
| 86 | await page.goto(LOGIN_URL, timeout=60000, wait_until="domcontentloaded") |
| 87 | await page.wait_for_timeout(4000) |
| 88 | # 进入登录弹窗 |
| 89 | await page.get_by_text(LOGIN_BTN_TEXT, exact=True).first.click(timeout=10000) |
| 90 | await page.wait_for_timeout(4000) |
| 91 | |
| 92 | qrcode_content = await _grab_qr(page, qrcode_path) |
| 93 | baijiahao_logger.info(f"🖼️ 二维码已保存: {qrcode_path}") |
| 94 | if qrcode_content: |
| 95 | print_terminal_qrcode(qrcode_content, qrcode_path, "百度APP/手机百度") |
| 96 | else: |
| 97 | print(f"未能解码二维码,请直接打开文件扫码:\n {qrcode_path}") |
| 98 | |
| 99 | if await _wait_login(page): |
| 100 | baijiahao_logger.success("[+] 扫码登录成功,正在保存 cookie...") |
| 101 | await context.storage_state(path=str(account_file)) |
| 102 | baijiahao_logger.success(f"[+] cookie 已保存: {account_file}") |
| 103 | else: |
| 104 | baijiahao_logger.error("[-] 等待扫码超时(约 6 分钟),未完成登录。") |
| 105 | await browser.close() |
| 106 | |
| 107 | |
| 108 | if __name__ == "__main__": |
| 109 | import asyncio |
| 110 | |
| 111 | asyncio.run(main()) |