1. 项目概述与核心价值最近在做一个内部数据整合的项目需要从公司一个基于 RuoYi-Vue 框架开发的后台管理系统里定期拉取最新的部门组织架构数据。这个系统本身有完善的登录验证机制包括账号密码和图形验证码而且前端还做了防自动化程序的检测。手动登录、截图、复制粘贴表格数据一次两次还行但每天都要搞效率太低还容易出错。于是我就琢磨着用 Python 写个脚本让它能像真人一样自动完成整个登录和数据抓取流程。为什么选 Playwright这得从几个痛点说起。之前也试过 Selenium但它在处理现代前端框架比如 Vue.js的动态渲染、等待页面稳定方面总感觉有点“力不从心”特别是面对一些基于事件驱动的交互等待策略写起来很繁琐。而 Playwright 是微软开源的天生对 Chromium、Firefox、WebKit 三大浏览器引擎支持得非常好它的auto-wait机制特别聪明能自动等待元素可操作、网络请求完成大大减少了我们写显式等待time.sleep或WebDriverWait的代码量。更关键的是Playwright 能更真实地模拟浏览器环境对反爬虫措施比如一些简单的“安全服务防护”检测的绕过能力更强这正是登录 RuoYi-Vue 这类系统所需要的。这个脚本的核心目标很明确第一全自动处理 RuoYi-Vue 的登录流程包括定位账号密码输入框、识别并输入图形验证码这里我们先采用手动输入的方式后面会讨论更自动化的思路、点击登录按钮。第二成功登录后自动导航到部门管理页面并准确抓取表格中的部门数据。第三整个过程要足够健壮能适应不同环境所以我加入了“系统浏览器路径自动检测”的功能让脚本在不同机器上都能无缝运行无需手动指定复杂的浏览器驱动路径。2. 环境准备与 Playwright 核心机制解析工欲善其事必先利其器。在开始写代码之前我们需要先把环境搭好。这里我强烈建议使用 Python 的虚拟环境venv来管理依赖避免和系统全局的 Python 包产生冲突。2.1 创建虚拟环境与安装依赖首先打开你的终端或命令行进入项目目录执行以下命令创建并激活虚拟环境# 创建名为 venv 的虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活后命令行提示符前面通常会显示(venv)表示你已经在这个独立的环境中了。接下来安装核心库 Playwright。Playwright 的安装分为两部分Python 客户端库和实际的浏览器二进制文件。# 安装 Playwright 的 Python 客户端库 pip install playwright # 安装 Playwright 所需的浏览器Chromium, Firefox, WebKit。我们主要用 Chromium。 # 这个命令会下载浏览器可能需要一些时间。 playwright install chromium这里有个小技巧playwright install命令默认会安装到用户目录下的缓存文件夹里例如~/.cache/ms-playwright/。这样做的好处是浏览器二进制文件与你的项目代码分离便于管理和更新。我们后续的“自动检测”功能就是去这个标准路径里找浏览器。2.2 Playwright 的核心优势与工作原理在写代码前理解 Playwright 怎么工作的能让你避开很多坑。它和 Selenium 最大的不同在于架构。Selenium 通过一个 WebDriver 协议与浏览器通信而 Playwright 则通过更底层的 DevTools Protocol (CDP) 直接与浏览器内核对话这使得它的控制更精细、性能更好。自动等待Auto-waiting这是 Playwright 的杀手级特性。当你执行page.click(‘button#submit’)时Playwright 内部会做一系列检查这个按钮在 DOM 里存在吗它是否可见它是否可点击没有disabled属性它没有被其他元素遮挡吧只有所有这些条件都满足它才会执行点击操作。这意味着你基本不用写time.sleep或复杂的WebDriverWait了代码简洁又可靠。浏览器上下文Browser Context你可以把它理解为一个独立的“隐身会话”。每个 Context 拥有独立的 cookies、localStorage 和会话状态。在我们的场景里登录成功后这个登录状态通常是 Cookie 或 Token就保存在这个 Context 里。后续所有在这个 Context 里打开的页面都会自动携带这个登录状态无需重复登录。这比直接用browser.new_page()更清晰也便于管理多个并行任务。网络拦截与模拟Network InterceptionPlaywright 可以监听和修改页面发出的所有网络请求。这对于处理验证码很有用。比如有些验证码图片是通过一个特定 API 接口获取的我们可以拦截这个请求拿到图片数据然后调用第三方 OCR 服务识别。不过RuoYi-Vue 自带的验证码通常比较简单我们第一步先采用手动输入后面再探讨自动化方案。3. 核心功能实现自动登录与验证码处理登录是自动化的第一道关卡也是最容易出问题的地方。RuoYi-Vue 的登录页面通常包含用户名、密码、验证码输入框和一个登录按钮。我们的脚本需要准确地找到它们并填入正确的信息。3.1 定位页面元素与输入信息首先我们需要用浏览器打开登录页面并分析页面结构。使用 Playwright 的 Codegen 工具可以快速生成定位代码。在终端运行playwright codegen http://your-ruoyi-system.com/login这会打开一个浏览器窗口和一个录制器。你在页面上手动操作输入、点击录制器会自动生成对应的 Python 代码。这是一个非常好的起点。假设通过分析我们得到以下元素选择器用户名输入框input[name‘username’]或#username密码输入框input[name‘password’]或#password验证码输入框input[name‘code’]或#validateCode验证码图片img#captchaImg用于获取图片可能需要右键检查元素确认其src属性登录按钮button[type‘submit’]或//button[contains(text(), ‘登录’)]基于此编写登录函数的核心代码如下async def login_to_ruoyi(page, username, password): 登录到 RuoYi-Vue 系统 # 导航到登录页 await page.goto(‘http://your-ruoyi-system.com/login‘) # 等待关键元素加载完成 # 使用 page.wait_for_selector 确保元素在DOM中稳定 await page.wait_for_selector(‘input[name“username”]‘) # 输入用户名和密码 await page.fill(‘input[name“username”]‘, username) await page.fill(‘input[name“password”]‘, password) # 处理验证码 - 方案一手动输入 # 首先确保验证码图片加载出来 captcha_selector ‘img#captchaImg‘ await page.wait_for_selector(captcha_selector) # 将验证码图片截图保存到本地方便人工查看 captcha_element await page.query_selector(captcha_selector) await captcha_element.screenshot(path‘./captcha.png‘) print(“验证码图片已保存为 ‘captcha.png‘请打开查看并输入。”) captcha_code input(“请输入验证码: “).strip() # 输入验证码 await page.fill(‘input[name“code”]‘, captcha_code) # 点击登录按钮 login_button page.locator(‘button:has-text(“登录”)‘) # 使用文本定位更健壮 await login_button.click() # 等待登录完成通常可以等待某个登录后才会出现的元素比如用户头像或导航菜单 try: # 假设登录成功后右上角会出现用户名的span await page.wait_for_selector(‘.el-dropdown-link span‘, timeout10000) print(“登录成功”) return True except Exception as e: # 登录失败可能是验证码错误 print(f“登录可能失败: {e}“) # 可以在这里捕获页面上的错误提示信息 error_msg await page.locator(‘.el-message--error‘).text_content() if error_msg: print(f“错误信息: {error_msg}“) return False注意元素选择器 (input[name...],#id,.class) 必须根据你目标系统的实际 HTML 结构进行调整。使用浏览器的开发者工具F12的“检查”功能仔细查看元素的id、name、class等属性选择唯一且稳定的选择器。避免使用可能随页面刷新变化的动态类名或ID。3.2 验证码处理策略与优化手动输入验证码显然不是全自动化的终极方案但它是最稳定、最通用的起点。很多企业内部系统的验证码并不复杂或者有后门比如固定的测试验证码。在实现更复杂的方案前先用手动方式跑通整个流程是明智的。如何向全自动迈进有几个方向可以考虑OCR 识别对于简单的数字、字母验证码可以使用开源的 Tesseract OCR 库。你需要先将验证码图片进行预处理灰度化、二值化、去噪再交给 Tesseract 识别。成功率取决于验证码的干扰程度。# 示例使用 pytesseract (需要先安装 Tesseract-OCR 引擎和 pytesseract 包) # pip install pytesseract pillow from PIL import Image import pytesseract # ... 截图保存为 captcha.png ... image Image.open(‘./captcha.png‘) # 预处理图像根据实际情况调整 image image.convert(‘L‘) # 灰度化 # 二值化等操作... captcha_text pytesseract.image_to_string(image, config‘--psm 8 --oem 3 -c tessedit_char_whitelist0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ‘) print(f“OCR识别结果: {captcha_text}“)机器学习/深度学习对于更复杂的验证码可以训练一个简单的 CNN 模型。但这需要大量的标注数据成本较高更适合长期、大规模的应用场景。接口绕过有些系统的验证码是在后端生成一个 token 或答案前端只是展示。通过抓包分析登录请求你可能会发现验证码的答案直接包含在某个请求参数或响应里。如果能找到这个规律就可以完全绕过前端的识别。但这需要谨慎并确保你有权进行此类操作。打码平台付费调用第三方打码平台的 API由人工进行识别。这是最省事但需要成本的方法适合验证码非常复杂或变化频繁的情况。我的建议是在项目初期优先使用手动输入或简单的 OCR。把主要精力放在登录后的页面导航和数据抓取逻辑的稳定性上。等核心流程稳定后再根据验证码的实际难度和项目需求决定是否投入精力做全自动识别。4. 系统浏览器路径自动检测的实现让脚本能在不同开发、测试、生产环境上无缝运行是提升工程化水平的关键一步。我们不想每次换台机器都要去修改代码里硬编码的浏览器路径。Playwright 安装的浏览器通常在一个固定的缓存目录我们可以通过编程方式找到它。4.1 探测 Playwright 安装的浏览器Playwright 的 Python 库提供了一个内部模块playwright._impl._driver来获取其驱动信息进而找到浏览器可执行文件的位置。下面是一个健壮的检测函数import os import sys from pathlib import Path def get_chromium_path(): 自动检测系统上 Playwright 安装的 Chromium 浏览器路径。 返回可执行文件的绝对路径。 try: # 方法一通过 playwright 的私有 API 获取推荐最准确 import playwright._impl._driver as driver # driver.get_driver_env() 返回一个字典包含各种路径信息 driver_env driver.get_driver_env() # 通常浏览器可执行文件在 driver_env[‘PLAYWRIGHT_BROWSERS_PATH’] 的子目录下 browsers_root driver_env.get(‘PLAYWRIGHT_BROWSERS_PATH‘) if browsers_root: # Chromium 的路径模式通常是 {browsers_root}/chromium-{version}/chrome-linux/chrome (Linux) # 或 .../chrome-win/chrome.exe (Windows), .../chrome-mac/Chromium.app/... (macOS) # 我们需要一个跨平台的方法来查找 chromium_dir_pattern ‘chromium-*‘ import glob possible_paths glob.glob(os.path.join(browsers_root, chromium_dir_pattern)) if possible_paths: # 取最新版本按文件夹名排序后的最后一个 latest_chromium_dir sorted(possible_paths)[-1] # 根据操作系统构建可执行文件路径 if sys.platform “win32“: exe_path os.path.join(latest_chromium_dir, ‘chrome-win‘, ‘chrome.exe‘) elif sys.platform “darwin“: # macOS 的路径结构不同 exe_path os.path.join(latest_chromium_dir, ‘chrome-mac‘, ‘Chromium.app‘, ‘Contents‘, ‘MacOS‘, ‘Chromium‘) else: # linux exe_path os.path.join(latest_chromium_dir, ‘chrome-linux‘, ‘chrome‘) if os.path.exists(exe_path): return exe_path except (ImportError, AttributeError, KeyError) as e: print(f“通过私有API检测失败: {e}尝试备用方法。“) # 方法二备用方案检查常见的缓存目录 common_paths [] home str(Path.home()) if sys.platform “win32“: common_paths.append(os.path.join(home, ‘AppData‘, ‘Local‘, ‘ms-playwright‘)) elif sys.platform “darwin“: common_paths.append(os.path.join(home, ‘Library‘, ‘Caches‘, ‘ms-playwright‘)) else: # linux common_paths.append(os.path.join(home, ‘.cache‘, ‘ms-playwright‘)) # 也检查当前项目目录下是否安装了浏览器通过 playwright install --path . common_paths.append(‘.‘) for base_path in common_paths: # 在 base_path 下递归查找名为 ‘chrome‘ 或 ‘chrome.exe‘ 或 ‘Chromium‘ 的可执行文件 for root, dirs, files in os.walk(base_path): for file in files: if file in [‘chrome‘, ‘chrome.exe‘, ‘Chromium‘]: full_path os.path.join(root, file) # 简单检查是否是可执行文件对于Unix还需要检查x权限 if os.path.isfile(full_path): return full_path # 如果都没找到返回 None 或抛出异常 raise FileNotFoundError(“未找到 Playwright Chromium 浏览器。请运行 ‘playwright install chromium‘ 进行安装。“) # 使用示例 try: chromium_path get_chromium_path() print(f“检测到 Chromium 路径: {chromium_path}“) except FileNotFoundError as e: print(e) sys.exit(1)4.2 在启动浏览器时使用检测到的路径获取到浏览器路径后我们在启动 Playwright 时就可以使用executable_path参数来指定而不是依赖 Playwright 自动查找虽然大多数情况下自动查找也能工作但显式指定可以避免一些环境变量问题。import asyncio from playwright.async_api import async_playwright async def main(): # 获取浏览器路径 browser_path get_chromium_path() async with async_playwright() as p: # 使用检测到的路径启动浏览器 browser await p.chromium.launch( executable_pathbrowser_path, # 关键在这里 headlessFalse, # 调试时设为 False 可以看到浏览器操作 args[‘--disable-blink-featuresAutomationControlled‘] # 可选隐藏自动化特征 ) # 创建浏览器上下文隔离会话 context await browser.new_context() page await context.new_page() # ... 调用你的登录和数据抓取函数 ... await login_to_ruoyi(page, ‘admin‘, ‘admin123‘) # ... 后续操作 ... await browser.close() if __name__ ‘__main__‘: asyncio.run(main())实操心得args[‘--disable-blink-featuresAutomationControlled‘]这个启动参数非常有用。它可以在一定程度上隐藏浏览器正在被自动化工具控制的特征比如navigator.webdriver属性。对于一些使用了简单反爬检测的网站加上这个参数可能会提高成功率。但这不是万能的更复杂的检测需要配合其他指纹伪装手段。5. 导航与数据抓取定位并解析部门表格成功登录后下一步就是导航到目标页面比如“系统管理” - “部门管理”并抓取表格数据。这里的关键在于稳健的页面等待和精确的数据提取。5.1 稳健的页面导航与元素等待不要使用time.sleep来等待页面加载。Playwright 提供了多种等待条件应该根据页面变化的具体情况来选用。async def navigate_to_department_page(page): 导航到部门管理页面 # 假设登录后主界面有一个侧边栏菜单通过点击菜单项进入 # 使用更精确的定位比如结合文本和属性 system_menu page.locator(‘.el-submenu__title:has-text(“系统管理”)‘) await system_menu.click() # 等待子菜单项出现并点击 # 注意有些菜单是鼠标悬停才展开可能需要 hover 操作 # await system_menu.hover() department_menu_item page.locator(‘.el-menu-item:has-text(“部门管理”)‘) # 等待元素可见并可点击 await department_menu_item.wait_for(state“visible“) await department_menu_item.click() # 等待部门管理页面加载完成。通常可以等待表格出现或者页面标题变化。 # 方法1等待表格的特定元素出现 await page.wait_for_selector(‘.el-table__body‘, state“attached“, timeout15000) # 方法2等待某个特定的网络请求完成如果页面是异步加载数据 # async with page.expect_response(lambda response: ‘/system/dept/list‘ in response.url) as response_info: # pass # 点击后等待这个接口响应 print(“已导航到部门管理页面。“)5.2 抓取表格数据并解析RuoYi-Vue 的前端表格组件如 Element UI 的el-table会在 DOM 中渲染出规整的结构。我们的目标是提取tbody里每一行tr中每个单元格td的文本。async def scrape_department_table(page): 抓取部门表格数据返回字典列表。 data [] # 定位到表格主体。注意Element UI的表格可能有多个tbody如汇总行我们通常需要第一个。 # 使用更具体的选择器比如结合父级容器的class table_body page.locator(‘div.el-table__body-wrapper tbody‘).first # 获取所有行 rows await table_body.locator(‘tr‘).all() for row in rows: # 获取该行所有单元格的文本内容 cells await row.locator(‘td‘).all_text_contents() # cells 是一个字符串列表例如: [‘1‘, ‘总公司‘, ‘0‘, ‘负责人‘, ‘启用‘] # 我们需要知道每一列对应的字段名。这需要事先查看页面确定。 # 假设列顺序为部门ID、部门名称、父部门ID、负责人、状态 if len(cells) 5: # 确保有足够的列 row_data { ‘dept_id‘: cells[0].strip(), ‘dept_name‘: cells[1].strip(), ‘parent_id‘: cells[2].strip(), ‘leader‘: cells[3].strip(), ‘status‘: cells[4].strip() } data.append(row_data) else: print(f“警告跳过列数不足的行: {cells}“) print(f“共抓取到 {len(data)} 条部门数据。“) return data处理分页如果表格数据有多页我们需要模拟点击分页按钮或跳转到指定页码。思路是先获取总页数然后循环每一页重复执行抓取操作。async def scrape_all_pages(page): 抓取所有分页的数据 all_data [] # 获取总页数。分页器通常有显示总页数的元素例如 ‘.el-pagination span:last-child‘ # 可能需要解析文本如 “共 100 条 10 页” total_pages_element await page.locator(‘.el-pagination__total‘).text_content() import re match re.search(r‘共\s*\d\s*条\s*(\d)\s*页‘, total_pages_element) if match: total_pages int(match.group(1)) else: # 如果无法解析先抓取第一页然后看是否有下一页按钮 total_pages 1 print(f“总页数: {total_pages}“) for current_page in range(1, total_pages 1): if current_page 1: # 如果不是第一页需要翻页 # 方法1点击具体的页码按钮 page_button page.locator(f‘.el-pager li.number:has-text(“{current_page}”)‘) # 方法2点击‘下一页’按钮更通用 # page_button page.locator(‘button.btn-next‘) await page_button.click() # 等待表格数据刷新。可以等待一个加载状态消失或者等待新的网络请求。 await page.wait_for_timeout(1000) # 简单等待生产环境应用更精确的等待 await page.wait_for_selector(‘.el-table__body‘, state“attached“) print(f“正在抓取第 {current_page} 页...“) page_data await scrape_department_table(page) all_data.extend(page_data) return all_data5.3 数据存储与导出抓取到的数据可以保存为多种格式如 JSON、CSV 或直接存入数据库。CSV 格式便于用 Excel 打开和后续处理。import csv import json def save_data_to_csv(data, filename‘departments.csv‘): 将数据列表保存为 CSV 文件 if not data: print(“没有数据可保存。“) return # 使用字典的键作为 CSV 的表头 fieldnames data[0].keys() with open(filename, ‘w‘, newline‘’, encoding‘utf-8-sig‘) as csvfile: # utf-8-sig 解决 Excel 中文乱码 writer csv.DictWriter(csvfile, fieldnamesfieldnames) writer.writeheader() writer.writerows(data) print(f“数据已保存到 {filename}“) def save_data_to_json(data, filename‘departments.json‘): 将数据列表保存为 JSON 文件 with open(filename, ‘w‘, encoding‘utf-8‘) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f“数据已保存到 {filename}“) # 在主函数中调用 async def main(): # ... 登录、导航 ... all_dept_data await scrape_all_pages(page) save_data_to_csv(all_dept_data) save_data_to_json(all_dept_data)6. 常见问题排查与脚本健壮性提升在实际运行中你肯定会遇到各种意想不到的问题。下面是我在多次实践中总结的一些常见坑点和解决方案。6.1 元素定位失败这是最常见的问题。页面结构可能随着版本更新而变化或者元素加载慢导致脚本执行时元素还不存在。排查步骤确认选择器使用浏览器的开发者工具在Console里输入document.querySelector(‘你的选择器‘)测试是否能找到元素。确保选择器是唯一的。增加等待在操作元素前使用page.wait_for_selector(selector, state“visible“)或page.wait_for_selector(selector, state“attached“)。visible要求元素可见attached只要求存在于 DOM 中。检查 iframe如果目标元素在iframe里面你需要先切换到对应的 frame。# 通过名称或选择器定位 iframe frame page.frame(name‘frameName‘) # 或 page.frame(selector‘iframe#id‘) if frame: await frame.fill(‘input‘, ‘value‘) # 在 frame 上下文中操作使用更稳健的定位器优先使用page.locator()它支持更丰富的定位方式如文本定位 (locator(‘text登录‘))、CSS 和 XPath 混合。XPath 虽然强大但可能脆弱谨慎使用。6.2 登录状态丢失或请求被拦截有时脚本运行中突然跳回登录页或者数据请求返回 403 错误。可能原因与对策Cookie/Token 过期检查系统的会话有效期。可以在登录成功后将上下文Context的存储状态保存下来下次直接加载避免重复登录需系统支持。# 保存状态 storage_state await context.storage_state(path‘state.json‘) # 下次启动时加载状态 context await browser.new_context(storage_state‘state.json‘)反爬虫机制系统可能检测到自动化特征。添加--disable-blink-featuresAutomationControlled启动参数前面已提。设置更真实的 User-Agentcontext await browser.new_context(user_agent‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...‘)减少操作频率在关键步骤间添加随机延迟await page.wait_for_timeout(random.randint(1000, 3000))模拟真人操作。网络环境问题确保脚本运行环境的网络可以稳定访问目标系统。6.3 验证码识别率低或变化频繁如果采用 OCR 方案但识别率不高图像预处理这是关键。尝试不同的预处理组合转灰度、二值化调整阈值、降噪、膨胀/腐蚀、去除边框等。OpenCV (pip install opencv-python) 是处理图像的好帮手。调整 Tesseract 参数--psm(页面分割模式) 和--oem(OCR 引擎模式) 对结果影响很大。对于单行验证码--psm 8单行文本通常有效。--oem 3是默认的基于 LSTM 的引擎。制作字库如果验证码字符集固定比如只有数字可以尝试为 Tesseract 训练专属的字库能大幅提升识别率但这需要一定的学习成本。6.4 脚本在后台服务器无图形界面运行在 Linux 服务器上通常没有图形界面浏览器必须以headlessTrue模式运行。这时手动输入验证码的方案就行不通了。解决方案切换到全自动验证码识别方案OCR 或打码平台。使用交互式输入如果必须手动可以考虑在脚本中通过某种方式如发送邮件、通知到手机将验证码图片传给操作者操作者再通过另一个接口如简单的 HTTP API将验证码回传给脚本。这比较复杂。预先获取有效的会话在本地有图形界面的环境中登录一次保存storage_state如上文所述然后将state.json文件复制到服务器。只要会话不过期服务器上的脚本就可以直接使用这个状态打开页面无需再次登录。这是最推荐的方法但需要注意会话有效期。6.5 性能优化与超时控制当抓取大量页面数据时需要优化脚本性能并处理好超时。合理设置超时在创建browser、context、page以及执行wait_for_*时设置合理的timeout参数单位毫秒。避免因网络慢导致脚本无限等待。browser await p.chromium.launch(timeout30000) # 浏览器启动超时 await page.wait_for_selector(‘.table‘, timeout15000) # 等待元素超时并行处理如果任务可以拆分例如抓取多个独立模块的数据可以考虑使用asyncio.gather创建多个 page 或 context 并行处理。但要注意目标服务器是否能承受并发压力。资源清理确保在脚本结束或异常时正确关闭 browser 和 context释放资源。使用async with语句块可以自动管理。7. 完整脚本整合与部署建议将上述所有模块整合起来形成一个完整的、可配置的脚本。下面是一个整合后的示例框架import asyncio import json import sys from pathlib import Path import os from playwright.async_api import async_playwright # 导入我们之前写的工具函数 from browser_detector import get_chromium_path from ruoyi_login import login_to_ruoyi from department_scraper import navigate_to_department_page, scrape_all_pages from data_saver import save_data_to_csv async def main(config): 主流程函数 config: 配置字典包含 url, username, password 等 # 1. 获取浏览器路径 try: browser_path get_chromium_path() except FileNotFoundError as e: print(f“致命错误: {e}“) sys.exit(1) # 2. 启动浏览器 async with async_playwright() as p: browser await p.chromium.launch( executable_pathbrowser_path, headlessconfig.get(‘headless‘, False), # 可从配置读取 args[‘--disable-blink-featuresAutomationControlled‘] ) # 创建上下文可设置视窗大小、User-Agent等 context await browser.new_context( viewport{‘width‘: 1920, ‘height‘: 1080}, user_agent‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...‘ ) page await context.new_page() try: # 3. 登录 login_success await login_to_ruoyi( page, config[‘username‘], config[‘password‘], config.get(‘login_url‘) ) if not login_success: print(“登录失败程序退出。“) return # 4. 导航到目标页面 await navigate_to_department_page(page) # 5. 抓取所有数据 all_data await scrape_all_pages(page) # 6. 保存数据 output_file config.get(‘output_file‘, ‘departments.csv‘) save_data_to_csv(all_data, output_file) print(f“任务完成数据已保存至 {output_file}“) except Exception as e: print(f“程序运行过程中发生错误: {e}“) # 可以在这里截图方便调试 await page.screenshot(path‘error_screenshot.png‘, full_pageTrue) print(“错误截图已保存为 ‘error_screenshot.png‘。“) finally: # 7. 清理资源 await browser.close() if __name__ ‘__main__‘: # 加载配置可以从配置文件、环境变量或命令行参数读取 config { ‘login_url‘: ‘http://your-ruoyi-system.com/login‘, ‘username‘: ‘your_username‘, # 建议从环境变量读取避免硬编码密码 ‘password‘: ‘your_password‘, ‘headless‘: True, # 生产环境建议设为 True ‘output_file‘: ‘./output/department_list.csv‘ } # 确保输出目录存在 os.makedirs(os.path.dirname(config[‘output_file‘]), exist_okTrue) asyncio.run(main(config))部署建议配置管理切勿将用户名、密码、URL 等敏感信息硬编码在脚本中。使用配置文件如config.yaml、.env文件或环境变量来管理。日志记录使用 Python 的logging模块替代print可以输出不同级别INFO, WARNING, ERROR的日志到文件方便问题追踪。异常处理与重试对于网络请求等可能失败的操作添加重试机制如tenacity库。定时任务在 Linux 服务器上可以使用cron定时执行脚本在 Windows 上可以使用“任务计划程序”。容器化考虑使用 Docker 将整个环境Python、Playwright、浏览器打包成一个镜像可以确保在任何地方运行环境一致。需要注意在 Docker 中安装 Playwright 的浏览器可能需要额外的系统依赖。这个项目从手动操作到自动化脚本不仅节省了大量重复劳动时间更重要的是建立了一套稳定、可复用的数据采集流程。过程中最深的体会是自动化脚本的健壮性远比功能丰富性重要。一个能处理各种边界情况、优雅失败并给出明确日志的脚本才是真正能在生产环境跑起来的工具。下次如果 RuoYi-Vue 系统升级导致页面结构变了我只需要调整相应的元素选择器整个流程的骨架依然可以快速复用。
基于Playwright的RuoYi-Vue系统自动化登录与数据抓取实战
1. 项目概述与核心价值最近在做一个内部数据整合的项目需要从公司一个基于 RuoYi-Vue 框架开发的后台管理系统里定期拉取最新的部门组织架构数据。这个系统本身有完善的登录验证机制包括账号密码和图形验证码而且前端还做了防自动化程序的检测。手动登录、截图、复制粘贴表格数据一次两次还行但每天都要搞效率太低还容易出错。于是我就琢磨着用 Python 写个脚本让它能像真人一样自动完成整个登录和数据抓取流程。为什么选 Playwright这得从几个痛点说起。之前也试过 Selenium但它在处理现代前端框架比如 Vue.js的动态渲染、等待页面稳定方面总感觉有点“力不从心”特别是面对一些基于事件驱动的交互等待策略写起来很繁琐。而 Playwright 是微软开源的天生对 Chromium、Firefox、WebKit 三大浏览器引擎支持得非常好它的auto-wait机制特别聪明能自动等待元素可操作、网络请求完成大大减少了我们写显式等待time.sleep或WebDriverWait的代码量。更关键的是Playwright 能更真实地模拟浏览器环境对反爬虫措施比如一些简单的“安全服务防护”检测的绕过能力更强这正是登录 RuoYi-Vue 这类系统所需要的。这个脚本的核心目标很明确第一全自动处理 RuoYi-Vue 的登录流程包括定位账号密码输入框、识别并输入图形验证码这里我们先采用手动输入的方式后面会讨论更自动化的思路、点击登录按钮。第二成功登录后自动导航到部门管理页面并准确抓取表格中的部门数据。第三整个过程要足够健壮能适应不同环境所以我加入了“系统浏览器路径自动检测”的功能让脚本在不同机器上都能无缝运行无需手动指定复杂的浏览器驱动路径。2. 环境准备与 Playwright 核心机制解析工欲善其事必先利其器。在开始写代码之前我们需要先把环境搭好。这里我强烈建议使用 Python 的虚拟环境venv来管理依赖避免和系统全局的 Python 包产生冲突。2.1 创建虚拟环境与安装依赖首先打开你的终端或命令行进入项目目录执行以下命令创建并激活虚拟环境# 创建名为 venv 的虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活后命令行提示符前面通常会显示(venv)表示你已经在这个独立的环境中了。接下来安装核心库 Playwright。Playwright 的安装分为两部分Python 客户端库和实际的浏览器二进制文件。# 安装 Playwright 的 Python 客户端库 pip install playwright # 安装 Playwright 所需的浏览器Chromium, Firefox, WebKit。我们主要用 Chromium。 # 这个命令会下载浏览器可能需要一些时间。 playwright install chromium这里有个小技巧playwright install命令默认会安装到用户目录下的缓存文件夹里例如~/.cache/ms-playwright/。这样做的好处是浏览器二进制文件与你的项目代码分离便于管理和更新。我们后续的“自动检测”功能就是去这个标准路径里找浏览器。2.2 Playwright 的核心优势与工作原理在写代码前理解 Playwright 怎么工作的能让你避开很多坑。它和 Selenium 最大的不同在于架构。Selenium 通过一个 WebDriver 协议与浏览器通信而 Playwright 则通过更底层的 DevTools Protocol (CDP) 直接与浏览器内核对话这使得它的控制更精细、性能更好。自动等待Auto-waiting这是 Playwright 的杀手级特性。当你执行page.click(‘button#submit’)时Playwright 内部会做一系列检查这个按钮在 DOM 里存在吗它是否可见它是否可点击没有disabled属性它没有被其他元素遮挡吧只有所有这些条件都满足它才会执行点击操作。这意味着你基本不用写time.sleep或复杂的WebDriverWait了代码简洁又可靠。浏览器上下文Browser Context你可以把它理解为一个独立的“隐身会话”。每个 Context 拥有独立的 cookies、localStorage 和会话状态。在我们的场景里登录成功后这个登录状态通常是 Cookie 或 Token就保存在这个 Context 里。后续所有在这个 Context 里打开的页面都会自动携带这个登录状态无需重复登录。这比直接用browser.new_page()更清晰也便于管理多个并行任务。网络拦截与模拟Network InterceptionPlaywright 可以监听和修改页面发出的所有网络请求。这对于处理验证码很有用。比如有些验证码图片是通过一个特定 API 接口获取的我们可以拦截这个请求拿到图片数据然后调用第三方 OCR 服务识别。不过RuoYi-Vue 自带的验证码通常比较简单我们第一步先采用手动输入后面再探讨自动化方案。3. 核心功能实现自动登录与验证码处理登录是自动化的第一道关卡也是最容易出问题的地方。RuoYi-Vue 的登录页面通常包含用户名、密码、验证码输入框和一个登录按钮。我们的脚本需要准确地找到它们并填入正确的信息。3.1 定位页面元素与输入信息首先我们需要用浏览器打开登录页面并分析页面结构。使用 Playwright 的 Codegen 工具可以快速生成定位代码。在终端运行playwright codegen http://your-ruoyi-system.com/login这会打开一个浏览器窗口和一个录制器。你在页面上手动操作输入、点击录制器会自动生成对应的 Python 代码。这是一个非常好的起点。假设通过分析我们得到以下元素选择器用户名输入框input[name‘username’]或#username密码输入框input[name‘password’]或#password验证码输入框input[name‘code’]或#validateCode验证码图片img#captchaImg用于获取图片可能需要右键检查元素确认其src属性登录按钮button[type‘submit’]或//button[contains(text(), ‘登录’)]基于此编写登录函数的核心代码如下async def login_to_ruoyi(page, username, password): 登录到 RuoYi-Vue 系统 # 导航到登录页 await page.goto(‘http://your-ruoyi-system.com/login‘) # 等待关键元素加载完成 # 使用 page.wait_for_selector 确保元素在DOM中稳定 await page.wait_for_selector(‘input[name“username”]‘) # 输入用户名和密码 await page.fill(‘input[name“username”]‘, username) await page.fill(‘input[name“password”]‘, password) # 处理验证码 - 方案一手动输入 # 首先确保验证码图片加载出来 captcha_selector ‘img#captchaImg‘ await page.wait_for_selector(captcha_selector) # 将验证码图片截图保存到本地方便人工查看 captcha_element await page.query_selector(captcha_selector) await captcha_element.screenshot(path‘./captcha.png‘) print(“验证码图片已保存为 ‘captcha.png‘请打开查看并输入。”) captcha_code input(“请输入验证码: “).strip() # 输入验证码 await page.fill(‘input[name“code”]‘, captcha_code) # 点击登录按钮 login_button page.locator(‘button:has-text(“登录”)‘) # 使用文本定位更健壮 await login_button.click() # 等待登录完成通常可以等待某个登录后才会出现的元素比如用户头像或导航菜单 try: # 假设登录成功后右上角会出现用户名的span await page.wait_for_selector(‘.el-dropdown-link span‘, timeout10000) print(“登录成功”) return True except Exception as e: # 登录失败可能是验证码错误 print(f“登录可能失败: {e}“) # 可以在这里捕获页面上的错误提示信息 error_msg await page.locator(‘.el-message--error‘).text_content() if error_msg: print(f“错误信息: {error_msg}“) return False注意元素选择器 (input[name...],#id,.class) 必须根据你目标系统的实际 HTML 结构进行调整。使用浏览器的开发者工具F12的“检查”功能仔细查看元素的id、name、class等属性选择唯一且稳定的选择器。避免使用可能随页面刷新变化的动态类名或ID。3.2 验证码处理策略与优化手动输入验证码显然不是全自动化的终极方案但它是最稳定、最通用的起点。很多企业内部系统的验证码并不复杂或者有后门比如固定的测试验证码。在实现更复杂的方案前先用手动方式跑通整个流程是明智的。如何向全自动迈进有几个方向可以考虑OCR 识别对于简单的数字、字母验证码可以使用开源的 Tesseract OCR 库。你需要先将验证码图片进行预处理灰度化、二值化、去噪再交给 Tesseract 识别。成功率取决于验证码的干扰程度。# 示例使用 pytesseract (需要先安装 Tesseract-OCR 引擎和 pytesseract 包) # pip install pytesseract pillow from PIL import Image import pytesseract # ... 截图保存为 captcha.png ... image Image.open(‘./captcha.png‘) # 预处理图像根据实际情况调整 image image.convert(‘L‘) # 灰度化 # 二值化等操作... captcha_text pytesseract.image_to_string(image, config‘--psm 8 --oem 3 -c tessedit_char_whitelist0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ‘) print(f“OCR识别结果: {captcha_text}“)机器学习/深度学习对于更复杂的验证码可以训练一个简单的 CNN 模型。但这需要大量的标注数据成本较高更适合长期、大规模的应用场景。接口绕过有些系统的验证码是在后端生成一个 token 或答案前端只是展示。通过抓包分析登录请求你可能会发现验证码的答案直接包含在某个请求参数或响应里。如果能找到这个规律就可以完全绕过前端的识别。但这需要谨慎并确保你有权进行此类操作。打码平台付费调用第三方打码平台的 API由人工进行识别。这是最省事但需要成本的方法适合验证码非常复杂或变化频繁的情况。我的建议是在项目初期优先使用手动输入或简单的 OCR。把主要精力放在登录后的页面导航和数据抓取逻辑的稳定性上。等核心流程稳定后再根据验证码的实际难度和项目需求决定是否投入精力做全自动识别。4. 系统浏览器路径自动检测的实现让脚本能在不同开发、测试、生产环境上无缝运行是提升工程化水平的关键一步。我们不想每次换台机器都要去修改代码里硬编码的浏览器路径。Playwright 安装的浏览器通常在一个固定的缓存目录我们可以通过编程方式找到它。4.1 探测 Playwright 安装的浏览器Playwright 的 Python 库提供了一个内部模块playwright._impl._driver来获取其驱动信息进而找到浏览器可执行文件的位置。下面是一个健壮的检测函数import os import sys from pathlib import Path def get_chromium_path(): 自动检测系统上 Playwright 安装的 Chromium 浏览器路径。 返回可执行文件的绝对路径。 try: # 方法一通过 playwright 的私有 API 获取推荐最准确 import playwright._impl._driver as driver # driver.get_driver_env() 返回一个字典包含各种路径信息 driver_env driver.get_driver_env() # 通常浏览器可执行文件在 driver_env[‘PLAYWRIGHT_BROWSERS_PATH’] 的子目录下 browsers_root driver_env.get(‘PLAYWRIGHT_BROWSERS_PATH‘) if browsers_root: # Chromium 的路径模式通常是 {browsers_root}/chromium-{version}/chrome-linux/chrome (Linux) # 或 .../chrome-win/chrome.exe (Windows), .../chrome-mac/Chromium.app/... (macOS) # 我们需要一个跨平台的方法来查找 chromium_dir_pattern ‘chromium-*‘ import glob possible_paths glob.glob(os.path.join(browsers_root, chromium_dir_pattern)) if possible_paths: # 取最新版本按文件夹名排序后的最后一个 latest_chromium_dir sorted(possible_paths)[-1] # 根据操作系统构建可执行文件路径 if sys.platform “win32“: exe_path os.path.join(latest_chromium_dir, ‘chrome-win‘, ‘chrome.exe‘) elif sys.platform “darwin“: # macOS 的路径结构不同 exe_path os.path.join(latest_chromium_dir, ‘chrome-mac‘, ‘Chromium.app‘, ‘Contents‘, ‘MacOS‘, ‘Chromium‘) else: # linux exe_path os.path.join(latest_chromium_dir, ‘chrome-linux‘, ‘chrome‘) if os.path.exists(exe_path): return exe_path except (ImportError, AttributeError, KeyError) as e: print(f“通过私有API检测失败: {e}尝试备用方法。“) # 方法二备用方案检查常见的缓存目录 common_paths [] home str(Path.home()) if sys.platform “win32“: common_paths.append(os.path.join(home, ‘AppData‘, ‘Local‘, ‘ms-playwright‘)) elif sys.platform “darwin“: common_paths.append(os.path.join(home, ‘Library‘, ‘Caches‘, ‘ms-playwright‘)) else: # linux common_paths.append(os.path.join(home, ‘.cache‘, ‘ms-playwright‘)) # 也检查当前项目目录下是否安装了浏览器通过 playwright install --path . common_paths.append(‘.‘) for base_path in common_paths: # 在 base_path 下递归查找名为 ‘chrome‘ 或 ‘chrome.exe‘ 或 ‘Chromium‘ 的可执行文件 for root, dirs, files in os.walk(base_path): for file in files: if file in [‘chrome‘, ‘chrome.exe‘, ‘Chromium‘]: full_path os.path.join(root, file) # 简单检查是否是可执行文件对于Unix还需要检查x权限 if os.path.isfile(full_path): return full_path # 如果都没找到返回 None 或抛出异常 raise FileNotFoundError(“未找到 Playwright Chromium 浏览器。请运行 ‘playwright install chromium‘ 进行安装。“) # 使用示例 try: chromium_path get_chromium_path() print(f“检测到 Chromium 路径: {chromium_path}“) except FileNotFoundError as e: print(e) sys.exit(1)4.2 在启动浏览器时使用检测到的路径获取到浏览器路径后我们在启动 Playwright 时就可以使用executable_path参数来指定而不是依赖 Playwright 自动查找虽然大多数情况下自动查找也能工作但显式指定可以避免一些环境变量问题。import asyncio from playwright.async_api import async_playwright async def main(): # 获取浏览器路径 browser_path get_chromium_path() async with async_playwright() as p: # 使用检测到的路径启动浏览器 browser await p.chromium.launch( executable_pathbrowser_path, # 关键在这里 headlessFalse, # 调试时设为 False 可以看到浏览器操作 args[‘--disable-blink-featuresAutomationControlled‘] # 可选隐藏自动化特征 ) # 创建浏览器上下文隔离会话 context await browser.new_context() page await context.new_page() # ... 调用你的登录和数据抓取函数 ... await login_to_ruoyi(page, ‘admin‘, ‘admin123‘) # ... 后续操作 ... await browser.close() if __name__ ‘__main__‘: asyncio.run(main())实操心得args[‘--disable-blink-featuresAutomationControlled‘]这个启动参数非常有用。它可以在一定程度上隐藏浏览器正在被自动化工具控制的特征比如navigator.webdriver属性。对于一些使用了简单反爬检测的网站加上这个参数可能会提高成功率。但这不是万能的更复杂的检测需要配合其他指纹伪装手段。5. 导航与数据抓取定位并解析部门表格成功登录后下一步就是导航到目标页面比如“系统管理” - “部门管理”并抓取表格数据。这里的关键在于稳健的页面等待和精确的数据提取。5.1 稳健的页面导航与元素等待不要使用time.sleep来等待页面加载。Playwright 提供了多种等待条件应该根据页面变化的具体情况来选用。async def navigate_to_department_page(page): 导航到部门管理页面 # 假设登录后主界面有一个侧边栏菜单通过点击菜单项进入 # 使用更精确的定位比如结合文本和属性 system_menu page.locator(‘.el-submenu__title:has-text(“系统管理”)‘) await system_menu.click() # 等待子菜单项出现并点击 # 注意有些菜单是鼠标悬停才展开可能需要 hover 操作 # await system_menu.hover() department_menu_item page.locator(‘.el-menu-item:has-text(“部门管理”)‘) # 等待元素可见并可点击 await department_menu_item.wait_for(state“visible“) await department_menu_item.click() # 等待部门管理页面加载完成。通常可以等待表格出现或者页面标题变化。 # 方法1等待表格的特定元素出现 await page.wait_for_selector(‘.el-table__body‘, state“attached“, timeout15000) # 方法2等待某个特定的网络请求完成如果页面是异步加载数据 # async with page.expect_response(lambda response: ‘/system/dept/list‘ in response.url) as response_info: # pass # 点击后等待这个接口响应 print(“已导航到部门管理页面。“)5.2 抓取表格数据并解析RuoYi-Vue 的前端表格组件如 Element UI 的el-table会在 DOM 中渲染出规整的结构。我们的目标是提取tbody里每一行tr中每个单元格td的文本。async def scrape_department_table(page): 抓取部门表格数据返回字典列表。 data [] # 定位到表格主体。注意Element UI的表格可能有多个tbody如汇总行我们通常需要第一个。 # 使用更具体的选择器比如结合父级容器的class table_body page.locator(‘div.el-table__body-wrapper tbody‘).first # 获取所有行 rows await table_body.locator(‘tr‘).all() for row in rows: # 获取该行所有单元格的文本内容 cells await row.locator(‘td‘).all_text_contents() # cells 是一个字符串列表例如: [‘1‘, ‘总公司‘, ‘0‘, ‘负责人‘, ‘启用‘] # 我们需要知道每一列对应的字段名。这需要事先查看页面确定。 # 假设列顺序为部门ID、部门名称、父部门ID、负责人、状态 if len(cells) 5: # 确保有足够的列 row_data { ‘dept_id‘: cells[0].strip(), ‘dept_name‘: cells[1].strip(), ‘parent_id‘: cells[2].strip(), ‘leader‘: cells[3].strip(), ‘status‘: cells[4].strip() } data.append(row_data) else: print(f“警告跳过列数不足的行: {cells}“) print(f“共抓取到 {len(data)} 条部门数据。“) return data处理分页如果表格数据有多页我们需要模拟点击分页按钮或跳转到指定页码。思路是先获取总页数然后循环每一页重复执行抓取操作。async def scrape_all_pages(page): 抓取所有分页的数据 all_data [] # 获取总页数。分页器通常有显示总页数的元素例如 ‘.el-pagination span:last-child‘ # 可能需要解析文本如 “共 100 条 10 页” total_pages_element await page.locator(‘.el-pagination__total‘).text_content() import re match re.search(r‘共\s*\d\s*条\s*(\d)\s*页‘, total_pages_element) if match: total_pages int(match.group(1)) else: # 如果无法解析先抓取第一页然后看是否有下一页按钮 total_pages 1 print(f“总页数: {total_pages}“) for current_page in range(1, total_pages 1): if current_page 1: # 如果不是第一页需要翻页 # 方法1点击具体的页码按钮 page_button page.locator(f‘.el-pager li.number:has-text(“{current_page}”)‘) # 方法2点击‘下一页’按钮更通用 # page_button page.locator(‘button.btn-next‘) await page_button.click() # 等待表格数据刷新。可以等待一个加载状态消失或者等待新的网络请求。 await page.wait_for_timeout(1000) # 简单等待生产环境应用更精确的等待 await page.wait_for_selector(‘.el-table__body‘, state“attached“) print(f“正在抓取第 {current_page} 页...“) page_data await scrape_department_table(page) all_data.extend(page_data) return all_data5.3 数据存储与导出抓取到的数据可以保存为多种格式如 JSON、CSV 或直接存入数据库。CSV 格式便于用 Excel 打开和后续处理。import csv import json def save_data_to_csv(data, filename‘departments.csv‘): 将数据列表保存为 CSV 文件 if not data: print(“没有数据可保存。“) return # 使用字典的键作为 CSV 的表头 fieldnames data[0].keys() with open(filename, ‘w‘, newline‘’, encoding‘utf-8-sig‘) as csvfile: # utf-8-sig 解决 Excel 中文乱码 writer csv.DictWriter(csvfile, fieldnamesfieldnames) writer.writeheader() writer.writerows(data) print(f“数据已保存到 {filename}“) def save_data_to_json(data, filename‘departments.json‘): 将数据列表保存为 JSON 文件 with open(filename, ‘w‘, encoding‘utf-8‘) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f“数据已保存到 {filename}“) # 在主函数中调用 async def main(): # ... 登录、导航 ... all_dept_data await scrape_all_pages(page) save_data_to_csv(all_dept_data) save_data_to_json(all_dept_data)6. 常见问题排查与脚本健壮性提升在实际运行中你肯定会遇到各种意想不到的问题。下面是我在多次实践中总结的一些常见坑点和解决方案。6.1 元素定位失败这是最常见的问题。页面结构可能随着版本更新而变化或者元素加载慢导致脚本执行时元素还不存在。排查步骤确认选择器使用浏览器的开发者工具在Console里输入document.querySelector(‘你的选择器‘)测试是否能找到元素。确保选择器是唯一的。增加等待在操作元素前使用page.wait_for_selector(selector, state“visible“)或page.wait_for_selector(selector, state“attached“)。visible要求元素可见attached只要求存在于 DOM 中。检查 iframe如果目标元素在iframe里面你需要先切换到对应的 frame。# 通过名称或选择器定位 iframe frame page.frame(name‘frameName‘) # 或 page.frame(selector‘iframe#id‘) if frame: await frame.fill(‘input‘, ‘value‘) # 在 frame 上下文中操作使用更稳健的定位器优先使用page.locator()它支持更丰富的定位方式如文本定位 (locator(‘text登录‘))、CSS 和 XPath 混合。XPath 虽然强大但可能脆弱谨慎使用。6.2 登录状态丢失或请求被拦截有时脚本运行中突然跳回登录页或者数据请求返回 403 错误。可能原因与对策Cookie/Token 过期检查系统的会话有效期。可以在登录成功后将上下文Context的存储状态保存下来下次直接加载避免重复登录需系统支持。# 保存状态 storage_state await context.storage_state(path‘state.json‘) # 下次启动时加载状态 context await browser.new_context(storage_state‘state.json‘)反爬虫机制系统可能检测到自动化特征。添加--disable-blink-featuresAutomationControlled启动参数前面已提。设置更真实的 User-Agentcontext await browser.new_context(user_agent‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...‘)减少操作频率在关键步骤间添加随机延迟await page.wait_for_timeout(random.randint(1000, 3000))模拟真人操作。网络环境问题确保脚本运行环境的网络可以稳定访问目标系统。6.3 验证码识别率低或变化频繁如果采用 OCR 方案但识别率不高图像预处理这是关键。尝试不同的预处理组合转灰度、二值化调整阈值、降噪、膨胀/腐蚀、去除边框等。OpenCV (pip install opencv-python) 是处理图像的好帮手。调整 Tesseract 参数--psm(页面分割模式) 和--oem(OCR 引擎模式) 对结果影响很大。对于单行验证码--psm 8单行文本通常有效。--oem 3是默认的基于 LSTM 的引擎。制作字库如果验证码字符集固定比如只有数字可以尝试为 Tesseract 训练专属的字库能大幅提升识别率但这需要一定的学习成本。6.4 脚本在后台服务器无图形界面运行在 Linux 服务器上通常没有图形界面浏览器必须以headlessTrue模式运行。这时手动输入验证码的方案就行不通了。解决方案切换到全自动验证码识别方案OCR 或打码平台。使用交互式输入如果必须手动可以考虑在脚本中通过某种方式如发送邮件、通知到手机将验证码图片传给操作者操作者再通过另一个接口如简单的 HTTP API将验证码回传给脚本。这比较复杂。预先获取有效的会话在本地有图形界面的环境中登录一次保存storage_state如上文所述然后将state.json文件复制到服务器。只要会话不过期服务器上的脚本就可以直接使用这个状态打开页面无需再次登录。这是最推荐的方法但需要注意会话有效期。6.5 性能优化与超时控制当抓取大量页面数据时需要优化脚本性能并处理好超时。合理设置超时在创建browser、context、page以及执行wait_for_*时设置合理的timeout参数单位毫秒。避免因网络慢导致脚本无限等待。browser await p.chromium.launch(timeout30000) # 浏览器启动超时 await page.wait_for_selector(‘.table‘, timeout15000) # 等待元素超时并行处理如果任务可以拆分例如抓取多个独立模块的数据可以考虑使用asyncio.gather创建多个 page 或 context 并行处理。但要注意目标服务器是否能承受并发压力。资源清理确保在脚本结束或异常时正确关闭 browser 和 context释放资源。使用async with语句块可以自动管理。7. 完整脚本整合与部署建议将上述所有模块整合起来形成一个完整的、可配置的脚本。下面是一个整合后的示例框架import asyncio import json import sys from pathlib import Path import os from playwright.async_api import async_playwright # 导入我们之前写的工具函数 from browser_detector import get_chromium_path from ruoyi_login import login_to_ruoyi from department_scraper import navigate_to_department_page, scrape_all_pages from data_saver import save_data_to_csv async def main(config): 主流程函数 config: 配置字典包含 url, username, password 等 # 1. 获取浏览器路径 try: browser_path get_chromium_path() except FileNotFoundError as e: print(f“致命错误: {e}“) sys.exit(1) # 2. 启动浏览器 async with async_playwright() as p: browser await p.chromium.launch( executable_pathbrowser_path, headlessconfig.get(‘headless‘, False), # 可从配置读取 args[‘--disable-blink-featuresAutomationControlled‘] ) # 创建上下文可设置视窗大小、User-Agent等 context await browser.new_context( viewport{‘width‘: 1920, ‘height‘: 1080}, user_agent‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...‘ ) page await context.new_page() try: # 3. 登录 login_success await login_to_ruoyi( page, config[‘username‘], config[‘password‘], config.get(‘login_url‘) ) if not login_success: print(“登录失败程序退出。“) return # 4. 导航到目标页面 await navigate_to_department_page(page) # 5. 抓取所有数据 all_data await scrape_all_pages(page) # 6. 保存数据 output_file config.get(‘output_file‘, ‘departments.csv‘) save_data_to_csv(all_data, output_file) print(f“任务完成数据已保存至 {output_file}“) except Exception as e: print(f“程序运行过程中发生错误: {e}“) # 可以在这里截图方便调试 await page.screenshot(path‘error_screenshot.png‘, full_pageTrue) print(“错误截图已保存为 ‘error_screenshot.png‘。“) finally: # 7. 清理资源 await browser.close() if __name__ ‘__main__‘: # 加载配置可以从配置文件、环境变量或命令行参数读取 config { ‘login_url‘: ‘http://your-ruoyi-system.com/login‘, ‘username‘: ‘your_username‘, # 建议从环境变量读取避免硬编码密码 ‘password‘: ‘your_password‘, ‘headless‘: True, # 生产环境建议设为 True ‘output_file‘: ‘./output/department_list.csv‘ } # 确保输出目录存在 os.makedirs(os.path.dirname(config[‘output_file‘]), exist_okTrue) asyncio.run(main(config))部署建议配置管理切勿将用户名、密码、URL 等敏感信息硬编码在脚本中。使用配置文件如config.yaml、.env文件或环境变量来管理。日志记录使用 Python 的logging模块替代print可以输出不同级别INFO, WARNING, ERROR的日志到文件方便问题追踪。异常处理与重试对于网络请求等可能失败的操作添加重试机制如tenacity库。定时任务在 Linux 服务器上可以使用cron定时执行脚本在 Windows 上可以使用“任务计划程序”。容器化考虑使用 Docker 将整个环境Python、Playwright、浏览器打包成一个镜像可以确保在任何地方运行环境一致。需要注意在 Docker 中安装 Playwright 的浏览器可能需要额外的系统依赖。这个项目从手动操作到自动化脚本不仅节省了大量重复劳动时间更重要的是建立了一套稳定、可复用的数据采集流程。过程中最深的体会是自动化脚本的健壮性远比功能丰富性重要。一个能处理各种边界情况、优雅失败并给出明确日志的脚本才是真正能在生产环境跑起来的工具。下次如果 RuoYi-Vue 系统升级导致页面结构变了我只需要调整相应的元素选择器整个流程的骨架依然可以快速复用。