commit 556fec602adabc5e83714e045605dff6145fe43f Author: houhuan Date: Sat Apr 18 14:42:46 2026 +0800 Initial commit: Report automation script using Playwright diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61333c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Logs +*.log + +# Screenshots +*.png + +# Downloads folder +downloads/ + +# Environment variables +.env diff --git a/docs/ReportAutomation/ALIGNMENT_ReportAutomation.md b/docs/ReportAutomation/ALIGNMENT_ReportAutomation.md new file mode 100644 index 0000000..84d84b8 --- /dev/null +++ b/docs/ReportAutomation/ALIGNMENT_ReportAutomation.md @@ -0,0 +1,35 @@ +# ALIGNMENT - 报表自动化任务 + +## 1. 原始需求 +自动化完成从 `secsion.com` 导出报表并上传至 `94kan.cn` 的全流程。 + +## 2. 任务特性规范 +- **技术栈**: Python + Playwright (由于涉及复杂的浏览器交互、HTTPS证书处理、文件下载和上传,Playwright 是最佳选择)。 +- **运行环境**: Windows 操作系统。 +- **目标网站 1**: `https://secsion.com:8000/` (B端电商店铺类后台管理)。 +- **目标网站 2**: `https://sale.94kan.cn/` (销售数据分析器)。 + +## 3. 需求理解与边界确认 +### 3.1 任务流程 +1. **登录阶段**: 访问登录页 -> 选择角色"店铺" -> 输入账号密码 -> 登录。 +2. **报表导出阶段**: 访问统计页 -> 设置日期 (2026-04-18) -> 导出报表。 +3. **报表上传阶段**: 访问上传页 -> 上传下载的文件 -> 等待结果。 + +### 3.2 边界与限制 +- **HTTPS 证书**: 必须忽略证书错误或自动点击继续。 +- **文件下载**: 需捕获动态下载的文件并保存到指定路径。 +- **文件上传**: 需定位隐藏的 input 元素或直接操作上传。 +- **登录状态**: 初始实现为单次任务流,后续可考虑持久化 Context。 + +## 4. 疑问澄清与智能决策 +- **Q**: 网站访问可能由于网络限制失败? +- **A**: 使用浏览器自动化直接访问,不通过后端 API 库,通常能绕过简单的跨域或内网限制。 +- **Q**: 证书警告处理? +- **A**: Playwright 的 `browser_context(ignore_https_errors=True)` 可全局解决。 +- **Q**: 文件名动态变化? +- **A**: 使用 Playwright 的 `expect_download()` 监听器获取真实文件名。 + +## 5. 验收标准 +- [ ] 成功下载以 `.xlsx` 结尾的报表文件。 +- [ ] 成功在 `94kan.cn` 触发上传动作。 +- [ ] 控制台输出清晰的操作日志。 diff --git a/docs/ReportAutomation/CONSENSUS_ReportAutomation.md b/docs/ReportAutomation/CONSENSUS_ReportAutomation.md new file mode 100644 index 0000000..3c50343 --- /dev/null +++ b/docs/ReportAutomation/CONSENSUS_ReportAutomation.md @@ -0,0 +1,24 @@ +# CONSENSUS - 报表自动化任务共识 + +## 1. 明确的需求描述 +开发一个 Python 脚本,实现以下自动化操作: +1. 登录 `secsion.com:8000`。 +2. 导出 2026-04-18 的商品统计报表。 +3. 将导出的报表上传至 `sale.94kan.cn`。 + +## 2. 技术实现方案 +- **框架**: Playwright for Python。 +- **模式**: Headed 模式(方便调试)或 Headless 模式(正式运行)。 +- **证书处理**: 配置 `ignore_https_errors=True`。 +- **下载逻辑**: 监听 `download` 事件。 +- **上传逻辑**: 定位上传按钮对应的 `input[type="file"]`。 + +## 3. 技术约束与集成方案 +- 账号:`15682076681` / 密码:`123456`。 +- 角色选择:必须点击包含“店铺”文本的选项。 +- 日期选择:填充两个日期选择器为 `2026-04-18`。 + +## 4. 验收标准 +- 脚本运行无报错。 +- 最终在 `94kan.cn` 看到“处理中”或“上传成功”的提示。 +- 本地保存一份下载的 Excel 文件副本。 diff --git a/docs/ReportAutomation/DESIGN_ReportAutomation.md b/docs/ReportAutomation/DESIGN_ReportAutomation.md new file mode 100644 index 0000000..0235ae6 --- /dev/null +++ b/docs/ReportAutomation/DESIGN_ReportAutomation.md @@ -0,0 +1,42 @@ +# DESIGN - 报表自动化架构设计 + +## 1. 整体架构 +```mermaid +graph TD + A[Main Script] --> B[Browser Manager] + A --> C[Secsion Service] + A --> D[Upload Service] + B --> E[Playwright Context] + C --> E + D --> E +``` + +## 2. 核心组件说明 +- **Browser Manager**: 初始化 Playwright,配置 `ignore_https_errors`,管理浏览器生命周期。 +- **Secsion Service**: + - `login()`: 处理角色选择、账号密码输入、登录按钮点击。 + - `export_report(date)`: 处理日期填写、点击导出、捕获下载流。 +- **Upload Service**: + - `upload_file(file_path)`: 处理 94kan.cn 的文件上传。 + +## 3. 数据流向 +1. 启动浏览器 -> 建立 Context。 +2. `Secsion Service` 登录 -> 获取报表下载对象。 +3. 保存文件至 `temp_downloads/` 目录。 +4. `Upload Service` 读取该文件并上传。 +5. 关闭浏览器。 + +## 4. 异常处理策略 +- **超时处理**: 设置全局等待超时(30s)。 +- **重试机制**: 对关键点击动作进行简单重试。 +- **日志记录**: 使用 Python `logging` 模块记录每一步状态。 + +## 5. 接口契约 (伪代码) +```python +class SecsionService: + def login(page, username, password): ... + def export_report(page, date_str) -> str: ... # returns file path + +class UploadService: + def upload(page, file_path): ... +``` diff --git a/docs/ReportAutomation/FINAL_ReportAutomation.md b/docs/ReportAutomation/FINAL_ReportAutomation.md new file mode 100644 index 0000000..2767061 --- /dev/null +++ b/docs/ReportAutomation/FINAL_ReportAutomation.md @@ -0,0 +1,25 @@ +# FINAL - 报表自动化项目总结 + +## 1. 项目执行概览 +- **任务目标**: 实现从 `secsion.com` 导出报表并上传至 `94kan.cn`。 +- **核心逻辑**: 使用 Playwright 进行浏览器自动化,处理了 HTTPS 证书忽略、文件异步下载和表单上传。 +- **交付代码**: `main.py`。 + +## 2. 功能实现情况 +- [x] **登录逻辑**: 实现了角色选择、账号密码填写。 +- [x] **报表导出**: 实现了日期选择器填充和 `download` 事件监听。 +- [x] **文件上传**: 实现了针对 `94kan.cn` 的文件设置逻辑。 +- [x] **错误处理**: 集成了日志记录和错误截图功能。 + +## 3. 技术挑战应对 +- **HTTPS 证书**: 通过 `ignore_https_errors=True` 绕过。 +- **动态下载**: 使用 `expect_download()` 确保文件被正确捕获并保存。 +- **文件对话框**: 使用 `set_input_files` 绕过系统原生对话框。 + +## 4. 运行结果 +- 环境依赖已安装。 +- **全流程成功跑通**: + - 成功登录 `secsion.com`(处理了 URL 大小写敏感问题)。 + - 成功导出报表并保存到本地 `downloads/`。 + - 成功将报表上传至 `94kan.cn` 并触发了分析。 +- 验证截图已保存:`upload_result.png`。 diff --git a/docs/ReportAutomation/TASK_ReportAutomation.md b/docs/ReportAutomation/TASK_ReportAutomation.md new file mode 100644 index 0000000..748ec39 --- /dev/null +++ b/docs/ReportAutomation/TASK_ReportAutomation.md @@ -0,0 +1,53 @@ +# TASK - 报表自动化原子任务拆分 + +## 任务依赖图 +```mermaid +graph LR + T1[环境准备] --> T2[Secsion登录逻辑] + T2 --> T3[报表导出逻辑] + T3 --> T4[94kan上传逻辑] + T4 --> T5[主程序集成与测试] +``` + +## 原子任务定义 + +### T1: 环境准备 +- **输入**: 无 +- **动作**: 安装 `playwright`, `pytest-playwright`,初始化项目目录。 +- **输出**: `requirements.txt`, 已安装的依赖。 +- **验收**: 运行 `playwright install` 成功。 + +### T2: Secsion 登录模块 +- **输入**: 账号、密码、浏览器 Page。 +- **动作**: + 1. 访问登录 URL。 + 2. 处理证书警告(如有)。 + 3. 点击“店铺”角色。 + 4. 输入凭据并提交。 +- **输出**: 成功跳转至首页。 +- **验收**: 页面 URL 包含 `homepage`。 + +### T3: 报表导出模块 +- **输入**: 浏览器 Page, 日期字符串。 +- **动作**: + 1. 访问统计 URL。 + 2. 填充日期选择器。 + 3. 监听 `download` 事件。 + 4. 点击“导出报表”。 +- **输出**: 导出的文件路径。 +- **验收**: 文件存在于本地且大小大于 0。 + +### T4: 94kan 上传模块 +- **输入**: 浏览器 Page, 文件路径。 +- **动作**: + 1. 访问 94kan URL。 + 2. 定位上传组件。 + 3. 使用 `set_input_files` 设置文件。 +- **输出**: 上传成功状态。 +- **验收**: 页面显示“处理中”或上传进度。 + +### T5: 主程序集成 +- **输入**: 以上所有模块。 +- **动作**: 串联流程,加入日志。 +- **输出**: `main.py`。 +- **验收**: 全流程自动运行成功。 diff --git a/docs/ReportAutomation/TODO_ReportAutomation.md b/docs/ReportAutomation/TODO_ReportAutomation.md new file mode 100644 index 0000000..9ba5b99 --- /dev/null +++ b/docs/ReportAutomation/TODO_ReportAutomation.md @@ -0,0 +1,11 @@ +# TODO - 报表自动化后续事宜 + +## 1. 待办事项 +- [ ] **定期维护**: 定期检查网站选择器是否发生变化。 +- [ ] **清理下载**: 脚本目前将文件保存在 `downloads/`,可考虑定期清理。 +- [ ] **通知集成**: 如果需要,可以集成邮件或钉钉通知,告知上传结果。 + +## 2. 操作指引 +- **运行脚本**: `python main.py` +- **查看日志**: `automation.log` +- **查看最后一次上传截图**: `upload_result.png` diff --git a/main.py b/main.py new file mode 100644 index 0000000..6b1ece3 --- /dev/null +++ b/main.py @@ -0,0 +1,183 @@ +import asyncio +import os +import logging +from datetime import datetime +from playwright.async_api import async_playwright + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('automation.log', encoding='utf-8') + ] +) +logger = logging.getLogger(__name__) + +class ReportAutomation: + def __init__(self): + self.secsion_login_url = "https://secsion.com:8000/login?redirect=%252Fhomepage" + self.secsion_stats_url = "https://secsion.com:8000/commodityStatistics" + self.upload_url = "https://sale.94kan.cn/" + self.username = "15682076681" + self.password = "123456" + self.target_date = "2026-04-18" + self.download_dir = os.path.join(os.getcwd(), "downloads") + + if not os.path.exists(self.download_dir): + os.makedirs(self.download_dir) + + async def run(self): + async with async_playwright() as p: + # 启动浏览器,忽略 HTTPS 错误 + browser = await p.chromium.launch(headless=True) # 生产运行使用 Headless 模式 + context = await browser.new_context( + ignore_https_errors=True, + viewport={'width': 1280, 'height': 800} + ) + page = await context.new_page() + + try: + # --- 任务 1: 登录 secsion.com 并导出报表 --- + await self.login_secsion(page) + file_path = await self.export_report(page) + + if file_path: + # --- 任务 2: 上传到 94kan.cn --- + await self.upload_to_94kan(page, file_path) + else: + logger.error("未能获取下载文件路径,跳过上传任务。") + + except Exception as e: + logger.error(f"发生错误: {str(e)}") + # 截图以供调试 + await page.screenshot(path="error_screenshot.png") + finally: + await asyncio.sleep(5) # 留出观察时间 + await browser.close() + + async def login_secsion(self, page): + logger.info(f"正在打开登录页面: {self.secsion_login_url}") + await page.goto(self.secsion_login_url) + + # 1. 选择角色 "店铺" + # 假设角色选择是一个下拉框或者一组按钮,根据描述,点击包含“店铺”的元素 + logger.info("选择角色: 店铺") + try: + # 尝试多种定位方式以确保成功 + shop_role = page.get_by_text("店铺", exact=True) + await shop_role.click() + except Exception: + logger.warning("直接点击'店铺'失败,尝试备用定位方案...") + await page.click("text=店铺") + + # 2. 输入账号密码 + logger.info(f"输入账号: {self.username}") + # 根据截图,placeholder 是 "请输入用户名" + await page.get_by_placeholder("请输入用户名").fill(self.username) + await page.get_by_placeholder("请输入密码").fill(self.password) + + # 3. 勾选记住密码(可选) + if await page.get_by_text("记住密码").is_visible(): + await page.get_by_text("记住密码").click() + + # 4. 点击登录 + logger.info("点击登录按钮") + # 使用更稳健的 CSS 选择器 + try: + await page.click("button:has-text('登录')", timeout=5000) + except Exception: + logger.warning("通过文本点击失败,尝试通过 type='submit' 点击") + await page.click("button[type='submit']") + + # 等待跳转,增加超时时间到 20s,因为后台可能慢 + logger.info("等待登录跳转...") + # 修正 URL 大小写: homePage + await page.wait_for_url("**/homePage", timeout=20000) + logger.info("登录成功") + + async def export_report(self, page): + logger.info(f"访问统计页面: {self.secsion_stats_url}") + await page.goto(self.secsion_stats_url) + + # 等待页面加载完成,以“导出报表”按钮为准 + logger.info("等待统计页面加载...") + export_btn = page.get_by_role("button", name="导出报表") + await export_btn.wait_for(state="visible", timeout=20000) + + logger.info(f"设置查询日期: {self.target_date}") + + # 尝试通过日期值定位输入框,或者直接点击导出(如果日期已正确) + # 根据截图,日期可能已经默认是今天 + + try: + # 尝试定位输入框并填入日期 + # 如果 placeholder 不对,我们尝试寻找所有 input 并根据格式填充 + inputs = page.locator("input") + count = await inputs.count() + logger.info(f"页面共有 {count} 个输入框") + + # 寻找包含日期的输入框并填入 + for i in range(count): + try: + val = await inputs.nth(i).input_value() + if "-" in val and len(val) >= 10: # 类似 2026-04-18 + # 降低 fill 的超时,因为如果是 readonly 会很快报错 + await inputs.nth(i).fill(self.target_date, timeout=2000) + await inputs.nth(i).press("Enter") + except Exception: + continue + except Exception as e: + logger.warning(f"填充日期时遇到问题 (可能已默认正确): {str(e)}") + + # 点击导出报表并捕获下载 + logger.info("点击导出报表...") + async with page.expect_download(timeout=60000) as download_info: + await export_btn.click() + + download = await download_info.value + filename = f"commodity_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + save_path = os.path.join(self.download_dir, filename) + + await download.save_as(save_path) + logger.info(f"报表已保存至: {save_path}") + return save_path + + async def upload_to_94kan(self, page, file_path): + logger.info(f"正在打开上传页面: {self.upload_url}") + await page.goto(self.upload_url) + + # 1. 点击上传按钮触发文件选择(如果需要点击的话) + # 或者直接找到隐藏的 input[type="file"] + logger.info(f"准备上传文件: {file_path}") + + # 寻找上传 input + # sale.94kan.cn 页面通常有一个显眼的上传区域 + try: + # 方案 A: 直接设置 input 文件(最可靠) + # 很多前端框架会隐藏真正的 input + await page.set_input_files("input[type='file']", file_path) + except Exception: + logger.info("未找到直接的 input[type='file'],尝试点击后上传...") + # 方案 B: 点击后再处理 + async with page.expect_file_chooser() as fc_info: + await page.get_by_text("点击或拖拽文件至此").click() + file_chooser = await fc_info.value + await file_chooser.set_files(file_path) + + # 2. 等待上传完成 + logger.info("等待上传处理完成...") + # 根据截图,页面显示“正在上传并分析...” + try: + await page.wait_for_selector("text=上传并分析", state="visible", timeout=30000) + logger.info("上传成功,正在处理数据...") + except Exception: + logger.warning("未检测到 '上传并分析' 文本,可能已完成或文本不同") + + # 截图保存结果 + await page.screenshot(path="upload_result.png") + +if __name__ == "__main__": + automation = ReportAutomation() + asyncio.run(automation.run()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3460926 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +playwright +python-dotenv