docs: update project structure and add database documentation

- Add tests/ directory to .gitignore
- Document database connectivity with pyodbc and config
- Add project structure and file organization guidelines
- Update installation steps to use requirements.txt
- Document new dependencies: pandas, openpyxl, pyodbc
This commit is contained in:
Misaka_Company
2026-01-23 15:00:55 +08:00
parent 4b0a882b69
commit bbf84b7f8c
9 changed files with 456 additions and 322 deletions

4
.gitignore vendored
View File

@@ -14,3 +14,7 @@ tmpclaude-*
*.png *.png
data/ data/
orderID.txt orderID.txt
ProductionID.txt
# 测试脚本
tests/

View File

@@ -12,8 +12,8 @@ This is a Python automation project using Playwright to interact with a Chinese
# Activate the virtual environment (Windows) # Activate the virtual environment (Windows)
.venv\Scripts\activate .venv\Scripts\activate
# Install dependencies # Install all dependencies
pip install playwright pip install -r requirements.txt
# Install Playwright browsers # Install Playwright browsers
playwright install chromium playwright install chromium
@@ -23,17 +23,53 @@ playwright install chromium
```bash ```bash
# Run the main test script # Run the main test script
python test_playwright.py python tests/test_playwright.py
# Run the record script # Run the record script
python record.py python record.py
# Run database query test
python tests/test_db_query.py
``` ```
## Key Dependencies ## Key Dependencies
- **playwright==1.57.0**: Browser automation framework - **playwright==1.57.0**: Browser automation framework
- **pyodbc**: SQL Server database connectivity
- **pandas**: Data processing and Excel file handling
- **openpyxl**: Excel file operations
- Uses synchronous API (`playwright.sync_api`) - Uses synchronous API (`playwright.sync_api`)
## Project Structure
```
playwrite/
├── config/ # Configuration files (database, etc.)
├── db/ # Database connection components
├── data/ # Data files (excluded from git)
├── docs/ # Documentation (e.g., DEPENDENCIES.md)
├── tests/ # Test scripts (excluded from git)
├── requirements.txt # Python dependencies
├── .gitignore # Git ignore rules
└── CLAUDE.md # This file
```
## File Organization Rules
**IMPORTANT**: When creating test scripts, always place them in the `tests/` folder:
- Test scripts should be prefixed with `test_`
- All files in `tests/` are excluded from git tracking
- Examples: `tests/test_playwright.py`, `tests/test_db_query.py`
**Configuration Management**:
- Place configuration files in `config/` folder
- Database credentials and settings go in `config/database_config.py`
**Data Storage**:
- Use `data/` folder for temporary data files
- This folder is excluded from git tracking
## Architecture ## Architecture
### Nested Iframe Structure ### Nested Iframe Structure
@@ -80,6 +116,35 @@ with page.expect_popup() as popup_info:
new_page = popup_info.value new_page = popup_info.value
``` ```
### Database Connection
The project uses `pyodbc` to connect to SQL Server for data queries:
```python
from db.connection import get_connection
# Using context manager
with get_connection() as db:
results = db.execute_query("SELECT * FROM table")
# Connection automatically closed
# Using the query helper
from db.connection import query_production_orders
results = query_production_orders(['ID1', 'ID2', 'ID3'])
```
Database configuration is stored in `config/database_config.py`:
```python
SQL_SERVER_CONFIG = {
'driver': 'ODBC Driver 18 for SQL Server',
'server': '192.168.110.114',
'database': 'CompanyDB',
'username': 'peng',
'password': 'Cqbld123456.',
'TrustServerCertificate': 'yes'
}
```
## Common Utilities ## Common Utilities
### `get_input_by_label(frame, label_text)` ### `get_input_by_label(frame, label_text)`

1
config/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Config package

View File

@@ -0,0 +1,9 @@
# ================= SQL Server 配置 =================
SQL_SERVER_CONFIG = {
'driver': 'ODBC Driver 18 for SQL Server',
'server': '192.168.110.114',
'database': 'CompanyDB',
'username': 'peng',
'password': 'Cqbld123456.',
'TrustServerCertificate': 'yes'
}

1
db/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Database package

184
db/connection.py Normal file
View File

@@ -0,0 +1,184 @@
"""
SQL Server 数据库连接组件
提供数据库连接和查询接口
"""
import pyodbc
from typing import List, Dict, Any, Optional
import sys
import os
# 添加项目根目录到 sys.path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from config.database_config import SQL_SERVER_CONFIG
class DatabaseConnection:
"""SQL Server 数据库连接类"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化数据库连接
Args:
config: 数据库配置字典,默认使用 SQL_SERVER_CONFIG
"""
self.config = config or SQL_SERVER_CONFIG
self.connection = None
def connect(self) -> pyodbc.Connection:
"""
建立数据库连接
Returns:
pyodbc.Connection: 数据库连接对象
"""
if self.connection is not None:
return self.connection
# 构建连接字符串
conn_str = (
f"DRIVER={{{self.config['driver']}}};"
f"SERVER={self.config['server']};"
f"DATABASE={self.config['database']};"
f"UID={self.config['username']};"
f"PWD={self.config['password']};"
f"TrustServerCertificate={self.config['TrustServerCertificate']};"
)
try:
self.connection = pyodbc.connect(conn_str)
print(f"成功连接到数据库: {self.config['server']}/{self.config['database']}")
return self.connection
except pyodbc.Error as e:
print(f"数据库连接失败: {e}")
raise
def disconnect(self):
"""关闭数据库连接"""
if self.connection:
self.connection.close()
self.connection = None
print("数据库连接已关闭")
def execute_query(self, sql: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]:
"""
执行查询语句并返回结果
Args:
sql: SQL 查询语句
params: 查询参数(可选)
Returns:
List[Dict[str, Any]]: 查询结果列表,每个元素为一行数据的字典
"""
if not self.connection:
self.connect()
cursor = self.connection.cursor()
try:
if params:
cursor.execute(sql, params)
else:
cursor.execute(sql)
# 获取列名
columns = [column[0] for column in cursor.description]
# 将结果转换为字典列表
results = []
for row in cursor.fetchall():
results.append(dict(zip(columns, row)))
return results
except pyodbc.Error as e:
print(f"查询执行失败: {e}")
raise
finally:
cursor.close()
def execute_update(self, sql: str, params: Optional[tuple] = None) -> int:
"""
执行更新/插入/删除语句
Args:
sql: SQL 语句
params: 参数(可选)
Returns:
int: 受影响的行数
"""
if not self.connection:
self.connect()
cursor = self.connection.cursor()
try:
if params:
cursor.execute(sql, params)
else:
cursor.execute(sql)
self.connection.commit()
return cursor.rowcount
except pyodbc.Error as e:
self.connection.rollback()
print(f"执行失败,已回滚: {e}")
raise
finally:
cursor.close()
def __enter__(self):
"""支持 with 语句的上下文管理器入口"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""支持 with 语句的上下文管理器出口"""
self.disconnect()
# 便捷函数
def query_production_orders(总排号_list: List[str]) -> List[Dict[str, Any]]:
"""
根据总排号列表查询生产订单号
Args:
总排号_list: 总排号列表
Returns:
List[Dict[str, Any]]: 查询结果
"""
db = DatabaseConnection()
# 构建占位符字符串
placeholders = ','.join(['?' for _ in 总排号_list])
sql = f"""
SELECT [总排号], [生产订单号], [序号], [订单号], [客户名称], [产品型号]
FROM [productionContractData].[26年压力表合同数据]
WHERE [总排号] IN ({placeholders})
ORDER BY [序号]
"""
try:
results = db.execute_query(sql, tuple(总排号_list))
return results
finally:
db.disconnect()
def get_connection() -> DatabaseConnection:
"""
获取数据库连接实例
Returns:
DatabaseConnection: 数据库连接对象
"""
return DatabaseConnection()

169
docs/DEPENDENCIES.md Normal file
View File

@@ -0,0 +1,169 @@
# 项目依赖说明
## Python 版本要求
- Python >= 3.8
## 依赖安装
```bash
# 激活虚拟环境 (Windows)
.venv\Scripts\activate
# 安装所有依赖
pip install -r requirements.txt
# 安装 Playwright 浏览器
playwright install chromium
```
## 核心依赖
### 1. playwright==1.57.0
**用途**: 浏览器自动化框架
**功能模块**:
- `playwright.sync_api` - 同步 API
- 浏览器启动与页面操作
- 元素定位器 (Locator)
- 弹窗处理 (expect_popup)
- 嵌套 iframe 处理
**使用文件**:
- `test_playwright.py` - 主要自动化脚本
- `record.py` - 录制脚本
**重要**: 安装后需运行 `playwright install chromium` 下载浏览器
---
### 2. pyodbc>=5.0.0
**用途**: SQL Server 数据库连接
**功能模块**:
- 数据库连接管理
- SQL 查询执行
- 事务处理
**使用文件**:
- `db/connection.py` - 数据库连接组件
- `test_db_query.py` - 数据库查询测试脚本
**注意**: Windows 系统通常已预装 ODBC 驱动,如遇连接问题请安装 [ODBC Driver 18 for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server)
---
### 3. pandas>=2.0.0
**用途**: 数据处理与分析
**功能模块**:
- DataFrame 数据结构
- Excel 文件读写
- 数据清洗与转换
**使用文件**:
- `excel_to_markdown.py` - Excel 转 Markdown
- `analyze_excel.py` - Excel 数据分析
---
### 4. openpyxl>=3.1.0
**用途**: Excel 文件操作
**功能模块**:
- 读取 `.xlsx` 文件
- 工作表操作
- 单元格读取
**使用文件**:
- `excel_to_markdown.py`
- `analyze_excel.py`
---
### 5. numpy>=1.24.0
**用途**: 数值计算
**说明**: pandas 的依赖包,自动安装
---
### 6. python-dateutil>=2.8.0
**用途**: 日期时间处理
**说明**: pandas 的依赖包,自动安装
---
### 7. pytz>=2023.0
**用途**: 时区处理
**说明**: pandas 的依赖包,自动安装
---
## 传递依赖
以下包由核心依赖自动安装,无需手动指定:
| 包名 | 版本 | 被依赖包 |
|------|------|----------|
| greenlet | 3.3.0 | playwright |
| pyee | 13.0.0 | playwright |
| typing_extensions | 4.15.0 | 多个包 |
| et_xmlfile | 2.0.0 | openpyxl |
| six | 1.17.0 | python-dateutil |
| tzdata | 2025.3 | pandas |
---
## 按功能分类
### 浏览器自动化
```
playwright==1.57.0
```
### 数据库
```
pyodbc>=5.0.0
```
### Excel/数据处理
```
pandas>=2.0.0
openpyxl>=3.1.0
numpy>=1.24.0
```
---
## 当前环境已安装包
```
Package Version
----------------- -----------
et_xmlfile 2.0.0
greenlet 3.3.0
numpy 2.2.6
openpyxl 3.1.5
pandas 2.3.3
pip 25.3
playwright 1.57.0
pyee 13.0.0
python-dateutil 2.9.0.post0
pytz 2025.2
setuptools 65.5.0
six 1.17.0
typing_extensions 4.15.0
tzdata 2025.3
```
**注意**: `pyodbc` 尚未安装,运行数据库相关脚本前请先安装。

19
requirements.txt Normal file
View File

@@ -0,0 +1,19 @@
# ===========================
# Playwright Python Automation
# ===========================
# Python 3.8+ required
# --- Browser Automation ---
playwright==1.57.0
# --- Database ---
pyodbc>=5.0.0
# --- Excel/Data Processing ---
pandas>=2.0.0
openpyxl>=3.1.0
numpy>=1.24.0
# --- System Utilities (installed via pip) ---
python-dateutil>=2.8.0
pytz>=2023.0

View File

@@ -1,318 +0,0 @@
import re
import time
from playwright.sync_api import Playwright, sync_playwright, expect, TimeoutError as PWTimeoutError
import pdb
def click_button_until_disappear(frame, button_name="保存提交", max_attempts=None, max_duration=None):
"""
持续点击按钮直到按钮消失。
包含业务逻辑:点击前检查【生产部门】是否为空,为空则自动回填数据。
"""
click_count = 0
start_time = time.time()
# 定义加载控件的定位器 (基于传入的 frame)
loading_locator = frame.locator("div").filter(has_text="加载中").nth(1)
print(f"开始监控'{button_name}'按钮 (含自动补全逻辑)...")
while True:
try:
# --- 1. 检查退出条件 ---
if max_attempts and click_count >= max_attempts:
print(f"已达到最大点击次数{max_attempts}次,停止操作")
break
if max_duration and (time.time() - start_time) >= max_duration:
elapsed = time.time() - start_time
print(f"已达到最大持续时间{max_duration}秒,停止操作")
break
# --- 2. 检查按钮状态 ---
button = frame.get_by_role("button", name=button_name)
# 如果按钮已经消失,任务完成
if button.count() == 0:
elapsed = time.time() - start_time
print(f"SUCCESS: '{button_name}'按钮已消失")
print(f"共点击{click_count}次,总耗时{elapsed:.1f}")
return {
"success": True,
"click_count": click_count,
"elapsed_time": elapsed
}
# ==========================================
# 插入业务逻辑:检查并回填数据
# ==========================================
try:
# 1. 获取生产部门输入框
dep_input = get_input_by_label(frame, "生产部门")
if dep_input:
current_val = dep_input.input_value()
# 检查是否为空 (strip去除空白字符)
if current_val != "压力表车间" and current_val != "CQ030101":
print(f" [补全] 检测到生产部门为空,开始自动填充...")
# A. 填入生产部门
dep_input.fill("压力表车间")
print(" [补全] -> 生产部门已填入: CQ030101")
# B. 填入订单类型 (仅在需要补全生产部门时才操作这个,根据你的需求逻辑)
type_input = get_input_by_label(frame, "订单类型")
if type_input:
type_input.fill("55C2-Cxx-02")
print(" [补全] -> 订单类型已填入: 55C2-Cxx-02")
else:
print(" [警告] 无法找到'订单类型'输入框")
# 稍微等待一下填入值的生效(可选)
time.sleep(0.5)
else:
print(f" [信息] 生产部门已有值: {current_val},跳过补全")
else:
print(" [警告] 无法找到'生产部门'输入框,跳过检查")
except Exception as logic_e:
print(f" [异常] 数据补全逻辑出错 (不影响继续点击): {logic_e}")
# ==========================================
# --- 3. 执行点击 ---
button.click()
click_count += 1
print(f"[{time.strftime('%H:%M:%S')}] 第{click_count}次点击'{button_name}'")
# --- 4. 智能等待加载逻辑 ---
# 4.1 缓冲等待点击后UI可能需要几秒钟才会弹出遮罩
# print(" - 等待遮罩层响应...")
time.sleep(2)
# 4.2 检测并等待遮罩消失
try:
# 尝试等待遮罩出现 (给它3秒的时间被检测到)
if loading_locator.is_visible(timeout=3000):
print(" - 检测到'加载中',正在等待数据载入...")
# 只要出现了,就无限等待它消失 (timeout=0)
loading_locator.wait_for(state="hidden", timeout=0)
print(" - 数据载入完毕")
else:
# 如果3秒内没检测到 visible可能是数据极少瞬间加载完了
pass
except Exception as e:
# 忽略定位超时等非致命错误
pass
# 稍微休息一下
time.sleep(0.5)
except Exception as e:
elapsed = time.time() - start_time
print(f"操作异常: {e}")
return {
"success": False,
"click_count": click_count,
"error": str(e)
}
def get_input_by_label(frame, label_text: str, label_locator=None):
"""
通过标签文本获取对应的输入框对象
参数:
frame: iframe对象
label_text: 标签文本(用于查找和日志输出)
label_locator: 可选已经定位好的标签locator。如果为None则函数内部查找
返回:
成功返回输入框的locator对象
失败返回None
"""
try:
# 如果没有传入label_locator则根据label_text查找
if label_locator is None:
label_locator = frame.locator("div").filter(has_text=re.compile(f"^{label_text}$")).first
print(f"找到{label_text}标签")
# 向上找到包含标签和输入框的共同父容器
parent_container = label_locator.locator("..") # 父元素
# 在父容器中查找输入框
input_box = parent_container.locator("input").first
# 如果父元素中没有,再向上一层
if input_box.count() == 0:
print(f"{label_text}: 在父元素中未找到,向上一层查找...")
grandparent = parent_container.locator("..") # 祖父元素
input_box = grandparent.locator("input").first
# 验证是否找到输入框
if input_box.count() > 0:
input_box.wait_for(state="visible", timeout=5000)
current_value = input_box.input_value()
print(f"{label_text}: {current_value}")
print(input_box.count())
return input_box
else:
print(f"{label_text}: 在父容器中找不到输入框")
return None
except Exception as e:
print(f"{label_text}失败: {e}")
return None
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context(ignore_https_errors=True)
page = context.new_page()
# 1. 登录主页面
try:
page.goto("https://68.11.34.30:8082/yonbip/resources/uap/rbac/login/main/index.html")
# 提取主 iframe
main_frame = page.locator("#forwardFrame").content_frame
main_frame.get_by_role("textbox", name="用户名").fill("BLDpengqiangqiang")
main_frame.get_by_role("textbox", name="密码").fill("Cqbld123456.")
main_frame.get_by_role("button", name="登录").click()
# 2. 处理强制登录弹窗(判断是否存在)
confirm_btn = main_frame.get_by_role("button", name="确定")
try:
# 短暂等待检测弹窗
if confirm_btn.is_visible(timeout=3000):
confirm_btn.click()
print("强制登录")
else:
print("正常登录")
except:
print("正常登录 (未检测到弹窗)")
# 3. 点击打开“补货安排”
main_frame.locator("i").first.click() # 假设是某个新建按钮
with page.expect_popup() as page1_info:
main_frame.get_by_title("补货安排", exact=True).click()
page1 = page1_info.value
# 4. 新页面:等待页面加载完成 + 提取嵌套 iframe
print("Page1 已打开,正在等待内层 iframe 加载...")
page1.wait_for_load_state("domcontentloaded")
# 提取外层 forwardFrame
outer_frame = page1.locator("#forwardFrame").content_frame
# 关键:等待内层 #mainiframe 出现并加载
inner_frame_locator = outer_frame.locator("#mainiframe")
inner_frame_locator.wait_for(state="visible", timeout=15000)
inner_frame = inner_frame_locator.content_frame
print("Page1 内层 iframe 已加载完成")
# 填写查询条件
textbox = inner_frame.get_by_role("textbox", name="期初标识≠")
textbox.fill("")
inner_frame.locator("#rc_select_0").fill("关闭")
textbox = inner_frame.get_by_role("textbox", name="排产号")
textbox.fill("R")
inner_frame.get_by_role("textbox", name="单据日期开始日期").fill("2025-12-28")
inner_frame.get_by_role("textbox", name="单据日期结束日期").fill("2025-12-31")
# inner_frame.get_by_text("今日").click()
# inner_frame.get_by_text("今日").click()
# 点击查询按钮
inner_frame.locator(".iconfont.icon-chaxun").click()
# 勾选所有合同
inner_frame.get_by_role("row", name="序号").get_by_label("").check()
# 获取订单合计数量
summary_locator = inner_frame.get_by_text(re.compile(r"合计:\s*\d+\s*行"))
try:
summary_text = summary_locator.inner_text(timeout=5000)
match = re.search(r"\d+", summary_text)
if match:
row_count = int(match.group())
print(f"当前总行数:{row_count}")
else:
print("未匹配到行数")
except:
print("未找到合计行")
# 进入 Page2
inner_frame.get_by_role("button").filter(has_text="补货安排").hover()
inner_frame.get_by_text("生产订单").click()
inner_frame.get_by_role("textbox", name="工厂").fill("10010705")
with page1.expect_popup(timeout=60000) as page2_info:
inner_frame.get_by_role("button", name="确定(Y)").click()
page2 = page2_info.value
# --- Page2 加载逻辑优化 ---
print("Page2 已打开,正在初始化...")
page2.wait_for_load_state("domcontentloaded")
outer_frame_p2 = page2.locator("#forwardFrame").content_frame
inner_frame_locator_p2 = outer_frame_p2.locator("#mainiframe")
inner_frame_locator_p2.wait_for(state="visible", timeout=15000)
inner_frame_p2 = inner_frame_locator_p2.content_frame
print("Page2 基础框架已就绪")
# 1. 按照建议等待3秒让加载遮罩有机会出现
print("等待 7 秒,检测加载遮罩是否出现...")
time.sleep(7)
# 2. 定义加载控件定位器
# 注意:这里直接基于 inner_frame_p2 构建,对应你的 path: ...locator("#mainiframe").content_frame.locator("div")...
loading_locator = inner_frame_p2.locator("div").filter(has_text="加载中").nth(1)
try:
# 尝试等待加载控件变为 visible。
# 给它 3秒 的检测时间,如果这 3秒 内出现了,说明页面正在加载数据。
# 如果没出现,触发 TimeoutError进入 except 块,说明数据量很少,可能瞬间加载完了。
loading_locator.wait_for(state="visible", timeout=3000)
print("检测到'加载中'控件,开始等待数据加载(时间不定)...")
# 3. 关键:等待控件消失 (state="hidden" 或 "detached")
# timeout=0 表示无限等待,防止数据量大加载时间过长导致报错
loading_locator.wait_for(state="hidden", timeout=0)
print(">>> 页面加载完毕!(加载控件已消失)")
except PWTimeoutError:
print(">>> 页面加载完毕!(未检测到'加载中'控件,可能已快速完成)")
except Exception as e:
print(f">>> 页面加载状态判断异常: {e},默认视为加载完毕")
# --- 这里可以继续 Page2 的后续操作 ---
input_box = get_input_by_label(inner_frame_p2, "生产部门")
input_box = get_input_by_label(inner_frame_p2, "订单类型")
input_box = get_input_by_label(inner_frame_p2, "数量")
print("\n>>> 开始执行连续点击操作 <<<")
# 这里的 inner_frame_p2 是你在 Page2 提取出来的内层 iframe
result = click_button_until_disappear(
frame=inner_frame_p2,
button_name="保存提交", # 修改为你实际要点击的按钮名字,比如 "下一页" 或 "加载更多"
max_attempts=500 # 防止死循环,设置一个最大上限
)
print("进入调试模式,你现在可以在终端输入 Python 代码来测试定位器...")
pdb.set_trace() # <--- 程序会在这里卡住,控制权交给终端
print("最终结果:", result)
input("操作完成,按回车关闭...")
finally:
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)