diff --git a/README.MD b/README.MD
index 2430e8b..9694b20 100644
--- a/README.MD
+++ b/README.MD
@@ -9,9 +9,52 @@

## 使用方法
+
+### 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
-如您所见,# 号开头即为标题,从一级到六级。
-列表项
-使用数字和减号来实现有序和无序列表:
-
-- 有序第一项,如果在里面内嵌粗体、斜体
-- 有序子项
-- 有序另一子项
-
-
-- 有序第二项
-
-无序列表二级
-
-- 无序第一项
-- 无序sub1
-- 无序sublist2
-- sub33333
-
-
-- 无序第二项
-
-此外,还有清单列表:
-
-- [x] 普通段落
-- [x] 大纲标题
-- [x] 图片
-- [ ] 有序列表、无序列表、TODO List
-- [x] 引用块
-- [x] 表格
-- [ ] 超链接
-- [ ] 分割线
-- [x] 加粗、斜体、下划线
-- [x] 上标、下标
-- [x] 高亮文本
-
-表格
-
-
-
-| Name |
-Age |
-Sex |
-
-
-
-
-| Jack |
-22 |
-male |
-
-
-| Grace |
-33 |
-female |
-
-
-
-啊?
-
-
-
-| 时间 |
-主要内容 |
-
-
-
-
-| 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 官网
-
-
-![https://p1.itc.cn/q_70/images01/20210608/2de4b5a9f4db46ee83b1081dc557929e.jpg 网络图片]()
\ 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 全量微调 |
+
+
+
+
+| 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 显存为参考标准。
+以下为官方资料截图:
+
+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 显存为参考标准。
+以下为官方资料截图:
+
+
+
+# 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/
+
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