Kylaan

Back

1. 项目目标

在 Linux 服务器(CentOS + 宝塔面板)上,利用 Python + Selenium 实现:

  1. 每天定时自动打开腾讯文档收集表。
  2. 自动登录(只需扫码一次,后续自动维持登录状态)。
  3. 自动填写指定内容(单选、文本等)。
  4. 自动提交并处理“二次确认”弹窗。

2. 环境搭建与依赖解决

2.1 基础环境

  • OS: CentOS (宝塔面板)
  • Language: Python 3.x
  • Library: Selenium

2.2 遇到的问题 & 解决方案

问题一:无法下载 ChromeDriver (网络问题)

  • 现象:使用 webdriver-manager 自动安装时报错 ConnectionResetError,因为国内服务器无法连接 Google API。
  • 解决
    1. 查看 Chrome 版本:google-chrome --version
    2. 从国内淘宝镜像源手动下载对应版本的驱动: https://npmmirror.com/mirrors/chrome-for-testing/
    3. 手动解压并移动到 /usr/bin/chromedriver,赋予 chmod +x 权限。

问题二:Chrome 启动即崩溃 (内存不足/环境限制)

  • 现象:脚本报错 Chrome instance exited 或无任何报错直接断开,无法生成截图。
  • 原因:Linux 无头模式下 /dev/shm(共享内存)过小,且服务器物理内存不足。
  • 解决
    1. 增加虚拟内存 (Swap):通过宝塔“Linux工具箱”添加 2GB~4GB Swap。
    2. 优化启动参数
      options.add_argument("--disable-dev-shm-usage") # 使用硬盘代替共享内存
      options.add_argument("--no-sandbox") #以此绕过沙盒限制
      options.add_argument("--headless=new") # 使用新版无头模式
      python

问题三:残留进程导致卡死

  • 现象:多次调试后,提示 SessionNotCreatedException,因为旧的 Chrome 僵尸进程占用了资源。
  • 解决:在脚本开头加入“暴力清理”逻辑:
    os.system("pkill -9 chrome")
    os.system("pkill -9 chromedriver")
    python

3. 核心逻辑演进

为了解决 “需要登录”“每日自动运行” 的矛盾,我们将脚本拆分为两个独立文件。

3.1 登录脚本 (login.py)

  • 功能:人工运行一次。打开网页 -> 点击登录 -> 截图二维码 -> 等待用户扫码 -> 保存 Cookie 到本地目录。
  • 难点攻克
    • 定位登录按钮:最初使用文字匹配失败(因中英文环境差异),最终通过分析 HTML 源码,锁定了唯一 ID header-login-btn
    • 多步弹窗处理:实现了 点击右上角 -> 点击中间"立即登录" -> 点击"同意协议" -> 截图二维码 的完整链条。

3.2 填表脚本 (submit.py)

  • 功能:定时任务运行。读取已保存的 Cookie -> 填表 -> 提交 -> 确认。
  • 难点攻克
    • 选项无法选中:最初只点击了文字,但腾讯文档的 React 框架未识别点击。
      • 修正:改为定位 role="radio" 的父容器,并优先点击内部的小圆圈元素。
      • 双重验证:点击后检查 aria-checked="true" 属性,未选中则重试。
    • 提交确认弹窗失效:脚本找不到“Confirm”按钮。
      • 原因:界面语言变成了英文(Confirm),脚本在找中文(确定)。
      • 修正:通过源码找到确认按钮的唯一 CSS 类名 button.dui-modal-footer-ok,无视语言差异,精准点击。

4. 最终代码

文件结构

  • /www/wwwroot/python_txwd/login.py (登录专用)
  • /www/wwwroot/python_txwd/submit.py (填表专用)
  • /www/wwwroot/python_txwd/chrome_user_data/ (存放登录凭证的文件夹)

脚本 A: 登录 (login.py)

(仅需运行一次,用于生成 Cookie)

# -*- coding: utf-8 -*-
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 配置区
USER_DATA_DIR = "/www/wwwroot/python_txwd/chrome_user_data"
FORM_URL = "https://docs.qq.com/form/page/DVGNMRU9WSHFNSUZQ" # 你的表单地址

def login_task():
    print("=== 启动登录脚本 ===")
    chrome_options = Options()
    chrome_options.add_argument("--headless=new") 
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("--lang=zh-CN") 
    chrome_options.add_argument(f"--user-data-dir={USER_DATA_DIR}")
    chrome_options.binary_location = "/usr/bin/google-chrome"
    chrome_options.add_argument("--window-size=1920,1080")

    service = Service("/usr/bin/chromedriver")
    driver = webdriver.Chrome(service=service, options=chrome_options)
    
    try:
        driver.get(FORM_URL)
        wait = WebDriverWait(driver, 20)
        
        # 1. 点击右上角登录 (ID定位)
        print("寻找登录按钮...")
        login_btn = wait.until(EC.element_to_be_clickable((By.ID, "header-login-btn")))
        driver.execute_script("arguments[0].click();", login_btn)

        # 2. 点击中间的“立即登录” (兼容中英文)
        print("点击弹窗登录...")
        login_now = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(., '立即登录')] | //button[contains(., 'Log in now')]")))
        driver.execute_script("arguments[0].click();", login_now)

        # 3. 处理协议 (如果有)
        try:
            agree = WebDriverWait(driver, 5).until(EC.element_to_be_clickable((By.XPATH, "//button[contains(., '同意')] | //button[contains(., 'Agree')]")))
            driver.execute_script("arguments[0].click();", agree)
        except:
            pass

        # 4. 截图二维码
        time.sleep(5)
        driver.save_screenshot("login_qrcode.png")
        print("✅ 二维码已生成: login_qrcode.png,请去宝塔扫码!")

        # 5. 等待登录成功
        for i in range(120):
            if i % 5 == 0: print(f"等待扫码... {i}s")
            try:
                # 如果登录按钮消失,说明登录成功
                if not driver.find_element(By.ID, "header-login-btn").is_displayed():
                    print("🎉 登录成功!")
                    break
            except:
                print("🎉 登录成功!")
                break
            time.sleep(1)

    finally:
        driver.quit()

if __name__ == "__main__":
    login_task()
python

脚本 B: 每日自动填表 (submit.py)

(适配每日归寝统计)

# -*- coding: utf-8 -*-
import time
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# === 配置 ===
USER_DATA_DIR = "/www/wwwroot/python_txwd/chrome_user_data"
FORM_URL = "https://docs.qq.com/form/page/DVGNMRU9WSHFNSUZQ" # 归寝统计表单
# ============

def clean_zombie_processes():
    os.system("pkill -9 chrome")
    os.system("pkill -9 google-chrome")
    os.system("pkill -9 chromedriver")
    time.sleep(2)

def ensure_select(driver, text):
    """根据文字选中单选框,带状态校验"""
    print(f"   -> 选择: '{text}'")
    try:
        # 寻找包含特定文字的单选框容器
        xpath = f"//div[@role='radio' and .//*[text()='{text}']]"
        option = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, xpath)))
        
        # 如果未选中则点击
        if option.get_attribute("aria-checked") != "true":
            try:
                # 尝试点圆圈
                circle = option.find_element(By.CSS_SELECTOR, "[class*='choice-option-normal']")
                driver.execute_script("arguments[0].click();", circle)
            except:
                driver.execute_script("arguments[0].click();", option)
            time.sleep(0.5)
        
        # 校验
        if option.get_attribute("aria-checked") == "true":
            print(f"      ✅ 成功选中")
            return True
        else:
            print(f"      ⚠️ 补刀点击...")
            option.click()
            return True
    except Exception as e:
        print(f"      ❌ 选择失败: {e}")
        return False

def submit_task():
    clean_zombie_processes()
    print("=== 开始填表任务 ===")
    
    chrome_options = Options()
    chrome_options.add_argument("--headless=new") 
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-gpu")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("--lang=zh-CN") 
    chrome_options.add_argument(f"--user-data-dir={USER_DATA_DIR}")
    chrome_options.binary_location = "/usr/bin/google-chrome"
    chrome_options.add_argument("--window-size=1920,1080")

    service = Service("/usr/bin/chromedriver")
    driver = webdriver.Chrome(service=service, options=chrome_options)
    
    try:
        driver.get(FORM_URL)
        time.sleep(5)

        # 检查登录
        try:
            if driver.find_element(By.ID, "header-login-btn").is_displayed():
                print("❌ 未登录,请先运行 login.py")
                return
        except:
            pass

        # --- 填表逻辑 ---
        ensure_select(driver, "6")        # 第1题
        # 第2题跳过
        ensure_select(driver, "全部归寝")  # 第3题
        
        time.sleep(1)

        # --- 提交 ---
        print("   -> 点击提交")
        submit_btn = WebDriverWait(driver, 5).until(
            EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Submit') or contains(text(), '提交')]"))
        )
        driver.execute_script("arguments[0].scrollIntoView(true);", submit_btn)
        driver.execute_script("arguments[0].click();", submit_btn)

        # --- 确认弹窗 (使用类名定位) ---
        print("   -> 等待确认弹窗")
        try:
            confirm_btn = WebDriverWait(driver, 5).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, "button.dui-modal-footer-ok"))
            )
            driver.execute_script("arguments[0].click();", confirm_btn)
            print("      ✅ 确认按钮已点击")
        except:
            print("      ⚠️ 未检测到弹窗(可能已成功)")

        time.sleep(3)
        driver.save_screenshot("final_result.png")
        print("✅ 任务完成,查看 final_result.png")

    except Exception as e:
        print(f"❌ 错误: {e}")
    finally:
        driver.quit()

if __name__ == "__main__":
    submit_task()
python

5. 定时任务设置

在宝塔面板的【计划任务】中添加 Shell 脚本:

  • 周期:每天 23:00
  • 脚本
    /usr/bin/python3 /www/wwwroot/python_txwd/submit.py >> /www/wwwroot/python_txwd/log.txt 2>&1
    bash

维护说明:如果某天发现日志提示“未登录”,或者截图显示登录按钮,只需手动运行一次 python login.py 重新扫码即可,无需修改代码。

腾讯文档自动填表脚本开发与部署记录
https://kylaan.top/blog/auto-submit/work
Author Kylaan
Published at 2025年12月10日
Comment seems to stuck. Try to refresh?✨