1. 项目目标
在 Linux 服务器(CentOS + 宝塔面板)上,利用 Python + Selenium 实现:
- 每天定时自动打开腾讯文档收集表。
- 自动登录(只需扫码一次,后续自动维持登录状态)。
- 自动填写指定内容(单选、文本等)。
- 自动提交并处理“二次确认”弹窗。
2. 环境搭建与依赖解决
2.1 基础环境
- OS: CentOS (宝塔面板)
- Language: Python 3.x
- Library: Selenium
2.2 遇到的问题 & 解决方案
问题一:无法下载 ChromeDriver (网络问题)
- 现象:使用
webdriver-manager自动安装时报错ConnectionResetError,因为国内服务器无法连接 Google API。 - 解决:
- 查看 Chrome 版本:
google-chrome --version。 - 从国内淘宝镜像源手动下载对应版本的驱动:
https://npmmirror.com/mirrors/chrome-for-testing/ - 手动解压并移动到
/usr/bin/chromedriver,赋予chmod +x权限。
- 查看 Chrome 版本:
问题二:Chrome 启动即崩溃 (内存不足/环境限制)
- 现象:脚本报错
Chrome instance exited或无任何报错直接断开,无法生成截图。 - 原因:Linux 无头模式下
/dev/shm(共享内存)过小,且服务器物理内存不足。 - 解决:
- 增加虚拟内存 (Swap):通过宝塔“Linux工具箱”添加 2GB~4GB Swap。
- 优化启动参数:
pythonoptions.add_argument("--disable-dev-shm-usage") # 使用硬盘代替共享内存 options.add_argument("--no-sandbox") #以此绕过沙盒限制 options.add_argument("--headless=new") # 使用新版无头模式
问题三:残留进程导致卡死
- 现象:多次调试后,提示
SessionNotCreatedException,因为旧的 Chrome 僵尸进程占用了资源。 - 解决:在脚本开头加入“暴力清理”逻辑:
pythonos.system("pkill -9 chrome") os.system("pkill -9 chromedriver")
3. 核心逻辑演进
为了解决 “需要登录” 和 “每日自动运行” 的矛盾,我们将脚本拆分为两个独立文件。
3.1 登录脚本 (login.py)
- 功能:人工运行一次。打开网页 -> 点击登录 -> 截图二维码 -> 等待用户扫码 -> 保存 Cookie 到本地目录。
- 难点攻克:
- 定位登录按钮:最初使用文字匹配失败(因中英文环境差异),最终通过分析 HTML 源码,锁定了唯一 ID
header-login-btn。 - 多步弹窗处理:实现了
点击右上角 -> 点击中间"立即登录" -> 点击"同意协议" -> 截图二维码的完整链条。
- 定位登录按钮:最初使用文字匹配失败(因中英文环境差异),最终通过分析 HTML 源码,锁定了唯一 ID
3.2 填表脚本 (submit.py)
- 功能:定时任务运行。读取已保存的 Cookie -> 填表 -> 提交 -> 确认。
- 难点攻克:
- 选项无法选中:最初只点击了文字,但腾讯文档的 React 框架未识别点击。
- 修正:改为定位
role="radio"的父容器,并优先点击内部的小圆圈元素。 - 双重验证:点击后检查
aria-checked="true"属性,未选中则重试。
- 修正:改为定位
- 提交确认弹窗失效:脚本找不到“Confirm”按钮。
- 原因:界面语言变成了英文(Confirm),脚本在找中文(确定)。
- 修正:通过源码找到确认按钮的唯一 CSS 类名
button.dui-modal-footer-ok,无视语言差异,精准点击。
- 选项无法选中:最初只点击了文字,但腾讯文档的 React 框架未识别点击。
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()python5. 定时任务设置
在宝塔面板的【计划任务】中添加 Shell 脚本:
- 周期:每天 23:00
- 脚本:
bash/usr/bin/python3 /www/wwwroot/python_txwd/submit.py >> /www/wwwroot/python_txwd/log.txt 2>&1
维护说明:如果某天发现日志提示“未登录”,或者截图显示登录按钮,只需手动运行一次 python login.py 重新扫码即可,无需修改代码。