From 3b27d2cfd4c90950519db58f7e6fc04941b5518d Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 18 Mar 2026 22:28:40 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=9C=AA=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=A5=BD=E7=9A=84=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/lyxy-kb/SKILL.md | 76 --- skills/lyxy-kb/references/query-strategy.md | 43 -- skills/lyxy-kb/references/structure.md | 125 ----- skills/lyxy-kb/references/workflow.md | 86 --- skills/lyxy-reader-html/SKILL.md | 75 --- .../references/error-handling.md | 54 -- .../lyxy-reader-html/references/examples.md | 59 -- skills/lyxy-reader-html/references/parsers.md | 68 --- skills/lyxy-reader-html/scripts/README.md | 323 ----------- skills/lyxy-reader-html/scripts/common.py | 225 -------- skills/lyxy-reader-html/scripts/downloader.py | 263 --------- .../lyxy-reader-html/scripts/html_parser.py | 140 ----- skills/lyxy-reader-html/scripts/parser.py | 128 ----- skills/lyxy-reader-office/SKILL.md | 74 --- .../references/error-handling.md | 41 -- .../lyxy-reader-office/references/examples.md | 55 -- .../lyxy-reader-office/references/parsers.md | 58 -- skills/lyxy-reader-office/scripts/README.md | 503 ------------------ .../__pycache__/common.cpython-311.pyc | Bin 18181 -> 0 bytes .../__pycache__/docx_parser.cpython-311.pyc | Bin 16060 -> 0 bytes .../__pycache__/pdf_parser.cpython-311.pyc | Bin 6948 -> 0 bytes .../__pycache__/pptx_parser.cpython-311.pyc | Bin 15272 -> 0 bytes .../__pycache__/xlsx_parser.cpython-311.pyc | Bin 16514 -> 0 bytes skills/lyxy-reader-office/scripts/common.py | 337 ------------ .../lyxy-reader-office/scripts/docx_parser.py | 308 ----------- skills/lyxy-reader-office/scripts/parser.py | 166 ------ .../lyxy-reader-office/scripts/pdf_parser.py | 134 ----- .../lyxy-reader-office/scripts/pptx_parser.py | 330 ------------ .../lyxy-reader-office/scripts/xlsx_parser.py | 286 ---------- 29 files changed, 3957 deletions(-) delete mode 100644 skills/lyxy-kb/SKILL.md delete mode 100644 skills/lyxy-kb/references/query-strategy.md delete mode 100644 skills/lyxy-kb/references/structure.md delete mode 100644 skills/lyxy-kb/references/workflow.md delete mode 100644 skills/lyxy-reader-html/SKILL.md delete mode 100644 skills/lyxy-reader-html/references/error-handling.md delete mode 100644 skills/lyxy-reader-html/references/examples.md delete mode 100644 skills/lyxy-reader-html/references/parsers.md delete mode 100644 skills/lyxy-reader-html/scripts/README.md delete mode 100644 skills/lyxy-reader-html/scripts/common.py delete mode 100644 skills/lyxy-reader-html/scripts/downloader.py delete mode 100644 skills/lyxy-reader-html/scripts/html_parser.py delete mode 100644 skills/lyxy-reader-html/scripts/parser.py delete mode 100644 skills/lyxy-reader-office/SKILL.md delete mode 100644 skills/lyxy-reader-office/references/error-handling.md delete mode 100644 skills/lyxy-reader-office/references/examples.md delete mode 100644 skills/lyxy-reader-office/references/parsers.md delete mode 100644 skills/lyxy-reader-office/scripts/README.md delete mode 100644 skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc delete mode 100644 skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc delete mode 100644 skills/lyxy-reader-office/scripts/__pycache__/pdf_parser.cpython-311.pyc delete mode 100644 skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc delete mode 100644 skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc delete mode 100644 skills/lyxy-reader-office/scripts/common.py delete mode 100644 skills/lyxy-reader-office/scripts/docx_parser.py delete mode 100644 skills/lyxy-reader-office/scripts/parser.py delete mode 100644 skills/lyxy-reader-office/scripts/pdf_parser.py delete mode 100644 skills/lyxy-reader-office/scripts/pptx_parser.py delete mode 100644 skills/lyxy-reader-office/scripts/xlsx_parser.py diff --git a/skills/lyxy-kb/SKILL.md b/skills/lyxy-kb/SKILL.md deleted file mode 100644 index 6ea653e..0000000 --- a/skills/lyxy-kb/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: lyxy-kb -description: 基于文件的个人知识库管理 skill。当用户说"创建知识库"、"初始化知识项目"、"入库文档"、"知识问答"、"基于文档回答"时使用。支持文档解析入库、增量摘要、渐进式问答。配合 /lyxy-kb-init、/lyxy-kb-ingest、/lyxy-kb-ask 等 command 使用。 -compatibility: 依赖 lyxy-reader-office skill 解析 office 文档(.docx/.pdf/.pptx/.xlsx),依赖 lyxy-runner-python skill 执行 Python 脚本。 ---- - -# 个人知识库 Skill - -基于文件的个人知识库管理系统。将项目相关文档组织为可被大模型高效检索和问答的知识库,支持文档解析入库、增量摘要、渐进式问答。 - -## Purpose - -**纯文件驱动**:不依赖数据库或向量存储,所有数据以文件形式存在于项目目录中。 - -**渐进式查询**:通过 project.md 摘要索引 + parsed 详细文件的分层结构,优先读取摘要,按需加载详细内容,节省 token 消耗。 - -**增量管理**:支持增量解析入库和增量更新摘要,避免重复处理已入库的文档。 - -## When to Use - -任何需要基于一组项目文档进行知识管理和问答的场景。 - -### 典型场景 -- **项目文档管理**:将需求文档、技术方案、数据表等组织为结构化知识库 -- **文档解析入库**:将 office 文档和纯文本文件解析为 markdown 并生成摘要 -- **知识问答**:基于已入库的文档回答问题,并标注信息来源 - -### 不适用场景 -- 需要语义搜索或向量化检索 -- 需要跨多个知识项目关联查询 -- 需要多人协作或权限控制 - -## Quick Reference - -| Command | 触发方式 | 说明 | -|---------|----------|------| -| init | `/lyxy-kb-init ` | 初始化知识项目目录结构 | -| ingest | `/lyxy-kb-ingest ` | 解析 sources/ 中新文件,增量更新 project.md | -| rebuild | `/lyxy-kb-rebuild ` | 全量重新生成 project.md | -| ask | `/lyxy-kb-ask ` | 基于知识库进行会话问答 | - -## Workflow - -### 知识项目目录结构 - -``` -/ -├── project.md # 高度摘要 + 文件索引 -├── manifest.json # 增量追踪 -├── parsed/ # 解析后的 markdown -├── sources/ # 待处理区(用户放入原始文档) -└── archive/ # 原始文件备份(带时间戳) -``` - -### 基本工作流程 - -1. **初始化**:使用 `/lyxy-kb-init ` 创建项目目录结构 -2. **入库**:将文档放入 `sources/`,执行 `/lyxy-kb-ingest ` -3. **问答**:使用 `/lyxy-kb-ask ` 基于知识库回答问题 - -### 渐进式查询策略 - -1. **读取 project.md**:获取项目概述和文件索引(低 token 开销) -2. **判断相关文件**:根据用户问题和摘要判断需要查阅哪些 parsed 文件 -3. **按需加载**:读取相关 parsed 文件的全部或部分内容 -4. **回答并标注来源**:基于获取的信息回答问题 - -## References - -详细文档请参阅 `references/` 目录: - -| 文件 | 内容 | -|------|------| -| `references/structure.md` | 目录结构规范、project.md 格式、manifest.json 结构、parsed 元信息标记 | -| `references/workflow.md` | 文档生命周期、归档命名规则、冲突检测、解析策略 | -| `references/query-strategy.md` | 渐进式查询策略、来源引用格式、依赖关系、限制说明 | diff --git a/skills/lyxy-kb/references/query-strategy.md b/skills/lyxy-kb/references/query-strategy.md deleted file mode 100644 index dd786c1..0000000 --- a/skills/lyxy-kb/references/query-strategy.md +++ /dev/null @@ -1,43 +0,0 @@ -# 渐进式查询策略 - -## 分层加载策略 - -问答时采用分层加载策略,节省 token: - -1. **读取 project.md**:获取项目概述和文件索引(低 token 开销) -2. **判断相关文件**:根据用户问题和文件索引中的摘要,判断需要查阅哪些 parsed 文件 -3. **按需加载**:读取相关 parsed 文件的全部或部分内容 -4. **回答并标注来源**:基于获取的信息回答问题 - -## 来源引用格式 - -回答中引用具体信息时,使用以下格式标注来源: - -``` -根据《文件名》(parsed/文件名.md),... -``` - -多个来源时分别标注各信息点的来源文件。 - -## 无相关信息 - -当知识库中未找到与用户问题相关的信息时,明确告知用户,不编造答案。 - -## 空知识库 - -如果 project.md 文件索引为空(尚无已入库文件),应告知用户知识库为空,建议先使用 `/lyxy-kb-ingest` 入库文档。 - -## 依赖关系 - -| 依赖 | 用途 | -|------|------| -| lyxy-reader-office | 解析 .docx、.pdf、.pptx、.xlsx 文件为 markdown | -| lyxy-runner-python | 通过 uv 执行 lyxy-reader-office 的 Python 解析脚本 | - -## 限制 - -- 不支持向量化语义搜索 -- 不支持跨知识项目关联查询 -- 不支持文档版本对比或 diff -- 不支持多用户协作或权限控制 -- 大量文件全量重写时 token 消耗较高 diff --git a/skills/lyxy-kb/references/structure.md b/skills/lyxy-kb/references/structure.md deleted file mode 100644 index 6eeac41..0000000 --- a/skills/lyxy-kb/references/structure.md +++ /dev/null @@ -1,125 +0,0 @@ -# 知识项目目录结构 - -## 目录结构 - -每个知识项目是当前工作目录(CWD)下的一个子目录,包含以下固定结构: - -``` -/ -├── project.md # 高度摘要 + 文件索引 -├── manifest.json # 增量追踪 -├── parsed/ # 解析后的 markdown(中间产物) -├── sources/ # 待处理区(用户放入原始文档) -└── archive/ # 原始文件备份(带时间戳) -``` - -### 各目录/文件职责 - -| 路径 | 职责 | -|------|------| -| `project.md` | 项目的高度摘要和文件索引,作为问答时的入口文件 | -| `manifest.json` | 记录已处理文件的元信息,用于增量检测和版本追踪 | -| `parsed/` | 存放解析后的 markdown 文件,便于大模型读取分析 | -| `sources/` | 用户放入待处理文档的目录,解析后文件会被移走 | -| `archive/` | 原始文件的备份,每个文件都带时间戳后缀 | - -### 结构完整性验证 - -执行任何 command(ingest / rebuild / ask)时,必须先验证项目目录结构是否完整,即以下 5 项是否全部存在: -- `/project.md` -- `/manifest.json` -- `/parsed/` -- `/sources/` -- `/archive/` - -若不完整,提示用户先执行 `/lyxy-kb-init `,终止当前操作。 - -## 项目名称规则 - -项目名称只允许使用以下字符: -- 中文字符 -- 英文字母(a-z、A-Z) -- 数字(0-9) -- 短横线(-) -- 下划线(_) - -**不允许包含空格或其他特殊字符。** 不符合规则时应提示用户修改。 - -## project.md 格式规范 - -```markdown -# <项目名称> - -## 概述 -(高度总结的项目信息,几百字以内) - -## 关键信息 -(从所有文档中提炼的核心要点) - -## 文件索引 - -| 文件名 | 解析文件 | 最新归档 | 摘要 | -|--------|----------|----------|------| -| 需求文档 | parsed/需求文档.md | archive/需求文档_202602181600.docx | 简要摘要... | - -## 更新记录 -- 2026-02-18 16:00: 解析 需求文档.docx -``` - -## manifest.json 结构 - -```json -{ - "project": "<项目名称>", - "created_at": "2026-02-18T16:00", - "last_ingest": "2026-02-18T17:25", - "files": [ - { - "name": "需求文档", - "ext": ".docx", - "parsed": "parsed/需求文档.md", - "versions": [ - { - "archived": "archive/需求文档_202602181600.docx", - "hash": "sha256:abc123...", - "ingested_at": "2026-02-18T16:00" - } - ] - } - ] -} -``` - -### 字段说明 - -| 字段 | 说明 | -|------|------| -| `project` | 项目名称 | -| `created_at` | 项目创建时间 | -| `last_ingest` | 最近一次 ingest 的时间 | -| `files[].name` | 文件名(不含扩展名) | -| `files[].ext` | 原始文件扩展名 | -| `files[].parsed` | 解析产物的相对路径 | -| `files[].versions` | 版本历史数组 | -| `files[].versions[].archived` | 归档文件的相对路径 | -| `files[].versions[].hash` | 文件内容的 SHA-256 哈希(使用 `sha256sum` 命令计算) | -| `files[].versions[].ingested_at` | 该版本的入库时间 | - -## parsed 文件元信息标记 - -每个 parsed markdown 文件头部必须包含元信息注释: - -```markdown - - - - -# 技术方案 -(文档正文内容...) -``` - -| 元信息 | 说明 | -|--------|------| -| `source` | 原始文件名(含扩展名) | -| `archived` | 对应的归档文件相对路径 | -| `parsed_at` | 解析时间(YYYY-MM-DD HH:mm 格式) | diff --git a/skills/lyxy-kb/references/workflow.md b/skills/lyxy-kb/references/workflow.md deleted file mode 100644 index 6f20439..0000000 --- a/skills/lyxy-kb/references/workflow.md +++ /dev/null @@ -1,86 +0,0 @@ -# 文档生命周期和处理策略 - -## 文档生命周期 - -``` -用户放入 sources/(支持子目录) - │ - ▼ - 检查文件(跳过空文件、检测冲突) - │ - ▼ - 解析文件内容(失败则保留在 sources/) - │ - ├──▶ 写入 parsed/<文件名>.md(含头部元信息) - │ - ├──▶ 移动原始文件到 archive/<文件名_YYYYMMDDHHmm>. - │ - ├──▶ 更新 manifest.json - │ - └──▶ 增量更新 project.md(追加文件索引和更新记录) -``` - -## 归档命名规则 - -所有进入 archive 的文件都必须带时间戳后缀,格式为 `<文件名_YYYYMMDDHHmm>.<扩展名>`,即使只有一个版本。 - -示例: -- `需求文档.docx` → `archive/需求文档_202602181600.docx` -- `技术方案.pdf`(第二次入库)→ `archive/技术方案_202602181725.pdf` - -## 同名文件更新 - -同名同扩展名的文件再次入库时: -- `parsed/` 中的 markdown 文件被覆盖为最新版本 -- `archive/` 中保留所有历史版本(每个版本独立的时间戳文件) -- `manifest.json` 中该文件条目的 `versions` 数组追加新版本记录 - -## 同名不同扩展名冲突检测 - -因为 parsed 产物以文件名(不含扩展名)+ `.md` 命名,同名不同扩展名的文件会产生冲突。 - -### 检测规则 - -1. **sources/ 内部检测**:扫描 sources/ 中所有文件(含子目录),如果存在同名但不同扩展名的文件(如 `技术方案.pdf` 和 `技术方案.docx`),拒绝处理并提示用户重命名 -2. **与已入库文件检测**:将 sources/ 中文件的名称(不含扩展名)与 manifest.json 中已有记录对比,如果名称相同但扩展名不同,拒绝处理并提示用户重命名 - -### 处理方式 - -冲突文件不予处理,保留在 sources/ 中,提示用户重命名后重新执行 ingest。非冲突文件正常处理。 - -## 文件类型解析策略 - -| 文件类型 | 解析方式 | -|----------|----------| -| `.docx`, `.pdf`, `.pptx`, `.xlsx` | 使用名为 **lyxy-reader-office** 的 skill 解析 | -| 其他所有文件(`.md`, `.txt`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.log`, `.html` 等) | 直接读取文件内容 | - -### Office 文档解析 - -解析 office 文档时,必须查找当前环境中名为 **lyxy-reader-office** 的 skill,阅读其 SKILL.md 获取具体的执行方式和命令。 - -**如果环境中不存在 lyxy-reader-office skill,且没有其他可替代的文档解析 skill,则提示用户无法处理 office 文档,中止整个 ingest 流程。** - -### sources/ 扫描规则 - -扫描 sources/ 时**递归检查所有子目录**中的文件。parsed 产物的路径仍为 `parsed/<文件名>.md`(扁平化存放),不保留 sources 中的子目录结构。 - -### 空文件处理 - -sources/ 中 0 字节的空文件应**跳过处理**,不解析、不归档、不更新 manifest。处理完成后向用户列出被跳过的空文件列表,提示用户检查。 - -### 解析失败处理 - -如果某个文件解析失败(如文档损坏、解析器报错),该文件**保留在 sources/ 中不移动**,报告错误信息,继续处理其他文件。 - -## 更新策略 - -**增量追加**(默认,由 ingest 触发): -- 新文件:在文件索引表追加新行,在更新记录追加条目 -- 已有文件更新:覆盖文件索引表中对应行的最新归档路径和摘要 -- 概述和关键信息部分**不**自动更新 - -**全量重写**(由 rebuild 触发): -- 读取所有 parsed/*.md 文件 -- 重新生成概述、关键信息、文件索引 -- 保留历史更新记录,追加本次 rebuild 条目 diff --git a/skills/lyxy-reader-html/SKILL.md b/skills/lyxy-reader-html/SKILL.md deleted file mode 100644 index 7f2336a..0000000 --- a/skills/lyxy-reader-html/SKILL.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -name: lyxy-reader-html -description: 解析 HTML 文件和 URL 网页内容的 skill,将 HTML 转换为 Markdown 格式,支持全文提取、标题提取、章节提取、正则搜索、字数统计、行数统计。URL 模式下自动下载网页内容,支持 JS 渲染。使用时请阅读 scripts/README.md 获取详细用法。 -compatibility: Requires Python 3.6+. 推荐通过 lyxy-runner-python skill 使用 uv 自动管理依赖。 ---- - -# HTML 网页解析 Skill - -将 HTML 文件或 URL 网页内容解析为 Markdown 格式,支持多种查询模式。 - -## Purpose - -**统一入口**:使用 `scripts/parser.py` 作为统一的命令行入口,自动识别输入类型(URL 或 HTML 文件)并执行解析。 - -**依赖选项**:此 skill 必须优先使用 lyxy-runner-python skill 执行,不可用时降级到直接 Python 执行。 - -## When to Use - -任何需要读取或解析 HTML 文件、URL 网页内容的任务都应使用此 skill。 - -### 典型场景 -- **网页内容提取**:将 URL 或本地 HTML 文件转换为可读的 Markdown 文本 -- **文档元数据**:获取文档的字数、行数等信息 -- **标题分析**:提取文档的标题结构 -- **章节提取**:提取特定章节的内容 -- **内容搜索**:在文档中搜索关键词或模式 - -### 触发词 -- 中文:"读取/解析/打开 html/htm 网页/URL" -- 英文:"read/parse/extract html/htm web page url" -- 文件扩展名:`.html`、`.htm` -- URL 模式:`http://`、`https://` - -## Quick Reference - -| 参数 | 说明 | -|------|------| -| (无参数) | 输出完整 Markdown 内容 | -| `-c` | 字数统计 | -| `-l` | 行数统计 | -| `-t` | 提取所有标题 | -| `-tc ` | 提取指定标题的章节内容 | -| `-s ` | 正则表达式搜索 | -| `-n ` | 与 `-s` 配合,指定上下文行数 | - -## Workflow - -1. **检查依赖**:优先使用 lyxy-runner-python,否则降级到直接 Python 执行 -2. **识别输入**:自动判断是 URL 还是本地 HTML 文件 -3. **下载内容**:URL 模式下按 pyppeteer → selenium → httpx → urllib 优先级下载 -4. **清理 HTML**:移除 script/style/link/svg 等标签和 URL 属性 -5. **执行解析**:按 trafilatura → domscribe → MarkItDown → html2text 优先级解析 -6. **输出结果**:返回 Markdown 格式内容或统计信息 - -### 基本语法 - -```bash -# 使用 lyxy-runner-python(推荐) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with httpx --with pyppeteer --with selenium --with beautifulsoup4 scripts/parser.py https://example.com - -# 降级到直接执行 -python3 scripts/parser.py https://example.com -``` - -## References - -详细文档请参阅 `references/` 目录: - -| 文件 | 内容 | -|------|------| -| `references/examples.md` | URL 和 HTML 文件的完整提取、字数统计、标题提取、章节提取、搜索等示例 | -| `references/parsers.md` | 解析器说明、依赖安装、各解析器输出特点、能力说明 | -| `references/error-handling.md` | 限制说明、最佳实践、依赖执行策略 | - -> **详细用法**:请阅读 `scripts/README.md` 获取完整的命令行参数和依赖安装指南。 diff --git a/skills/lyxy-reader-html/references/error-handling.md b/skills/lyxy-reader-html/references/error-handling.md deleted file mode 100644 index 12f0072..0000000 --- a/skills/lyxy-reader-html/references/error-handling.md +++ /dev/null @@ -1,54 +0,0 @@ -# 错误处理和限制说明 - -## 限制 - -- 不支持图片提取(仅纯文本) -- 不支持复杂的格式保留(字体、颜色、布局等) -- 不支持文档编辑或修改 -- 仅支持 URL、.html、.htm 格式 -- pyppeteer 和 selenium 需要额外配置环境变量 - -## 最佳实践 - -1. **必须优先使用 lyxy-runner-python**:如果环境中存在,必须使用 lyxy-runner-python 执行脚本 -2. **查阅 README**:详细参数、依赖安装、下载器/解析器对比等信息请阅读 `scripts/README.md` -3. **JS 渲染网页**:对于需要 JS 渲染的网页,确保安装 pyppeteer 或 selenium 并正确配置环境变量 -4. **轻量使用**:如果目标网页不需要 JS 渲染,可以只安装 httpx/urllib 以获得更快的下载速度 -5. **禁止自动安装**:降级到直接 Python 执行时,仅向用户提示安装依赖,不得自动执行 pip install - -## 依赖执行策略 - -### 必须使用 lyxy-runner-python - -如果环境中存在 lyxy-runner-python skill,**必须**使用它来执行 parser.py 脚本: -- lyxy-runner-python 使用 uv 管理依赖,自动安装所需的第三方库 -- 环境隔离,不污染系统 Python -- 跨平台兼容(Windows/macOS/Linux) - -### 降级到直接执行 - -**仅当** lyxy-runner-python skill 不存在时,才降级到直接 Python 执行: -- 需要用户手动安装依赖 -- 至少需要安装 html2text 和 beautifulsoup4 -- **禁止自动执行 pip install**,仅向用户提示安装建议 - -## JS 渲染配置 - -### pyppeteer 配置 - -- 首次运行会自动下载 Chromium(需要网络连接) -- 或设置 `LYXY_CHROMIUM_BINARY` 环境变量指定 Chromium/Chrome 可执行文件路径 - -### selenium 配置 - -必须设置两个环境变量: -- `LYXY_CHROMIUM_DRIVER` - ChromeDriver 可执行文件路径 -- `LYXY_CHROMIUM_BINARY` - Chromium/Chrome 可执行文件路径 - -## 不适用场景 - -- 需要提取图片内容(仅支持纯文本) -- 需要保留复杂的格式信息(字体、颜色、布局) -- 需要编辑或修改文档 -- 需要登录或认证才能访问的网页(需自行处理 Cookie/Token) -- 需要处理动态内容加载但不使用 JS 渲染的情况 diff --git a/skills/lyxy-reader-html/references/examples.md b/skills/lyxy-reader-html/references/examples.md deleted file mode 100644 index a872e1a..0000000 --- a/skills/lyxy-reader-html/references/examples.md +++ /dev/null @@ -1,59 +0,0 @@ -# 示例 - -## URL 输入 - 提取完整文档内容 - -```bash -# 使用 uv(推荐) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with httpx --with beautifulsoup4 scripts/parser.py https://example.com - -# 直接使用 Python -python scripts/parser.py https://example.com -``` - -## HTML 文件输入 - 提取完整文档内容 - -```bash -# 使用 uv(推荐) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with beautifulsoup4 scripts/parser.py page.html - -# 直接使用 Python -python scripts/parser.py page.html -``` - -## 获取文档字数 - -```bash -uv run --with trafilatura --with html2text --with beautifulsoup4 scripts/parser.py -c https://example.com -``` - -## 获取文档行数 - -```bash -uv run --with trafilatura --with html2text --with beautifulsoup4 scripts/parser.py -l https://example.com -``` - -## 提取所有标题 - -```bash -uv run --with trafilatura --with html2text --with beautifulsoup4 scripts/parser.py -t https://example.com -``` - -## 提取指定章节 - -```bash -uv run --with trafilatura --with html2text --with beautifulsoup4 scripts/parser.py -tc "关于我们" https://example.com -``` - -## 搜索关键词 - -```bash -uv run --with trafilatura --with html2text --with beautifulsoup4 scripts/parser.py -s "关键词" -n 3 https://example.com -``` - -## 降级到直接 Python 执行 - -仅当 lyxy-runner-python skill 不存在时使用: - -```bash -python3 scripts/parser.py https://example.com -``` diff --git a/skills/lyxy-reader-html/references/parsers.md b/skills/lyxy-reader-html/references/parsers.md deleted file mode 100644 index 3dc08a5..0000000 --- a/skills/lyxy-reader-html/references/parsers.md +++ /dev/null @@ -1,68 +0,0 @@ -# 解析器说明和依赖安装 - -## 多策略解析降级 - -URL 下载器按 pyppeteer → selenium → httpx → urllib 优先级依次尝试;HTML 解析器按 trafilatura → domscribe → MarkItDown → html2text 优先级依次尝试。前一个失败自动回退到下一个。 - -详细的优先级和对比请查阅 `scripts/README.md`。 - -## 依赖安装 - -### 使用 uv(推荐) - -```bash -# 完整安装(所有下载器和解析器) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with httpx --with pyppeteer --with selenium --with beautifulsoup4 scripts/parser.py https://example.com - -# 轻量安装(仅 httpx + html2text) -uv run --with html2text --with beautifulsoup4 scripts/parser.py https://example.com -``` - -> **说明**:以上为推荐安装命令,包含所有组件以获得最佳兼容性。详细的优先级和对比请查阅 `scripts/README.md`。 - -## 下载器对比 - -| 下载器 | 优点 | 缺点 | 适用场景 | -|--------|------|------|---------| -| **pyppeteer** | 支持 JS 渲染;现代网页兼容性好 | 依赖重;首次需下载 Chromium | 需要 JS 渲染的现代网页 | -| **selenium** | 支持 JS 渲染;成熟稳定 | 需配置 Chromium driver 和 binary | 需要 JS 渲染的现代网页 | -| **httpx** | 轻量快速;现代 HTTP 客户端 | 不支持 JS 渲染 | 静态网页;快速下载 | -| **urllib** | Python 标准库;无需安装 | 不支持 JS 渲染 | 静态网页;兜底方案 | - -## 解析器对比 - -| 解析器 | 优点 | 缺点 | 适用场景 | -|--------|------|------|---------| -| **trafilatura** | 专门用于网页正文提取;输出质量高 | 可能无法提取某些页面 | 大多数网页正文提取 | -| **domscribe** | 专注内容提取 | 相对较新 | 网页内容提取 | -| **MarkItDown** | 微软官方;格式规范 | 输出较简洁 | 标准格式转换 | -| **html2text** | 经典库;兼容性好 | 作为兜底方案 | 兜底解析 | - -## 能力说明 - -### 1. URL / HTML 文件输入 -支持两种输入方式: -- URL:自动下载网页内容(支持 JS 渲染) -- 本地 HTML 文件:直接读取并解析 - -### 2. 全文转换为 Markdown -将完整 HTML 解析为 Markdown 格式,移除图片但保留文本格式(标题、列表、表格、粗体、斜体等)。 - -### 3. HTML 预处理清理 -解析前自动清理 HTML: -- 移除 script/style/link/svg 标签 -- 移除 href/src/srcset/action 等 URL 属性 -- 移除 style 属性 - -### 4. 获取文档元信息 -- 字数统计(`-c` 参数) -- 行数统计(`-l` 参数) - -### 5. 标题列表提取 -提取文档中所有 1-6 级标题(`-t` 参数),按原始层级关系返回。 - -### 6. 指定章节内容提取 -根据标题名称提取特定章节的完整内容(`-tc` 参数),包含上级标题链和所有下级内容。 - -### 7. 正则表达式搜索 -在文档中搜索关键词或模式(`-s` 参数),支持自定义上下文行数(`-n` 参数,默认 2 行)。 diff --git a/skills/lyxy-reader-html/scripts/README.md b/skills/lyxy-reader-html/scripts/README.md deleted file mode 100644 index 5a0c42f..0000000 --- a/skills/lyxy-reader-html/scripts/README.md +++ /dev/null @@ -1,323 +0,0 @@ -# HTML Parser 使用说明 - -HTML/URL 解析器,将网页内容或本地 HTML 文件转换为 Markdown 格式。 - -支持两种输入源:URL(自动下载网页内容)或本地 HTML 文件。下载器按 pyppeteer → selenium → httpx → urllib 优先级依次尝试;解析器按 trafilatura → domscribe → MarkItDown → html2text 优先级依次尝试。 - -## 快速开始 - -```bash -# 最简运行(仅 urllib + html2text) -python parser.py https://example.com - -# 安装推荐依赖后运行 -pip install trafilatura domscribe markitdown html2text httpx beautifulsoup4 -python parser.py https://example.com - -# 使用 uv 一键运行(自动安装依赖,无需手动 pip install) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with httpx --with beautifulsoup4 parser.py https://example.com -``` - -## 命令行用法 - -### 基本语法 - -```bash -python parser.py [options] -``` - -`input` 可以是: -- 以 `http://` 或 `https://` 开头的 URL -- 本地 `.html` 或 `.htm` 文件路径 - -不带任何选项时输出完整 Markdown 内容。 - -### 参数说明 - -以下参数互斥,一次只能使用一个: - -| 短选项 | 长选项 | 说明 | -|--------|--------|------| -| `-c` | `--count` | 输出解析后文档的总字符数(不含换行符) | -| `-l` | `--lines` | 输出解析后文档的总行数 | -| `-t` | `--titles` | 输出所有标题行(1-6 级,含 `#` 前缀) | -| `-tc ` | `--title-content ` | 提取指定标题及其下级内容(`name` 不包含 `#` 号) | -| `-s ` | `--search ` | 使用正则表达式搜索文档,返回匹配结果 | - -搜索辅助参数(与 `-s` 配合使用): - -| 短选项 | 长选项 | 说明 | -|--------|--------|------| -| `-n ` | `--context ` | 每个匹配结果包含的前后非空行数(默认:2) | - -### 退出码 - -| 退出码 | 含义 | -|--------|------| -| `0` | 解析成功 | -| `1` | 错误(文件不存在、格式无效、所有下载器失败、所有解析器失败、标题未找到、正则无效或无匹配) | - -### 使用示例 - -**URL 输入:** - -```bash -# 输出完整 Markdown -python parser.py https://example.com -python parser.py https://example.com > output.md -``` - -**HTML 文件输入:** - -```bash -# 输出完整 Markdown -python parser.py page.html -python parser.py page.html > output.md -``` - -**统计信息(`-c` / `-l`):** - -输出单个数字,适合管道处理。 - -```bash -$ python parser.py https://example.com -c -8500 - -$ python parser.py https://example.com -l -215 -``` - -**提取标题(`-t`):** - -每行一个标题,保留 `#` 前缀和层级。 - -```bash -$ python parser.py https://example.com -t -# 欢迎来到 Example -## 关于我们 -## 联系方式 -``` - -**提取标题内容(`-tc`):** - -输出指定标题及其下级内容。如果文档中有多个同名标题,用 `---` 分隔。每段输出包含上级标题链。 - -```bash -$ python parser.py https://example.com -tc "关于我们" -# 欢迎来到 Example -## 关于我们 -这是关于我们的详细内容... -``` - -**搜索(`-s`):** - -支持 Python 正则表达式语法。多个匹配结果用 `---` 分隔。`-n` 控制上下文行数。 - -```bash -$ python parser.py https://example.com -s "测试" -n 1 -上一行内容 -包含**测试**关键词的行 -下一行内容 ---- -另一处上一行 -另一处**测试**内容 -另一处下一行 -``` - -### 批量处理 - -```bash -# Linux/Mac - 批量处理 HTML 文件 -for file in *.html; do - python parser.py "$file" > "${file%.html}.md" -done - -# Windows PowerShell - 批量处理 HTML 文件 -Get-ChildItem *.html | ForEach-Object { - python parser.py $_.FullName > ($_.BaseName + ".md") -} -``` - -### 管道使用 - -```bash -# 过滤包含关键词的行 -python parser.py https://example.com | grep "重要" > important.md - -# 统计标题数量 -python parser.py https://example.com -t | wc -l -``` - -## 安装 - -脚本基于 Python 3.6+ 运行。下载器和解析器按优先级依次尝试,建议安装所有依赖以获得最佳兼容性。也可以只安装部分依赖,脚本会自动选择可用的组件。 - -### 完整安装(推荐) - -```bash -# pip -pip install trafilatura domscribe markitdown html2text httpx pyppeteer selenium beautifulsoup4 - -# uv(一键运行,无需预安装) -uv run --with trafilatura --with domscribe --with markitdown --with html2text --with httpx --with pyppeteer --with selenium --with beautifulsoup4 parser.py https://example.com -``` - -### 最小安装 - -仅使用标准库(urllib + html2text): - -```bash -pip install html2text beautifulsoup4 -``` - -### 各组件说明 - -**下载器**: - -| 下载器 | 优点 | 缺点 | 依赖 | -|--------|------|------|------| -| pyppeteer | 支持 JS 渲染,现代网页兼容性好 | 依赖重,需下载 Chromium | pyppeteer | -| selenium | 支持 JS 渲染,成熟稳定 | 需配置 Chromium driver 和 binary | selenium | -| httpx | 轻量快速,现代 HTTP 客户端 | 不支持 JS 渲染 | httpx | -| urllib | Python 标准库,无需安装 | 不支持 JS 渲染 | (无) | - -**解析器**: - -| 解析器 | 优点 | 缺点 | 依赖 | -|--------|------|------|------| -| trafilatura | 专门用于网页正文提取,质量高 | 可能无法提取某些页面 | trafilatura | -| domscribe | 专注内容提取 | 相对较新 | domscribe | -| MarkItDown | 微软官方,格式规范 | 输出较简洁 | markitdown | -| html2text | 经典库,兼容性好 | 作为兜底方案 | html2text | - -**其他依赖**: - -- `beautifulsoup4` - HTML 清理必需 - -### JS 渲染配置 - -pyppeteer 和 selenium 支持 JS 渲染,但需要额外配置: - -**pyppeteer**: -- 首次运行会自动下载 Chromium -- 或设置 `LYXY_CHROMIUM_BINARY` 环境变量指定 Chromium 路径 - -**selenium**: -- 必须设置两个环境变量: - - `LYXY_CHROMIUM_DRIVER` - ChromeDriver 路径 - - `LYXY_CHROMIUM_BINARY` - Chromium/Chrome 路径 - -## 输出格式 - -### Markdown 文档结构 - -无选项时输出完整 Markdown,包含以下元素: - -```markdown -# 一级标题 - -正文段落 - -## 二级标题 - -- 无序列表项 -- 无序列表项 - -1. 有序列表项 -2. 有序列表项 - -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | - -**粗体** *斜体* -``` - -### 内容自动处理 - -输出前会自动进行以下处理: - -| 处理 | 说明 | -|------|------| -| HTML 清理 | 移除 script/style/link/svg 标签和 URL 属性 | -| 图片移除 | 删除 `![alt](url)` 语法 | -| 空行规范化 | 连续多个空行合并为一个 | - -## 错误处理 - -### 错误消息 - -```bash -# 输入不是 URL 也不是 HTML 文件 -$ python parser.py invalid.txt -错误: 不是有效的 HTML 文件: invalid.txt - -# 文件不存在 -$ python parser.py missing.html -错误: 文件不存在: missing.html - -# 所有下载器失败(URL 示例) -$ python parser.py https://example.com -所有下载方法均失败: -- pyppeteer: pyppeteer 库未安装 -- selenium: selenium 库未安装 -- httpx: httpx 库未安装 -- urllib: HTTP 404 - -# 所有解析器失败 -$ python parser.py page.html -所有解析方法均失败: -- trafilatura: trafilatura 库未安装 -- domscribe: domscribe 库未安装 -- MarkItDown: MarkItDown 库未安装 -- html2text: html2text 库未安装 - -# 标题未找到 -$ python parser.py https://example.com -tc "不存在的标题" -错误: 未找到标题 '不存在的标题' - -# 无效正则或无匹配 -$ python parser.py https://example.com -s "[invalid" -错误: 正则表达式无效或未找到匹配: '[invalid' -``` - -### 解析器回退机制 - -脚本按优先级依次尝试各下载器和解析器。每个组件失败后记录原因(库未安装 / 下载失败 / 解析失败 / 文档为空),然后自动尝试下一个。全部失败时输出汇总信息并以退出码 1 退出。 - -## 常见问题 - -### 为什么有些内容没有提取到? - -不同解析器输出详细度不同。建议安装所有解析器依赖,脚本会自动选择优先级最高的可用解析器。 - -### 如何只使用特定下载器/解析器? - -当前版本不支持指定,总是按优先级自动选择。可以通过只安装目标组件的依赖来间接控制——未安装的组件会被跳过。 - -### URL 下载慢或被反爬? - -pyppeteer 和 selenium 支持 JS 渲染但较慢。如果目标网页不需要 JS 渲染,可以只安装 httpx 或 urllib,让脚本回退到这些轻量下载器。 - -### 中文显示乱码? - -脚本输出 UTF-8 编码,确保终端支持: - -```bash -# Linux/Mac -export LANG=en_US.UTF-8 - -# Windows PowerShell -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -``` - -## 文件结构 - -``` -scripts/ -├── common.py # 公共函数(HTML 清理、Markdown 处理) -├── downloader.py # URL 下载模块 -├── html_parser.py # HTML 解析模块 -├── parser.py # 命令行入口 -└── README.md # 本文档 -``` diff --git a/skills/lyxy-reader-html/scripts/common.py b/skills/lyxy-reader-html/scripts/common.py deleted file mode 100644 index c66aa71..0000000 --- a/skills/lyxy-reader-html/scripts/common.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env python3 -"""HTML 解析器的公共模块,包含 HTML 清理、Markdown 处理等工具函数。""" - -import re -from typing import List, Optional -from bs4 import BeautifulSoup - -IMAGE_PATTERN = re.compile(r"!\[[^\]]*\]\([^)]+\)") -_CONSECUTIVE_BLANK_LINES = re.compile(r"\n{3,}") - - -def clean_html_content(html_content: str) -> str: - """清理 HTML 内容,移除 script/style/link/svg 标签和 URL 属性。""" - soup = BeautifulSoup(html_content, "html.parser") - - # Remove all script tags - for script in soup.find_all("script"): - script.decompose() - - # Remove all style tags - for style in soup.find_all("style"): - style.decompose() - - # Remove all svg tags - for svg in soup.find_all("svg"): - svg.decompose() - - # Remove all link tags - for link in soup.find_all("link"): - link.decompose() - - # Remove URLs from href and src attributes - for tag in soup.find_all(True): - if "href" in tag.attrs: - del tag["href"] - if "src" in tag.attrs: - del tag["src"] - if "srcset" in tag.attrs: - del tag["srcset"] - if "action" in tag.attrs: - del tag["action"] - data_attrs = [ - attr - for attr in tag.attrs - if attr.startswith("data-") and "src" in attr.lower() - ] - for attr in data_attrs: - del tag[attr] - - # Remove all style attributes from all tags - for tag in soup.find_all(True): - if "style" in tag.attrs: - del tag["style"] - - # Remove data-href attributes - for tag in soup.find_all(True): - if "data-href" in tag.attrs: - del tag["data-href"] - - # Remove URLs from title attributes - for tag in soup.find_all(True): - if "title" in tag.attrs: - title = tag["title"] - cleaned_title = re.sub(r"https?://\S+", "", title, flags=re.IGNORECASE) - tag["title"] = cleaned_title - - # Remove class attributes that contain URL-like patterns - for tag in soup.find_all(True): - if "class" in tag.attrs: - classes = tag["class"] - cleaned_classes = [c for c in classes if not c.startswith("url ") and not "hyperlink-href:" in c] - tag["class"] = cleaned_classes - - return str(soup) - - -def remove_markdown_images(markdown_text: str) -> str: - """移除 Markdown 文本中的图片标记。""" - return IMAGE_PATTERN.sub("", markdown_text) - - -def normalize_markdown_whitespace(content: str) -> str: - """规范化 Markdown 空白字符,保留单行空行。""" - return _CONSECUTIVE_BLANK_LINES.sub("\n\n", content) - - -def get_heading_level(line: str) -> int: - """获取 Markdown 行的标题级别(1-6),非标题返回 0。""" - stripped = line.lstrip() - if not stripped.startswith("#"): - return 0 - without_hash = stripped.lstrip("#") - level = len(stripped) - len(without_hash) - if not (1 <= level <= 6): - return 0 - if len(stripped) == level: - return level - if stripped[level] != " ": - return 0 - return level - - -def extract_titles(markdown_text: str) -> List[str]: - """提取 markdown 文本中的所有标题行(1-6级)。""" - title_lines = [] - for line in markdown_text.split("\n"): - if get_heading_level(line) > 0: - title_lines.append(line.lstrip()) - return title_lines - - -def extract_title_content(markdown_text: str, title_name: str) -> Optional[str]: - """提取所有指定标题及其下级内容(每个包含上级标题)。""" - lines = markdown_text.split("\n") - match_indices = [] - - for i, line in enumerate(lines): - level = get_heading_level(line) - if level > 0: - stripped = line.lstrip() - title_text = stripped[level:].strip() - if title_text == title_name: - match_indices.append(i) - - if not match_indices: - return None - - result_lines = [] - for match_num, idx in enumerate(match_indices): - if match_num > 0: - result_lines.append("\n---\n") - - target_level = get_heading_level(lines[idx]) - - parent_titles = [] - current_level = target_level - for i in range(idx - 1, -1, -1): - line_level = get_heading_level(lines[i]) - if line_level > 0 and line_level < current_level: - parent_titles.append(lines[i]) - current_level = line_level - if current_level == 1: - break - - parent_titles.reverse() - result_lines.extend(parent_titles) - - result_lines.append(lines[idx]) - for i in range(idx + 1, len(lines)): - line = lines[i] - line_level = get_heading_level(line) - if line_level == 0 or line_level > target_level: - result_lines.append(line) - else: - break - - return "\n".join(result_lines) - - -def search_markdown( - content: str, pattern: str, context_lines: int = 0 -) -> Optional[str]: - """使用正则表达式搜索 markdown 文档,返回匹配结果及其上下文。""" - try: - regex = re.compile(pattern) - except re.error: - return None - - lines = content.split("\n") - - non_empty_indices = [] - non_empty_to_original = {} - for i, line in enumerate(lines): - if line.strip(): - non_empty_indices.append(i) - non_empty_to_original[i] = len(non_empty_indices) - 1 - - matched_non_empty_indices = [] - for orig_idx in non_empty_indices: - if regex.search(lines[orig_idx]): - matched_non_empty_indices.append(non_empty_to_original[orig_idx]) - - if not matched_non_empty_indices: - return None - - merged_ranges = [] - current_start = matched_non_empty_indices[0] - current_end = matched_non_empty_indices[0] - - for idx in matched_non_empty_indices[1:]: - if idx - current_end <= context_lines * 2: - current_end = idx - else: - merged_ranges.append((current_start, current_end)) - current_start = idx - current_end = idx - merged_ranges.append((current_start, current_end)) - - results = [] - for start, end in merged_ranges: - context_start_idx = max(0, start - context_lines) - context_end_idx = min(len(non_empty_indices) - 1, end + context_lines) - - start_line_idx = non_empty_indices[context_start_idx] - end_line_idx = non_empty_indices[context_end_idx] - - result_lines = [ - line - for i, line in enumerate(lines) - if start_line_idx <= i <= end_line_idx - ] - results.append("\n".join(result_lines)) - - return "\n---\n".join(results) - - -def is_url(input_str: str) -> bool: - """判断输入是否为 URL。""" - return input_str.startswith("http://") or input_str.startswith("https://") - - -def is_html_file(file_path: str) -> bool: - """判断文件是否为 HTML 文件(仅检查扩展名)。""" - ext = file_path.lower() - return ext.endswith(".html") or ext.endswith(".htm") diff --git a/skills/lyxy-reader-html/scripts/downloader.py b/skills/lyxy-reader-html/scripts/downloader.py deleted file mode 100644 index 5f2d71d..0000000 --- a/skills/lyxy-reader-html/scripts/downloader.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python3 -"""URL 下载模块,按 pyppeteer → selenium → httpx → urllib 优先级尝试下载。""" - -import os -import asyncio -import tempfile -import urllib.request -import urllib.error -from typing import Optional, Tuple - - -# 公共配置 -USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" -WINDOW_SIZE = "1920,1080" -LANGUAGE_SETTING = "zh-CN,zh" - -# Chrome 浏览器启动参数(pyppeteer 和 selenium 共用) -CHROME_ARGS = [ - "--no-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--disable-software-rasterizer", - "--disable-extensions", - "--disable-background-networking", - "--disable-default-apps", - "--disable-sync", - "--disable-translate", - "--hide-scrollbars", - "--metrics-recording-only", - "--mute-audio", - "--no-first-run", - "--safebrowsing-disable-auto-update", - "--blink-settings=imagesEnabled=false", - "--disable-plugins", - "--disable-ipc-flooding-protection", - "--disable-renderer-backgrounding", - "--disable-background-timer-throttling", - "--disable-hang-monitor", - "--disable-prompt-on-repost", - "--disable-client-side-phishing-detection", - "--disable-component-update", - "--disable-domain-reliability", - "--disable-features=site-per-process", - "--disable-features=IsolateOrigins", - "--disable-features=VizDisplayCompositor", - "--disable-features=WebRTC", - f"--window-size={WINDOW_SIZE}", - f"--lang={LANGUAGE_SETTING}", - f"--user-agent={USER_AGENT}", -] - -# 隐藏自动化特征的脚本(pyppeteer 和 selenium 共用) -HIDE_AUTOMATION_SCRIPT = """ - () => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); - Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] }); - } -""" - -# pyppeteer 额外的隐藏自动化脚本(包含 notifications 处理) -HIDE_AUTOMATION_SCRIPT_PUPPETEER = """ - () => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); - Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] }); - const originalQuery = window.navigator.permissions.query; - window.navigator.permissions.query = (parameters) => ( - parameters.name === 'notifications' ? - Promise.resolve({ state: Notification.permission }) : - originalQuery(parameters) - ); - } -""" - - -def download_with_pyppeteer(url: str) -> Tuple[Optional[str], Optional[str]]: - """使用 pyppeteer 下载 URL(支持 JS 渲染)。""" - try: - from pyppeteer import launch - except ImportError: - return None, "pyppeteer 库未安装" - - async def _download(): - pyppeteer_temp_dir = os.path.join(tempfile.gettempdir(), "pyppeteer_home") - chromium_path = os.environ.get("LYXY_CHROMIUM_BINARY") - if not chromium_path: - os.environ["PYPPETEER_HOME"] = pyppeteer_temp_dir - executable_path = chromium_path if (chromium_path and os.path.exists(chromium_path)) else None - - browser = None - try: - browser = await launch( - headless=True, - executablePath=executable_path, - args=CHROME_ARGS - ) - page = await browser.newPage() - - await page.evaluateOnNewDocument(HIDE_AUTOMATION_SCRIPT_PUPPETEER) - - await page.setJavaScriptEnabled(True) - await page.goto(url, {"waitUntil": "networkidle2", "timeout": 30000}) - return await page.content() - finally: - if browser is not None: - try: - await browser.close() - except Exception: - pass - - try: - content = asyncio.run(_download()) - if not content or not content.strip(): - return None, "下载内容为空" - return content, None - except Exception as e: - return None, f"pyppeteer 下载失败: {str(e)}" - - -def download_with_selenium(url: str) -> Tuple[Optional[str], Optional[str]]: - """使用 selenium 下载 URL(支持 JS 渲染)。""" - try: - from selenium import webdriver - from selenium.webdriver.chrome.service import Service - from selenium.webdriver.chrome.options import Options - from selenium.webdriver.support.ui import WebDriverWait - except ImportError: - return None, "selenium 库未安装" - - driver_path = os.environ.get("LYXY_CHROMIUM_DRIVER") - binary_path = os.environ.get("LYXY_CHROMIUM_BINARY") - - if not driver_path or not os.path.exists(driver_path): - return None, "LYXY_CHROMIUM_DRIVER 环境变量未设置或文件不存在" - if not binary_path or not os.path.exists(binary_path): - return None, "LYXY_CHROMIUM_BINARY 环境变量未设置或文件不存在" - - chrome_options = Options() - chrome_options.binary_location = binary_path - chrome_options.add_argument("--headless=new") - for arg in CHROME_ARGS: - chrome_options.add_argument(arg) - - # 隐藏自动化特征 - chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) - chrome_options.add_experimental_option("useAutomationExtension", False) - - driver = None - try: - import time - service = Service(driver_path) - driver = webdriver.Chrome(service=service, options=chrome_options) - - # 隐藏 webdriver 属性 - driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { - "source": HIDE_AUTOMATION_SCRIPT - }) - - driver.get(url) - - # 等待页面内容稳定 - WebDriverWait(driver, 30).until( - lambda d: d.execute_script("return document.readyState") == "complete" - ) - - last_len = 0 - stable_count = 0 - for _ in range(30): - current_len = len(driver.page_source) - if current_len == last_len: - stable_count += 1 - if stable_count >= 2: - break - else: - stable_count = 0 - last_len = current_len - time.sleep(0.5) - - content = driver.page_source - if not content or not content.strip(): - return None, "下载内容为空" - return content, None - except Exception as e: - return None, f"selenium 下载失败: {str(e)}" - finally: - if driver is not None: - try: - driver.quit() - except Exception: - pass - - -def download_with_httpx(url: str) -> Tuple[Optional[str], Optional[str]]: - """使用 httpx 下载 URL(轻量级 HTTP 客户端)。""" - try: - import httpx - except ImportError: - return None, "httpx 库未安装" - - headers = { - "User-Agent": USER_AGENT - } - - try: - with httpx.Client(timeout=30.0) as client: - response = client.get(url, headers=headers) - if response.status_code == 200: - content = response.text - if not content or not content.strip(): - return None, "下载内容为空" - return content, None - return None, f"HTTP {response.status_code}" - except Exception as e: - return None, f"httpx 下载失败: {str(e)}" - - -def download_with_urllib(url: str) -> Tuple[Optional[str], Optional[str]]: - """使用 urllib 下载 URL(标准库,兜底方案)。""" - headers = { - "User-Agent": USER_AGENT - } - - try: - req = urllib.request.Request(url, headers=headers) - with urllib.request.urlopen(req, timeout=30) as response: - if response.status == 200: - content = response.read().decode("utf-8") - if not content or not content.strip(): - return None, "下载内容为空" - return content, None - return None, f"HTTP {response.status}" - except Exception as e: - return None, f"urllib 下载失败: {str(e)}" - - -def download_html(url: str) -> Tuple[Optional[str], List[str]]: - """ - 统一的 HTML 下载入口函数,按优先级尝试各下载器。 - - 返回: (content, failures) - - content: 成功时返回 HTML 内容,失败时返回 None - - failures: 各下载器的失败原因列表 - """ - failures = [] - content = None - - # 按优先级尝试各下载器 - downloaders = [ - ("pyppeteer", download_with_pyppeteer), - ("selenium", download_with_selenium), - ("httpx", download_with_httpx), - ("urllib", download_with_urllib), - ] - - for name, func in downloaders: - content, error = func(url) - if content is not None: - return content, failures - else: - failures.append(f"- {name}: {error}") - - return None, failures diff --git a/skills/lyxy-reader-html/scripts/html_parser.py b/skills/lyxy-reader-html/scripts/html_parser.py deleted file mode 100644 index ad7a88d..0000000 --- a/skills/lyxy-reader-html/scripts/html_parser.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -"""HTML 解析模块,按 trafilatura → domscribe → MarkItDown → html2text 优先级尝试解析。""" - -from typing import Optional, Tuple - - -def parse_with_trafilatura(html_content: str) -> Tuple[Optional[str], Optional[str]]: - """使用 trafilatura 解析 HTML。""" - try: - import trafilatura - except ImportError: - return None, "trafilatura 库未安装" - - try: - markdown_content = trafilatura.extract( - html_content, - output_format="markdown", - include_formatting=True, - include_links=True, - include_images=False, - include_tables=True, - favor_recall=True, - include_comments=True, - ) - if markdown_content is None: - return None, "trafilatura 返回 None" - if not markdown_content.strip(): - return None, "解析内容为空" - return markdown_content, None - except Exception as e: - return None, f"trafilatura 解析失败: {str(e)}" - - -def parse_with_domscribe(html_content: str) -> Tuple[Optional[str], Optional[str]]: - """使用 domscribe 解析 HTML。""" - try: - from domscribe import html_to_markdown - except ImportError: - return None, "domscribe 库未安装" - - try: - options = { - 'extract_main_content': True, - } - markdown_content = html_to_markdown(html_content, options) - if not markdown_content.strip(): - return None, "解析内容为空" - return markdown_content, None - except Exception as e: - return None, f"domscribe 解析失败: {str(e)}" - - -def parse_with_markitdown(html_content: str, temp_file_path: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 解析 HTML。""" - try: - from markitdown import MarkItDown - except ImportError: - return None, "MarkItDown 库未安装" - - try: - import tempfile - import os - - input_path = temp_file_path - if not input_path or not os.path.exists(input_path): - # 创建临时文件 - fd, input_path = tempfile.mkstemp(suffix='.html') - with os.fdopen(fd, 'w', encoding='utf-8') as f: - f.write(html_content) - - md = MarkItDown() - result = md.convert( - input_path, - heading_style="ATX", - strip=["img", "script", "style", "noscript"], - ) - markdown_content = result.text_content - - if not temp_file_path: - try: - os.unlink(input_path) - except Exception: - pass - - if not markdown_content.strip(): - return None, "解析内容为空" - return markdown_content, None - except Exception as e: - return None, f"MarkItDown 解析失败: {str(e)}" - - -def parse_with_html2text(html_content: str) -> Tuple[Optional[str], Optional[str]]: - """使用 html2text 解析 HTML(兜底方案)。""" - try: - import html2text - except ImportError: - return None, "html2text 库未安装" - - try: - converter = html2text.HTML2Text() - converter.ignore_emphasis = False - converter.ignore_links = False - converter.ignore_images = True - converter.body_width = 0 - converter.skip_internal_links = True - markdown_content = converter.handle(html_content) - if not markdown_content.strip(): - return None, "解析内容为空" - return markdown_content, None - except Exception as e: - return None, f"html2text 解析失败: {str(e)}" - - -def parse_html(html_content: str, temp_file_path: Optional[str] = None) -> Tuple[Optional[str], List[str]]: - """ - 统一的 HTML 解析入口函数,按优先级尝试各解析器。 - - 返回: (content, failures) - - content: 成功时返回 Markdown 内容,失败时返回 None - - failures: 各解析器的失败原因列表 - """ - failures = [] - content = None - - # 按优先级尝试各解析器 - parsers = [ - ("trafilatura", lambda c: parse_with_trafilatura(c)), - ("domscribe", lambda c: parse_with_domscribe(c)), - ("MarkItDown", lambda c: parse_with_markitdown(c, temp_file_path)), - ("html2text", lambda c: parse_with_html2text(c)), - ] - - for name, func in parsers: - content, error = func(html_content) - if content is not None: - return content, failures - else: - failures.append(f"- {name}: {error}") - - return None, failures diff --git a/skills/lyxy-reader-html/scripts/parser.py b/skills/lyxy-reader-html/scripts/parser.py deleted file mode 100644 index 093dce7..0000000 --- a/skills/lyxy-reader-html/scripts/parser.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -"""HTML 解析器命令行交互模块,提供命令行接口。支持 URL 和 HTML 文件。""" - -import argparse -import logging -import os -import sys -import warnings - -# 抑制第三方库的进度条和日志,仅保留解析结果输出 -os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1" -os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" -os.environ["TQDM_DISABLE"] = "1" -warnings.filterwarnings("ignore") -logging.disable(logging.WARNING) - -import common -import downloader -import html_parser - - -def main() -> None: - parser = argparse.ArgumentParser( - description="将 URL 或 HTML 文件解析为 Markdown" - ) - - parser.add_argument("input", help="URL 或 HTML 文件的路径") - - parser.add_argument( - "-n", - "--context", - type=int, - default=2, - help="与 -s 配合使用,指定每个检索结果包含的前后行数(不包含空行)", - ) - - group = parser.add_mutually_exclusive_group() - group.add_argument( - "-c", "--count", action="store_true", help="返回解析后的 markdown 文档的总字数" - ) - group.add_argument( - "-l", "--lines", action="store_true", help="返回解析后的 markdown 文档的总行数" - ) - group.add_argument( - "-t", - "--titles", - action="store_true", - help="返回解析后的 markdown 文档的标题行(1-6级)", - ) - group.add_argument( - "-tc", - "--title-content", - help="指定标题名称,输出该标题及其下级内容(不包含#号)", - ) - group.add_argument( - "-s", - "--search", - help="使用正则表达式搜索文档,返回所有匹配结果(用---分隔)", - ) - - args = parser.parse_args() - - # 判断输入类型 - html_content = None - temp_file_path = None - - if common.is_url(args.input): - # URL 模式 - html_content, download_failures = downloader.download_html(args.input) - if html_content is None: - print("所有下载方法均失败:") - for failure in download_failures: - print(failure) - sys.exit(1) - else: - # HTML 文件模式 - if not os.path.exists(args.input): - print(f"错误: 文件不存在: {args.input}") - sys.exit(1) - if not common.is_html_file(args.input): - print(f"错误: 不是有效的 HTML 文件: {args.input}") - sys.exit(1) - with open(args.input, "r", encoding="utf-8") as f: - html_content = f.read() - temp_file_path = args.input - - # HTML 预处理清理 - cleaned_html = common.clean_html_content(html_content) - - # 解析 HTML - markdown_content, parse_failures = html_parser.parse_html(cleaned_html, temp_file_path) - if markdown_content is None: - print("所有解析方法均失败:") - for failure in parse_failures: - print(failure) - sys.exit(1) - - # Markdown 后处理 - markdown_content = common.remove_markdown_images(markdown_content) - markdown_content = common.normalize_markdown_whitespace(markdown_content) - - # 根据参数输出 - if args.count: - print(len(markdown_content.replace("\n", ""))) - elif args.lines: - print(len(markdown_content.split("\n"))) - elif args.titles: - titles = common.extract_titles(markdown_content) - for title in titles: - print(title) - elif args.title_content: - title_content = common.extract_title_content(markdown_content, args.title_content) - if title_content is None: - print(f"错误: 未找到标题 '{args.title_content}'") - sys.exit(1) - print(title_content, end="") - elif args.search: - search_result = common.search_markdown(markdown_content, args.search, args.context) - if search_result is None: - print(f"错误: 正则表达式无效或未找到匹配: '{args.search}'") - sys.exit(1) - print(search_result, end="") - else: - print(markdown_content, end="") - - -if __name__ == "__main__": - main() diff --git a/skills/lyxy-reader-office/SKILL.md b/skills/lyxy-reader-office/SKILL.md deleted file mode 100644 index dcaaa5b..0000000 --- a/skills/lyxy-reader-office/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: lyxy-reader-office -description: 优先解析 docx、xlsx、pptx、pdf 四种办公文档的 skill,将文档转换为 Markdown 格式,支持全文提取、标题提取、章节提取、正则搜索、字数统计、行数统计,PDF 额外支持 OCR 高精度模式。使用时请阅读 scripts/README.md 获取详细用法。 -compatibility: Requires Python 3.6+. DOCX/PPTX/XLSX 无需额外依赖(XML 原生解析);PDF 至少需要 pypdf。推荐通过 lyxy-runner-python skill 使用 uv 自动管理依赖。 ---- - -# 办公文档解析 Skill - -将 Microsoft Office 文档(.docx、.pptx、.xlsx)和 PDF 文件解析为 Markdown 格式,支持多种查询模式。 - -## Purpose - -**统一入口**:使用 `scripts/parser.py` 作为统一的命令行入口,自动识别文件类型并分派到对应的格式解析器。 - -**依赖选项**:此 skill 必须优先使用 lyxy-runner-python skill 执行,不可用时降级到直接 Python 执行。 - -## When to Use - -任何需要读取或解析 .docx、.xlsx、.pptx、.pdf 文件内容的任务都应使用此 skill。 - -### 典型场景 -- **文档内容提取**:将 Word/PPT/Excel/PDF 文档转换为可读的 Markdown 文本 -- **文档元数据**:获取文档的字数、行数等信息 -- **标题分析**:提取文档的标题结构 -- **章节提取**:提取特定章节的内容 -- **内容搜索**:在文档中搜索关键词或模式 -- **PDF OCR**:对扫描版 PDF 启用 OCR 高精度解析 - -### 触发词 -- 中文:"读取/解析/打开 docx/word/xlsx/excel/pptx/pdf 文档" -- 英文:"read/parse/extract docx/word/xlsx/excel/pptx/powerpoint/pdf" -- 文件扩展名:`.docx`、`.xlsx`、`.pptx`、`.pdf` - -## Quick Reference - -| 参数 | 说明 | -|------|------| -| (无参数) | 输出完整 Markdown 内容 | -| `-c` | 字数统计 | -| `-l` | 行数统计 | -| `-t` | 提取所有标题 | -| `-tc ` | 提取指定标题的章节内容 | -| `-s ` | 正则表达式搜索 | -| `-n ` | 与 `-s` 配合,指定上下文行数 | -| `--high-res` | PDF 专用,启用 OCR 版面分析 | - -## Workflow - -1. **检查依赖**:优先使用 lyxy-runner-python,否则降级到直接 Python 执行 -2. **选择格式**:根据文件扩展名自动识别格式 -3. **执行解析**:调用 `scripts/parser.py` 并传入参数 -4. **输出结果**:返回 Markdown 格式内容或统计信息 - -### 基本语法 - -```bash -# 使用 lyxy-runner-python(推荐) -uv run --with "markitdown[docx]" scripts/parser.py /path/to/file.docx - -# 降级到直接执行 -python3 scripts/parser.py /path/to/file.docx -``` - -## References - -详细文档请参阅 `references/` 目录: - -| 文件 | 内容 | -|------|------| -| `references/examples.md` | 各格式完整提取、字数统计、标题提取、章节提取、搜索等示例 | -| `references/parsers.md` | 解析器说明、依赖安装、各格式输出特点、能力说明 | -| `references/error-handling.md` | 限制说明、最佳实践、依赖执行策略 | - -> **详细用法**:请阅读 `scripts/README.md` 获取完整的命令行参数和依赖安装指南。 diff --git a/skills/lyxy-reader-office/references/error-handling.md b/skills/lyxy-reader-office/references/error-handling.md deleted file mode 100644 index 75d5d44..0000000 --- a/skills/lyxy-reader-office/references/error-handling.md +++ /dev/null @@ -1,41 +0,0 @@ -# 错误处理和限制说明 - -## 限制 - -- 不支持图片提取(仅纯文本) -- 不支持复杂的格式保留(字体、颜色、布局等) -- 不支持文档编辑或修改 -- 仅支持 .docx、.xlsx、.pptx、.pdf 格式(不支持 .doc、.xls、.ppt 等旧格式) -- PDF 无内置 XML 原生解析,至少需要安装 pypdf - -## 最佳实践 - -1. **必须优先使用 lyxy-runner-python**:如果环境中存在,必须使用 lyxy-runner-python 执行脚本 -2. **查阅 README**:详细参数、依赖安装、解析器对比等信息请阅读 `scripts/README.md` -3. **大文件处理**:对于大文档,建议使用章节提取(`-tc`)或搜索(`-s`)来限制处理范围 -4. **PDF 标题**:PDF 是版面描述格式,默认不含语义化标题;需要标题层级时使用 `--high-res` -5. **禁止自动安装**:降级到直接 Python 执行时,仅向用户提示安装依赖,不得自动执行 pip install - -## 依赖执行策略 - -### 必须使用 lyxy-runner-python - -如果环境中存在 lyxy-runner-python skill,**必须**使用它来执行 parser.py 脚本: -- lyxy-runner-python 使用 uv 管理依赖,自动安装所需的第三方库 -- 环境隔离,不污染系统 Python -- 跨平台兼容(Windows/macOS/Linux) - -### 降级到直接执行 - -**仅当** lyxy-runner-python skill 不存在时,才降级到直接 Python 执行: -- 需要用户手动安装依赖 -- DOCX/PPTX/XLSX 无需依赖也可通过 XML 原生解析工作 -- PDF 至少需要安装 pypdf -- **禁止自动执行 pip install**,仅向用户提示安装建议 - -## 不适用场景 - -- 需要提取图片内容(仅支持纯文本) -- 需要保留复杂的格式信息(字体、颜色、布局) -- 需要编辑或修改文档 -- 需要处理 .doc、.xls、.ppt 等旧格式 diff --git a/skills/lyxy-reader-office/references/examples.md b/skills/lyxy-reader-office/references/examples.md deleted file mode 100644 index 1edbcbb..0000000 --- a/skills/lyxy-reader-office/references/examples.md +++ /dev/null @@ -1,55 +0,0 @@ -# 示例 - -## 提取完整文档内容 - -```bash -# DOCX -uv run --with "markitdown[docx]" scripts/parser.py /path/to/report.docx - -# PPTX -uv run --with "markitdown[pptx]" scripts/parser.py /path/to/slides.pptx - -# XLSX -uv run --with "markitdown[xlsx]" scripts/parser.py /path/to/data.xlsx - -# PDF -uv run --with "markitdown[pdf]" --with pypdf scripts/parser.py /path/to/doc.pdf -``` - -## 获取文档字数 - -```bash -uv run --with "markitdown[docx]" scripts/parser.py -c /path/to/report.docx -``` - -## 提取所有标题 - -```bash -uv run --with "markitdown[docx]" scripts/parser.py -t /path/to/report.docx -``` - -## 提取指定章节 - -```bash -uv run --with "markitdown[docx]" scripts/parser.py -tc "第一章" /path/to/report.docx -``` - -## 搜索关键词 - -```bash -uv run --with "markitdown[docx]" scripts/parser.py -s "关键词" -n 3 /path/to/report.docx -``` - -## PDF OCR 高精度解析 - -```bash -uv run --with docling --with pypdf scripts/parser.py /path/to/scanned.pdf --high-res -``` - -## 降级到直接 Python 执行 - -仅当 lyxy-runner-python skill 不存在时使用: - -```bash -python3 scripts/parser.py /path/to/file.docx -``` diff --git a/skills/lyxy-reader-office/references/parsers.md b/skills/lyxy-reader-office/references/parsers.md deleted file mode 100644 index 9c3af71..0000000 --- a/skills/lyxy-reader-office/references/parsers.md +++ /dev/null @@ -1,58 +0,0 @@ -# 解析器说明和依赖安装 - -## 多策略解析降级 - -每种文件格式配备多个解析器,按优先级依次尝试,前一个失败自动回退到下一个。 - -详细的解析器优先级和对比请查阅 `scripts/README.md`。 - -## 依赖安装 - -### 使用 uv(推荐) - -```bash -# DOCX - 全依赖 -uv run --with docling --with "unstructured[docx]" --with markdownify --with pypandoc-binary --with "markitdown[docx]" --with python-docx scripts/parser.py /path/to/file.docx - -# PPTX - 全依赖 -uv run --with docling --with "unstructured[pptx]" --with markdownify --with "markitdown[pptx]" --with python-pptx scripts/parser.py /path/to/file.pptx - -# XLSX - 全依赖 -uv run --with docling --with "unstructured[xlsx]" --with markdownify --with "markitdown[xlsx]" --with pandas --with tabulate scripts/parser.py /path/to/file.xlsx - -# PDF - 全依赖(基础文本提取) -uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf scripts/parser.py /path/to/file.pdf - -# PDF OCR 高精度模式(全依赖) -uv run --with docling --with "unstructured[pdf]" --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with markdownify --with "markitdown[pdf]" --with pypdf scripts/parser.py /path/to/file.pdf --high-res -``` - -> **说明**:以上为全依赖安装命令,包含所有解析器以获得最佳兼容性。详细的解析器优先级和对比请查阅 `scripts/README.md`。 - -## 各格式输出特点 - -- **DOCX**:标准 Markdown 文档结构 -- **PPTX**:每张幻灯片以 `## Slide N` 为标题,幻灯片之间以 `---` 分隔 -- **XLSX**:以 `## SheetName` 区分工作表,数据以 Markdown 表格呈现 -- **PDF**:纯文本流,使用 `--high-res` 可启用 OCR 版面分析识别标题 - -## 能力说明 - -### 1. 全文转换为 Markdown -将完整文档解析为 Markdown 格式,移除图片但保留文本格式(标题、列表、表格、粗体、斜体等)。 - -### 2. 获取文档元信息 -- 字数统计(`-c` 参数) -- 行数统计(`-l` 参数) - -### 3. 标题列表提取 -提取文档中所有 1-6 级标题(`-t` 参数),按原始层级关系返回。 - -### 4. 指定章节内容提取 -根据标题名称提取特定章节的完整内容(`-tc` 参数),包含上级标题链和所有下级内容。 - -### 5. 正则表达式搜索 -在文档中搜索关键词或模式(`-s` 参数),支持自定义上下文行数(`-n` 参数,默认 2 行)。 - -### 6. PDF OCR 高精度模式 -对 PDF 文件启用 OCR 版面分析(`--high-res` 参数),适用于扫描版 PDF 或需要识别标题层级的场景。 diff --git a/skills/lyxy-reader-office/scripts/README.md b/skills/lyxy-reader-office/scripts/README.md deleted file mode 100644 index b4f2f3c..0000000 --- a/skills/lyxy-reader-office/scripts/README.md +++ /dev/null @@ -1,503 +0,0 @@ -# Document Parser 使用说明 - -模块化文档解析器,将 DOCX、PPTX、XLSX、PDF 文件转换为 Markdown 格式。 - -每种文档类型配备多个解析器,按优先级依次尝试,前一个失败自动回退到下一个。不安装任何第三方库时,DOCX/PPTX/XLSX 仍可通过内置 XML 原生解析工作(PDF 至少需要 pypdf)。 - -## 快速开始 - -```bash -# 最简运行(XML 原生解析,无需安装依赖) -python parser.py report.docx - -# 安装推荐依赖后运行 -pip install "markitdown[docx]" -python parser.py report.docx - -# 使用 uv 一键运行(自动安装依赖,无需手动 pip install) -uv run --with "markitdown[docx]" parser.py report.docx -``` - -## 命令行用法 - -### 基本语法 - -```bash -python parser.py [options] -``` - -`file_path` 为 DOCX、PPTX、XLSX 或 PDF 文件路径(相对或绝对路径)。不带任何选项时输出完整 Markdown 内容。 - -### 参数说明 - -以下参数互斥,一次只能使用一个: - -| 短选项 | 长选项 | 说明 | -|--------|--------|------| -| `-c` | `--count` | 输出解析后文档的总字符数(不含换行符) | -| `-l` | `--lines` | 输出解析后文档的总行数 | -| `-t` | `--titles` | 输出所有标题行(1-6 级,含 `#` 前缀) | -| `-tc ` | `--title-content ` | 提取指定标题及其下级内容(`name` 不包含 `#` 号) | -| `-s ` | `--search ` | 使用正则表达式搜索文档,返回匹配结果 | - -搜索辅助参数(与 `-s` 配合使用): - -| 短选项 | 长选项 | 说明 | -|--------|--------|------| -| `-n ` | `--context ` | 每个匹配结果包含的前后非空行数(默认:2) | - -PDF 专用参数: - -| 长选项 | 说明 | -|--------|------| -| `--high-res` | 启用 OCR 版面分析(需要额外依赖,处理较慢) | - -### 退出码 - -| 退出码 | 含义 | -|--------|------| -| `0` | 解析成功 | -| `1` | 错误(文件不存在、格式无效、所有解析器失败、标题未找到、正则无效或无匹配) | - -### 使用示例 - -**输出完整 Markdown:** - -```bash -python parser.py report.docx # 输出到终端 -python parser.py report.docx > output.md # 重定向到文件 -``` - -**统计信息(`-c` / `-l`):** - -输出单个数字,适合管道处理。 - -```bash -$ python parser.py report.docx -c -8500 - -$ python parser.py report.docx -l -215 -``` - -**提取标题(`-t`):** - -每行一个标题,保留 `#` 前缀和层级。PDF 通常不包含语义化标题层级。 - -```bash -$ python parser.py report.docx -t -# 第一章 概述 -## 1.1 背景 -## 1.2 目标 -# 第二章 实现 -``` - -**提取标题内容(`-tc`):** - -输出指定标题及其下级内容。如果文档中有多个同名标题,用 `---` 分隔。每段输出包含上级标题链。 - -```bash -$ python parser.py report.docx -tc "1.1 背景" -# 第一章 概述 -## 1.1 背景 -这是背景的详细内容... -``` - -**搜索(`-s`):** - -支持 Python 正则表达式语法。多个匹配结果用 `---` 分隔。`-n` 控制上下文行数。 - -```bash -$ python parser.py report.docx -s "测试" -n 1 -上一行内容 -包含**测试**关键词的行 -下一行内容 ---- -另一处上一行 -另一处**测试**内容 -另一处下一行 -``` - -### 批量处理 - -```bash -# Linux/Mac -for file in *.docx; do - python parser.py "$file" > "${file%.docx}.md" -done - -# Windows PowerShell -Get-ChildItem *.docx | ForEach-Object { - python parser.py $_.FullName > ($_.BaseName + ".md") -} -``` - -### 管道使用 - -```bash -# 过滤包含关键词的行 -python parser.py report.docx | grep "重要" > important.md - -# 统计含表格行数 -python parser.py data.xlsx | grep -c "^|" -``` - -## 安装 - -脚本基于 Python 3.6+ 运行。每种文档类型有多个解析器按优先级依次尝试,建议安装对应类型的**所有**依赖以获得最佳兼容性。也可以只安装部分依赖,脚本会自动选择可用的解析器。 - -### DOCX - -优先级:Docling → unstructured → pypandoc-binary → MarkItDown → python-docx → XML 原生 - -```bash -# pip -pip install docling "unstructured[docx]" markdownify pypandoc-binary "markitdown[docx]" python-docx - -# uv(一键运行,无需预安装) -uv run --with docling --with "unstructured[docx]" --with markdownify --with pypandoc-binary --with "markitdown[docx]" --with python-docx parser.py report.docx -``` - -### PPTX - -优先级:Docling → unstructured → MarkItDown → python-pptx → XML 原生 - -```bash -# pip -pip install docling "unstructured[pptx]" markdownify "markitdown[pptx]" python-pptx - -# uv -uv run --with docling --with "unstructured[pptx]" --with markdownify --with "markitdown[pptx]" --with python-pptx parser.py presentation.pptx -``` - -### XLSX - -优先级:Docling → unstructured → MarkItDown → pandas → XML 原生 - -```bash -# pip -pip install docling "unstructured[xlsx]" markdownify "markitdown[xlsx]" pandas tabulate - -# uv -uv run --with docling --with "unstructured[xlsx]" --with markdownify --with "markitdown[xlsx]" --with pandas --with tabulate parser.py data.xlsx -``` - -### PDF - -默认优先级:Docling → unstructured (fast) → MarkItDown → pypdf - -`--high-res` 优先级:Docling OCR → unstructured OCR (hi_res) → Docling → unstructured (fast) → MarkItDown → pypdf - -> **平台差异说明**:`--high-res` 模式在不同平台上需要不同的依赖配置,详见下方各平台说明。 - -#### Windows x86_64 - -```bash -# pip - 基础文本提取(fast 策略,无需 OCR) -pip install docling "unstructured[pdf]" markdownify "markitdown[pdf]" pypdf - -# pip - OCR 版面分析(--high-res 所需依赖) -pip install docling "unstructured[pdf]" unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes markdownify "markitdown[pdf]" pypdf - -# uv - 基础文本提取 -uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf - -# uv - OCR 版面分析 -uv run --with docling --with "unstructured[pdf]" --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf --high-res -``` - -> **Windows 说明**:hi_res 策略需要额外安装 `unstructured-paddleocr`、`paddlepaddle==2.6.2`、`ml-dtypes`。PaddlePaddle 必须锁定 2.x 版本,3.x 在 Windows 上有 OneDNN 兼容问题。 - -#### macOS x86_64 (Intel) - -macOS x86_64 上 `docling-parse` 5.x 没有预编译 wheel,需要使用 4.0.0 版本;同时 `easyocr` 与 NumPy 2.x 不兼容,需要锁定 `numpy<2`。 - -```bash -# uv - 基础文本提取 -uv run --python 3.12 --with "docling==2.40.0" --with "docling-parse==4.0.0" --with "numpy<2" --with "markitdown[pdf]" --with pypdf parser.py report.pdf - -# uv - OCR 版面分析(--high-res) -uv run --python 3.12 --with "docling==2.40.0" --with "docling-parse==4.0.0" --with "numpy<2" --with "markitdown[pdf]" --with pypdf parser.py report.pdf --high-res -``` - -> **macOS x86_64 说明**: -> - 必须指定 `--python 3.12`(PaddlePaddle 不支持 Python 3.14) -> - `docling==2.40.0` 兼容 `docling-parse==4.0.0`(唯一有 x86_64 预编译 wheel 的版本) -> - `numpy<2` 避免 easyocr 与 NumPy 2.x 的兼容性问题 -> - Docling 在 macOS 上使用 EasyOCR 作为 OCR 后端,无需安装 PaddleOCR - -#### macOS arm64 (Apple Silicon) / Linux - -```bash -# uv - 基础文本提取 -uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf - -# uv - OCR 版面分析 -uv run --with docling --with "unstructured[pdf]" --with markdownify --with "markitdown[pdf]" --with pypdf parser.py report.pdf --high-res -``` - -> **通用说明**:PDF 无内置 XML 原生解析,至少需要安装 pypdf。默认模式下 Docling 不启用 OCR,unstructured 使用 fast 策略。指定 `--high-res` 后,Docling 启用 OCR。 - -### 安装所有依赖 - -#### Windows x86_64 - -```bash -# pip - 基础文本提取(不包含 PDF OCR) -pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify pypandoc-binary "markitdown[docx,pptx,xlsx]" python-docx python-pptx pandas tabulate pypdf - -# pip - 完整版(包含 PDF OCR) -pip install docling "unstructured[docx,pptx,xlsx,pdf]" markdownify unstructured-paddleocr "paddlepaddle==2.6.2" ml-dtypes pypandoc-binary "markitdown[docx,pptx,xlsx,pdf]" python-docx python-pptx pandas tabulate pypdf - -# uv - 基础文本提取 -uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with pypandoc-binary --with "markitdown[docx,pptx,xlsx]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx - -# uv - 完整版(包含 PDF OCR) -uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with unstructured-paddleocr --with "paddlepaddle==2.6.2" --with ml-dtypes --with pypandoc-binary --with "markitdown[docx,pptx,xlsx,pdf]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx -``` - -#### macOS x86_64 (Intel) - -```bash -# uv - 完整版(包含 PDF OCR) -uv run --python 3.12 --with "docling==2.40.0" --with "docling-parse==4.0.0" --with "numpy<2" --with "markitdown[docx,pptx,xlsx,pdf]" --with pypandoc-binary --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx -``` - -> macOS x86_64 上由于依赖兼容性问题,无法使用 unstructured,但 docling 可以处理所有格式。 - -#### macOS arm64 (Apple Silicon) / Linux - -```bash -# uv - 基础文本提取 -uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with pypandoc-binary --with "markitdown[docx,pptx,xlsx]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx - -# uv - 完整版(包含 PDF OCR) -uv run --with docling --with "unstructured[docx,pptx,xlsx,pdf]" --with markdownify --with pypandoc-binary --with "markitdown[docx,pptx,xlsx,pdf]" --with python-docx --with python-pptx --with pandas --with tabulate --with pypdf parser.py file.docx -``` - -### 依赖说明 - -**MarkItDown**:需要按文档类型安装可选依赖,直接 `pip install markitdown` 不包含任何格式支持。 - -```bash -pip install "markitdown[docx]" # DOCX -pip install "markitdown[pptx]" # PPTX -pip install "markitdown[xlsx]" # XLSX -pip install "markitdown[pdf]" # PDF -pip install "markitdown[docx,pptx,xlsx,pdf]" # 全部 -``` - -**Docling**:DOCX/PPTX/XLSX 使用 SimplePipeline 直接解析 XML 结构,不涉及 OCR。PDF 默认不启用 OCR(`do_ocr=False`),指定 `--high-res` 后启用 OCR(`do_ocr=True`)。首次运行 OCR 模式会自动下载模型到缓存目录,需保持网络连通。 - -**unstructured**:需同时安装 `markdownify`。支持按文档类型安装特定 extras 以减少依赖量: - -- `unstructured[docx]` - DOCX 处理(仅需 `python-docx`) -- `unstructured[pptx]` - PPTX 处理(仅需 `python-pptx`) -- `unstructured[xlsx]` - XLSX 处理(需 `openpyxl`、`xlrd`、`pandas` 等) -- `unstructured` - 基础包(用于 PDF fast 策略) -- `unstructured[all-docs]` - 所有文档类型(包含大量不必要的 OCR/视觉依赖) - -**PaddleOCR**:不能用 `paddleocr` 代替 `unstructured-paddleocr`,unstructured 查找的模块名是 `unstructured_paddleocr`。 - -## 输出格式 - -### Markdown 文档结构 - -无选项时输出完整 Markdown,包含以下元素: - -```markdown -# 一级标题 - -正文段落 - -## 二级标题 - -- 无序列表项 -- 无序列表项 - -1. 有序列表项 -2. 有序列表项 - -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | - -**粗体** *斜体* 下划线 -``` - -### 各格式特有结构 - -**PPTX** — 每张幻灯片以 `## Slide N` 为标题,幻灯片之间以 `---` 分隔: - -```markdown -## Slide 1 - -幻灯片 1 的内容 - ---- - -## Slide 2 - -幻灯片 2 的内容 - ---- -``` - -**XLSX** — 以 `## SheetName` 区分工作表,数据以 Markdown 表格呈现: - -```markdown -# Excel数据转换结果 (原生XML解析) - -## Sheet1 - -| 列1 | 列2 | 列3 | -|------|------|------| -| 数据1 | 数据2 | 数据3 | - -## Sheet2 - -| 列A | 列B | -|------|------| -| 值1 | 值2 | -``` - -**PDF** — 纯文本流,通常不包含语义化标题层级(PDF 是版面描述格式,标题只是视觉样式)。使用 Docling 或 unstructured hi_res 策略可通过版面分析识别部分标题,但准确度取决于排版质量。 - -### 内容自动处理 - -输出前会自动进行以下处理: - -| 处理 | 说明 | -|------|------| -| 图片移除 | 删除 `![alt](url)` 语法 | -| 空行规范化 | 连续多个空行合并为一个 | -| RGB 噪声过滤 | 移除 `R:255 G:128 B:0` 格式的颜色值行(仅 unstructured 解析器) | -| 页码噪声过滤 | 移除 `— 3 —` 格式的页码行(仅 unstructured 解析器) | -| 页眉/页脚过滤 | 自动跳过 Header/Footer 元素(仅 unstructured 解析器) | - -## 错误处理 - -### 错误消息 - -```bash -# 文件不存在 -$ python parser.py missing.docx -错误: 文件不存在: missing.docx - -# 格式无效 -$ python parser.py readme.txt -错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: readme.txt - -# 所有解析器失败(DOCX 示例) -$ python parser.py report.docx -所有解析方法均失败: -- Docling: docling 库未安装 -- unstructured: unstructured 库未安装 -- pypandoc-binary: pypandoc-binary 库未安装 -- MarkItDown: MarkItDown 库未安装 -- python-docx: python-docx 库未安装 -- XML 原生解析: document.xml 不存在或无法访问 - -# 标题未找到 -$ python parser.py report.docx -tc "不存在的标题" -错误: 未找到标题 '不存在的标题' - -# 无效正则或无匹配 -$ python parser.py report.docx -s "[invalid" -错误: 正则表达式无效或未找到匹配: '[invalid' -``` - -### 解析器回退机制 - -脚本按优先级依次尝试各解析器。每个解析器失败后记录原因(库未安装 / 解析失败 / 文档为空),然后自动尝试下一个。全部失败时输出汇总信息并以退出码 1 退出。 - -## 解析器对比 - -### DOCX - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | 单一依赖覆盖全格式;自动 OCR;输出结构稳定 | 首次需下载模型;内存占用较高 | 一键解析;需要 OCR | -| **unstructured** | 元素类型感知;自动过滤噪声;HTML 表格转 Markdown | 需 `unstructured[docx]` / `[pptx]` / `[xlsx]` + `markdownify` | 结构化输出;表格转换 | -| **pypandoc-binary** | 自带 Pandoc;输出整洁;错误信息清晰 | 仅 DOCX;包体积大 | 标准化 Markdown | -| **MarkItDown** | 微软官方;格式规范 | 输出简洁 | 标准格式;自动化处理 | -| **python-docx** | 输出最详细;保留完整结构;支持复杂样式 | 可能含多余空行 | 精确控制输出 | -| **XML 原生** | 无需依赖;速度快 | 样式处理有限 | 依赖不可用时兜底 | - -### PPTX - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | 文本/表格/图片 OCR;统一 Markdown | 需下载模型 | 一次性转换;含图片的 PPTX | -| **unstructured** | 元素感知;过滤 RGB 噪声;表格转换 | 需 `unstructured[pptx]` + `markdownify` | 结构化输出 | -| **MarkItDown** | 自动 Slide 分隔;简洁 | 详细度低 | 快速预览 | -| **python-pptx** | 输出最详细;支持层级列表 | 依赖私有 API | 完整内容提取 | -| **XML 原生** | 无需依赖;速度快 | 分组简单 | 依赖不可用时兜底 | - -### XLSX - -| 解析器 | 优点 | 缺点 | 适用场景 | -|---------|------|--------|---------| -| **Docling** | 全表导出;处理合并单元格/图像 OCR | 大表可能慢 | 快速全表转换 | -| **unstructured** | 元素感知;过滤噪声;表格转换 | 需 `unstructured[xlsx]` + `markdownify` | 结构化输出 | -| **MarkItDown** | 支持多工作表;简洁 | 详细度低 | 快速预览 | -| **pandas** | 功能强大;支持复杂表格 | 需 `pandas` + `tabulate` | 数据分析 | -| **XML 原生** | 无需依赖;支持所有单元格类型 | 无数据处理能力 | 依赖不可用时兜底 | - -### PDF - -| 解析器 | 模式 | 优点 | 缺点 | 适用场景 | -|---------|------|------|--------|---------| -| **Docling** | 默认 | 结构化 Markdown;表格/图片占位 | 首次需下载模型 | 有文本层的 PDF | -| **Docling OCR** | `--high-res` | 内置 OCR;结构化 Markdown | 模型体积大;OCR 耗时长 | 扫描版 PDF;多语言 | -| **unstructured** | 默认 | fast 策略;速度快 | 不做版面分析;标题不可靠 | 快速文本提取 | -| **unstructured OCR** | `--high-res` | hi_res 版面分析 + PaddleOCR;标题识别 | 需额外 PaddleOCR 依赖 | 版面分析;OCR | -| **MarkItDown** | 通用 | 微软官方;格式规范 | 输出简洁 | 标准格式 | -| **pypdf** | 通用 | 轻量;速度快;安装简单 | 功能简单 | 快速文本提取 | - -## 常见问题 - -### 为什么有些内容没有提取到? - -不同解析器输出详细度不同。优先级高的解析器不一定输出最详细——Docling 和 unstructured 侧重结构化,python-docx/python-pptx 输出最详细但不做噪声过滤。建议安装对应类型的所有依赖,脚本会自动选择优先级最高的可用解析器。 - -### PDF 文件没有标题层级? - -PDF 是版面描述格式,不包含语义化标题结构。使用 `--high-res` 参数可启用 Docling OCR 或 unstructured hi_res 策略,通过版面分析识别部分标题,准确度取决于排版质量。默认模式下建议用 `-s` 搜索定位内容,或用 `-c` / `-l` 了解文档规模。 - -### 表格格式不正确? - -XML 原生解析器对复杂表格(合并单元格、嵌套表格)支持有限。安装 Docling、unstructured 或对应的专用库可获得更好的表格处理效果。 - -### 中文显示乱码? - -脚本输出 UTF-8 编码,确保终端支持: - -```bash -# Linux/Mac -export LANG=en_US.UTF-8 - -# Windows PowerShell -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -``` - -### 如何只使用特定解析器? - -当前版本不支持指定解析器,总是按优先级自动选择。可以通过只安装目标解析器的依赖来间接控制——未安装的解析器会被跳过。 - -### 大文件处理慢? - -Docling 和 unstructured 对大文件较慢(尤其是 OCR)。如果只需要快速提取文本,可以只安装轻量依赖(如 pypdf、python-docx),让脚本回退到这些解析器。DOCX/PPTX/XLSX 不安装任何依赖时使用 XML 原生解析,速度最快。 - -## 文件结构 - -``` -scripts/ -├── common.py # 公共函数和常量 -├── docx_parser.py # DOCX 文件解析 -├── pptx_parser.py # PPTX 文件解析 -├── xlsx_parser.py # XLSX 文件解析 -├── pdf_parser.py # PDF 文件解析 -├── parser.py # 命令行入口 -└── README.md # 本文档 -``` diff --git a/skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/common.cpython-311.pyc deleted file mode 100644 index 3bcdf2c2f10d2fec645071e6938fb13558f8413e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18181 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 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/docx_parser.cpython-311.pyc deleted file mode 100644 index 5bfc924c3681eab38156533e2711fcc80ccb4ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16060 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 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/pptx_parser.cpython-311.pyc deleted file mode 100644 index 104e349c575fb15c5a8f52ebd986489c3f173977..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15272 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 diff --git a/skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc b/skills/lyxy-reader-office/scripts/__pycache__/xlsx_parser.cpython-311.pyc deleted file mode 100644 index a4bcec09988f3467fddb5769ee803fd001448612..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16514 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 kC>&q(j Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析文件""" - try: - from markitdown import MarkItDown - - md = MarkItDown() - result = md.convert(file_path) - if not result.text_content.strip(): - return None, "文档为空" - return result.text_content, None - except ImportError: - return None, "MarkItDown 库未安装" - except Exception as e: - return None, f"MarkItDown 解析失败: {str(e)}" - - -def parse_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析文件""" - try: - from docling.document_converter import DocumentConverter - except ImportError: - return None, "docling 库未安装" - - try: - converter = DocumentConverter() - result = converter.convert(file_path) - markdown_content = result.document.export_to_markdown() - if not markdown_content.strip(): - return None, "文档为空" - return markdown_content, None - except Exception as e: - return None, f"docling 解析失败: {str(e)}" - - -def build_markdown_table(rows_data: List[List[str]]) -> str: - """将二维列表转换为 Markdown 表格格式""" - if not rows_data or not rows_data[0]: - return "" - - md_lines = [] - for i, row_data in enumerate(rows_data): - row_text = [cell if cell else "" for cell in row_data] - md_lines.append("| " + " | ".join(row_text) + " |") - if i == 0: - md_lines.append("| " + " | ".join(["---"] * len(row_text)) + " |") - return "\n".join(md_lines) + "\n\n" - - -def flush_list_stack(list_stack: List[str], target: List[str]) -> None: - """将列表堆栈中的非空项添加到目标列表并清空堆栈""" - for item in list_stack: - if item: - target.append(item + "\n") - list_stack.clear() - - -def safe_open_zip(zip_file: zipfile.ZipFile, name: str) -> Optional[zipfile.ZipExtFile]: - """安全地从 ZipFile 中打开文件,防止路径遍历攻击""" - if not name: - return None - if name.startswith("/") or name.startswith(".."): - return None - if "/../" in name or name.endswith("/.."): - return None - if "\\" in name: - return None - return zip_file.open(name) - - -_CONSECUTIVE_BLANK_LINES = re.compile(r"\n{3,}") - - -def normalize_markdown_whitespace(content: str) -> str: - """规范化 Markdown 空白字符,保留单行空行""" - return _CONSECUTIVE_BLANK_LINES.sub("\n\n", content) - - -def _is_valid_ooxml(file_path: str, required_files: List[str]) -> bool: - try: - with zipfile.ZipFile(file_path, "r") as zip_file: - names = set(zip_file.namelist()) - return all(r in names for r in required_files) - except (zipfile.BadZipFile, zipfile.LargeZipFile): - return False - - -_DOCX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "word/document.xml"] -_PPTX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "ppt/presentation.xml"] -_XLSX_REQUIRED = ["[Content_Types].xml", "_rels/.rels", "xl/workbook.xml"] - - -def is_valid_docx(file_path: str) -> bool: - """验证文件是否为有效的 DOCX 格式""" - return _is_valid_ooxml(file_path, _DOCX_REQUIRED) - - -def is_valid_pptx(file_path: str) -> bool: - """验证文件是否为有效的 PPTX 格式""" - return _is_valid_ooxml(file_path, _PPTX_REQUIRED) - - -def is_valid_xlsx(file_path: str) -> bool: - """验证文件是否为有效的 XLSX 格式""" - return _is_valid_ooxml(file_path, _XLSX_REQUIRED) - - -def is_valid_pdf(file_path: str) -> bool: - """验证文件是否为有效的 PDF 格式""" - try: - with open(file_path, "rb") as f: - header = f.read(4) - return header == b"%PDF" - except (IOError, OSError): - return False - - -def remove_markdown_images(markdown_text: str) -> str: - """移除 Markdown 文本中的图片标记""" - return IMAGE_PATTERN.sub("", markdown_text) - - -def get_heading_level(line: str) -> int: - """获取 Markdown 行的标题级别(1-6),非标题返回 0""" - stripped = line.lstrip() - if not stripped.startswith("#"): - return 0 - without_hash = stripped.lstrip("#") - level = len(stripped) - len(without_hash) - if not (1 <= level <= 6): - return 0 - if len(stripped) == level: - return level - if stripped[level] != " ": - return 0 - return level - - -def extract_titles(markdown_text: str) -> List[str]: - """提取 markdown 文本中的所有标题行(1-6级)""" - title_lines = [] - for line in markdown_text.split("\n"): - if get_heading_level(line) > 0: - title_lines.append(line.lstrip()) - return title_lines - - -def extract_title_content(markdown_text: str, title_name: str) -> Optional[str]: - """提取所有指定标题及其下级内容(每个包含上级标题)""" - lines = markdown_text.split("\n") - match_indices = [] - - for i, line in enumerate(lines): - level = get_heading_level(line) - if level > 0: - stripped = line.lstrip() - title_text = stripped[level:].strip() - if title_text == title_name: - match_indices.append(i) - - if not match_indices: - return None - - result_lines = [] - for match_num, idx in enumerate(match_indices): - if match_num > 0: - result_lines.append("\n---\n") - - target_level = get_heading_level(lines[idx]) - - parent_titles = [] - current_level = target_level - for i in range(idx - 1, -1, -1): - line_level = get_heading_level(lines[i]) - if line_level > 0 and line_level < current_level: - parent_titles.append(lines[i]) - current_level = line_level - if current_level == 1: - break - - parent_titles.reverse() - result_lines.extend(parent_titles) - - result_lines.append(lines[idx]) - for i in range(idx + 1, len(lines)): - line = lines[i] - line_level = get_heading_level(line) - if line_level == 0 or line_level > target_level: - result_lines.append(line) - else: - break - - return "\n".join(result_lines) - - -def search_markdown( - content: str, pattern: str, context_lines: int = 0 -) -> Optional[str]: - """使用正则表达式搜索 markdown 文档,返回匹配结果及其上下文""" - try: - regex = re.compile(pattern) - except re.error: - return None - - lines = content.split("\n") - - non_empty_indices = [] - non_empty_to_original = {} - for i, line in enumerate(lines): - if line.strip(): - non_empty_indices.append(i) - non_empty_to_original[i] = len(non_empty_indices) - 1 - - matched_non_empty_indices = [] - for orig_idx in non_empty_indices: - if regex.search(lines[orig_idx]): - matched_non_empty_indices.append(non_empty_to_original[orig_idx]) - - if not matched_non_empty_indices: - return None - - merged_ranges = [] - current_start = matched_non_empty_indices[0] - current_end = matched_non_empty_indices[0] - - for idx in matched_non_empty_indices[1:]: - if idx - current_end <= context_lines * 2: - current_end = idx - else: - merged_ranges.append((current_start, current_end)) - current_start = idx - current_end = idx - merged_ranges.append((current_start, current_end)) - - results = [] - for start, end in merged_ranges: - context_start_idx = max(0, start - context_lines) - context_end_idx = min(len(non_empty_indices) - 1, end + context_lines) - - start_line_idx = non_empty_indices[context_start_idx] - end_line_idx = non_empty_indices[context_end_idx] - - result_lines = [ - line - for i, line in enumerate(lines) - if start_line_idx <= i <= end_line_idx - ] - results.append("\n".join(result_lines)) - - return "\n---\n".join(results) - - -_FILE_TYPE_VALIDATORS = { - ".docx": is_valid_docx, - ".pptx": is_valid_pptx, - ".xlsx": is_valid_xlsx, - ".pdf": is_valid_pdf, -} - - -def detect_file_type(file_path: str) -> Optional[str]: - """检测文件类型,返回 'docx'、'pptx'、'xlsx' 或 'pdf'""" - ext = os.path.splitext(file_path)[1].lower() - validator = _FILE_TYPE_VALIDATORS.get(ext) - if validator and validator(file_path): - return ext.lstrip(".") - return None - - -def _unstructured_elements_to_markdown( - elements: list, trust_titles: bool = True -) -> str: - """将 unstructured 解析出的元素列表转换为 Markdown 文本""" - try: - import markdownify as md_lib - from unstructured.documents.elements import ( - Footer, - Header, - Image, - ListItem, - PageBreak, - PageNumber, - Table, - Title, - ) - except ImportError: - return "\n\n".join( - el.text for el in elements if hasattr(el, "text") and el.text and el.text.strip() - ) - - skip_types = (Header, Footer, PageBreak, PageNumber) - parts = [] - - for el in elements: - if isinstance(el, skip_types): - continue - text = el.text.strip() if hasattr(el, "text") else str(el).strip() - if not text or _RGB_PATTERN.match(text) or _PAGE_NUMBER_PATTERN.match(text): - continue - - if isinstance(el, Table): - html = getattr(el.metadata, "text_as_html", None) - if html: - parts.append(md_lib.markdownify(html, strip=["img"]).strip()) - else: - parts.append(str(el)) - elif isinstance(el, Title) and trust_titles: - depth = getattr(el.metadata, "category_depth", None) or 1 - depth = min(max(depth, 1), 4) - parts.append(f"{'#' * depth} {text}") - elif isinstance(el, ListItem): - parts.append(f"- {text}") - elif isinstance(el, Image): - path = getattr(el.metadata, "image_path", None) or "" - if path: - parts.append(f"![image]({path})") - else: - parts.append(text) - - return "\n\n".join(parts) diff --git a/skills/lyxy-reader-office/scripts/docx_parser.py b/skills/lyxy-reader-office/scripts/docx_parser.py deleted file mode 100644 index a5403bc..0000000 --- a/skills/lyxy-reader-office/scripts/docx_parser.py +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env python3 -"""DOCX 文件解析模块,提供多种解析方法。""" - -import xml.etree.ElementTree as ET -import zipfile -from typing import Any, List, Optional, Tuple - -from common import ( - _unstructured_elements_to_markdown, - build_markdown_table, - parse_with_docling, - parse_with_markitdown, - safe_open_zip, -) - - -def parse_docx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析 DOCX 文件""" - return parse_with_docling(file_path) - - -def parse_docx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 DOCX 文件""" - try: - from unstructured.partition.docx import partition_docx - except ImportError: - return None, "unstructured 库未安装" - - try: - elements = partition_docx(filename=file_path, infer_table_structure=True) - content = _unstructured_elements_to_markdown(elements) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"unstructured 解析失败: {str(e)}" - - -def parse_docx_with_pypandoc(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 pypandoc-binary 库解析 DOCX 文件。""" - try: - import pypandoc - except ImportError: - return None, "pypandoc-binary 库未安装" - - try: - content = pypandoc.convert_file( - source_file=file_path, - to="md", - format="docx", - outputfile=None, - extra_args=["--wrap=none"], - ) - except OSError as exc: - return None, f"pypandoc-binary 缺少 Pandoc 可执行文件: {exc}" - except RuntimeError as exc: - return None, f"pypandoc-binary 解析失败: {exc}" - - content = content.strip() - if not content: - return None, "文档为空" - return content, None - - -def parse_docx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析 DOCX 文件""" - return parse_with_markitdown(file_path) - - -def parse_docx_with_python_docx(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 python-docx 库解析 DOCX 文件""" - try: - from docx import Document - except ImportError: - return None, "python-docx 库未安装" - - try: - doc = Document(file_path) - - _HEADING_LEVELS = { - "Title": 1, "Heading 1": 1, "Heading 2": 2, "Heading 3": 3, - "Heading 4": 4, "Heading 5": 5, "Heading 6": 6, - } - - def get_heading_level(para: Any) -> int: - if para.style and para.style.name: - return _HEADING_LEVELS.get(para.style.name, 0) - return 0 - - _LIST_STYLES = { - "Bullet": "bullet", "Number": "number", - } - - def get_list_style(para: Any) -> Optional[str]: - if not para.style or not para.style.name: - return None - style_name = para.style.name - if style_name in _LIST_STYLES: - return _LIST_STYLES[style_name] - if style_name.startswith("List Bullet"): - return "bullet" - if style_name.startswith("List Number"): - return "number" - return None - - def convert_runs_to_markdown(runs: List[Any]) -> str: - result = [] - for run in runs: - text = run.text - if not text: - continue - if run.bold: - text = f"**{text}**" - if run.italic: - text = f"*{text}*" - if run.underline: - text = f"{text}" - result.append(text) - return "".join(result) - - def convert_table_to_markdown(table: Any) -> str: - rows_data = [] - for row in table.rows: - row_data = [] - for cell in row.cells: - cell_text = cell.text.strip().replace("\n", " ") - row_data.append(cell_text) - rows_data.append(row_data) - return build_markdown_table(rows_data) - - markdown_lines = [] - prev_was_list = False - - from docx.table import Table as DocxTable - from docx.text.paragraph import Paragraph - - for element in doc.element.body: - if element.tag.endswith('}p'): - para = Paragraph(element, doc) - text = convert_runs_to_markdown(para.runs) - if not text.strip(): - continue - - heading_level = get_heading_level(para) - if heading_level > 0: - markdown_lines.append(f"{'#' * heading_level} {text}") - prev_was_list = False - else: - list_style = get_list_style(para) - if list_style == "bullet": - if not prev_was_list and markdown_lines: - markdown_lines.append("") - markdown_lines.append(f"- {text}") - prev_was_list = True - elif list_style == "number": - if not prev_was_list and markdown_lines: - markdown_lines.append("") - markdown_lines.append(f"1. {text}") - prev_was_list = True - else: - if prev_was_list and markdown_lines: - markdown_lines.append("") - markdown_lines.append(text) - markdown_lines.append("") - prev_was_list = False - - elif element.tag.endswith('}tbl'): - table = DocxTable(element, doc) - table_md = convert_table_to_markdown(table) - if table_md: - markdown_lines.append(table_md) - markdown_lines.append("") - prev_was_list = False - - content = "\n".join(markdown_lines) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"python-docx 解析失败: {str(e)}" - - -def parse_docx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 XML 原生解析 DOCX 文件""" - word_namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" - namespaces = {"w": word_namespace} - - _STYLE_NAME_TO_HEADING = { - "title": 1, "heading 1": 1, "heading 2": 2, "heading 3": 3, - "heading 4": 4, "heading 5": 5, "heading 6": 6, - } - - def get_heading_level(style_id: Optional[str], style_to_level: dict) -> int: - return style_to_level.get(style_id, 0) - - def get_list_style(style_id: Optional[str], style_to_list: dict) -> Optional[str]: - return style_to_list.get(style_id, None) - - def extract_text_with_formatting(para: Any, namespaces: dict) -> str: - texts = [] - for run in para.findall(".//w:r", namespaces=namespaces): - text_elem = run.find(".//w:t", namespaces=namespaces) - if text_elem is not None and text_elem.text: - text = text_elem.text - bold = run.find(".//w:b", namespaces=namespaces) is not None - italic = run.find(".//w:i", namespaces=namespaces) is not None - if bold: - text = f"**{text}**" - if italic: - text = f"*{text}*" - texts.append(text) - return "".join(texts).strip() - - def convert_table_to_markdown(table_elem: Any, namespaces: dict) -> str: - rows = table_elem.findall(".//w:tr", namespaces=namespaces) - if not rows: - return "" - rows_data = [] - for row in rows: - cells = row.findall(".//w:tc", namespaces=namespaces) - cell_texts = [] - for cell in cells: - cell_text = extract_text_with_formatting(cell, namespaces) - cell_text = cell_text.replace("\n", " ").strip() - cell_texts.append(cell_text if cell_text else "") - if cell_texts: - rows_data.append(cell_texts) - return build_markdown_table(rows_data) - - try: - style_to_level = {} - style_to_list = {} - markdown_lines = [] - - with zipfile.ZipFile(file_path) as zip_file: - try: - styles_file = safe_open_zip(zip_file, "word/styles.xml") - if styles_file: - styles_root = ET.parse(styles_file).getroot() - for style in styles_root.findall( - ".//w:style", namespaces=namespaces - ): - style_id = style.get(f"{{{word_namespace}}}styleId") - style_name_elem = style.find("w:name", namespaces=namespaces) - if style_id and style_name_elem is not None: - style_name = style_name_elem.get(f"{{{word_namespace}}}val") - if style_name: - style_name_lower = style_name.lower() - if style_name_lower in _STYLE_NAME_TO_HEADING: - style_to_level[style_id] = _STYLE_NAME_TO_HEADING[style_name_lower] - elif ( - style_name_lower.startswith("list bullet") - or style_name_lower == "bullet" - ): - style_to_list[style_id] = "bullet" - elif ( - style_name_lower.startswith("list number") - or style_name_lower == "number" - ): - style_to_list[style_id] = "number" - except Exception: - pass - - document_file = safe_open_zip(zip_file, "word/document.xml") - if not document_file: - return None, "document.xml 不存在或无法访问" - - root = ET.parse(document_file).getroot() - body = root.find(".//w:body", namespaces=namespaces) - if body is None: - return None, "document.xml 中未找到 w:body 元素" - - for child in body.findall("./*", namespaces=namespaces): - if child.tag.endswith("}p"): - style_elem = child.find(".//w:pStyle", namespaces=namespaces) - style_id = ( - style_elem.get(f"{{{word_namespace}}}val") - if style_elem is not None - else None - ) - - heading_level = get_heading_level(style_id, style_to_level) - list_style = get_list_style(style_id, style_to_list) - para_text = extract_text_with_formatting(child, namespaces) - - if para_text: - if heading_level > 0: - markdown_lines.append(f"{'#' * heading_level} {para_text}") - elif list_style == "bullet": - markdown_lines.append(f"- {para_text}") - elif list_style == "number": - markdown_lines.append(f"1. {para_text}") - else: - markdown_lines.append(para_text) - markdown_lines.append("") - - elif child.tag.endswith("}tbl"): - table_md = convert_table_to_markdown(child, namespaces) - if table_md: - markdown_lines.append(table_md) - markdown_lines.append("") - - content = "\n".join(markdown_lines) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"XML 解析失败: {str(e)}" diff --git a/skills/lyxy-reader-office/scripts/parser.py b/skills/lyxy-reader-office/scripts/parser.py deleted file mode 100644 index f8c251a..0000000 --- a/skills/lyxy-reader-office/scripts/parser.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -"""文档解析器命令行交互模块,提供命令行接口。支持 DOCX、PPTX、XLSX 和 PDF 文件。""" - -import argparse -import logging -import os -import sys -import warnings - -# 抑制第三方库的进度条和日志,仅保留解析结果输出 -os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1" -os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" -os.environ["TQDM_DISABLE"] = "1" -warnings.filterwarnings("ignore") -logging.disable(logging.WARNING) - -import common -import docx_parser -import pdf_parser -import pptx_parser -import xlsx_parser - - -def main() -> None: - parser = argparse.ArgumentParser( - description="将 DOCX、PPTX、XLSX 或 PDF 文件解析为 Markdown" - ) - - parser.add_argument("file_path", help="DOCX、PPTX、XLSX 或 PDF 文件的绝对路径") - - parser.add_argument( - "-n", - "--context", - type=int, - default=2, - help="与 -s 配合使用,指定每个检索结果包含的前后行数(不包含空行)", - ) - - parser.add_argument( - "--high-res", - action="store_true", - help="PDF 解析时启用 OCR 版面分析(需要额外依赖,处理较慢)", - ) - - group = parser.add_mutually_exclusive_group() - group.add_argument( - "-c", "--count", action="store_true", help="返回解析后的 markdown 文档的总字数" - ) - group.add_argument( - "-l", "--lines", action="store_true", help="返回解析后的 markdown 文档的总行数" - ) - group.add_argument( - "-t", - "--titles", - action="store_true", - help="返回解析后的 markdown 文档的标题行(1-6级)", - ) - group.add_argument( - "-tc", - "--title-content", - help="指定标题名称,输出该标题及其下级内容(不包含#号)", - ) - group.add_argument( - "-s", - "--search", - help="使用正则表达式搜索文档,返回所有匹配结果(用---分隔)", - ) - - args = parser.parse_args() - - if not os.path.exists(args.file_path): - print(f"错误: 文件不存在: {args.file_path}") - sys.exit(1) - - file_type = common.detect_file_type(args.file_path) - if not file_type: - print(f"错误: 不是有效的 DOCX、PPTX、XLSX 或 PDF 格式: {args.file_path}") - sys.exit(1) - - if file_type == "docx": - parsers = [ - ("docling", docx_parser.parse_docx_with_docling), - ("unstructured", docx_parser.parse_docx_with_unstructured), - ("pypandoc-binary", docx_parser.parse_docx_with_pypandoc), - ("MarkItDown", docx_parser.parse_docx_with_markitdown), - ("python-docx", docx_parser.parse_docx_with_python_docx), - ("XML 原生解析", docx_parser.parse_docx_with_xml), - ] - elif file_type == "pptx": - parsers = [ - ("docling", pptx_parser.parse_pptx_with_docling), - ("unstructured", pptx_parser.parse_pptx_with_unstructured), - ("MarkItDown", pptx_parser.parse_pptx_with_markitdown), - ("python-pptx", pptx_parser.parse_pptx_with_python_pptx), - ("XML 原生解析", pptx_parser.parse_pptx_with_xml), - ] - elif file_type == "xlsx": - parsers = [ - ("docling", xlsx_parser.parse_xlsx_with_docling), - ("unstructured", xlsx_parser.parse_xlsx_with_unstructured), - ("MarkItDown", xlsx_parser.parse_xlsx_with_markitdown), - ("pandas", xlsx_parser.parse_xlsx_with_pandas), - ("XML 原生解析", xlsx_parser.parse_xlsx_with_xml), - ] - else: - if args.high_res: - parsers = [ - ("docling OCR", pdf_parser.parse_pdf_with_docling_ocr), - ("unstructured OCR", pdf_parser.parse_pdf_with_unstructured_ocr), - ("docling", pdf_parser.parse_pdf_with_docling), - ("unstructured", pdf_parser.parse_pdf_with_unstructured), - ("MarkItDown", pdf_parser.parse_pdf_with_markitdown), - ("pypdf", pdf_parser.parse_pdf_with_pypdf), - ] - else: - parsers = [ - ("docling", pdf_parser.parse_pdf_with_docling), - ("unstructured", pdf_parser.parse_pdf_with_unstructured), - ("MarkItDown", pdf_parser.parse_pdf_with_markitdown), - ("pypdf", pdf_parser.parse_pdf_with_pypdf), - ] - - failures = [] - content = None - - for parser_name, parser_func in parsers: - content, error = parser_func(args.file_path) - if content is not None: - content = common.remove_markdown_images(content) - content = common.normalize_markdown_whitespace(content) - break - else: - failures.append(f"- {parser_name}: {error}") - - if content is None: - print("所有解析方法均失败:") - for failure in failures: - print(failure) - sys.exit(1) - - if args.count: - print(len(content.replace("\n", ""))) - elif args.lines: - print(len(content.split("\n"))) - elif args.titles: - titles = common.extract_titles(content) - for title in titles: - print(title) - elif args.title_content: - title_content = common.extract_title_content(content, args.title_content) - if title_content is None: - print(f"错误: 未找到标题 '{args.title_content}'") - sys.exit(1) - print(title_content, end="") - elif args.search: - search_result = common.search_markdown(content, args.search, args.context) - if search_result is None: - print(f"错误: 正则表达式无效或未找到匹配: '{args.search}'") - sys.exit(1) - print(search_result, end="") - else: - print(content, end="") - - -if __name__ == "__main__": - main() diff --git a/skills/lyxy-reader-office/scripts/pdf_parser.py b/skills/lyxy-reader-office/scripts/pdf_parser.py deleted file mode 100644 index 392bc76..0000000 --- a/skills/lyxy-reader-office/scripts/pdf_parser.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -"""PDF 文件解析模块,提供多种解析方法。""" - -from typing import Optional, Tuple - -from common import _unstructured_elements_to_markdown, parse_with_markitdown - - -def parse_pdf_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析 PDF 文件(不启用 OCR)""" - try: - from docling.datamodel.base_models import InputFormat - from docling.datamodel.pipeline_options import PdfPipelineOptions - from docling.document_converter import DocumentConverter, PdfFormatOption - except ImportError: - return None, "docling 库未安装" - - try: - converter = DocumentConverter( - format_options={ - InputFormat.PDF: PdfFormatOption( - pipeline_options=PdfPipelineOptions(do_ocr=False) - ) - } - ) - result = converter.convert(file_path) - markdown_content = result.document.export_to_markdown() - if not markdown_content.strip(): - return None, "文档为空" - return markdown_content, None - except Exception as e: - return None, f"docling 解析失败: {str(e)}" - - -def parse_pdf_with_docling_ocr(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析 PDF 文件(启用 OCR)""" - try: - from docling.document_converter import DocumentConverter - except ImportError: - return None, "docling 库未安装" - - try: - converter = DocumentConverter() - result = converter.convert(file_path) - markdown_content = result.document.export_to_markdown() - if not markdown_content.strip(): - return None, "文档为空" - return markdown_content, None - except Exception as e: - return None, f"docling OCR 解析失败: {str(e)}" - - -def parse_pdf_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 PDF 文件(fast 策略)""" - try: - from unstructured.partition.pdf import partition_pdf - except ImportError: - return None, "unstructured 库未安装" - - try: - elements = partition_pdf( - filename=file_path, - infer_table_structure=True, - strategy="fast", - languages=["chi_sim"], - ) - # fast 策略不做版面分析,Title 类型标注不可靠 - content = _unstructured_elements_to_markdown(elements, trust_titles=False) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"unstructured 解析失败: {str(e)}" - - -def parse_pdf_with_unstructured_ocr( - file_path: str, -) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 PDF 文件(hi_res 策略 + PaddleOCR)""" - try: - from unstructured.partition.pdf import partition_pdf - except ImportError: - return None, "unstructured 库未安装" - - try: - from unstructured.partition.utils.constants import OCR_AGENT_PADDLE - except ImportError: - return None, "unstructured-paddleocr 库未安装" - - try: - elements = partition_pdf( - filename=file_path, - infer_table_structure=True, - strategy="hi_res", - languages=["chi_sim"], - ocr_agent=OCR_AGENT_PADDLE, - table_ocr_agent=OCR_AGENT_PADDLE, - ) - content = _unstructured_elements_to_markdown(elements, trust_titles=True) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"unstructured OCR 解析失败: {str(e)}" - - -def parse_pdf_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析 PDF 文件""" - return parse_with_markitdown(file_path) - - -def parse_pdf_with_pypdf(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 pypdf 库解析 PDF 文件""" - try: - from pypdf import PdfReader - except ImportError: - return None, "pypdf 库未安装" - - try: - reader = PdfReader(file_path) - md_content = [] - - for page in reader.pages: - text = page.extract_text(extraction_mode="plain") - if text and text.strip(): - md_content.append(text.strip()) - md_content.append("") - - content = "\n".join(md_content).strip() - if not content: - return None, "文档为空" - return content, None - except Exception as e: - return None, f"pypdf 解析失败: {str(e)}" diff --git a/skills/lyxy-reader-office/scripts/pptx_parser.py b/skills/lyxy-reader-office/scripts/pptx_parser.py deleted file mode 100644 index dce07eb..0000000 --- a/skills/lyxy-reader-office/scripts/pptx_parser.py +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python3 -"""PPTX 文件解析模块,提供三种解析方法。""" - -import re -import xml.etree.ElementTree as ET -import zipfile -from typing import Any, List, Optional, Tuple - -from common import ( - _unstructured_elements_to_markdown, - build_markdown_table, - flush_list_stack, - parse_with_docling, - parse_with_markitdown, -) - - -def parse_pptx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析 PPTX 文件""" - return parse_with_docling(file_path) - - -def parse_pptx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 PPTX 文件""" - try: - from unstructured.partition.pptx import partition_pptx - except ImportError: - return None, "unstructured 库未安装" - - try: - elements = partition_pptx( - filename=file_path, infer_table_structure=True, include_metadata=True - ) - content = _unstructured_elements_to_markdown(elements) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"unstructured 解析失败: {str(e)}" - - -def parse_pptx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析 PPTX 文件""" - return parse_with_markitdown(file_path) - - -def extract_formatted_text_pptx(runs: List[Any]) -> str: - """从 PPTX 文本运行中提取带有格式的文本""" - result = [] - for run in runs: - if not run.text: - continue - - text = run.text - - font = run.font - is_bold = getattr(font, "bold", False) or False - is_italic = getattr(font, "italic", False) or False - - if is_bold and is_italic: - text = f"***{text}***" - elif is_bold: - text = f"**{text}**" - elif is_italic: - text = f"*{text}*" - - result.append(text) - - return "".join(result).strip() - - -def convert_table_to_md_pptx(table: Any) -> str: - """将 PPTX 表格转换为 Markdown 格式""" - rows_data = [] - for row in table.rows: - row_data = [] - for cell in row.cells: - cell_content = [] - for para in cell.text_frame.paragraphs: - text = extract_formatted_text_pptx(para.runs) - if text: - cell_content.append(text) - cell_text = " ".join(cell_content).strip() - row_data.append(cell_text if cell_text else "") - rows_data.append(row_data) - return build_markdown_table(rows_data) - - -def parse_pptx_with_python_pptx(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 python-pptx 库解析 PPTX 文件""" - try: - from pptx import Presentation - from pptx.enum.shapes import MSO_SHAPE_TYPE - except ImportError: - return None, "python-pptx 库未安装" - - _A_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"} - - try: - prs = Presentation(file_path) - md_content = [] - - for slide_num, slide in enumerate(prs.slides, 1): - md_content.append(f"\n## Slide {slide_num}\n") - - list_stack = [] - - for shape in slide.shapes: - if shape.shape_type == MSO_SHAPE_TYPE.PICTURE: - continue - - if hasattr(shape, "has_table") and shape.has_table: - if list_stack: - flush_list_stack(list_stack, md_content) - - table_md = convert_table_to_md_pptx(shape.table) - md_content.append(table_md) - - if hasattr(shape, "text_frame"): - for para in shape.text_frame.paragraphs: - pPr = para._element.pPr - is_list = False - if pPr is not None: - is_list = ( - para.level > 0 - or pPr.find(".//a:buChar", namespaces=_A_NS) is not None - or pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None - ) - - if is_list: - level = para.level - - while len(list_stack) <= level: - list_stack.append("") - - text = extract_formatted_text_pptx(para.runs) - if text: - is_ordered = ( - pPr is not None - and pPr.find(".//a:buAutoNum", namespaces=_A_NS) is not None - ) - marker = "1. " if is_ordered else "- " - indent = " " * level - list_stack[level] = f"{indent}{marker}{text}" - - for i in range(len(list_stack)): - if list_stack[i]: - md_content.append(list_stack[i] + "\n") - list_stack[i] = "" - else: - if list_stack: - flush_list_stack(list_stack, md_content) - - text = extract_formatted_text_pptx(para.runs) - if text: - md_content.append(f"{text}\n") - - if list_stack: - flush_list_stack(list_stack, md_content) - - md_content.append("---\n") - - content = "\n".join(md_content) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"python-pptx 解析失败: {str(e)}" - - -def parse_pptx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 XML 原生解析 PPTX 文件""" - pptx_namespace = { - "a": "http://schemas.openxmlformats.org/drawingml/2006/main", - "p": "http://schemas.openxmlformats.org/presentationml/2006/main", - "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - } - - def extract_text_with_formatting_xml(text_elem: Any, namespaces: dict) -> str: - result = [] - runs = text_elem.findall(".//a:r", namespaces=namespaces) - for run in runs: - t_elem = run.find(".//a:t", namespaces=namespaces) - if t_elem is None or not t_elem.text: - continue - - text = t_elem.text - - rPr = run.find(".//a:rPr", namespaces=namespaces) - is_bold = False - is_italic = False - - if rPr is not None: - is_bold = rPr.find(".//a:b", namespaces=namespaces) is not None - is_italic = rPr.find(".//a:i", namespaces=namespaces) is not None - - if is_bold and is_italic: - text = f"***{text}***" - elif is_bold: - text = f"**{text}**" - elif is_italic: - text = f"*{text}*" - - result.append(text) - - return "".join(result).strip() if result else "" - - def convert_table_to_md_xml(table_elem: Any, namespaces: dict) -> str: - rows = table_elem.findall(".//a:tr", namespaces=namespaces) - if not rows: - return "" - - rows_data = [] - for row in rows: - cells = row.findall(".//a:tc", namespaces=namespaces) - row_data = [] - for cell in cells: - cell_text = extract_text_with_formatting_xml(cell, namespaces) - if cell_text: - cell_text = cell_text.replace("\n", " ").replace("\r", "") - row_data.append(cell_text if cell_text else "") - rows_data.append(row_data) - return build_markdown_table(rows_data) - - def is_list_item_xml(p_elem: Any, namespaces: dict) -> Tuple[bool, bool]: - if p_elem is None: - return False, False - - pPr = p_elem.find(".//a:pPr", namespaces=namespaces) - if pPr is None: - return False, False - - buChar = pPr.find(".//a:buChar", namespaces=namespaces) - if buChar is not None: - return True, False - - buAutoNum = pPr.find(".//a:buAutoNum", namespaces=namespaces) - if buAutoNum is not None: - return True, True - - return False, False - - def get_indent_level_xml(p_elem: Any, namespaces: dict) -> int: - if p_elem is None: - return 0 - - pPr = p_elem.find(".//a:pPr", namespaces=namespaces) - if pPr is None: - return 0 - - lvl = pPr.get("lvl") - return int(lvl) if lvl else 0 - - try: - md_content = [] - - with zipfile.ZipFile(file_path) as zip_file: - slide_files = [ - f - for f in zip_file.namelist() - if re.match(r"ppt/slides/slide\d+\.xml$", f) - ] - slide_files.sort( - key=lambda f: int(re.search(r"slide(\d+)\.xml$", f).group(1)) - ) - - for slide_idx, slide_file in enumerate(slide_files, 1): - md_content.append("\n## Slide {}\n".format(slide_idx)) - - with zip_file.open(slide_file) as slide_xml: - slide_root = ET.parse(slide_xml).getroot() - - tx_bodies = slide_root.findall( - ".//p:sp/p:txBody", namespaces=pptx_namespace - ) - - tables = slide_root.findall(".//a:tbl", namespaces=pptx_namespace) - for table in tables: - table_md = convert_table_to_md_xml(table, pptx_namespace) - if table_md: - md_content.append(table_md) - - for tx_body in tx_bodies: - paragraphs = tx_body.findall( - ".//a:p", namespaces=pptx_namespace - ) - list_stack = [] - - for para in paragraphs: - is_list, is_ordered = is_list_item_xml(para, pptx_namespace) - - if is_list: - level = get_indent_level_xml(para, pptx_namespace) - - while len(list_stack) <= level: - list_stack.append("") - - text = extract_text_with_formatting_xml( - para, pptx_namespace - ) - if text: - marker = "1. " if is_ordered else "- " - indent = " " * level - list_stack[level] = f"{indent}{marker}{text}" - - for i in range(len(list_stack)): - if list_stack[i]: - md_content.append(list_stack[i] + "\n") - list_stack[i] = "" - else: - if list_stack: - flush_list_stack(list_stack, md_content) - - text = extract_text_with_formatting_xml( - para, pptx_namespace - ) - if text: - md_content.append(f"{text}\n") - - if list_stack: - flush_list_stack(list_stack, md_content) - - md_content.append("---\n") - - content = "\n".join(md_content) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"XML 解析失败: {str(e)}" diff --git a/skills/lyxy-reader-office/scripts/xlsx_parser.py b/skills/lyxy-reader-office/scripts/xlsx_parser.py deleted file mode 100644 index 154307b..0000000 --- a/skills/lyxy-reader-office/scripts/xlsx_parser.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -"""XLSX 文件解析模块,提供三种解析方法。""" - -import xml.etree.ElementTree as ET -import zipfile -from typing import List, Optional, Tuple - -from common import _unstructured_elements_to_markdown, parse_with_docling, parse_with_markitdown - - -def parse_xlsx_with_docling(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 docling 库解析 XLSX 文件""" - return parse_with_docling(file_path) - - -def parse_xlsx_with_unstructured(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 unstructured 库解析 XLSX 文件""" - try: - from unstructured.partition.xlsx import partition_xlsx - except ImportError: - return None, "unstructured 库未安装" - - try: - elements = partition_xlsx(filename=file_path, infer_table_structure=True) - content = _unstructured_elements_to_markdown(elements) - if not content.strip(): - return None, "文档为空" - return content, None - except Exception as e: - return None, f"unstructured 解析失败: {str(e)}" - - -def parse_xlsx_with_markitdown(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 MarkItDown 库解析 XLSX 文件""" - return parse_with_markitdown(file_path) - - -def parse_xlsx_with_pandas(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 pandas 库解析 XLSX 文件""" - try: - import pandas as pd - from tabulate import tabulate - except ImportError as e: - missing_lib = "pandas" if "pandas" in str(e) else "tabulate" - return None, f"{missing_lib} 库未安装" - - try: - sheets = pd.read_excel(file_path, sheet_name=None) - - markdown_parts = [] - for sheet_name, df in sheets.items(): - if len(df) == 0: - markdown_parts.append(f"## {sheet_name}\n\n*工作表为空*") - continue - - table_md = tabulate( - df, headers="keys", tablefmt="pipe", showindex=True, missingval="" - ) - markdown_parts.append(f"## {sheet_name}\n\n{table_md}") - - if not markdown_parts: - return None, "Excel 文件为空" - - markdown_content = "# Excel数据转换结果\n\n" + "\n\n".join(markdown_parts) - - return markdown_content, None - except Exception as e: - return None, f"pandas 解析失败: {str(e)}" - - -def parse_xlsx_with_xml(file_path: str) -> Tuple[Optional[str], Optional[str]]: - """使用 XML 原生解析 XLSX 文件""" - xlsx_namespace = { - "main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - } - - def parse_col_index(cell_ref: str) -> int: - col_index = 0 - for char in cell_ref: - if char.isalpha(): - col_index = col_index * 26 + (ord(char) - ord("A") + 1) - else: - break - return col_index - 1 - - def parse_cell_value(cell: ET.Element, shared_strings: List[str]) -> str: - cell_type = cell.attrib.get("t") - - if cell_type == "inlineStr": - is_elem = cell.find("main:is", xlsx_namespace) - if is_elem is not None: - t_elem = is_elem.find("main:t", xlsx_namespace) - if t_elem is not None and t_elem.text: - return t_elem.text.replace("\n", " ").replace("\r", "") - return "" - - cell_value_elem = cell.find("main:v", xlsx_namespace) - if cell_value_elem is None or not cell_value_elem.text: - return "" - - cell_value = cell_value_elem.text - - if cell_type == "s": - try: - idx = int(cell_value) - if 0 <= idx < len(shared_strings): - text = shared_strings[idx] - return text.replace("\n", " ").replace("\r", "") - except (ValueError, IndexError): - pass - return "" - elif cell_type == "b": - return "TRUE" if cell_value == "1" else "FALSE" - elif cell_type == "str": - return cell_value.replace("\n", " ").replace("\r", "") - elif cell_type == "e": - _ERROR_CODES = { - "#NULL!": "空引用错误", - "#DIV/0!": "除零错误", - "#VALUE!": "值类型错误", - "#REF!": "无效引用", - "#NAME?": "名称错误", - "#NUM!": "数值错误", - "#N/A": "值不可用", - } - return _ERROR_CODES.get(cell_value, f"错误: {cell_value}") - elif cell_type == "d": - return f"[日期] {cell_value}" - elif cell_type == "n": - return cell_value - elif cell_type is None: - try: - float_val = float(cell_value) - if float_val.is_integer(): - return str(int(float_val)) - return cell_value - except ValueError: - return cell_value - else: - return cell_value - - def get_non_empty_columns(data: List[List[str]]) -> set: - non_empty_cols = set() - for row in data: - for col_idx, cell in enumerate(row): - if cell and cell.strip(): - non_empty_cols.add(col_idx) - return non_empty_cols - - def filter_columns(row: List[str], non_empty_cols: set) -> List[str]: - return [row[i] if i < len(row) else "" for i in sorted(non_empty_cols)] - - def data_to_markdown(data: List[List[str]], sheet_name: str) -> str: - if not data or not data[0]: - return f"## {sheet_name}\n\n*工作表为空*" - - md_lines = [] - md_lines.append(f"## {sheet_name}") - md_lines.append("") - - headers = data[0] - - non_empty_cols = get_non_empty_columns(data) - - if not non_empty_cols: - return f"## {sheet_name}\n\n*工作表为空*" - - filtered_headers = filter_columns(headers, non_empty_cols) - header_line = "| " + " | ".join(filtered_headers) + " |" - md_lines.append(header_line) - - separator_line = "| " + " | ".join(["---"] * len(filtered_headers)) + " |" - md_lines.append(separator_line) - - for row in data[1:]: - filtered_row = filter_columns(row, non_empty_cols) - row_line = "| " + " | ".join(filtered_row) + " |" - md_lines.append(row_line) - - md_lines.append("") - - return "\n".join(md_lines) - - try: - with zipfile.ZipFile(file_path, "r") as zip_file: - sheet_names = [] - sheet_rids = [] - try: - with zip_file.open("xl/workbook.xml") as f: - root = ET.parse(f).getroot() - rel_ns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - sheet_elements = root.findall(".//main:sheet", xlsx_namespace) - for sheet in sheet_elements: - sheet_name = sheet.attrib.get("name", "") - rid = sheet.attrib.get(f"{{{rel_ns}}}id", "") - if sheet_name: - sheet_names.append(sheet_name) - sheet_rids.append(rid) - except KeyError: - return None, "无法解析工作表名称" - - if not sheet_names: - return None, "未找到工作表" - - rid_to_target = {} - try: - rels_ns = "http://schemas.openxmlformats.org/package/2006/relationships" - with zip_file.open("xl/_rels/workbook.xml.rels") as f: - rels_root = ET.parse(f).getroot() - for rel in rels_root.findall(f"{{{rels_ns}}}Relationship"): - rid = rel.attrib.get("Id", "") - target = rel.attrib.get("Target", "") - if rid and target: - rid_to_target[rid] = target - except KeyError: - pass - - shared_strings = [] - try: - with zip_file.open("xl/sharedStrings.xml") as f: - root = ET.parse(f).getroot() - for si in root.findall(".//main:si", xlsx_namespace): - t_elem = si.find(".//main:t", xlsx_namespace) - if t_elem is not None and t_elem.text: - shared_strings.append(t_elem.text) - else: - shared_strings.append("") - except KeyError: - pass - - markdown_content = "# Excel数据转换结果 (原生XML解析)\n\n" - - for sheet_index, sheet_name in enumerate(sheet_names): - rid = sheet_rids[sheet_index] if sheet_index < len(sheet_rids) else "" - target = rid_to_target.get(rid, "") - if target: - if target.startswith("/"): - worksheet_path = target.lstrip("/") - else: - worksheet_path = f"xl/{target}" - else: - worksheet_path = f"xl/worksheets/sheet{sheet_index + 1}.xml" - - try: - with zip_file.open(worksheet_path) as f: - root = ET.parse(f).getroot() - sheet_data = root.find("main:sheetData", xlsx_namespace) - - rows = [] - if sheet_data is not None: - row_elements = sheet_data.findall( - "main:row", xlsx_namespace - ) - - for row_elem in row_elements: - cells = row_elem.findall("main:c", xlsx_namespace) - - col_dict = {} - for cell in cells: - cell_ref = cell.attrib.get("r", "") - if not cell_ref: - continue - - col_index = parse_col_index(cell_ref) - cell_value = parse_cell_value(cell, shared_strings) - col_dict[col_index] = cell_value - - if col_dict: - max_col = max(col_dict.keys()) - row_data = [ - col_dict.get(i, "") for i in range(max_col + 1) - ] - rows.append(row_data) - - table_md = data_to_markdown(rows, sheet_name) - markdown_content += table_md + "\n\n" - - except KeyError: - markdown_content += f"## {sheet_name}\n\n*工作表解析失败*\n\n" - - if not markdown_content.strip(): - return None, "解析结果为空" - - return markdown_content, None - except Exception as e: - return None, f"XML 解析失败: {str(e)}"