活动介绍
file-type

使用R Markdown和Docker创建PDF报告的实践教程

ZIP文件

下载需积分: 50 | 6KB | 更新于2025-08-13 | 114 浏览量 | 0 下载量 举报 收藏
download 立即下载
在介绍如何使用R Markdown和Docker创建PDF报告之前,我们需要对几个关键概念有所了解。首先,R是一种用于统计分析和数据可视化的编程语言和软件环境。R Markdown是R的一个扩展,允许用户将R代码嵌入到Markdown文档中,这样文档就可以包含数据处理的代码、结果和文本说明,便于生成动态文档和报告。 Docker是一个开源的应用容器引擎,它允许开发者打包应用以及应用的依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上。容器是完全使用沙箱机制,相互之间不会有任何接口(类似iPhone的app)。 现在让我们详细分析这个示例文件“R-docker-report:使用R Markdown和Docker创建pdf报告的示例文件”。 1. R Markdown基础 R Markdown提供了一种方便的方式来生成具有可重复性的报告,这些报告将最新的数据和分析结果与文本描述相结合。使用R Markdown,可以轻松插入R代码片段(chunks),代码运行后的输出(包括表格和图形)会直接嵌入到生成的文档中。 R Markdown文档通常具有.md或.Rmd扩展名。一个典型的R Markdown文件包括YAML头部(用于设置文档参数)、R代码块和Markdown文本。YAML头部定义了输出格式和其他全局设置,R代码块用于执行R代码并展示输出,而Markdown文本则用于提供文档内容。 2. Docker基础 Docker容器化技术可以看作是一种轻量级的虚拟化方法。传统的虚拟机通过运行一个完整的操作系统来提供虚拟环境,这会消耗较多资源。而Docker容器则共享宿主机的操作系统内核,因此启动快、资源占用小。 要使用Docker,需要编写一个Dockerfile,这是一个包含创建Docker镜像所有步骤的文本文件。Dockerfile定义了基础镜像、需要安装的软件包、工作目录、复制文件到容器中以及如何运行容器中的应用等。 3. 结合R Markdown和Docker创建PDF报告 在使用Docker来生成R Markdown PDF报告的上下文中,整个工作流程大致如下: a. 开发者编写一个R Markdown文档,这个文档中包含了数据处理和分析的代码块以及文本说明。 b. 开发者创建一个Dockerfile,在文件中指定基于R的Docker镜像,并配置安装所需的软件(例如,pandoc、LaTeX等),以及任何额外的R包。 c. 开发者将R Markdown文档放到Docker容器的适当位置,并编写脚本来执行R Markdown渲染成PDF的过程。 d. Docker构建出包含R Markdown和所有依赖项的镜像,然后可以在任何安装了Docker的系统上运行这个镜像来生成PDF报告。 4. 环境一致性和可重复性 使用Docker的一个重要优势是它保证了环境的一致性。无论在何种环境下构建和运行容器,只要Dockerfile一致,生成的PDF报告也将是一致的。这避免了因环境依赖导致的“在我机器上能行,在你机器上不行”的问题,大大提高了工作流程的可重复性。 5. 示例文件细节 由于提供的信息有限,我们不清楚“R-docker-report-master”压缩包中具体包含了哪些文件,但可以合理猜测它至少包含一个R Markdown文档、一个Dockerfile,以及构建和运行容器的说明或脚本。 6. 额外资源和工具 除了R Markdown和Docker之外,创建PDF报告可能还需要其他工具。例如,pandoc是一个文档转换工具,可以将Markdown转换成多种格式,包括PDF。LaTeX是一个文档排版系统,它提供了更专业的排版控制,适合生成高质量的PDF文档。在Dockerfile中通常会包含安装pandoc和LaTeX的步骤,以便在容器内部生成PDF。 7. 实际应用和示例 在实际工作中,一个典型的使用场景可能是这样的:数据分析师创建了一个包含数据分析代码和结果的R Markdown文档。为了确保其他人或在其他机器上复现分析结果,他们可以使用Docker来创建一个包含所有依赖项的容器,确保分析脚本无论在哪都能以相同的方式运行。 总结来说,“R-docker-report”提供了一个很好的范例,它通过整合R Markdown与Docker技术,简化了复杂报告的生成过程,同时确保了过程的可重复性。这不仅减少了因环境差异导致的问题,也使得跨平台的数据报告工作更加顺畅和高效。

相关推荐

filetype

以下是针对地质找矿和水工环地质勘查行业的详细部署指南,所有组件均安装在D盘,充分利用GPU资源,实现本地化知识库管理和Word报告自动化生成: --- ### **一、系统准备与目录创建** #### **1. 创建主目录结构** ```powershell # 打开PowerShell(管理员权限) # 创建主目录 mkdir D:\personal ai # 创建子目录 mkdir D:\personal ai\docker-data mkdir D:\personal ai\ollama mkdir D:\personal ai\ragflow mkdir D:\personal ai\dify mkdir D:\personal ai\models mkdir D:\personal ai\templates mkdir D:\personal ai\output ``` #### **2. 更新显卡驱动** 1. 访问[NVIDIA驱动下载页](https://www.nvidia.cn/Download/index.aspx) 2. 选择匹配显卡的驱动程序 3. 安装时选择: - **自定义安装** - 勾选**清洁安装** - 安装位置选择:`D:\personal ai\NVIDIA` --- ### **二、Docker Desktop安装与汉化** #### **1. 安装Docker Desktop** 1. 下载安装程序:[Docker Desktop for Windows](https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe) 2. 运行安装程序: - 取消勾选"Use WSL 2 instead of Hyper-V" - 点击"Advanced": - 安装路径:`D:\personal ai\Docker` - 数据存储路径:`D:\personal ai\docker-data` - 勾选: - ☑ Add shortcut to desktop - ☑ Enable WSL 2 Features - ☑ Add Docker binaries to PATH #### **2. Docker汉化** ```powershell # 下载汉化包 Invoke-WebRequest -Uri "https://ghproxy.com/https://github.com/Docker-Hub-frproxy/docker-desktop-zh/releases/download/v4.30.0/zh-CN.zip" -OutFile "D:\personal ai\docker-zh.zip" # 解压并替换文件 Expand-Archive -Path "D:\personal ai\docker-zh.zip" -DestinationPath "D:\personal ai\Docker\resources" -Force # 重启Docker Restart-Service -Name "Docker Desktop Service" ``` #### **3. 配置GPU支持** 1. 创建配置文件: ```powershell notepad $env:USERPROFILE\.wslconfig ``` 2. 输入以下内容: ```ini [wsl2] memory=16GB # 根据实际内存调整,建议≥16GB processors=8 # 根据CPU核心数调整 swap=0 localhostForwarding=true [nvidia] enabled=true cudaVersion=12.2 # 与安装的CUDA版本一致 ``` --- ### **三、Ollama + DeepSeek部署** #### **1. 安装Ollama** ```powershell # 下载安装程序 Invoke-WebRequest -Uri "https://ollama.com/download/OllamaSetup.exe" -OutFile "D:\personal ai\OllamaSetup.exe" # 静默安装到指定目录 Start-Process "D:\personal ai\OllamaSetup.exe" -ArgumentList "/S /D=D:\personal ai\ollama" -Wait ``` #### **2. 配置模型存储路径** ```powershell # 设置环境变量 [Environment]::SetEnvironmentVariable("OLLAMA_MODELS", "D:\personal ai\models", "Machine") # 重启Ollama服务 Restart-Service -Name "Ollama" ``` #### **3. 下载DeepSeek模型** ```powershell # 拉取7B参数模型(适合44GB显存) ollama pull deepseek-llm:7b # 验证安装 ollama run deepseek-llm:7b "地质找矿的基本流程是什么?" ``` --- ### **四、RAGFlow本地部署** #### **1. 创建docker-compose.yml** ```powershell # 创建配置文件 @" version: '3.8' services: ragflow: image: infiniflow/ragflow:latest container_name: ragflow ports: - "9380:9380" volumes: - "D:/personal ai/ragflow/data:/opt/ragflow/data" - "D:/personal ai/models:/opt/ragflow/models" environment: - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=compute,utility deploy: resources: reservations: devices: - driver: nvidia count: 2 capabilities: [gpu] "@ | Out-File -FilePath "D:\personal ai\ragflow\docker-compose.yml" -Encoding utf8 ``` #### **2. 启动RAGFlow** ```powershell # 进入目录 cd D:\personal ai\ragflow # 启动容器 docker compose up -d # 查看日志(确保正常运行) docker logs ragflow ``` --- ### **五、Dify工作流部署** #### **1. 创建docker-compose.yml** ```powershell @" version: '3' services: dify: image: langgenius/dify:latest container_name: dify ports: - "80:3000" volumes: - "D:/personal ai/dify/data:/data" environment: - DB_ENGINE=sqlite - GPU_ENABLED=true depends_on: - ragflow "@ | Out-File -FilePath "D:\personal ai\dify\docker-compose.yml" -Encoding utf8 ``` #### **2. 启动Dify** ```powershell cd D:\personal ai\dify docker compose up -d ``` --- ### **六、地质行业知识库配置** #### **1. 上传地质资料** 1. 访问 `http://localhost:9380` 2. 创建知识库 → 命名"地质矿产知识库" 3. 上传文件类型: - 地质调查报告(PDF/DOCX) - 矿产储量估算表(XLSX) - 水文地质图件(JPG/PNG) - 工程地质剖面图(DWG) #### **2. 配置检索策略** ```yaml # 在RAGFlow高级设置中 chunk_size: 1024 # 适合技术文档 chunk_overlap: 200 metadata_fields: # 地质专用元数据 - project_name - geological_period - mineral_type - gis_coordinates ``` --- ### **七、报告生成工作流配置** #### **1. 在Dify中创建工作流** 1. 访问 `http://localhost` 2. 创建应用 → 选择"工作流" 3. 节点配置: ``` [输入] → [RAGFlow检索] → [Ollama处理] → [Word生成] ``` #### **2. 配置Ollama节点** ```json { "model": "deepseek-llm:7b", "parameters": { "temperature": 0.3, "max_tokens": 4096, "system_prompt": "你是一位资深地质工程师,负责编写专业地质报告。使用规范的地质术语,遵循GB/T 9649地质矿产术语标准。" } } ``` #### **3. 创建Word模板** 1. 在 `D:\personal ai\templates` 创建 `地质报告模板.docx` 2. 包含字段: ```markdown ## {{project_name}}地质调查报告 ### 一、区域地质背景 {{regional_geology}} ### 二、矿产特征 {{mineral_characteristics}} ### 三、水文地质条件 {{hydrogeological_conditions}} [附图:{{figure_number}}] ### 四、资源量估算(单位:万吨) | 矿种 | 332 | 333 | 334 | |---|---|---|---| {{resource_table}} ``` #### **4. Python报告生成脚本** 在Dify中创建 `report_generator.py`: ```python from docx import Document from docx.shared import Pt import pandas as pd import json def generate_geological_report(data): # 加载模板 doc = Document(r'D:\personal ai\templates\地质报告模板.docx') # 填充文本内容 for p in doc.paragraphs: p.text = p.text.replace('{{project_name}}', data['project_name']) p.text = p.text.replace('{{regional_geology}}', data['regional_geology']) p.text = p.text.replace('{{hydrogeological_conditions}}', data['hydro_conditions']) # 填充资源表格 table = doc.tables[0] resources = json.loads(data['resource_table']) for i, mineral in enumerate(resources): row = table.add_row() row.cells[0].text = mineral['type'] row.cells[1].text = str(mineral['332']) row.cells[2].text = str(mineral['333']) row.cells[3].text = str(mineral['334']) # 保存报告 output_path = fr"D:\personal ai\output\{data['project_name']}_地质调查报告.docx" doc.save(output_path) return {"status": "success", "path": output_path} ``` --- ### **八、工作流测试与使用** #### **1. 触发报告生成** ```powershell curl -X POST http://localhost/v1/workflows/run \ -H "Content-Type: application/json" \ -d '{ "inputs": { "project_name": "云南某铜矿勘探", "requirements": "需要包含:\n1. 矿区水文地质分析\n2. 铜矿体三维模型描述\n3. JORC标准资源量估算" } }' ``` #### **2. 输出结果** - 生成文件:`D:\personal ai\output\云南某铜矿勘探_地质调查报告.docx` - 日志位置:`D:\personal ai\dify\data\logs\workflow.log` #### **3. 典型报告结构** ```markdown ## 云南某铜矿勘探地质调查报告 ### 一、区域地质背景 位于扬子地块西缘,出露地层主要为二叠系阳新组灰岩... ### 二、矿产特征 发现3条铜矿体,呈层状产出,平均品位Cu 1.2%... ### 三、水文地质条件 矿区内发育两条季节性河流,地下水类型主要为基岩裂隙水...[附图:图3] ### 四、资源量估算(单位:万吨) | 矿种 | 332 | 333 | 334 | |------|-----|-----|-----| | 铜矿 | 120 | 280 | 150 | ``` --- ### **九、维护与优化** #### **1. GPU监控** ```powershell # 查看GPU利用率 nvidia-smi --query-gpu=utilization.gpu --format=csv -l 5 # Ollama GPU加速验证 ollama run deepseek-llm:7b --verbose ``` #### **2. 地质专业词库增强** 1. 在 `D:\personal ai\models` 创建 `geology_terms.txt` 2. 添加专业术语: ```text 水工环地质 矿产普查 资源量估算 地层划分 构造解析 ``` 3. 在RAGFlow配置中加载术语库 #### **3. 常见问题解决** **问题1:Docker容器无法访问GPU** ```powershell # 验证NVIDIA容器工具包 docker run --rm --gpus all nvidia/cuda:12.2.0-base nvidia-smi # 解决方案 nvidia-smi --gpu-reset ``` **问题2:中文PDF解析乱码** ```yaml # 在RAGFlow配置中添加 parser_config: pdf: text_extraction: lang: chi_sim # 使用中文OCR ``` **问题3:报告生成格式错误** ```python # 在Python脚本中添加格式修复 def fix_table_format(table): for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: paragraph.paragraph_format.space_before = Pt(0) paragraph.paragraph_format.space_after = Pt(0) ``` --- ### **十、地质行业应用场景** #### **1. 自动化报告类型** 1. 矿产勘探阶段性报告 2. 水文地质调查评价 3. 矿山环境影响评估 4. 地质灾害风险分析 5. 资源储量动态报表 #### **2. 效率提升对比** | 任务类型 | 传统耗时 | 系统耗时 | |---------|---------|---------| | 矿产调查报告 | 40小时 | 2小时 | | 水文地质图件说明 | 16小时 | 45分钟 | | 资源量估算表 | 8小时 | 实时生成 | 分析优化以上部署方案

filetype

非常好!我们将在前文的基础上,**重新整理一套完整、清晰、适合地质行业电脑小白用户的 AI 工作流部署方案**。该方案将基于: - **Windows 11 + WSL2** - **Docker Desktop(Linux 容器模式)** - **Ollama(本地大模型)** - **RAGFlow(文档知识库 + 向量检索)** - **Dify(可视化 AI 工作流)** - **Python 脚本(自动生成 Word 报告)** --- ## 📁 部署目标结构 ``` D:\AI_Studio\ ├── apps\ # 所有应用程序安装目录 │ ├── docker\ # Docker Desktop 安装路径 │ ├── python\ # Python 安装路径 │ └── ollama\ # Ollama 原生可执行文件(备选) │ ├── data\ # 数据存储目录 │ ├── models\ # 模型缓存(如 Ollama 模型) │ ├── ragflow_data\ # RAGFlow 数据目录 │ ├── dify_data\ # Dify 数据目录 │ ├── docker\ # Docker 数据卷 │ └── reports\ # Word 报告模板与输出 │ ├── templates\ # Word 模板 │ └── output\ # 自动生成的报告 │ ├── installer\ # 安装包统一存放目录 │ ├── Docker Desktop Installer.exe │ ├── ollama-windows-amd64.exe │ └── python-3.11.9-amd64.exe │ └── scripts\ # 自定义脚本目录 └── generate_report.py # 自动生成 Word 报告的 Python 脚本 ``` --- ## 🔧 第一步:准备系统环境 ### ✅ 1. 启用 WSL2(无需手动安装 Ubuntu) 以管理员身份打开 PowerShell: ```powershell # 启用 WSL 功能并安装默认发行版(Ubuntu) wsl --install # 设置默认版本为 WSL2 wsl --set-default-version 2 # 查看已安装的发行版 wsl --list --verbose ``` > ⚠️ 如果未自动安装 Ubuntu,运行: ```powershell wsl --install -d Ubuntu ``` 安装完成后设置用户名和密码。 --- ## 🐳 第二步:安装 Docker Desktop(集成 WSL2) ### ✅ 1. 下载安装包 放入: ``` D:\AI_Studio\installer\Docker%20Desktop%20Installer.exe ``` ### ✅ 2. 安装 Docker Desktop 双击运行安装程序,并选择安装路径为: ``` D:\AI_Studio\apps\docker ``` > 💡 如果安装程序不支持更改路径,可以先安装到默认位置,再移动文件夹并创建软链接。 ### ✅ 3. 设置 Docker 数据卷路径 编辑或新建配置文件: ``` C:\ProgramData\Docker\config\daemon.json ``` 写入内容: ```json { "data-root": "D:/AI_Studio/data/docker" } ``` 重启 Docker 服务生效。 ### ✅ 4. 切换容器类型为 Linux 模式 右键任务栏 Docker Desktop 图标 → Switch to Linux containers --- ## 🧠 第三步:安装 Ollama(推荐使用 Docker 模式) ### ✅ 方法一:使用 Docker 运行 Ollama(推荐) ```powershell docker run -d -p 11434:11434 -v D:/AI_Studio/data/models:/root/.ollama infiniflow/ollama:latest ``` 拉取模型: ```powershell ollama pull llama3 ``` ### ✅ 方法二:使用原生 Windows 版本(备选) 将下载好的 `ollama-windows-amd64.exe` 放入: ``` D:\AI_Studio\apps\ollama ``` 重命名为 `ollama.exe`,并在 PowerShell 中运行: ```powershell cd D:\AI_Studio\apps\ollama .\ollama.exe serve ``` --- ## 📚 第四步:部署 RAGFlow(基于 Docker) ```powershell mkdir D:\AI_Studio\data\ragflow_data docker run -d ` --name ragflow ` -p 8080:8080 ` -v D:/AI_Studio/data/ragflow_data:/data ` -e LOCAL_MODE=1 ` -e MODEL_PROVIDER=ollama ` -e OLLAMA_BASE_URL=http://host.docker.internal:11434 ` -e DEFAULT_VICUNA_MODEL=llama3 ` infiniflow/ragflow:latest ``` 访问: ``` http://localhost:8080 ``` --- ## 🔧 第五步:部署 Dify(基于 Docker) ```powershell mkdir D:\AI_Studio\data\dify_data docker run -d ` --name dify ` -p 3000:3000 ` -v D:/AI_Studio/data/dify_data:/app/data ` -e DATABASE_URL=file:///app/data/dify.db ` -e OLLAMA_API_BASE=http://host.docker.internal:11434 ` -e MODEL_PROVIDER=ollama ` chatchat/oneclick:latest ``` 访问: ``` http://localhost:3000 ``` --- ## 🐍 第六步:安装 Python(用于自动化脚本) ### ✅ 安装建议版本:Python 3.11.x 下载地址: 🔗 [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/) 安装时请勾选: - ✅ Add Python to PATH(添加到系统环境变量) 修改安装路径为: ``` D:\AI_Studio\apps\python ``` ### ✅ 安装常用库 ```powershell pip install docxtpl requests python-docx pandas flask langchain ``` --- ## 📝 第七步:编写自动化生成 Word 报告脚本 保存路径:`D:\AI_Studio\scripts\generate_report.py` ```python from docxtpl import DocxTemplate import os def generate_report(template_name, context): template_path = os.path.join("D:\\AI_Studio\\data\\reports\\templates", template_name) doc = DocxTemplate(template_path) doc.render(context) output_path = os.path.join("D:\\AI_Studio\\data\\reports\\output", f"report_{context['project']}.docx") doc.save(output_path) print(f"Report saved to {output_path}") # 示例调用 generate_report( "geology_template.docx", { "project": "某矿区勘探项目", "location": "新疆某地", "conclusion": "发现高品位铜矿带" } ) ``` --- ## 🔁 第八步:整合工作流(RAGFlow + Dify + Python 报告生成) 你可以通过 Dify 构建如下 AI 工作流: 1. 用户上传 PDF 地质资料 2. Dify 调用 RAGFlow 提取关键信息(如构造、岩性、矿体等) 3. 输出结构化 JSON 数据 4. 传入 Python 脚本填充 Word 模板 5. 自动生成 Word 报告并归档至指定路径 --- ## ✅ 总结部署步骤一览表 | 步骤 | 操作说明 | |------|----------| | 1 | 创建完整目录结构:`installer`, `apps`, `data`, `scripts` | | 2 | 启用 WSL2 并安装 Ubuntu 子系统 | | 3 | 安装 Docker Desktop 到 `D:\AI_Studio\apps\docker` | | 4 | 设置 Docker 数据卷路径为 `D:\AI_Studio\data\docker` | | 5 | 使用 Docker 安装并运行 Ollama | | 6 | 设置 Ollama 模型缓存路径为 `D:\AI_Studio\data\models` | | 7 | 使用 Docker 部署 RAGFlow 到 `D:\AI_Studio\data\ragflow_data` | | 8 | 使用 Docker 部署 Dify 到 `D:\AI_Studio\data\dify_data` | | 9 | 安装 Python 到 `D:\AI_Studio\apps\python` | | 10 | 编写 Python 脚本实现 Word 报告自动生成 | | 11 | 使用 Dify 构建 AI 工作流,整合 RAGFlow 和 Word 生成模块 | --- 分析完善以上部署方案

filetype

import sys import os import fitz # PyMuPDF import pandas as pd import numpy as np import tempfile import shutil import re import time from datetime import datetime from PIL import Image, ImageDraw import cv2 import csv import json from collections import defaultdict from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog, QListWidget, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QLineEdit, QFrame, QSizePolicy, QProgressBar, QMessageBox, QComboBox, QGridLayout, QTextEdit, QDialog, QDialogButtonBox ) from PyQt5.QtCore import Qt, QSize, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QIcon, QFont, QColor, QPixmap, QBrush, QPainter # 集成PaddleOCR try: from paddleocr import PaddleOCR except ImportError: print("PaddleOCR not installed. Please install with: pip install paddlepaddle paddleocr") sys.exit(1) class OCRWorker(QThread): progress_updated = pyqtSignal(int, str) extraction_complete = pyqtSignal(list, str) watermark_removed = pyqtSignal(str, str) def __init__(self, pdf_path, output_dir, watermark_text=None, parent=None): super().__init__(parent) self.pdf_path = pdf_path self.output_dir = output_dir self.watermark_text = watermark_text self.file_name = os.path.basename(pdf_path) self.canceled = False self.ocr = PaddleOCR(use_angle_cls=True, lang='en', use_gpu=True) def run(self): try: # 第一步:去除水印(如果需要) processed_path = self.pdf_path if self.watermark_text: processed_path = self.remove_watermark() if processed_path: self.watermark_removed.emit(processed_path, self.file_name) else: self.progress_updated.emit(100, "水印去除失败") return # 第二步:提取内容 self.extract_content(processed_path) except Exception as e: self.progress_updated.emit(100, f"处理失败: {str(e)}") def remove_watermark(self): """使用OCR检测并去除水印""" try: doc = fitz.open(self.pdf_path) new_doc = fitz.open() output_path = os.path.join(self.output_dir, f"processed_{self.file_name}") total_pages = len(doc) for page_num in range(total_pages): if self.canceled: return None self.progress_updated.emit(int(30 * page_num / total_pages), f"正在处理第 {page_num+1} 页水印") page = doc.load_page(page_num) pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) img_np = np.array(img) # 使用PaddleOCR检测文本 result = self.ocr.ocr(img_np, cls=True) # 检测水印文本 watermark_boxes = [] for line in result: for word_info in line: text = word_info[1][0] if self.watermark_text.lower() in text.lower(): box = word_info[0] # 将浮点坐标转换为整数 int_box = [(int(x), int(y)) for x, y in box] watermark_boxes.append(int_box) # 去除水印(用白色覆盖) if watermark_boxes: img_pil = Image.fromarray(img_np) draw = ImageDraw.Draw(img_pil) for box in watermark_boxes: # 创建覆盖矩形 min_x = min(point[0] for point in box) max_x = max(point[0] for point in box) min_y = min(point[1] for point in box) max_y = max(point[1] for point in box) # 扩展矩形范围确保完全覆盖 expand = 5 draw.rectangle( [min_x - expand, min_y - expand, max_x + expand, max_y + expand], fill=(255, 255, 255) img_np = np.array(img_pil) # 保存处理后的页面 img_bytes = Image.fromarray(img_np).tobytes() new_page = new_doc.new_page(width=img_np.shape[1], height=img_np.shape[0]) new_page.insert_image(fitz.Rect(0, 0, img_np.shape[1], img_np.shape[0]), stream=img_bytes) new_doc.save(output_path) new_doc.close() doc.close() return output_path except Exception as e: print(f"Error removing watermark: {e}") return None def extract_content(self, pdf_path): """使用PaddleOCR提取内容""" try: doc = fitz.open(pdf_path) extracted_data = [] total_pages = len(doc) for page_num in range(total_pages): if self.canceled: return self.progress_updated.emit(30 + int(70 * page_num / total_pages), f"正在提取第 {page_num+1} 页内容") page = doc.load_page(page_num) pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) img_np = np.array(img) # 使用PaddleOCR提取文本 result = self.ocr.ocr(img_np, cls=True) # 处理OCR结果 page_text = "" for line in result: line_text = " ".join([word_info[1][0] for word_info in line]) page_text += line_text + "\n" # 提取结构化数据(示例逻辑) extracted = self.extract_structured_data(page_text, page_num + 1) extracted_data.extend(extracted) doc.close() self.extraction_complete.emit(extracted_data, self.file_name) except Exception as e: self.progress_updated.emit(100, f"内容提取失败: {str(e)}") def extact_structured_data(self, text, page_num): """从文本中提取结构化数据(示例实现)""" extracted = [] # 提取发票信息 invoice_match = re.search(r'Invoice\s+Number\s*:\s*(\w+)', text, re.IGNORECASE) date_match = re.search(r'Date\s*:\s*(\d{2}/\d{2}/\d{4})', text, re.IGNORECASE) total_match = re.search(r'Total\s+Amount\s*:\s*([\d,]+\.\d{2})', text, re.IGNORECASE) if invoice_match or date_match or total_match: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "Invoice", "Invoice Number": invoice_match.group(1) if invoice_match else "N/A", "Date": date_match.group(1) if date_match else "N/A", "Amount": f"${total_match.group(1)}" if total_match else "N/A" }) # 提取报告信息 report_match = re.search(r'Report\s+Title\s*:\s*(.+)', text, re.IGNORECASE) author_match = re.search(r'Author\s*:\s*(.+)', text, re.IGNORECASE) if report_match or author_match: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "Report", "Report Title": report_match.group(1) if report_match else "N/A", "Author": author_match.group(1) if author_match else "N/A", "Summary": text[:200] + "..." if len(text) > 200 else text }) # 如果没有匹配到特定结构,返回整个页面文本 if not extracted: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "General", "Content": text }) return extracted def cancel(self): self.canceled = True class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PDF智能处理工具") self.setGeometry(100, 100, 1200, 800) self.setMinimumSize(1000, 700) # 应用主色调 self.primary_color = "#2c3e50" self.secondary_color = "#3498db" self.accent_color = "#e67e22" self.light_color = "#ecf0f1" self.dark_color = "#34495e" # 初始化状态 self.current_files = [] self.extracted_data = [] self.history_data = [] self.ocr_worker = None self.temp_dir = tempfile.mkdtemp() # 创建中央部件和主布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) # 创建顶部栏 self.create_header(main_layout) # 创建主内容区域 self.content_stack = QStackedWidget() main_layout.addWidget(self.content_stack, 1) # 创建各个页面 self.home_page = self.create_home_page() self.upload_page = self.create_upload_page() self.history_page = self.create_history_page() self.analysis_page = self.create_analysis_page() self.content_stack.addWidget(self.home_page) self.content_stack.addWidget(self.upload_page) self.content_stack.addWidget(self.history_page) self.content_stack.addWidget(self.analysis_page) # 创建底部导航栏 self.create_footer(main_layout) # 模拟一些历史数据 self.simulate_history_data() # 设置首页为默认页面 self.content_stack.setCurrentIndex(0) self.update_home_page() def closeEvent(self, event): """清理临时文件""" try: shutil.rmtree(self.temp_dir, ignore_errors=True) except: pass event.accept() def simulate_history_data(self): """模拟一些历史数据用于展示""" for i in range(5): self.history_data.append({ "id": i, "file_name": f"document_{i+1}.pdf", "date": f"2023-0{i+1}-15", "status": "Completed", "pages": i+3, "type": "Invoice" if i % 2 == 0 else "Report", "extracted_data": [ { "Document": f"document_{i+1}.pdf", "Page": 1, "Type": "Invoice", "Invoice Number": f"INV-2023-{i+1:04d}", "Date": f"2023-0{i+1}-15", "Amount": f"${(i+1)*250:.2f}" } ] }) def create_header(self, main_layout): """创建应用头部""" header = QWidget() header.setStyleSheet(f"background-color: {self.primary_color}; padding: 15px;") header_layout = QHBoxLayout(header) header_layout.setContentsMargins(20, 10, 20, 10) # 应用标题 title_label = QLabel("PDF智能处理工具") title_label.setStyleSheet(f"color: white; font-size: 24px; font-weight: bold;") header_layout.addWidget(title_label) # 右侧用户区域 user_widget = QWidget() user_layout = QHBoxLayout(user_widget) user_layout.setSpacing(15) user_icon = QLabel() user_icon.setPixmap(self.create_icon("👤", 40)) user_layout.addWidget(user_icon) user_name = QLabel("管理员") user_name.setStyleSheet("color: white; font-size: 16px;") user_layout.addWidget(user_name) header_layout.addWidget(user_widget) main_layout.addWidget(header) def create_footer(self, main_layout): """创建底部导航栏""" footer = QWidget() footer.setStyleSheet(f"background-color: {self.dark_color};") footer.setFixedHeight(60) footer_layout = QHBoxLayout(footer) footer_layout.setContentsMargins(0, 0, 0, 0) footer_layout.setSpacing(0) # 导航按钮 nav_items = [ ("首页", "home", 0), ("上传与提取", "upload", 1), ("历史记录", "history", 2), ("数据分析", "analysis", 3) ] for text, icon_name, index in nav_items: btn = QPushButton(text) btn.setIcon(QIcon(self.create_icon("🏠" if icon_name=="home" else "📤" if icon_name=="upload" else "📋" if icon_name=="history" else "📊", 24))) btn.setIconSize(QSize(24, 24)) btn.setFixedHeight(60) btn.setStyleSheet(f""" QPushButton {{ color: {self.light_color}; font-size: 14px; font-weight: bold; border: none; background-color: {self.dark_color}; }} QPushButton:hover {{ background-color: {self.primary_color}; }} """) btn.clicked.connect(lambda _, idx=index: self.navigate_to(idx)) footer_layout.addWidget(btn) main_layout.addWidget(footer) def create_icon(self, emoji, size=24): """创建表情符号图标""" pixmap = QPixmap(size, size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setFont(QFont("Arial", size - 4)) painter.drawText(pixmap.rect(), Qt.AlignCenter, emoji) painter.end() return pixmap def navigate_to(self, index): """导航到指定页面""" self.content_stack.setCurrentIndex(index) # 更新页面内容 if index == 0: # 首页 self.update_home_page() elif index == 1: # 上传与提取 pass elif index == 2: # 历史记录 self.update_history_page() elif index == 3: # 数据分析 self.update_analysis_page() def create_home_page(self): """创建首页""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 欢迎卡片 welcome_card = QFrame() welcome_card.setStyleSheet(f""" QFrame {{ background-color: white; border-radius: 10px; padding: 20px; }} """) welcome_layout = QVBoxLayout(welcome_card) welcome_title = QLabel("欢迎使用PDF智能处理工具") welcome_title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") welcome_layout.addWidget(welcome_title) welcome_text = QLabel("本工具提供PDF水印去除、内容提取和数据分析功能,支持批量处理PDF文件,快速提取结构化数据并导出为Excel或CSV格式。") welcome_text.setStyleSheet("font-size: 16px; color: #7f8c8d;") welcome_text.setWordWrap(True) welcome_layout.addWidget(welcome_text) # OCR信息 ocr_info = QLabel("当前使用PaddleOCR引擎,支持中英文识别") ocr_info.setStyleSheet("font-size: 14px; color: #3498db; font-weight: bold;") welcome_layout.addWidget(ocr_info) layout.addWidget(welcome_card) # 最近上传区域 recent_label = QLabel("最近上传") recent_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;") layout.addWidget(recent_label) # 最近上传列表 self.recent_list = QListWidget() self.recent_list.setStyleSheet(""" QListWidget { background-color: white; border-radius: 10px; border: 1px solid #ddd; } QListWidget::item { padding: 15px; border-bottom: 1px solid #eee; } QListWidget::item:selected { background-color: #e6f7ff; } """) self.recent_list.setAlternatingRowColors(True) layout.addWidget(self.recent_list, 1) # 快捷操作按钮 quick_actions = QWidget() quick_layout = QHBoxLayout(quick_actions) quick_layout.setSpacing(15) actions = [ ("上传新文件", "upload", self.navigate_to_upload), ("查看历史记录", "history", lambda: self.navigate_to(2)), ("数据分析", "analysis", lambda: self.navigate_to(3)) ] for text, icon_name, action in actions: btn = QPushButton(text) btn.setIcon(QIcon(self.create_icon("📤" if icon_name=="upload" else "📋" if icon_name=="history" else "📊", 24))) btn.setIconSize(QSize(24, 24)) btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; font-size: 16px; font-weight: bold; padding: 12px 20px; border-radius: 8px; }} QPushButton:hover {{ background-color: #2980b9; }} """) btn.clicked.connect(action) quick_layout.addWidget(btn) layout.addWidget(quick_actions) return page def update_home_page(self): """更新首页内容""" self.recent_list.clear() # 显示最近的5条记录 for item in self.history_data[:5]: list_item = QLabel(f"""
{item['file_name']}
上传时间: {item['date']} | 状态: {item['status']} | 类型: {item['type']}
""") list_widget = QListWidgetItem(self.recent_list) list_widget.setSizeHint(list_item.sizeHint()) self.recent_list.addItem(list_widget) self.recent_list.setItemWidget(list_widget, list_item) def navigate_to_upload(self): """导航到上传页面""" self.content_stack.setCurrentIndex(1) def create_upload_page(self): """创建上传与提取页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题 title = QLabel("上传与提取") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # 上传区域 upload_card = QFrame() upload_card.setStyleSheet(f""" QFrame {{ background-color: white; border-radius: 10px; border: 2px dashed {self.secondary_color}; padding: 40px; }} """) upload_layout = QVBoxLayout(upload_card) upload_layout.setAlignment(Qt.AlignCenter) upload_icon = QLabel() upload_icon.setPixmap(self.create_icon("📤", 80)) upload_icon.setAlignment(Qt.AlignCenter) upload_layout.addWidget(upload_icon) upload_text = QLabel("拖放PDF文件到此处,或点击选择文件") upload_text.setStyleSheet("font-size: 18px; color: #7f8c8d; margin-top: 20px;") upload_text.setAlignment(Qt.AlignCenter) upload_layout.addWidget(upload_text) upload_btn = QPushButton("选择PDF文件") upload_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; font-size: 16px; padding: 10px 20px; border-radius: 5px; margin-top: 20px; }} QPushButton:hover {{ background-color: #2980b9; }} """) upload_btn.clicked.connect(self.select_pdf_files) upload_layout.addWidget(upload_btn, alignment=Qt.AlignCenter) # 水印选项 watermark_layout = QHBoxLayout() watermark_layout.setSpacing(10) watermark_label = QLabel("水印文本(可选):") watermark_label.setStyleSheet("font-size: 14px;") watermark_layout.addWidget(watermark_label) self.watermark_input = QLineEdit() self.watermark_input.setPlaceholderText("输入要删除的水印文本") self.watermark_input.setStyleSheet("padding: 5px; border: 1px solid #ddd; border-radius: 3px;") watermark_layout.addWidget(self.watermark_input, 1) upload_layout.addLayout(watermark_layout) layout.addWidget(upload_card, 1) # 进度区域 progress_layout = QVBoxLayout() progress_layout.setSpacing(10) progress_label = QLabel("处理进度") progress_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2c3e50;") progress_layout.addWidget(progress_label) self.progress_bar = QProgressBar() self.progress_bar.setStyleSheet(""" QProgressBar { border: 1px solid #ddd; border-radius: 5px; text-align: center; height: 25px; } QProgressBar::chunk { background-color: #3498db; width: 10px; } """) progress_layout.addWidget(self.progress_bar) self.progress_text = QLabel("等待处理文件...") self.progress_text.setStyleSheet("font-size: 14px; color: #7f8c8d;") progress_layout.addWidget(self.progress_text) # 取消按钮 self.cancel_btn = QPushButton("取消处理") self.cancel_btn.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 8px 20px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #c0392b; } """) self.cancel_btn.clicked.connect(self.cancel_processing) self.cancel_btn.setVisible(False) progress_layout.addWidget(self.cancel_btn, alignment=Qt.AlignRight) layout.addLayout(progress_layout) return page def select_pdf_files(self): """选择PDF文件""" files, _ = QFileDialog.getOpenFileNames( self, "选择PDF文件", "", "PDF文件 (*.pdf)" ) if files: self.current_files = files self.process_files() def process_files(self): """处理选中的文件""" if not self.current_files: return # 重置状态 self.extracted_data = [] # 显示取消按钮 self.cancel_btn.setVisible(True) # 处理第一个文件 file_path = self.current_files[0] file_name = os.path.basename(file_path) # 获取水印文本 watermark_text = self.watermark_input.text().strip() or None # 创建工作线程 self.ocr_worker = OCRWorker( file_path, self.temp_dir, watermark_text ) # 连接信号 self.ocr_worker.progress_updated.connect(self.update_progress) self.ocr_worker.watermark_removed.connect(self.on_watermark_removed) self.ocr_worker.extraction_complete.connect(self.on_extraction_complete) # 开始处理 self.ocr_worker.start() def update_progress(self, progress, message): """更新处理进度""" self.progress_bar.setValue(progress) self.progress_text.setText(message) def on_watermark_removed(self, output_path, file_name): """水印去除完成""" self.progress_text.setText(f"水印已移除: {file_name}") def on_extraction_complete(self, extracted_data, file_name): """内容提取完成""" self.extracted_data.extend(extracted_data) # 保存到历史记录 self.history_data.insert(0, { "id": len(self.history_data), "file_name": file_name, "date": datetime.now().strftime("%Y-%m-%d %H:%M"), "status": "Completed", "pages": len(extracted_data), "type": "Invoice" if any(d['Type'] == 'Invoice' for d in extracted_data) else "Report", "extracted_data": extracted_data }) # 显示预览 self.show_preview(extracted_data, file_name) # 处理下一个文件(如果有) if len(self.current_files) > 1: self.current_files.pop(0) self.process_files() else: self.current_files = [] self.cancel_btn.setVisible(False) def cancel_processing(self): """取消处理""" if self.ocr_worker and self.ocr_worker.isRunning(): self.ocr_worker.cancel() self.ocr_worker.wait() self.progress_text.setText("处理已取消") self.cancel_btn.setVisible(False) def show_preview(self, data, file_name): """显示预览窗口""" # 创建预览对话框 preview_dialog = QDialog(self) preview_dialog.setWindowTitle(f"预览 - {file_name}") preview_dialog.resize(1000, 700) layout = QVBoxLayout(preview_dialog) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) # 标题 title = QLabel(f"文件内容提取结果: {file_name}") title.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # OCR引擎信息 ocr_info = QLabel("使用PaddleOCR引擎提取内容") ocr_info.setStyleSheet("font-size: 14px; color: #3498db; font-weight: bold;") layout.addWidget(ocr_info) # 数据表格 if data: table = QTableWidget() table.setRowCount(len(data)) # 获取所有可能的列 all_columns = set() for item in data: all_columns.update(item.keys()) columns = sorted(all_columns) table.setColumnCount(len(columns)) table.setHorizontalHeaderLabels(columns) # 填充数据 for row_idx, row_data in enumerate(data): for col_idx, col_name in enumerate(columns): value = str(row_data.get(col_name, "")) item = QTableWidgetItem(value) table.setItem(row_idx, col_idx, item) # 表格样式 table.setStyleSheet(""" QTableWidget { background-color: white; border: 1px solid #ddd; gridline-color: #eee; } QHeaderView::section { background-color: #f8f9fa; padding: 8px; border: none; font-weight: bold; } """) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked) table.setSelectionMode(QAbstractItemView.SingleSelection) table.setSelectionBehavior(QAbstractItemView.SelectRows) layout.addWidget(table, 1) else: no_data_label = QLabel("未提取到有效数据") no_data_label.setStyleSheet("font-size: 16px; color: #7f8c8d;") no_data_label.setAlignment(Qt.AlignCenter) layout.addWidget(no_data_label, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) export_excel_btn = QPushButton("导出为Excel") export_excel_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} QPushButton:hover {{ background-color: #2980b9; }} """) export_excel_btn.clicked.connect(self.export_to_excel) btn_layout.addWidget(export_excel_btn) export_csv_btn = QPushButton("导出为CSV") export_csv_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} QPushButton:hover {{ background-color: #d35400; }} """) export_csv_btn.clicked.connect(self.export_to_csv) btn_layout.addWidget(export_csv_btn) close_btn = QPushButton("关闭") close_btn.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #7f8c8d; } """) close_btn.clicked.connect(preview_dialog.accept) btn_layout.addWidget(close_btn) layout.addLayout(btn_layout) preview_dialog.exec_() def export_to_excel(self): """导出为Excel""" if not self.extracted_data: QMessageBox.warning(self, "导出失败", "没有可导出的数据") return file_path, _ = QFileDialog.getSaveFileName( self, "保存Excel文件", "", "Excel文件 (*.xlsx)" ) if file_path: if not file_path.endswith('.xlsx'): file_path += '.xlsx' try: # 将数据转换为DataFrame df = pd.DataFrame(self.extracted_data) # 导出到Excel df.to_excel(file_path, index=False) QMessageBox.information(self, "导出成功", f"数据已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def export_to_csv(self): """导出为CSV""" if not self.extracted_data: QMessageBox.warning(self, "导出失败", "没有可导出的数据") return file_path, _ = QFileDialog.getSaveFileName( self, "保存CSV文件", "", "CSV文件 (*.csv)" ) if file_path: if not file_path.endswith('.csv'): file_path += '.csv' try: # 获取所有可能的字段 all_fields = set() for item in self.extracted_data: all_fields.update(item.keys()) # 写入CSV with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=sorted(all_fields)) writer.writeheader() writer.writerows(self.extracted_data) QMessageBox.information(self, "导出成功", f"数据已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def create_history_page(self): """创建历史记录页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题和搜索 header_layout = QHBoxLayout() title = QLabel("历史记录") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") header_layout.addWidget(title) search_layout = QHBoxLayout() search_layout.setSpacing(10) self.search_input = QLineEdit() self.search_input.setPlaceholderText("搜索文件名...") self.search_input.setStyleSheet(""" QLineEdit { padding: 8px 15px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; } """) self.search_input.setFixedWidth(300) self.search_input.returnPressed.connect(self.search_history) search_layout.addWidget(self.search_input) search_btn = QPushButton("搜索") search_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 8px 20px; border-radius: 5px; }} """) search_btn.clicked.connect(self.search_history) search_layout.addWidget(search_btn) header_layout.addLayout(search_layout) layout.addLayout(header_layout) # 历史记录表格 self.history_table = QTableWidget() self.history_table.setColumnCount(5) self.history_table.setHorizontalHeaderLabels(["文件名", "上传时间", "状态", "页数", "类型"]) self.history_table.setStyleSheet(""" QTableWidget { background-color: white; border: 1px solid #ddd; gridline-color: #eee; } QHeaderView::section { background-color: #f8f9fa; padding: 12px; border: none; font-weight: bold; } """) self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.history_table.verticalHeader().setVisible(False) self.history_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.history_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.history_table.setSortingEnabled(True) layout.addWidget(self.history_table, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) view_btn = QPushButton("查看详情") view_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) view_btn.clicked.connect(self.view_history_detail) btn_layout.addWidget(view_btn) export_btn = QPushButton("导出记录") export_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) export_btn.clicked.connect(self.export_history) btn_layout.addWidget(export_btn) delete_btn = QPushButton("删除记录") delete_btn.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; } """) delete_btn.clicked.connect(self.delete_history) btn_layout.addWidget(delete_btn) layout.addLayout(btn_layout) return page def update_history_page(self): """更新历史记录页面""" self.history_table.setRowCount(len(self.history_data)) for row_idx, item in enumerate(self.history_data): self.history_table.setItem(row_idx, 0, QTableWidgetItem(item["file_name"])) self.history_table.setItem(row_idx, 1, QTableWidgetItem(item["date"])) status_item = QTableWidgetItem(item["status"]) if item["status"] == "Completed": status_item.setForeground(QBrush(QColor("#27ae60"))) else: status_item.setForeground(QBrush(QColor("#e74c3c"))) self.history_table.setItem(row_idx, 2, status_item) self.history_table.setItem(row_idx, 3, QTableWidgetItem(str(item["pages"]))) self.history_table.setItem(row_idx, 4, QTableWidgetItem(item["type"])) def search_history(self): """搜索历史记录""" search_text = self.search_input.text().lower() if not search_text: self.update_history_page() return filtered_data = [item for item in self.history_data if search_text in item["file_name"].lower()] self.history_table.setRowCount(len(filtered_data)) for row_idx, item in enumerate(filtered_data): self.history_table.setItem(row_idx, 0, QTableWidgetItem(item["file_name"])) self.history_table.setItem(row_idx, 1, QTableWidgetItem(item["date"])) status_item = QTableWidgetItem(item["status"]) if item["status"] == "Completed": status_item.setForeground(QBrush(QColor("#27ae60"))) else: status_item.setForeground(QBrush(QColor("#e74c3c"))) self.history_table.setItem(row_idx, 2, status_item) self.history_table.setItem(row_idx, 3, QTableWidgetItem(str(item["pages"]))) self.history_table.setItem(row_idx, 4, QTableWidgetItem(item["type"])) def view_history_detail(self): """查看历史记录详情""" selected_row = self.history_table.currentRow() if selected_row >= 0: file_name = self.history_table.item(selected_row, 0).text() history_item = next((item for item in self.history_data if item["file_name"] == file_name), None) if history_item: self.show_preview(history_item["extracted_data"], file_name) else: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") def export_history(self): """导出历史记录""" if not self.history_data: QMessageBox.warning(self, "导出失败", "没有可导出的历史记录") return file_path, _ = QFileDialog.getSaveFileName( self, "保存历史记录", "", "CSV文件 (*.csv)" ) if file_path: if not file_path.endswith('.csv'): file_path += '.csv' try: with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ['id', 'file_name', 'date', 'status', 'pages', 'type'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for item in self.history_data: writer.writerow({ 'id': item['id'], 'file_name': item['file_name'], 'date': item['date'], 'status': item['status'], 'pages': item['pages'], 'type': item['type'] }) QMessageBox.information(self, "导出成功", f"历史记录已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def delete_history(self): """删除历史记录""" selected_row = self.history_table.currentRow() if selected_row >= 0: file_name = self.history_table.item(selected_row, 0).text() reply = QMessageBox.question( self, '确认删除', f"确定要删除 '{file_name}' 的记录吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.history_data = [item for item in self.history_data if item['file_name'] != file_name] self.update_history_page() else: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") def create_analysis_page(self): """创建数据分析页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题 title = QLabel("数据分析") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # 选择历史记录 select_layout = QHBoxLayout() select_label = QLabel("选择历史记录:") select_label.setStyleSheet("font-size: 16px;") select_layout.addWidget(select_label) self.history_combo = QComboBox() self.history_combo.setFixedWidth(300) self.history_combo.setStyleSheet("padding: 5px; border: 1px solid #ddd; border-radius: 3px;") select_layout.addWidget(self.history_combo) analyze_btn = QPushButton("分析数据") analyze_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 8px 20px; border-radius: 5px; }} """) analyze_btn.clicked.connect(self.analyze_data) select_layout.addWidget(analyze_btn) layout.addLayout(select_layout) # 图表区域 chart_container = QWidget() chart_layout = QGridLayout(chart_container) # 图表1 self.chart1_label = QLabel() self.chart1_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart1_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart1_label, 0, 0) # 图表2 self.chart2_label = QLabel() self.chart2_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart2_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart2_label, 0, 1) # 图表3 self.chart3_label = QLabel() self.chart3_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart3_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart3_label, 1, 0) # 图表4 self.chart4_label = QLabel() self.chart4_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart4_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart4_label, 1, 1) layout.addWidget(chart_container, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) refresh_btn = QPushButton("刷新数据") refresh_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) refresh_btn.clicked.connect(self.update_analysis_page) btn_layout.addWidget(refresh_btn) export_btn = QPushButton("导出报告") export_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) export_btn.clicked.connect(self.export_report) btn_layout.addWidget(export_btn) layout.addLayout(btn_layout) return page def update_analysis_page(self): """更新数据分析页面""" # 更新历史记录选择框 self.history_combo.clear() for item in self.history_data: self.history_combo.addItem(f"{item['file_name']} - {item['date']}", item) # 显示初始图表 self.show_chart(self.chart1_label, "📊", "数据统计") self.show_chart(self.chart2_label, "📈", "趋势分析") self.show_chart(self.chart3_label, "📉", "比较分析") self.show_chart(self.chart4_label, "🧮", "类型分布") def show_chart(self, label, emoji, title): """显示模拟图表""" pixmap = QPixmap(400, 250) pixmap.fill(Qt.white) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) # 绘制标题 painter.setFont(QFont("Arial", 14, QFont.Bold)) painter.drawText(pixmap.rect().adjusted(0, 10, 0, 0), Qt.AlignTop | Qt.AlignHCenter, title) # 绘制图表图标 painter.setFont(QFont("Arial", 80)) painter.drawText(pixmap.rect(), Qt.AlignCenter, emoji) # 绘制边框 painter.setPen(QColor("#ddd")) painter.drawRect(pixmap.rect().adjusted(0, 0, -1, -1)) painter.end() label.setPixmap(pixmap) def analyze_data(self): """分析数据""" if self.history_combo.currentIndex() < 0: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") return selected_item = self.history_combo.currentData() extracted_data = selected_item.get('extracted_data', []) if not extracted_data: QMessageBox.warning(self, "分析失败", "所选记录没有可分析的数据") return # 在实际应用中,这里会使用真实的数据分析逻辑 # 这里仅显示消息 QMessageBox.information( self, "数据分析", f"已对 '{selected_item['file_name']}' 进行数据分析\n" f"包含 {len(extracted_data)} 条记录" ) def export_report(self): """导出分析报告""" if self.history_combo.currentIndex() < 0: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") return selected_item = self.history_combo.currentData() file_path, _ = QFileDialog.getSaveFileName( self, "保存分析报告", "", "PDF文件 (*.pdf)" ) if file_path: if not file_path.endswith('.pdf'): file_path += '.pdf' try: # 在实际应用中,这里会生成真实的PDF报告 # 这里仅模拟导出 time.sleep(1) # 模拟生成报告的时间 QMessageBox.information( self, "导出成功", f"分析报告已成功导出到: {file_path}\n" f"包含对 '{selected_item['file_name']}' 的分析结果" ) except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") if __name__ == "__main__": # 设置应用程序 app = QApplication(sys.argv) # 设置应用程序样式 app.setStyle("Fusion") # 创建并显示主窗口 window = MainWindow() window.show() # 执行应用程序 sys.exit(app.exec_())将数据分析功能改为接入本地部署的AI大模型进行分析和绘制图表

filetype

周次模块上午(10:00-12:00)下午(14:00-18:00)实战项目/作业 上午(10:00-12:00) 下午(14:00-18:00) 实战项目/作业 1 Python基础 Python语法基础:变量、数据类型、流程控制 函数、模块、异常处理;文件操作(JSON/CSV) 编写一个文件处理工具,支持JSON/CSV转换 2 Python进阶 面向对象编程:类、继承、多态 常用库:requests, logging, unittest;单元测试实践 实现一个支持日志记录的API请求工具 3 Python测试库 Pytest框架入门:用例编写、断言、夹具 参数化测试、Allure报告生成;Mock技术 为API工具编写Pytest测试用例并生成报告 4 UI自动化 Selenium基础:元素定位、页面操作 Page Object模式;Pytest集成Selenium 网站登录、搜索功能自动化 5 接口自动化 Requests深度使用:HTTP协议、会话管理 接口测试框架设计:封装请求、数据驱动 搭建接口测试框架,实现登录接口测试 6 接口高级实战 接口安全:OAuth2.0;接口依赖处理 Mock服务(使用Python库);持续集成(GitHub Actions) 实现带Token验证的接口测试 7 接口性能一体化 接口性能测试基础:Locust核心概念 编写Locust性能测试脚本;分布式压测 对接口进行压力测试并生成报告 8 性能测试 性能监控与分析:资源监控、结果分析 性能调优实战;Locust与Prometheus集成 分析性能瓶颈并优化 9 测试开发(上) 测试框架优化:插件机制、配置管理 测试报告定制:Allure二次开发;钉钉/邮件通知 开发一个带通知功能的测试报告插件 10 测试开发(下) 测试工具链:Docker化测试环境 低代码测试平台初探;测试数据工厂设计 构建一个测试用例管理平台原型 我这是给0基础的测试同学,的课程内容,但是现在课程要变成12节课,所以得修改下,另外上面的课程10需要改掉,现在这个没懂啥意思,可以构建一个测试用例管理平台没错,但是没看出和10 节课教的东西有什么关联

filetype

Rscript sangeranalyseR2.R -i 013-G2.ab1 -r GJB2.fasta -q -p -m 0.1 Bioconductor version '3.20' is out-of-date; the current release version '3.21' is available with R version '4.5'; see https://bioconductor.org/install INFO [2025-08-11 17:21:28] ------------------------------------------------ INFO [2025-08-11 17:21:28] -------- Creating 'SangerRead' instance -------- INFO [2025-08-11 17:21:28] ------------------------------------------------ INFO [2025-08-11 17:21:28] >> Forward Read: Creating abif & sangerseq ... INFO [2025-08-11 17:21:28] >> Creating Forward Read raw abif ... INFO [2025-08-11 17:21:28] >> Creating Forward Read raw sangerseq ... INFO [2025-08-11 17:21:28] * Making basecall !! INFO [2025-08-11 17:21:28] * Updating slots in 'SangerRead' instance !! SUCCESS [2025-08-11 17:21:29] -------------------------------------------------------- SUCCESS [2025-08-11 17:21:29] -------- 'SangerRead' S4 instance is created !! -------- SUCCESS [2025-08-11 17:21:29] -------------------------------------------------------- SUCCESS [2025-08-11 17:21:29] >> '013-G2.ab1' is created (Forward Read; ABIF). INFO [2025-08-11 17:21:29] >> Read is trimmed by 'M1 - Mott’s trimming algorithm'. DEBUG [2025-08-11 17:21:29] >> For more information, please run 'object'. DEBUG [2025-08-11 17:21:29] >> Run 'object@objectResults@readResultTable' to check the result of the Sanger read INFO [2025-08-11 17:21:29] Your input is 'SangerRead' S4 instance INFO [2025-08-11 17:21:29] >>> outputDir : C:\Users\Administrator\AppData\Local\Temp\RtmpYBVwty processing file: SangerRead_Report_ab1.Rmd |................................................. | 95% [unnamed-chunk-9]INFO [2025-08-11 17:21:31] * Making basecall !! INFO [2025-08-11 17:21:31] * Updating slots in 'SangerRead' instance !! output file: SangerRead_Report_ab1.knit.md "C:/PROGRA~1/Pandoc/pandoc" +RTS -K512m -RTS SangerRead_Report_ab1.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output pandoc237871d63756.html --lua-filter "C:\PROGRA~1\R\R-44~1.3\library\RMARKD~1\RMARKD~1\lua\PAGEBR~1.LUA" --lua-filter "C:\PROGRA~1\R\R-44~1.3\library\RMARKD~1\RMARKD~1\lua\LATEX-~1.LUA" --lua-filter "C:\PROGRA~1\R\R-44~1.3\library\RMARKD~1\RMARKD~1\lua\TABLE-~1.LUA" --embed-resources --standalone --variable bs3=TRUE --section-divs --table-of-contents --toc-depth 3 --variable toc_float=1 --variable toc_selectors=h1,h2,h3 --variable toc_collapsed=1 --variable toc_smooth_scroll=1 --variable toc_print=1 --template "C:\PROGRA~1\R\R-44~1.3\library\RMARKD~1\rmd\h\DEFAUL~1.HTM" --no-highlight --variable highlightjs=1 --variable theme=bootstrap --mathjax --variable "mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" --include-in-header "C:\Users\ADMINI~1\AppData\Local\Temp\RtmpYBVwty\rmarkdown-str237857086851.html" [WARNING] Div at SangerRead_Report_ab1.knit.md line 153 column 1 unclosed at SangerRead_Report_ab1.knit.md line 232 column 1, closing implicitly. [WARNING] Div at SangerRead_Report_ab1.knit.md line 147 column 1 unclosed at SangerRead_Report_ab1.knit.md line 232 column 1, closing implicitly.

彷徨的牛
  • 粉丝: 64
上传资源 快速赚钱