From 9e36099e42e4d50b833057459a4c4e3afd3bb27a Mon Sep 17 00:00:00 2001 From: tukuaiai Date: Sun, 14 Dec 2025 07:20:38 +0800 Subject: [PATCH] feat: Add backup and image-to-pdf utilities --- .gitignore | 3 + .../XHS-image-to-PDF-conversion/README.md | 176 ++++++++++++ .../utils/XHS-image-to-PDF-conversion/pdf.bat | 2 + .../utils/XHS-image-to-PDF-conversion/pdf.py | 195 +++++++++++++ .../requirements.txt | 1 + libs/common/utils/backups/README.md | 53 ++++ libs/common/utils/backups/一键备份.sh | 83 ++++++ libs/common/utils/backups/快速备份.py | 265 ++++++++++++++++++ 8 files changed, 778 insertions(+) create mode 100644 libs/common/utils/XHS-image-to-PDF-conversion/README.md create mode 100644 libs/common/utils/XHS-image-to-PDF-conversion/pdf.bat create mode 100644 libs/common/utils/XHS-image-to-PDF-conversion/pdf.py create mode 100644 libs/common/utils/XHS-image-to-PDF-conversion/requirements.txt create mode 100644 libs/common/utils/backups/README.md create mode 100644 libs/common/utils/backups/一键备份.sh create mode 100644 libs/common/utils/backups/快速备份.py diff --git a/.gitignore b/.gitignore index b97f095..71eaab6 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ logs/ *.tmp backups/gz/ libs/common/utils/my-nvim/ + +# Ignore PDFs in XHS utility +libs/common/utils/XHS-image-to-PDF-conversion/*.pdf diff --git a/libs/common/utils/XHS-image-to-PDF-conversion/README.md b/libs/common/utils/XHS-image-to-PDF-conversion/README.md new file mode 100644 index 0000000..155a8aa --- /dev/null +++ b/libs/common/utils/XHS-image-to-PDF-conversion/README.md @@ -0,0 +1,176 @@ +
+ +# 📚 小红书图文批量转 PDF +### Batch Convert XHS Images to PDF + +**一个智能 Python 脚本,用于将从小红书批量下载的 ZIP 压缩包,按顺序自动拼接为清晰的 PDF 文件。** +*A smart Python script that automatically converts ZIP archives from Xiaohongshu into well-ordered PDF files.* + +--- + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) +[![Status: Active](https://img.shields.io/badge/Status-Active-success.svg?style=for-the-badge)]() +[![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-brightgreen.svg?style=for-the-badge)]() +[![Language: Python](https://img.shields.io/badge/Language-Python-orange.svg?style=for-the-badge)]() + +[✨ 功能特性](#-功能特性) • [⚙️ 工作流程](#️-工作流程) • [🚀 快速开始](#-快速开始) • [🗂️ 项目结构](#️-项目结构) • [🤝 参与贡献](#-参与贡献) + +
+ +--- + +## ✨ 功能特性 + +| 特性 | 描述 | +|:---:|:---| +| 📦 **全自动处理** | 无需手动解压,脚本自动处理 `.zip` 压缩包。 | +| 🔢 **智能自然排序** | 完美处理 `1, 2, ... 10, 11` 这样的文件名排序,确保图片顺序正确。 | +| 🚀 **批量转换** | 支持一次性转换文件夹内的所有 `.zip` 文件,省时省力。 | +| 🗑️ **自动清理** | 转换成功后,自动删除原始的 `.zip` 文件和临时文件,保持目录整洁。 | +| 📖 **PDF 优化** | 生成的 PDF 文件经过优化,保证清晰度的同时控制文件大小。 | +| 💻 **跨平台兼容** | 依赖的 `Pillow` 库和 Python 脚本可在 Windows, macOS, Linux 上运行。 | + +--- + +## ⚙️ 工作流程 + + + + + + +
+ +脚本的核心逻辑非常简单直接:监控并处理文件夹内的 ZIP 文件,通过一系列自动化步骤输出 PDF。 + +### 核心步骤 +1. **扫描**: 查找当前目录下的所有 `.zip` 文件。 +2. **解压**: 将找到的 `.zip` 文件解压到临时目录。 +3. **排序**: 智能地对所有图片文件进行“自然排序”。 +4. **合并**: 将排序后的图片合并成一个 PDF 文件。 +5. **清理**: 删除原始的 `.zip` 文件和临时文件夹。 + + + +```mermaid +graph TD + A[📁 放置 .zip 文件] --> B{运行 pdf.py 脚本}; + B --> C[🔄 解压到临时目录]; + C --> D[🔢 按文件名自然排序]; + D --> E[🖼️ 合并图片为 PDF]; + E --> F[📄 生成 output.pdf]; + F --> G[🗑️ 删除原 .zip 文件]; + G --> H[✅ 完成]; +``` + +
+ +--- + +## 🚀 快速开始 + +### 1. 环境准备 + +首先,确保你的电脑上安装了 **Python 3**。 + +然后,将本项目克隆到你的本地: +```bash +git clone https://github.com/tukuaiai/XHS-image-to-PDF-conversion.git +cd XHS-image-to-PDF-conversion +``` + +### 2. 安装依赖 + +本项目依赖 `Pillow` 库来处理图片。运行以下命令安装它: +```bash +pip install -r requirements.txt +``` +或者,你也可以使用 `Makefile` (如果你的系统支持 `make`): +```bash +make install +``` + +### 3. 下载图文 + +使用你喜欢的浏览器扩展(如推荐的 **小地瓜**)从小红书下载图文,并确保它们是 `.zip` 格式。 + +- **小地瓜 - 小红书图片视频下载助手**: [Firefox 扩展](https://addons.mozilla.org/zh-CN/firefox/addon/%E5%B0%8F%E5%9C%B0%E7%93%9C-%E5%B0%8F%E7%BA%A2%E4%B9%A6%E5%9B%BE%E7%89%87%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD%E5%8A%A9%E6%89%8B/) + +### 4. 运行脚本 + +
+模式一:批量处理所有 ZIP 文件 (推荐) + +1. 将所有下载的 `.zip` 文件移动到本项目文件夹中。 +2. 直接运行 `pdf.bat` (Windows) 或在终端中运行以下命令: + ```bash + python pdf.py + ``` + 或者使用 `make`: + ```bash + make run + ``` +3. 脚本会自动处理文件夹内所有的 `.zip` 文件。 + +
+ +
+模式二:处理单个 ZIP 文件 + +如果你只想处理一个文件,可以使用拖放或命令行参数: + +1. **拖放 (Windows)**: 将一个 `.zip` 文件直接拖到 `pdf.bat` 图标上。 +2. **命令行**: + ```bash + python pdf.py "你的文件路径.zip" + ``` +
+ +--- + +## 🗂️ 项目结构 + +``` +XHS-image-to-PDF-conversion/ +├── .git/ +├── docs/ # (未来可能添加的文档) +├── 📚...pdf # (示例文件) +├── pdf.bat # Windows 批处理脚本 +├── pdf.py # 核心 Python 脚本 +├── Makefile # 自动化命令 +├── requirements.txt # Python 依赖 +├── README.md # 你正在阅读的这个文件 +├── LICENSE # MIT 许可证 +├── CODE_OF_CONDUCT.md # 社区行为准则 +└── CONTRIBUTING.md # 贡献指南 +``` + +--- + +## 🤝 参与贡献 + +我们欢迎任何形式的贡献!无论是报告 Bug、提出功能建议还是直接贡献代码。 + +请参考我们的 [**贡献指南 (CONTRIBUTING.md)**](CONTRIBUTING.md) 来了解如何参与。 + +--- + +## 📜 许可证 + +本项目采用 [MIT](LICENSE) 许可证。 + +--- + +
+ +**如果这个项目对你有帮助,请给一个 Star ⭐!** + +[![Star History Chart](https://api.star-history.com/svg?repos=tukuaiai/XHS-image-to-PDF-conversion&type=Date)](https://star-history.com/#tukuaiai/XHS-image-to-PDF-conversion&Date) + +--- + +**Made with 🐍 & ❤️ by tukuaiai** + +[⬆ 回到顶部](#-小红书图文批量转-pdf) + +
diff --git a/libs/common/utils/XHS-image-to-PDF-conversion/pdf.bat b/libs/common/utils/XHS-image-to-PDF-conversion/pdf.bat new file mode 100644 index 0000000..ed89d89 --- /dev/null +++ b/libs/common/utils/XHS-image-to-PDF-conversion/pdf.bat @@ -0,0 +1,2 @@ +python pdf.py +pause \ No newline at end of file diff --git a/libs/common/utils/XHS-image-to-PDF-conversion/pdf.py b/libs/common/utils/XHS-image-to-PDF-conversion/pdf.py new file mode 100644 index 0000000..b8b37ba --- /dev/null +++ b/libs/common/utils/XHS-image-to-PDF-conversion/pdf.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +图片ZIP转PDF脚本 +将ZIP文件中的图片按序号排序并拼接成PDF文件 +""" + +import zipfile +import os +import re +from PIL import Image +import shutil + +def 自然排序键(文件名): + """将文件名转换为自然排序键,支持数字排序""" + return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', 文件名)] + +def zip转pdf(zip路径): + """ + 将ZIP文件中的图片排序后转换为PDF + + Args: + zip路径: ZIP文件的完整路径 + + Returns: + 成功返回PDF路径,失败返回None + """ + try: + # 获取ZIP文件名(不含扩展名) + zip目录, zip文件名 = os.path.split(zip路径) + pdf名称 = os.path.splitext(zip文件名)[0] + '.pdf' + pdf路径 = os.path.join(zip目录, pdf名称) + + print(f"正在处理: {zip文件名}") + + # 创建临时目录解压文件 + 临时目录 = os.path.join(zip目录, 'temp_extract') + if os.path.exists(临时目录): + shutil.rmtree(临时目录) + os.makedirs(临时目录) + + # 解压ZIP文件 + with zipfile.ZipFile(zip路径, 'r') as zip文件: + zip文件.extractall(临时目录) + + # 获取所有图片文件 + 支持的格式 = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'} + 图片文件 = [] + + for 根目录, 目录, 文件列表 in os.walk(临时目录): + for 文件 in 文件列表: + if any(文件.lower().endswith(格式) for 格式 in 支持的格式): + 完整路径 = os.path.join(根目录, 文件) + 图片文件.append(完整路径) + + if not 图片文件: + print("错误: ZIP文件中没有找到图片文件") + shutil.rmtree(临时目录) + return None + + # 按文件名自然排序 + 图片文件.sort(key=lambda x: 自然排序键(os.path.basename(x))) + + print(f"找到 {len(图片文件)} 张图片,开始转换...") + + # 打开第一张图片作为基础 + 图片对象列表 = [] + for 图片路径 in 图片文件: + try: + 图片 = Image.open(图片路径) + # 转换为RGB模式(确保兼容性) + if 图片.mode != 'RGB': + 图片 = 图片.convert('RGB') + 图片对象列表.append(图片) + except Exception as e: + print(f"警告: 无法打开图片 {图片路径}: {e}") + continue + + if not 图片对象列表: + print("错误: 没有成功加载任何图片") + shutil.rmtree(临时目录) + return None + + # 保存为PDF(第一张作为主图,其余附加) + 图片对象列表[0].save( + pdf路径, + "PDF", + quality=95, + optimize=True, + save_all=True, + append_images=图片对象列表[1:] + ) + + # 关闭所有图片对象 + for 图片 in 图片对象列表: + 图片.close() + + # 清理临时文件 + shutil.rmtree(临时目录) + + # 删除原ZIP文件 + os.remove(zip路径) + + print(f"✓ 成功创建PDF: {pdf名称}") + print(f"✓ 已删除原ZIP文件: {zip文件名}") + + return pdf路径 + + except zipfile.BadZipFile: + print(f"错误: {zip路径} 不是有效的ZIP文件") + return None + except Exception as e: + print(f"处理过程中出错: {e}") + # 清理临时文件 + if '临时目录' in locals() and os.path.exists(临时目录): + shutil.rmtree(临时目录) + return None + +def 批量处理当前目录(): + """批量处理当前目录下所有ZIP文件""" + 当前目录 = os.getcwd() + zip文件列表 = [] + + # 扫描当前目录所有ZIP文件 + for 文件 in os.listdir(当前目录): + if 文件.lower().endswith('.zip'): + zip文件列表.append(os.path.join(当前目录, 文件)) + + if not zip文件列表: + print("当前目录下没有找到ZIP文件") + return + + print(f"发现 {len(zip文件列表)} 个ZIP文件,开始批量处理...") + print("-" * 50) + + 成功计数 = 0 + 失败计数 = 0 + + for zip路径 in zip文件列表: + print(f"\n[{zip文件列表.index(zip路径) + 1}/{len(zip文件列表)}] 处理: {os.path.basename(zip路径)}") + + # 执行转换 + 结果 = zip转pdf(zip路径) + + if 结果: + 成功计数 += 1 + else: + 失败计数 += 1 + + print("-" * 50) + print(f"批量处理完成!成功: {成功计数} 个,失败: {失败计数} 个") + +def 处理指定文件(zip路径): + """处理指定的单个ZIP文件""" + if not os.path.exists(zip路径): + print(f"错误: 找不到文件 {zip路径}") + return False + + if not zip路径.lower().endswith('.zip'): + print("错误: 请提供ZIP格式的文件") + return False + + # 执行转换 + 结果 = zip转pdf(zip路径) + + if 结果: + print(f"\n🎉 任务完成!PDF文件已保存为: {结果}") + return True + else: + print("\n❌ 任务失败,请检查错误信息") + return False + +def 主函数(): + """主函数:智能处理模式""" + import sys + + # 优先处理指定文件(如果存在) + 指定文件路径 = r"C:\Users\lenovo\Desktop\新建文件夹\剥头皮量化策略全拆解:低延迟、高频的底层.zip" + + # 检查是否有命令行参数 + if len(sys.argv) > 1: + # 命令行指定了ZIP文件路径 + zip路径 = sys.argv[1] + print(f"通过命令行参数指定文件: {zip路径}") + 处理指定文件(zip路径) + elif os.path.exists(指定文件路径): + # 处理默认指定文件 + print(f"处理默认指定文件: {os.path.basename(指定文件路径)}") + 处理指定文件(指定文件路径) + else: + # 自动扫描当前目录所有ZIP文件 + print("开始扫描当前目录所有ZIP文件...") + 批量处理当前目录() + +if __name__ == "__main__": + 主函数() \ No newline at end of file diff --git a/libs/common/utils/XHS-image-to-PDF-conversion/requirements.txt b/libs/common/utils/XHS-image-to-PDF-conversion/requirements.txt new file mode 100644 index 0000000..7e2fba5 --- /dev/null +++ b/libs/common/utils/XHS-image-to-PDF-conversion/requirements.txt @@ -0,0 +1 @@ +Pillow diff --git a/libs/common/utils/backups/README.md b/libs/common/utils/backups/README.md new file mode 100644 index 0000000..fc03cca --- /dev/null +++ b/libs/common/utils/backups/README.md @@ -0,0 +1,53 @@ +# 快速备份工具 + +基于 `.gitignore` 规则的项目备份工具,自动排除不需要的文件。 + +## 功能特性 + +- 自动读取 `.gitignore` 规则 +- 支持取反规则(`!` 语法) +- 目录级剪枝优化 +- 生成 `.tar.gz` 压缩包 +- 零依赖(仅使用 Python 内置模块) + +## 文件结构 + +``` +backups/ +├── 快速备份.py # 核心备份引擎 +├── 一键备份.sh # Shell 启动脚本 +└── README.md # 本文档 +``` + +## 使用方法 + +```bash +# 方式一:Shell 脚本(推荐) +bash backups/一键备份.sh + +# 方式二:直接运行 Python +python3 backups/快速备份.py + +# 指定输出文件 +python3 backups/快速备份.py -o my_backup.tar.gz + +# 指定项目目录 +python3 backups/快速备份.py -p /path/to/project +``` + +## 输出位置 + +默认输出到 `backups/gz/备份_YYYYMMDD_HHMMSS.tar.gz` + +## 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `-p, --project` | 项目根目录 | 当前目录 | +| `-o, --output` | 输出文件路径 | `backups/gz/备份_时间戳.tar.gz` | +| `-g, --gitignore` | gitignore 文件路径 | `.gitignore` | + +## 依赖 + +- Python 3.x(无需额外包) +- Bash(用于 Shell 脚本) diff --git a/libs/common/utils/backups/一键备份.sh b/libs/common/utils/backups/一键备份.sh new file mode 100644 index 0000000..d20f14d --- /dev/null +++ b/libs/common/utils/backups/一键备份.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# 一键备份项目脚本 +# 自动读取 .gitignore 规则并排除匹配的文件 +# bash backups/一键备份.sh + +set -e + +# 颜色输出 +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 项目根目录(脚本所在目录的父目录) +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# 项目backups目录 +BACKUPS_DIR="${PROJECT_ROOT}/backups" + +# 备份脚本路径(始终在项目的backups目录中) +BACKUP_SCRIPT="${BACKUPS_DIR}/快速备份.py" + +# 检查备份脚本是否存在 +if [ ! -f "${BACKUP_SCRIPT}" ]; then + echo -e "${YELLOW}⚠️ 错误: 备份脚本不存在${NC}" + echo "" + echo "备份工具应位于项目的 backups/ 目录中:" + echo " ${BACKUPS_DIR}/" + echo "" + echo "请确保:" + echo " 1. 复制快速备份.py到 ${BACKUPS_DIR}/" + echo " 2. 复制一键备份.sh到 ${BACKUPS_DIR}/" + echo "" + echo "或者使用方式:" + echo " • 在项目根目录执行: bash backups/一键备份.sh" + echo " • 或直接执行: python3 backups/快速备份.py" + exit 1 +fi + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 项目快速备份工具${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" +echo -e "${GREEN}✓${NC} 找到备份脚本: backups/快速备份.py" + +# 检查 Python3 是否可用 +if ! command -v python3 &> /dev/null; then + echo -e "${YELLOW}⚠️ 错误: 未找到 python3 命令${NC}" + exit 1 +fi + +echo -e "${GREEN}✓${NC} 项目目录: ${PROJECT_ROOT}" +echo -e "${GREEN}✓${NC} 备份脚本: ${BACKUP_SCRIPT}" +echo -e "${GREEN}✓${NC} Python 版本: $(python3 --version)" +echo "" + +# 执行备份 +echo -e "${YELLOW}▶ 正在执行备份...${NC}" +echo "" + +# 切换到项目根目录 +cd "${PROJECT_ROOT}" + +# 运行备份脚本 +python3 "${BACKUP_SCRIPT}" + +# 检查执行结果 +if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} ✓ 备份完成!${NC}" + echo -e "${GREEN}========================================${NC}" +else + echo "" + echo -e "${YELLOW}========================================${NC}" + echo -e "${YELLOW} ✗ 备份失败${NC}" + echo -e "${YELLOW}========================================${NC}" + exit 1 +fi diff --git a/libs/common/utils/backups/快速备份.py b/libs/common/utils/backups/快速备份.py new file mode 100644 index 0000000..3e03fd4 --- /dev/null +++ b/libs/common/utils/backups/快速备份.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +快速备份项目工具 +读取 .gitignore 规则并打包项目文件(排除匹配的文件) + +bash backups/一键备份.sh + +文件位置: + backups/快速备份.py + +工具清单(backups/目录): + • 快速备份.py - 核心备份引擎(7.3 KB) + • 一键备份.sh - 一键执行脚本(2.4 KB) + +使用方法: + $ bash backups/一键备份.sh + 或 + $ python3 backups/快速备份.py + +备份输出: + backups/gz/备份_YYYYMMDD_HHMMSS.tar.gz + +适用项目: + 任何包含 .gitignore 文件的项目(自动读取规则并排除匹配文件) + +依赖: + 无需额外安装包,仅使用Python内置模块 +""" + +import os +import tarfile +import fnmatch +from pathlib import Path +from datetime import datetime +import argparse +import sys + + +class GitignoreFilter: + """解析 .gitignore 文件并过滤文件""" + + def __init__(self, gitignore_path: Path, project_root: Path): + self.project_root = project_root + # 规则按照出现顺序存储,支持取反(!)语义,后匹配覆盖前匹配 + # 每项: {"pattern": str, "dir_only": bool, "negate": bool, "has_slash": bool} + self.rules = [] + self.load_gitignore(gitignore_path) + + def load_gitignore(self, gitignore_path: Path): + """加载并解析 .gitignore 文件""" + if not gitignore_path.exists(): + print(f"⚠️ 警告: {gitignore_path} 不存在,将不应用任何过滤规则") + return + + try: + with open(gitignore_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + + # 跳过空行和注释 + if not line or line.startswith('#'): + continue + + negate = line.startswith('!') + if negate: + line = line[1:].lstrip() + if not line: + continue + + dir_only = line.endswith('/') + has_slash = '/' in line.rstrip('/') + + self.rules.append({ + "pattern": line, + "dir_only": dir_only, + "negate": negate, + "has_slash": has_slash, + }) + + print(f"✓ 已加载 {len(self.rules)} 条规则(含取反)") + + except Exception as e: + print(f"❌ 读取 .gitignore 失败: {e}") + sys.exit(1) + + def _match_rule(self, rule: dict, relative_path_str: str, is_dir: bool) -> bool: + """按规则匹配路径,返回是否命中""" + pattern = rule["pattern"] + dir_only = rule["dir_only"] + has_slash = rule["has_slash"] + + # 目录规则:匹配目录自身或其子路径 + if dir_only: + normalized = pattern.rstrip('/') + if relative_path_str == normalized or relative_path_str.startswith(normalized + '/'): + return True + return False + + # 带路径分隔的规则:按相对路径匹配 + if has_slash: + return fnmatch.fnmatch(relative_path_str, pattern) + + # 无斜杠:匹配任意层级的基本名 + if fnmatch.fnmatch(Path(relative_path_str).name, pattern): + return True + # 额外处理目录命中:无通配符时,若任一父级目录名等于 pattern 也视为命中 + if pattern.isalpha() and pattern in relative_path_str.split('/'): + return True + return False + + def should_exclude(self, path: Path, is_dir: bool = False) -> bool: + """ + 判断路径是否应该被排除(支持 ! 取反,后匹配覆盖前匹配) + 返回 True 表示应该排除(不备份) + """ + try: + # 统一使用 POSIX 路径风格进行匹配 + relative_path_str = path.relative_to(self.project_root).as_posix() + except ValueError: + return False # 不在项目根目录内,不处理 + + # Git 风格:从上到下最后一次匹配决定去留 + matched = None + for rule in self.rules: + if self._match_rule(rule, relative_path_str, is_dir): + matched = not rule["negate"] # negate 表示显式允许 + + return bool(matched) + + +def create_backup(project_root: Path, output_file: Path, filter_obj: GitignoreFilter): + """创建备份压缩包""" + + # 统计信息 + total_files = 0 + excluded_files = 0 + included_files = 0 + + print(f"\n{'='*60}") + print(f"开始备份项目: {project_root}") + print(f"输出文件: {output_file}") + print(f"{'='*60}\n") + + try: + with tarfile.open(output_file, 'w:gz') as tar: + # 使用 os.walk 可在目录层级提前剪枝,避免进入已忽略目录 + for root, dirs, files in os.walk(project_root, topdown=True): + root_path = Path(root) + + # 目录剪枝:命中忽略规则或 .git 时不再深入 + pruned_dirs = [] + for d in dirs: + dir_path = root_path / d + if d == '.git' or filter_obj.should_exclude(dir_path, is_dir=True): + print(f" 排除目录: {dir_path.relative_to(project_root)}") + excluded_files += 1 + continue + pruned_dirs.append(d) + dirs[:] = pruned_dirs + + for name in files: + path = root_path / name + total_files += 1 + + # 文件忽略判定 + if '.git' in path.parts or filter_obj.should_exclude(path): + excluded_files += 1 + print(f" 排除: {path.relative_to(project_root)}") + continue + + arcname = path.relative_to(project_root) + tar.add(path, arcname=arcname) + included_files += 1 + print(f" 备份: {arcname}") + + print(f"\n{'='*60}") + print("备份完成!") + print(f"{'='*60}") + print(f"总文件数: {total_files}") + print(f"已备份: {included_files} 个文件") + print(f"已排除: {excluded_files} 个文件/目录") + print(f"压缩包大小: {output_file.stat().st_size / 1024 / 1024:.2f} MB") + print(f"{'='*60}") + + return True + + except Exception as e: + print(f"\n❌ 备份失败: {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + parser = argparse.ArgumentParser( + description='快速备份项目(根据 .gitignore 排除文件)', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +使用示例: + # 基本用法(备份到 backups/gz/ 目录) + python backups/快速备份.py + + # 指定输出文件 + python backups/快速备份.py -o my_backup.tar.gz + + # 指定项目根目录 + python backups/快速备份.py -p /path/to/project + """ + ) + + parser.add_argument( + '-p', '--project', + type=str, + default='.', + help='项目根目录路径(默认: 当前目录)' + ) + + parser.add_argument( + '-o', '--output', + type=str, + help='输出文件路径(默认: backups/备份_YYYYMMDD_HHMMSS.tar.gz)' + ) + + parser.add_argument( + '-g', '--gitignore', + type=str, + default='.gitignore', + help='.gitignore 文件路径(默认: .gitignore)' + ) + + args = parser.parse_args() + + # 解析路径 + project_root = Path(args.project).resolve() + gitignore_path = Path(args.gitignore).resolve() + + if not project_root.exists(): + print(f"❌ 错误: 项目目录不存在: {project_root}") + sys.exit(1) + + # 确定输出文件路径 + if args.output: + output_file = Path(args.output).resolve() + else: + # 默认输出到 backups/gz/ 目录 + backup_dir = project_root / 'backups' / 'gz' + backup_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + output_file = backup_dir / f'备份_{timestamp}.tar.gz' + + # 确保输出目录存在 + output_file.parent.mkdir(parents=True, exist_ok=True) + + # 创建过滤器 + filter_obj = GitignoreFilter(gitignore_path, project_root) + + # 执行备份 + success = create_backup(project_root, output_file, filter_obj) + + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main()