feat: 添加自启动机制,移除 --advice 参数

- 创建 bootstrap.py 承载实际 CLI 逻辑
- 重写 lyxy_document_reader.py 为轻量入口,自动检测依赖并启动
- 使用 subprocess.run() 实现跨平台兼容的自启动
- 移除 --advice 参数及相关测试
- 更新文档和规范,简化使用方式
This commit is contained in:
2026-03-11 23:49:39 +08:00
parent e67ec24dfd
commit 229f17bfee
8 changed files with 252 additions and 202 deletions

111
scripts/bootstrap.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""文档解析器实际执行模块,承载业务逻辑。"""
import argparse
import logging
import os
import sys
import warnings
from pathlib import Path
# 将 scripts/ 目录添加到 sys.path支持从任意位置执行脚本
scripts_dir = Path(__file__).resolve().parent
if str(scripts_dir) not in sys.path:
sys.path.append(str(scripts_dir))
# 抑制第三方库的进度条和日志,仅保留解析结果输出
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
os.environ["TQDM_DISABLE"] = "1"
warnings.filterwarnings("ignore")
# 配置日志系统,只输出 ERROR 级别
logging.basicConfig(level=logging.ERROR, format='%(levelname)s: %(message)s')
# 设置第三方库日志等级
logging.getLogger('docling').setLevel(logging.ERROR)
logging.getLogger('unstructured').setLevel(logging.ERROR)
from core import (
FileDetectionError,
ReaderNotFoundError,
output_result,
parse_input,
process_content,
)
from readers import READERS
def run_normal(args) -> None:
"""正常执行模式:解析文件并输出结果"""
# 实例化所有 readers
readers = [ReaderCls() for ReaderCls in READERS]
try:
content, failures = parse_input(args.input_path, readers)
except FileDetectionError as e:
print(f"错误: {e}")
sys.exit(1)
except ReaderNotFoundError as e:
print(f"错误: {e}")
sys.exit(1)
if content is None:
print("所有解析方法均失败:")
for failure in failures:
print(failure)
sys.exit(1)
# 处理内容
content = process_content(content)
# 输出结果
output_result(content, args)
def main() -> None:
"""主函数:解析命令行参数并执行"""
parser = argparse.ArgumentParser(
description="将 DOCX、XLS、XLSX、PPTX、PDF、HTML 文件或 URL 解析为 Markdown"
)
parser.add_argument("input_path", help="DOCX、XLS、XLSX、PPTX、PDF、HTML 文件或 URL")
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()
run_normal(args)
if __name__ == "__main__":
main()

View File

@@ -1,56 +1,31 @@
#!/usr/bin/env python3
"""文档解析器命令行交互模块,提供命令行接口。支持 DOCX、XLS、XLSX、PPTX、PDF、HTML 和 URL。"""
"""文档解析器入口 - 环境检测和自启动"""
import argparse
import logging
import os
import shutil
import subprocess
import sys
import warnings
from pathlib import Path
# 将 scripts/ 目录添加到 sys.path,支持从任意位置执行脚本
# 将 scripts/ 目录添加到 sys.path
scripts_dir = Path(__file__).resolve().parent
if str(scripts_dir) not in sys.path:
sys.path.append(str(scripts_dir))
# 抑制第三方库的进度条和日志,仅保留解析结果输出
# 抑制第三方库日志
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
os.environ["TQDM_DISABLE"] = "1"
warnings.filterwarnings("ignore")
# 配置日志系统,只输出 ERROR 级别
logging.basicConfig(level=logging.ERROR, format='%(levelname)s: %(message)s')
# 设置第三方库日志等级
logging.getLogger('docling').setLevel(logging.ERROR)
logging.getLogger('unstructured').setLevel(logging.ERROR)
from core import (
FileDetectionError,
ReaderNotFoundError,
output_result,
parse_input,
process_content,
generate_advice,
)
from readers import READERS
def main() -> None:
def main():
"""主函数:环境检测和决策"""
# 解析命令行参数(轻量,仅识别必要参数)
parser = argparse.ArgumentParser(
description="将 DOCX、XLS、XLSX、PPTX、PDF、HTML 文件或 URL 解析为 Markdown"
)
parser.add_argument("input_path", help="DOCX、XLS、XLSX、PPTX、PDF、HTML 文件或 URL")
parser.add_argument(
"-a",
"--advice",
action="store_true",
help="仅显示执行建议,不实际解析文件",
)
parser.add_argument(
"-n",
"--context",
@@ -58,7 +33,6 @@ def main() -> None:
default=2,
help="与 -s 配合使用,指定每个检索结果包含的前后行数(不包含空行)",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-c", "--count", action="store_true", help="返回解析后的 markdown 文档的总字数"
@@ -85,39 +59,64 @@ def main() -> None:
args = parser.parse_args()
# 实例化所有 readers
readers = [ReaderCls() for ReaderCls in READERS]
# 检测 uv 是否可用
uv_path = shutil.which("uv")
# --advice 模式:仅显示建议,不解析
if args.advice:
advice = generate_advice(args.input_path, readers, "scripts/lyxy_document_reader.py")
if advice:
print(advice)
else:
print(f"错误: 无法识别文件类型: {args.input_path}")
sys.exit(1)
if not uv_path:
# uv 不可用,降级为直接执行 bootstrap.py
import bootstrap
bootstrap.run_normal(args)
return
try:
content, failures = parse_input(args.input_path, readers)
except FileDetectionError as e:
print(f"错误: {e}")
sys.exit(1)
except ReaderNotFoundError as e:
print(f"错误: {e}")
sys.exit(1)
# uv 可用,需要自启动
# 导入依赖检测模块
from config import DEPENDENCIES
from core.advice_generator import (
detect_file_type_light,
get_platform,
get_dependencies,
)
from readers import READERS
if content is None:
print("所有解析方法均失败:")
for failure in failures:
print(failure)
sys.exit(1)
# 检测文件类型
readers = [ReaderCls() for ReaderCls in READERS]
reader_cls = detect_file_type_light(args.input_path, readers)
# 处理内容
content = process_content(content)
if not reader_cls:
# 无法识别文件类型,降级执行让它报错
import bootstrap
bootstrap.run_normal(args)
return
# 输出结果
output_result(content, args)
# 获取平台和依赖配置
platform_id = get_platform()
python_version, dependencies = get_dependencies(reader_cls, platform_id)
# 生成 uv 命令参数列表
uv_args = ["uv", "run"]
if python_version:
uv_args.extend(["--python", python_version])
# 始终添加 pyarmor 依赖(混淆后脚本需要)
uv_args.extend(["--with", "pyarmor"])
for dep in dependencies:
uv_args.extend(["--with", dep])
# 目标脚本是 bootstrap.py
uv_args.append("scripts/bootstrap.py")
# 添加所有命令行参数
uv_args.extend(sys.argv[1:])
# 设置环境变量
env = os.environ.copy()
env["PYTHONPATH"] = "."
# 自启动:使用 subprocess 替代 execvpeWindows 兼容)
result = subprocess.run(uv_args, env=env)
sys.exit(result.returncode)
if __name__ == "__main__":