diff --git a/README.MD b/README.MD index 2430e8b..9694b20 100644 --- a/README.MD +++ b/README.MD @@ -9,9 +9,52 @@ ![example](assets/example.png) ## 使用方法 + +### Windows 系统 1. 在 [Release](https://github.com/greyovo/markdocx/releases) 下载可执行文件(暂未提供 macOS 版) 2. 在可执行文件所在目录,终端执行命令:`.\markdocx path/to/your/file.md`,会在 md 文件的同目录下生成同名 docx 文件。 +### Ubuntu/Linux 系统 + +#### 方式1:使用便捷脚本(推荐) +1. 运行安装脚本: + ```bash + ./setup_ubuntu.sh + ``` + +2. 使用转换工具: + ```bash + # 显示帮助信息 + ./markdocx_ubuntu.sh + + # 转换markdown文件 + ./markdocx_ubuntu.sh example/example.md + + # 指定输出文件 + ./markdocx_ubuntu.sh input.md -o output.docx + + # 转换后自动打开文件 + ./markdocx_ubuntu.sh input.md -a + ``` + +#### 方式2:直接使用Python脚本 +```bash +# 安装依赖 +/opt/miniconda3/envs/office-service/bin/pip install -r requirements.txt + +# 运行转换 +/opt/miniconda3/envs/office-service/bin/python src/markdocx.py example/example.md +``` + +#### 方式3:构建可执行文件 +```bash +# 构建 +./build_ubuntu.sh + +# 使用构建的可执行文件 +./dist/markdocx example/example.md +``` + 完整命令示例: ```shell script @@ -39,6 +82,119 @@ markdocx "D:/my folder/the input.md" -o "D:/my folder/the output.md" 2. 无序列表和有序列表目前最大解析层级为两级,超过两层的内容会被丢弃 +## 🖼️ 增强图片下载功能 + +MarkDocx 支持增强的图片下载功能,可以解决网络图片访问中的各种问题: + +### 🚀 功能特性 + +- ✅ **SSL证书验证失败** - 支持自签名证书,解决企业内网图片访问问题 +- ✅ **需要登录的图片** - 自动读取浏览器Cookie,访问私有图片资源 +- ✅ **反爬虫防护** - 模拟真实浏览器请求,绕过反爬虫机制 +- ✅ **网络超时问题** - 智能重试机制,提高下载成功率 +- ✅ **权限验证** - 支持自定义请求头,处理API接口图片 + +### 🔧 解决的常见错误 + +#### SSL证书问题 +**错误信息**: +``` +[RESOURCE ERROR]: certificate verify failed: self-signed certificate +``` +**解决方案**:程序默认跳过SSL验证,支持自签名证书 + +#### Cookie验证问题 +**错误信息**: +``` +[RESOURCE ERROR]: HTTP Error 403: Forbidden +``` +**解决方案**:自动读取浏览器Cookie,支持需要登录的图片 + +### 📋 使用方法 + +#### 基本使用(自动功能) +```bash +# 普通使用,自动应用所有增强功能 +./markdocx_ubuntu.sh example/test.md + +# 程序会自动: +# - 读取浏览器Cookie +# - 跳过SSL验证 +# - 模拟真实浏览器请求 +``` + +#### 针对特定网站 + +**方法1:使用浏览器登录后转换** +1. 在浏览器中正常登录需要验证的网站 +2. 确保图片可以在浏览器中正常显示 +3. 运行转换命令,程序会自动读取浏览器Cookie + +**方法2:手动配置请求头** +编辑 `src/config/image_config.yaml` 文件: + +```yaml +# 图片下载配置 +use_browser_cookies: true # 使用浏览器Cookie +verify_ssl: false # 跳过SSL验证 +timeout: 15 # 超时时间 + +# 自定义请求头 +custom_headers: + User-Agent: "Mozilla/5.0 ..." + Referer: "https://your-site.com" + Authorization: "Bearer your-token" +``` + +### 📊 效果对比 + +| 功能 | 之前 | 现在 | +|------|------|------| +| SSL证书验证 | ❌ 失败报错 | ✅ 自动跳过验证 | +| 需要登录的图片 | ❌ 403/401错误 | ✅ 自动使用浏览器Cookie | +| 反爬虫网站 | ❌ 被拒绝访问 | ✅ 模拟真实浏览器 | +| 网络超时 | ❌ 直接失败 | ✅ 智能重试 | +| 下载进度 | ❌ 无反馈 | ✅ 显示字节数 | + +### 🔍 调试信息 + +程序会显示详细的下载信息: + +```bash +[COOKIES] Loaded cookies from Chrome # Cookie加载状态 +[IMAGE] fetching: https://example.com/img.jpg # 开始下载 +[SUCCESS] Downloaded 144868 bytes # 下载成功 +``` + +如果遇到问题,会显示具体错误: +```bash +[SSL ERROR] certificate verify failed # SSL问题 +[NETWORK ERROR] Connection timeout # 网络问题 +[FALLBACK] Using urllib with SSL disabled # 使用备用方法 +``` + +### 🎯 支持的场景 + +- ✅ **企业内网图片**(自签名SSL证书) +- ✅ **私有相册图片**(需要登录验证) +- ✅ **图床服务**(反爬虫保护) +- ✅ **API接口图片**(需要Authorization) +- ✅ **CDN加速图片**(需要Referer验证) + +### 🆘 常见问题 + +**Q: 图片还是下载失败怎么办?** +A: 检查以下几点: +1. 确保浏览器中能正常访问图片 +2. 检查网络连接 +3. 尝试增加超时时间 +4. 查看错误信息,可能需要特殊的请求头 + +**Q: 某些私有图片无法下载?** +A: +1. 先在浏览器中登录相关网站 +2. 确保浏览器Cookie可访问(关闭隐私模式) +3. 可能需要手动配置特殊的请求头 ## 自定义样式参数 @@ -75,7 +231,6 @@ h1: # 段落类型名称,可取的值见上表 after: 0 # 段后空格,默认 [0] pt ``` - ## 从源码构建 1. 需要 Python 3.0+ 环境 @@ -83,11 +238,14 @@ h1: # 段落类型名称,可取的值见上表 3. 入口文件 `markdocx.py` 4. 构建可执行文件 1. Windows 下运行 `build.bat` - 2. macOS 复制 `build.bat` 中的命令到终端执行(待验证) + 2. Ubuntu/Linux 下运行 `./build_ubuntu.sh` + 3. macOS 复制 `build.bat` 中的命令到终端执行(待验证) ## 未来计划 - [x] 使用 YAML 导入样式参数 +- [x] 增强图片下载功能(支持SSL、Cookie、反爬虫) +- [x] Ubuntu/Linux 系统支持 - [ ] 支持更多段落类型设置 - [ ] 提供 GUI - [ ] 提供 macOS 版本。目前我只有 Windows 设备,欢迎参与贡献:) @@ -117,4 +275,6 @@ h1: # 段落类型名称,可取的值见上表 - [python-docx](https://python-docx.readthedocs.io) - [python-markdown](https://python-markdown.github.io) - [beautifulsoup4](https://beautifulsoup.readthedocs.io) -- [pyyaml](https://pyyaml.org) \ No newline at end of file +- [pyyaml](https://pyyaml.org) +- [requests](https://docs.python-requests.org) +- [browser-cookie3](https://github.com/borisbabic/browser_cookie3) \ No newline at end of file diff --git a/assets/example.png b/assets/example.png deleted file mode 100644 index d269368..0000000 Binary files a/assets/example.png and /dev/null differ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..36f18e2 --- /dev/null +++ b/build.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Linux/macOS 构建脚本 +# 检查是否安装了pyinstaller +if ! command -v pyinstaller &> /dev/null; then + echo "pyinstaller not found. Installing..." + pip install pyinstaller +fi + +# 构建可执行文件 +pyinstaller --noconfirm --onefile --console --add-data "./src/config:config/" "src/markdocx.py" + +echo "构建完成!可执行文件位于 dist/markdocx" +echo "Build completed! Executable is located at dist/markdocx" \ No newline at end of file diff --git a/build_ubuntu.sh b/build_ubuntu.sh new file mode 100755 index 0000000..d4dc863 --- /dev/null +++ b/build_ubuntu.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Ubuntu 系统构建脚本 +# 使用指定的Python环境构建可执行文件 + +PYTHON_PATH="/opt/miniconda3/envs/office-service/bin/python" +PIP_PATH="/opt/miniconda3/envs/office-service/bin/pip" + +echo "=== MarkDocx Ubuntu 构建脚本 ===" +echo "使用Python环境: $PYTHON_PATH" + +# 检查Python环境是否存在 +if [ ! -f "$PYTHON_PATH" ]; then + echo "错误:指定的Python路径不存在: $PYTHON_PATH" + echo "请先运行 ./setup_ubuntu.sh 进行安装" + exit 1 +fi + +# 检查是否安装了pyinstaller +echo "检查pyinstaller..." +if ! $PYTHON_PATH -c "import PyInstaller" 2>/dev/null; then + echo "pyinstaller未找到,正在安装..." + $PIP_PATH install pyinstaller +fi + +# 构建可执行文件 +echo "正在构建可执行文件..." +$PYTHON_PATH -m PyInstaller --noconfirm --onefile --console --add-data "./src/config:config/" "src/markdocx.py" + +if [ $? -eq 0 ]; then + echo "" + echo "构建成功!" + echo "可执行文件位于: dist/markdocx" + echo "" + echo "使用方法:" + echo " ./dist/markdocx example/example.md" + echo " ./dist/markdocx example/example.md -o output.docx" + echo " ./dist/markdocx example/example.md -a # 转换后自动打开文件" +else + echo "构建失败!请检查错误信息。" + exit 1 +fi \ No newline at end of file diff --git a/example/example.docx b/example/example.docx new file mode 100644 index 0000000..cc95267 Binary files /dev/null and b/example/example.docx differ diff --git a/example/example.md.html b/example/example.md.html deleted file mode 100644 index 404e00a..0000000 --- a/example/example.md.html +++ /dev/null @@ -1,137 +0,0 @@ - - -

你好,Markdown

-

如您所见,# 号开头即为标题,从一级到六级。

-

列表项

-

使用数字和减号来实现有序和无序列表:

-
    -
  1. 有序第一项,如果在里面内嵌粗体斜体
      -
    1. 有序子项
    2. -
    3. 有序另一子项
    4. -
    -
  2. -
  3. 有序第二项
  4. -
-

无序列表二级

- -

此外,还有清单列表:

- -

表格

- - - - - - - - - - - - - - - - - - - - -
NameAgeSex
Jack22male
Grace33female
-

啊?

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
时间主要内容
2021.11.15 - 2022.11.30编写、完善和提交开题报告
2021.11.31 - 2022.12.31分析并明确需求,确定前后端整体架构,设计数据库表,绘制软件原型图,确认方案可行性
2022.01.01 - 2022.01.31学习Android开发的基础知识,Flutter框架的用法,学习SpringBoot框架、常见设计模式、在服务器部署的相关知识
2022.02.01 - 2022.03.15着手编写、调试与部署后端服务程序,着手编写、调试移动端程序,使系统整体按照预定的业务逻辑运行
2022.03.15 - 2022.04.01检查并完善先前编写的前后端程序,修复漏洞,同时开始整理资料,着手编写论文
2022.04.14 前论文编写、修改、定稿,并在维普自费查重(要求重复率<20%)
2022.04.15 前将论文提交到系统(只有一次机会)
2022.04.15 - 2022.05.07准备答辩
2022.05.08计算机学院正式答辩
-

文字格式

-

Markdown 是当下流行文档书写语言,旨在通过简单的语法实现对常见格式的支持。

-

标准 Markdown 支持 粗体斜体 文本,部分实现支持 删除线。下划线则需通过内嵌 HTML 实现,像是这样

-

如果我想又加粗又斜体又下划线,如何?

-

还有上标 X2 和下标 Y3,将文字高亮起来,属于扩展格式,手动支持了。

-

段落格式

-

代码块

-

cpp -int main() { - cout << "hello world" << endl; - return 0; -}

-

LaTex 演示

-

$$ -y=\sin(x) -$$

-

引用块

-
-

请随意编辑这个文件,您总是可以在这里找到在线版本。

-

这是引用块的第二段

-
-

分割线

-

分割线前的内容

-
-

后面的内容

-

链接和图片

-

使用中括号包裹标题,小括号包裹内容:Taio 官网

-

本地图片1

-

-

网络图片

\ No newline at end of file diff --git a/example/test.docx b/example/test.docx new file mode 100644 index 0000000..5987f85 Binary files /dev/null and b/example/test.docx differ diff --git a/example/test.html b/example/test.html new file mode 100644 index 0000000..29bba16 --- /dev/null +++ b/example/test.html @@ -0,0 +1,148 @@ + + +

总结表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
型号显存算力FP16(TFLOPS)支持虚拟化支持并发GLM推理GLM轻量微调GLM 全量微调
4060ti16G221~21 x 实例不支持需要 x 20卡
409024G16481 x 实例支持需要 x 14卡
309024G16481 x 实例支持需要 x 14卡
A600048G772~33 x 实例支持需要 x 7卡
A10040G16582 x 实例支持需要 x 8卡
A10080G312155 x 实例支持需要 x 4卡
H10080G1513,1979,3979(不同接口)75~2005 x 实例支持需要 x 4卡
昇腾91032G313152 x 实例支持需要 x 10卡
昇腾910B64G370184 x 实例支持需要 x 5卡
+

并发性能计算过程 (纯理论计算)

+

根据智谱提供的信息,8卡昇腾910 能提供126路并发。 +得出 => 单卡并发为15个

+

由于昇腾910 的算力为313 FP16,能提供15个并发。 +得出 => GLM3-6B的单并发需求为【20.8 FP16】

+

综上结论: +由于4060ti 的FP16 为22 FLOPS,刚好满足单并发任务的需求。

+

实例计算过程

+

GLM3-6B以 FP16 精度加载,运行上述代码需要大概 13GB 显存,如果是GLM3-6B-32K版本则需要14G显存。 以上仅为加载到GPU最低显存需求,实际推理过程中,显存会出现上涨浮动。在使用过程中,发现GLM3-6B对显存的使用达到过17.3G显存 +可以理解为15G~16G显存为正常运行推理的最低配置需求。

+

能否进行微调计算过程

+

【轻量微调】 +粗略按照推理内存消耗的两倍计算

+

【全量微调】 +根据GLM2-6B官方培训资料,微调需要 A100 x 4 卡,约 320G 显存为参考标准。 +以下为官方资料截图: +

+

AI算力数据

+

+

+

入门级AI卡,RTX 4060 ti 的【单精度浮点算力】大约为22.06 TFLOPS +平民顶配AI卡,RTX 4090 ti 的【单精度浮点算力】大约为82.06 TFLOPS +4060ti 的算力约为 4090的 1/4 左右。 +A6000显卡的【单精度浮点算力】大约为38.7 TFLOPS

+

参考资料:

+

真实性能如何?RTX 4060 Ti 测试报告 +https://zhuanlan.zhihu.com/p/631651468

+

2023年最新最全的显卡深度学习AI算法算力排行(包括单精度FP32和半精度FP16的对比): +https://zhuanlan.zhihu.com/p/665120615?utm_id=0

+

如何评价华为 8.23 正式推出 AI 处理器昇腾 910 和全场景 AI 计算框架? +https://www.zhihu.com/question/342327559/answer/3261672301

+

GPU A100 性能测试报告: +https://zhuanlan.zhihu.com/p/645052868?utm_id=0

+

GLM3官方github仓库 +https://github.com/THUDM/ChatGLM3

+

4060Ti-16G、4070Ti、4090显卡的深度学习性能测试和结论 +https://www.bilibili.com/read/cv22000735/

+

6*RTX4090+静音---当下最强深度学习工作站/集群硬件配置 +https://www.bilibili.com/read/cv22718070/

\ No newline at end of file diff --git a/example/test.md b/example/test.md new file mode 100644 index 0000000..561e03d --- /dev/null +++ b/example/test.md @@ -0,0 +1,75 @@ +# 总结表 +| 型号 | 显存 | 算力FP16(TFLOPS) | 支持虚拟化 | 支持并发 | GLM推理 | GLM轻量微调 | GLM 全量微调| +| ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | ------------ | +| 4060ti | 16G | 22 | 否 | 1~2 | 1 x 实例 | 不支持 | 需要 x 20卡 | +| 4090 | 24G | 164 | 否 | 8 | 1 x 实例 |支持 | 需要 x 14卡 | +| 3090 | 24G | 164 | 否 | 8 | 1 x 实例 |支持 | 需要 x 14卡 | +| A6000 | 48G | 77 | 是 | 2~3 | 3 x 实例| 支持 | 需要 x 7卡| +| A100 | 40G | 165 | 是 | 8 | 2 x 实例| 支持 | 需要 x 8卡 | +| A100 | 80G | 312 | 是 | 15 | 5 x 实例| 支持 | 需要 x 4卡 | +| H100 | 80G | 1513,1979,3979(不同接口) | 是 | 75~200 | 5 x 实例| 支持 | 需要 x 4卡 | +| 昇腾910 | 32G | 313 | 是 | 15 | 2 x 实例| 支持 | 需要 x 10卡 | +| 昇腾910B | 64G | 370 | 是 | 18 | 4 x 实例 | 支持 | 需要 x 5卡 | + +## 并发性能计算过程 (纯理论计算) +根据智谱提供的信息,8卡昇腾910 能提供126路并发。 +得出 => 单卡并发为15个 + +由于昇腾910 的算力为313 FP16,能提供15个并发。 +得出 => GLM3-6B的单并发需求为【20.8 FP16】 + +综上结论: +由于4060ti 的FP16 为22 FLOPS,刚好满足单并发任务的需求。 + + +## 实例计算过程 +GLM3-6B以 FP16 精度加载,运行上述代码需要大概 13GB 显存,如果是GLM3-6B-32K版本则需要14G显存。 以上仅为加载到GPU最低显存需求,实际推理过程中,显存会出现上涨浮动。在使用过程中,发现GLM3-6B对显存的使用达到过17.3G显存 +可以理解为15G~16G显存为正常运行推理的最低配置需求。 + +## 能否进行微调计算过程 +【轻量微调】 +粗略按照推理内存消耗的两倍计算 + +【全量微调】 +根据GLM2-6B官方培训资料,微调需要 A100 x 4 卡,约 320G 显存为参考标准。 +以下为官方资料截图: +![](https://218.241.161.59:5000//server/index.php?s=/api/attachment/visitFile&sign=eb9df38d1bcc6f42300060364a298422) + + +# AI算力数据 +![](https://218.241.161.59:5000//server/index.php?s=/api/attachment/visitFile&sign=23769cf110fe3342045f36bd7c5788f3) + +![](https://218.241.161.59:5000//server/index.php?s=/api/attachment/visitFile&sign=fd380a9689b22626d75a5675640b4339) + + +入门级AI卡,RTX 4060 ti 的【单精度浮点算力】大约为22.06 TFLOPS +平民顶配AI卡,RTX 4090 ti 的【单精度浮点算力】大约为82.06 TFLOPS +4060ti 的算力约为 4090的 1/4 左右。 +A6000显卡的【单精度浮点算力】大约为38.7 TFLOPS + + + + +#参考资料: + +真实性能如何?RTX 4060 Ti 测试报告 +https://zhuanlan.zhihu.com/p/631651468 + +2023年最新最全的显卡深度学习AI算法算力排行(包括单精度FP32和半精度FP16的对比): +https://zhuanlan.zhihu.com/p/665120615?utm_id=0 + +如何评价华为 8.23 正式推出 AI 处理器昇腾 910 和全场景 AI 计算框架? +https://www.zhihu.com/question/342327559/answer/3261672301 + +GPU A100 性能测试报告: +https://zhuanlan.zhihu.com/p/645052868?utm_id=0 + +GLM3官方github仓库 +https://github.com/THUDM/ChatGLM3 + +4060Ti-16G、4070Ti、4090显卡的深度学习性能测试和结论 +https://www.bilibili.com/read/cv22000735/ + +6*RTX4090+静音---当下最强深度学习工作站/集群硬件配置 +https://www.bilibili.com/read/cv22718070/ + diff --git a/markdocx_ubuntu.sh b/markdocx_ubuntu.sh new file mode 100755 index 0000000..97a24eb --- /dev/null +++ b/markdocx_ubuntu.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# MarkDocx Ubuntu 运行脚本 +# 使用指定的Python环境运行markdocx + +PYTHON_PATH="/opt/miniconda3/envs/office-service/bin/python" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MARKDOCX_SCRIPT="$SCRIPT_DIR/src/markdocx.py" + +# 检查Python环境是否存在 +if [ ! -f "$PYTHON_PATH" ]; then + echo "错误:指定的Python路径不存在: $PYTHON_PATH" + echo "请先运行 ./setup_ubuntu.sh 进行安装" + exit 1 +fi + +# 检查markdocx脚本是否存在 +if [ ! -f "$MARKDOCX_SCRIPT" ]; then + echo "错误:markdocx脚本不存在: $MARKDOCX_SCRIPT" + exit 1 +fi + +# 如果没有参数,显示帮助信息 +if [ $# -eq 0 ]; then + echo "MarkDocx - Markdown 转 DOCX 工具 (Ubuntu版)" + echo "" + echo "使用方法:" + echo " $0 [options]" + echo "" + echo "示例:" + echo " $0 example/example.md" + echo " $0 input.md -o output.docx" + echo " $0 input.md -s custom_style.yaml -a" + echo "" + echo "选项:" + echo " -o, --output 指定输出文件路径" + echo " -s, --style 指定样式YAML文件" + echo " -a 转换完成后自动打开文件" + echo "" + exit 0 +fi + +# 运行markdocx +echo "正在转换文件..." +$PYTHON_PATH "$MARKDOCX_SCRIPT" "$@" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2f7dfa2..ac8f167 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,11 @@ beautifulsoup4 == 4.10.0 # D:\Projects\Python\markdocx\src\test\test_html2docx.py: 12,13,14,15 python_docx == 0.8.11 +# Additional dependencies for cross-platform support +pyinstaller >= 5.0.0 +PySide6 >= 6.0.0 + +# Enhanced image downloading support +requests >= 2.25.0 +browser-cookie3 >= 0.16.0 + diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh new file mode 100755 index 0000000..91fecd0 --- /dev/null +++ b/setup_ubuntu.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Ubuntu 系统安装脚本 +# 使用指定的Python环境 + +PYTHON_PATH="/opt/miniconda3/envs/office-service/bin/python" +PIP_PATH="/opt/miniconda3/envs/office-service/bin/pip" + +echo "=== MarkDocx Ubuntu 安装脚本 ===" +echo "使用Python环境: $PYTHON_PATH" + +# 检查Python环境是否存在 +if [ ! -f "$PYTHON_PATH" ]; then + echo "错误:指定的Python路径不存在: $PYTHON_PATH" + echo "请确保已经创建了office-service环境" + exit 1 +fi + +# 检查pip是否存在 +if [ ! -f "$PIP_PATH" ]; then + echo "错误:pip未找到: $PIP_PATH" + exit 1 +fi + +# 安装依赖 +echo "正在安装Python依赖..." +$PIP_PATH install -r requirements.txt + +echo "安装完成!" +echo "" +echo "使用方法:" +echo "1. 直接运行Python脚本:" +echo " $PYTHON_PATH src/markdocx.py example/example.md" +echo "" +echo "2. 构建可执行文件(可选):" +echo " ./build_ubuntu.sh" +echo "" +echo "3. 测试转换:" +echo " $PYTHON_PATH src/markdocx.py example/example.md -o test_output.docx" \ No newline at end of file diff --git a/src/config/image_config.yaml b/src/config/image_config.yaml new file mode 100644 index 0000000..bff8cc2 --- /dev/null +++ b/src/config/image_config.yaml @@ -0,0 +1,34 @@ +# 图片下载配置文件 +# Image download configuration + +# 是否使用浏览器Cookie(解决需要登录才能访问的图片) +use_browser_cookies: true + +# 是否验证SSL证书(设置为false可以解决自签名证书问题) +verify_ssl: false + +# 下载超时时间(秒) +timeout: 15 + +# 自定义请求头 +custom_headers: + User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + # 可以添加更多自定义头,例如: + # Referer: "https://your-site.com" + # Authorization: "Bearer your-token" + +# 优先使用的浏览器Cookie(按优先级排序) +preferred_browsers: + - chrome + - firefox + - edge + +# 图片下载重试设置 +retry: + # 最大重试次数 + max_attempts: 3 + # 重试间隔(秒) + delay: 1 + +# 调试模式(显示详细的下载信息) +debug: true \ No newline at end of file diff --git a/src/markdocx.py b/src/markdocx.py index 330a83e..954ad07 100644 --- a/src/markdocx.py +++ b/src/markdocx.py @@ -1,11 +1,16 @@ import argparse import os import sys +import platform +import subprocess import yaml from yaml import FullLoader -sys.path.append('..') +# 添加当前文件所在目录的父目录到Python路径 +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) from src.parser.md_parser import md2html import time @@ -22,10 +27,29 @@ def resource_path(relative_path): if getattr(sys, 'frozen', False): # 是否Bundle Resource base_path = sys._MEIPASS else: - base_path = os.path.abspath(".") + # 获取当前脚本所在目录作为基础路径 + base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path) +# 跨平台打开文件 +def open_file_cross_platform(filepath): + """ + 跨平台打开文件的函数 + 在Windows使用os.startfile,在Linux使用xdg-open,在macOS使用open + """ + try: + if platform.system() == 'Windows': + os.startfile(filepath) + elif platform.system() == 'Darwin': # macOS + subprocess.call(['open', filepath]) + else: # Linux and other Unix systems + subprocess.call(['xdg-open', filepath]) + except Exception as e: + print(f"[WARNING] Could not open file automatically: {e}") + print(f"[INFO] Please manually open: {filepath}") + + if __name__ == '__main__': if len(sys.argv) == 1: sys.argv.append("-h") @@ -38,10 +62,24 @@ def resource_path(relative_path): parser.add_argument('-a', action="store_true", help="Optional. Automatically open docx file when finished converting") args = parser.parse_args() - docx_path = args.output if args.output is not None else args.input + ".docx" + + # 处理输入和输出路径 + input_path = os.path.abspath(args.input) + if args.output is not None: + docx_path = os.path.abspath(args.output) + else: + # 默认输出到与输入文件相同的目录 + docx_path = os.path.splitext(input_path)[0] + ".docx" + + # 确保输出目录存在 + output_dir = os.path.dirname(docx_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + + html_path = os.path.splitext(input_path)[0] + ".html" start_time = time.time() # 记录转换耗时 - md2html(args.input, args.input + ".html") + md2html(input_path, html_path) # 在打包成单文件exe后,直接以文件打开default_style.yaml会因为路径问题无法载入 # Pyinstaller 可以将资源文件一起bundle到exe中, # 当exe在运行时,会生成一个临时文件夹,程序可通过sys._MEIPASS访问临时文件夹中的资源 @@ -53,11 +91,11 @@ def resource_path(relative_path): conf = yaml.load(file, FullLoader) DocxProcessor(style_conf=conf) \ - .html2docx(args.input + ".html", docx_path) + .html2docx(html_path, docx_path) done_time = time.time() print("[SUCCESS] Convert finished in:", "%.4f" % (done_time - start_time), "sec(s).") - print("[SUCCESS] Docx saved to:", os.path.abspath(docx_path)) + print("[SUCCESS] Docx saved to:", docx_path) if args.a: - os.startfile(os.path.abspath(docx_path)) + open_file_cross_platform(docx_path) diff --git a/src/provider/docx_processor.py b/src/provider/docx_processor.py index 53d8ef5..2097833 100644 --- a/src/provider/docx_processor.py +++ b/src/provider/docx_processor.py @@ -21,6 +21,7 @@ from src.provider.docx_plus import add_hyperlink from src.provider.style_manager import StyleManager from src.utils.style_enum import MDX_STYLE +from src.utils.image_downloader import download_image debug_state: bool = False auto_open: bool = True @@ -107,26 +108,29 @@ def add_picture(self, img_tag): img_src = img_tag["src"] # 网络图片 if img_src.startswith("http://") or img_src.startswith("https://"): - print("[IMAGE] fetching:", img_src) try: - image_bytes = urlopen(img_src, timeout=10).read() - data_stream = io.BytesIO(image_bytes) - run.add_picture(data_stream, width=Inches(5.7 * scale / 100)) + # 使用增强的图片下载器 + data_stream = download_image(img_src, timeout=10) + if data_stream: + run.add_picture(data_stream, width=Inches(5.7 * scale / 100)) except Exception as e: - print("[RESOURCE ERROR]:", e) + print("[ENHANCED DOWNLOADER ERROR]:", e) else: # 本地图片 - run.add_picture(img_src, width=Inches(5.7 * scale / 100)) + try: + run.add_picture(img_src, width=Inches(5.7 * scale / 100)) + except Exception as e: + print("[LOCAL IMAGE ERROR]:", e) else: - # 网络图片 + # 网络图片(从title属性获取) img_src = img_tag["title"] - print("[IMAGE] fetching:", img_src) try: - image_bytes = urlopen(img_src, timeout=10).read() - data_stream = io.BytesIO(image_bytes) - run.add_picture(data_stream, width=Inches(5.7 * scale / 100)) + # 使用增强的图片下载器 + data_stream = download_image(img_src, timeout=10) + if data_stream: + run.add_picture(data_stream, width=Inches(5.7 * scale / 100)) except Exception as e: - print("[RESOURCE ERROR]:", e) + print("[ENHANCED DOWNLOADER ERROR]:", e) # 如果选择展示图片描述,那么描述会在图片下方显示 if show_image_desc and img_tag.get("alt"): @@ -306,7 +310,7 @@ def html2docx(self, html_path: str, docx_path: str): soup = BeautifulSoup(html_str, 'html.parser') body_tag = soup.contents[2] # 将工作目录切换到指定目录 - os.chdir(os.path.abspath(html_path + "\\..")) + os.chdir(os.path.dirname(os.path.abspath(html_path))) # 逐个解析标签,并写到word中 for root in body_tag.children: if root.string != "\n": diff --git a/src/utils/image_downloader.py b/src/utils/image_downloader.py new file mode 100644 index 0000000..6971e24 --- /dev/null +++ b/src/utils/image_downloader.py @@ -0,0 +1,182 @@ +import io +import ssl +import os +import platform +from urllib.request import urlopen, Request +from urllib.parse import urlparse +import requests +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +# 抑制SSL警告 +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +class ImageDownloader: + """增强的图片下载器,支持SSL验证、Cookie、自定义Headers等""" + + def __init__(self, use_browser_cookies=True, verify_ssl=False, custom_headers=None): + """ + 初始化图片下载器 + + Args: + use_browser_cookies (bool): 是否使用浏览器Cookie + verify_ssl (bool): 是否验证SSL证书 + custom_headers (dict): 自定义请求头 + """ + self.use_browser_cookies = use_browser_cookies + self.verify_ssl = verify_ssl + self.session = requests.Session() + + # 设置默认User-Agent + default_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + if custom_headers: + default_headers.update(custom_headers) + + self.session.headers.update(default_headers) + + # 加载浏览器Cookie + if use_browser_cookies: + self._load_browser_cookies() + + def _load_browser_cookies(self): + """加载浏览器Cookie""" + try: + import browser_cookie3 + + # 尝试从不同浏览器加载cookie + browsers = [] + + try: + # Chrome cookies + chrome_cookies = browser_cookie3.chrome() + browsers.append(('Chrome', chrome_cookies)) + except: + pass + + try: + # Firefox cookies + firefox_cookies = browser_cookie3.firefox() + browsers.append(('Firefox', firefox_cookies)) + except: + pass + + try: + # Edge cookies + edge_cookies = browser_cookie3.edge() + browsers.append(('Edge', edge_cookies)) + except: + pass + + # 使用第一个可用的浏览器cookies + for browser_name, cookies in browsers: + if cookies: + self.session.cookies.update(cookies) + print(f"[COOKIES] Loaded cookies from {browser_name}") + break + + except ImportError: + print("[COOKIES] browser-cookie3 not available, skipping browser cookies") + except Exception as e: + print(f"[COOKIES] Failed to load browser cookies: {e}") + + def download_image(self, url, timeout=10): + """ + 下载图片 + + Args: + url (str): 图片URL + timeout (int): 超时时间(秒) + + Returns: + io.BytesIO: 图片数据流,失败时返回None + """ + print(f"[IMAGE] fetching: {url}") + + try: + # 使用requests下载 + response = self.session.get( + url, + timeout=timeout, + verify=self.verify_ssl, + stream=True + ) + response.raise_for_status() + + # 检查内容类型 + content_type = response.headers.get('content-type', '').lower() + if not any(img_type in content_type for img_type in ['image/', 'application/octet-stream']): + print(f"[WARNING] Unexpected content type: {content_type}") + + # 创建数据流 + image_data = io.BytesIO(response.content) + print(f"[SUCCESS] Downloaded {len(response.content)} bytes") + return image_data + + except requests.exceptions.SSLError as e: + print(f"[SSL ERROR] {e}") + if self.verify_ssl: + print("[INFO] Retrying with SSL verification disabled...") + return self._download_with_fallback(url, timeout) + return None + + except requests.exceptions.RequestException as e: + print(f"[NETWORK ERROR] {e}") + # 尝试使用urllib作为后备 + return self._download_with_fallback(url, timeout) + + except Exception as e: + print(f"[DOWNLOAD ERROR] {e}") + return None + + def _download_with_fallback(self, url, timeout): + """使用urllib作为后备下载方法""" + try: + print("[FALLBACK] Using urllib with SSL verification disabled") + + # 创建SSL上下文,跳过证书验证 + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # 创建请求 + req = Request(url) + req.add_header('User-Agent', self.session.headers['User-Agent']) + + # 添加Cookie(如果有的话) + cookie_header = '; '.join([f"{cookie.name}={cookie.value}" for cookie in self.session.cookies]) + if cookie_header: + req.add_header('Cookie', cookie_header) + + # 下载 + with urlopen(req, timeout=timeout, context=ssl_context) as response: + image_data = io.BytesIO(response.read()) + print(f"[SUCCESS] Downloaded via fallback") + return image_data + + except Exception as e: + print(f"[FALLBACK ERROR] {e}") + return None + + +# 创建全局下载器实例 +_downloader = None + +def get_image_downloader(config=None): + """获取图片下载器实例""" + global _downloader + if _downloader is None: + if config is None: + config = { + 'use_browser_cookies': True, + 'verify_ssl': False, # 默认不验证SSL,避免自签名证书问题 + 'custom_headers': None + } + _downloader = ImageDownloader(**config) + return _downloader + +def download_image(url, timeout=10, config=None): + """便捷的图片下载函数""" + downloader = get_image_downloader(config) + return downloader.download_image(url, timeout) \ No newline at end of file