Просмотр исходного кода

feat: Add backup and image-to-pdf utilities

tukuaiai 1 месяц назад
Родитель
Сommit
9e36099e42

+ 3 - 0
.gitignore

@@ -101,3 +101,6 @@ logs/
 *.tmp
 *.tmp
 backups/gz/
 backups/gz/
 libs/common/utils/my-nvim/
 libs/common/utils/my-nvim/
+
+# Ignore PDFs in XHS utility
+libs/common/utils/XHS-image-to-PDF-conversion/*.pdf

+ 176 - 0
libs/common/utils/XHS-image-to-PDF-conversion/README.md

@@ -0,0 +1,176 @@
+<div align="center">
+
+# 📚 小红书图文批量转 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)]()
+
+[✨ 功能特性](#-功能特性) • [⚙️ 工作流程](#️-工作流程) • [🚀 快速开始](#-快速开始) • [🗂️ 项目结构](#️-项目结构) • [🤝 参与贡献](#-参与贡献)
+
+</div>
+
+---
+
+## ✨ 功能特性
+
+| 特性 | 描述 |
+|:---:|:---|
+| 📦 **全自动处理** | 无需手动解压,脚本自动处理 `.zip` 压缩包。 |
+| 🔢 **智能自然排序** | 完美处理 `1, 2, ... 10, 11` 这样的文件名排序,确保图片顺序正确。 |
+| 🚀 **批量转换** | 支持一次性转换文件夹内的所有 `.zip` 文件,省时省力。 |
+| 🗑️ **自动清理** | 转换成功后,自动删除原始的 `.zip` 文件和临时文件,保持目录整洁。 |
+| 📖 **PDF 优化** | 生成的 PDF 文件经过优化,保证清晰度的同时控制文件大小。 |
+| 💻 **跨平台兼容** | 依赖的 `Pillow` 库和 Python 脚本可在 Windows, macOS, Linux 上运行。 |
+
+---
+
+## ⚙️ 工作流程
+
+<table>
+<tr>
+<td width="50%">
+
+脚本的核心逻辑非常简单直接:监控并处理文件夹内的 ZIP 文件,通过一系列自动化步骤输出 PDF。
+
+### 核心步骤
+1.  **扫描**: 查找当前目录下的所有 `.zip` 文件。
+2.  **解压**: 将找到的 `.zip` 文件解压到临时目录。
+3.  **排序**: 智能地对所有图片文件进行“自然排序”。
+4.  **合并**: 将排序后的图片合并成一个 PDF 文件。
+5.  **清理**: 删除原始的 `.zip` 文件和临时文件夹。
+
+</td>
+<td width="50%">
+
+```mermaid
+graph TD
+    A[📁 放置 .zip 文件] --> B{运行 pdf.py 脚本};
+    B --> C[🔄 解压到临时目录];
+    C --> D[🔢 按文件名自然排序];
+    D --> E[🖼️ 合并图片为 PDF];
+    E --> F[📄 生成 output.pdf];
+    F --> G[🗑️ 删除原 .zip 文件];
+    G --> H[✅ 完成];
+```
+
+</td>
+</tr>
+</table>
+
+---
+
+## 🚀 快速开始
+
+### 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. 运行脚本
+
+<details>
+<summary><b>模式一:批量处理所有 ZIP 文件 (推荐)</b></summary>
+
+1.  将所有下载的 `.zip` 文件移动到本项目文件夹中。
+2.  直接运行 `pdf.bat` (Windows) 或在终端中运行以下命令:
+    ```bash
+    python pdf.py
+    ```
+    或者使用 `make`:
+    ```bash
+    make run
+    ```
+3.  脚本会自动处理文件夹内所有的 `.zip` 文件。
+
+</details>
+
+<details>
+<summary><b>模式二:处理单个 ZIP 文件</b></summary>
+
+如果你只想处理一个文件,可以使用拖放或命令行参数:
+
+1.  **拖放 (Windows)**: 将一个 `.zip` 文件直接拖到 `pdf.bat` 图标上。
+2.  **命令行**:
+    ```bash
+    python pdf.py "你的文件路径.zip"
+    ```
+</details>
+
+---
+
+## 🗂️ 项目结构
+
+```
+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) 许可证。
+
+---
+
+<div align="center">
+
+**如果这个项目对你有帮助,请给一个 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)
+
+</div>

+ 2 - 0
libs/common/utils/XHS-image-to-PDF-conversion/pdf.bat

@@ -0,0 +1,2 @@
+python pdf.py
+pause

+ 195 - 0
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__":
+    主函数()

+ 1 - 0
libs/common/utils/XHS-image-to-PDF-conversion/requirements.txt

@@ -0,0 +1 @@
+Pillow

+ 53 - 0
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 脚本)

+ 83 - 0
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

+ 265 - 0
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()