Linux下zip压缩如何排除指定文件?-x参数与备份实战
2026/9/16 18:26:57 网站建设 项目流程

接手一台新服务器,给项目做备份,我习惯于直接敲zip -r backup.zip /data/app/这条命令。第一次这么干的时候还没什么感觉,等压缩包传到本地一解压,发现node_modules占了两百多兆,日志文件几百个,还有个.git目录也堂而皇之躺在里面——备份一个不到 100M 的代码工程,硬生生压出了 3G 的垃圾包。

这应该也是不少人在 Linux 下用 zip 压缩时的共同痛点:压缩命令本身不难,难的是怎么把不需要的文件从压缩包里剔除干净。这篇文章就围绕这件事展开,把 zip 命令的排除机制、容易踩的坑、以及实际服务器备份场景里的完整思路一次性讲透。不管你是刚接触 Linux 命令的新手,还是已经在生产环境里打过很多次包的老手,里面提到的几个细节都值得细看。

1. 明明有 tar.gz,服务器场景里我却常用 zip

1.1 zip 在做“跨平台传输”时确实无可替代

我知道在 Linux 生态里,tar.gz才是绝对的主流,压缩率更高、打包速度也不差,服务器之间互传文件几乎都是用它。但如果你跟我一样,需要频繁把服务器上的文件打包发给 Windows 或 macOS 的同事,zip才是那个最省心的选择——Windows 资源管理器原生支持解压 zip,macOS 也一样,双击就完事。tar.gz在 Windows 上默认是打不开的,你还得让人装 7-Zip 或者 WinRAR,一个简单的交付动作立刻变得复杂。

如果是做服务器本地备份、或者你有把握接收方一定在 Linux 环境,那用tar.gz更合理。但跨平台需求一旦出现,zip就是最优解。这也是为什么在很多企业内部,明明服务器都是 Linux,大家做交付包、上传到对象存储、发邮件附件时依然默认用 zip。

还有个更实际的原因:zip命令支持追加、更新和测试操作,比如zip -u可以只把新增或变化的文件压进去,zip -m压缩后自动删除原文件,这些在增量备份场景里非常顺手。tar不是不能做增量,但实现起来复杂得多,日常使用没有必要。

1.2 zip 命令的基础操作速查

在讲排除文件之前,先把最常用的几条命令列出来,后面所有的操作都建立在这几个基础命令之上:

# 将 source_dir 目录递归压缩为 archive.zip zip -r archive.zip source_dir # 查看压缩包里的文件列表,不解压 unzip -l archive.zip # 将压缩包解压到指定目录 unzip -d /path/to/extract archive.zip # 只把新文件或更新过的文件加入压缩包 zip -u archive.zip source_dir # 压缩后删除原文件 zip -m archive.zip file1 file2 # 测试压缩包完整性 unzip -t archive.zip

这些命令里,unzip -lunzip -t是后续验证排除效果最重要的两个工具。很多人只知道打包,不知道验收,压缩完直接扔出去,结果别人拿到手解压一看才发现一堆垃圾文件混在里面。养成压缩完立刻列一下内容、测一下完整性的习惯,能规避掉很大一部分低级问题。

2. 排除不需要的文件:-x 参数的正确打开方式

2.1 一条最简单的排除命令,却有一半人写错

zip排除文件靠的是-x参数,后面跟要排除的路径模式,可以写多个。比如:

zip -r backup.zip /data/app -x "*/node_modules/*"

这条命令的意思是:把/data/app整个目录压进backup.zip,但排除所有层级下的node_modules目录里的全部内容。

这里第一个坑就来了:-x后面的模式必须用双引号引起来。如果不加引号写成:

zip -r backup.zip /data/app -x */node_modules/*

那么这里的*会被 shell 先展开成当前目录下匹配*/node_modules/*的文件列表。你当前目录下如果没有这样的路径,zip 收到的可能是一个空列表,排除规则完全失效,node_modules依然会被原封不动打进去。更恶心的是你不会收到任何报错,压缩包照常生成,问题要等解压时才暴露。

我见过很多人在网上提问“为什么 -x 排除了还是没有用”,点进聊天记录一看,十有八九是引号问题。所以这条规则建议直接形成肌肉记忆:看到-x,顺手打一个双引号再说。

2.2 理解 -x 的匹配机制,才不会被奇怪的现象坑到

zip-x模式匹配的是“当前工作目录 + 传入路径”拼出来的完整路径,而不是压缩包内部的相对路径。这句话听起来有点绕,直接看例子。

假设你当前在/opt目录下,执行这条命令:

cd /opt zip -r backup.zip /data/app -x "/data/app/node_modules/*"

因为 zip 打包时看到的是完整的/data/app/...路径,所以-x "/data/app/node_modules/*"能精确匹配到并排除。

但很多时候我们更喜欢在目标目录的上一级执行相对路径打包:

cd /data zip -r backup.zip app -x "app/node_modules/*"

这种写法也能排除。可问题来了,很多人会写成-x "node_modules/*",心想“反正我排除的是 node_modules,写这么长干嘛”。结果就是排除规则匹配不上,node_modules 依然被打包进去。

为什么?因为 zip 在匹配路径时,是基于它从命令行收到的整个路径(这种情况下是app/node_modules/...)去匹配的,node_modules/*这个模式从头开始就无法命中app/node_modules/...

这也带出了第二个关键建议:统一用*/开头的匹配模式。你不需要关心当前工作目录是什么、打包路径是相对还是绝对,只要写成:

zip -r backup.zip /data/app -x "*/node_modules/*"

它的匹配逻辑是“只要路径里的任意一级出现了 node_modules 目录,就把它下面所有内容排除掉”。这种模式适应性强,基本不会因为路径写法不同而翻车。后面我所有的实际命令,都推荐用这种形式。

2.3 排除目录与排除文件,规则写法有区别

排除目录和排除文件,模式写法不太一样,这里容易产生误解。

排除目录内的所有内容,写法是目录名后面跟/*

-x "*/logs/*"

如果只写-x "*/logs",在部分 zip 版本里可能会出现目录本身的条目被排除,但目录下的文件依然进包的情况。为了行为一致,建议总是写*/logs/*

排除所有日志文件,写法是:

-x "*.log"

模式匹配没有“必须从路径开头匹配”的限制,*.log能匹配任意目录下以.log结尾的文件。同理,要排除临时文件可以写-x "*.tmp",排除备份文件可以写-x "*.bak"

多个排除规则可以写在同一个-x后面,也可以拆成多个-x

zip -r backup.zip app \ -x "*/node_modules/*" \ -x "*/.git/*" \ -x "*/logs/*" \ -x "*.log" \ -x ".DS_Store"

这种写法可读性最好,每一条规则一眼能看懂是干什么的,也方便后续增删。

3. 实战:一条命令备份线上项目目录

3.1 先搞清楚目录到底有多大,再决定排除什么

拿一个典型的 Node.js 项目来演示。假设项目路径是/opt/app/blog,我一般先跑两条命令看清楚状况:

du -sh /opt/app/blog

如果输出显示有 2.3G,别急着打压缩命令,先用du看看到底是哪一层目录占了空间:

du -h --max-depth=2 /opt/app/blog | sort -hr | head -20

大概率你会看到这样的结果:

2.3G /opt/app/blog 1.9G /opt/app/blog/node_modules 250M /opt/app/blog/.git 60M /opt/app/blog/logs

看到这个输出,基本可以判断这个项目里必须排除的三样东西:node_modules(依赖随时可以重新安装)、.git(版本历史不属于交付物)、logs(日志只会越积越多)。

3.2 手写排除命令,把风险项也一并挡在包外

/opt/app目录下执行:

cd /opt/app zip -r blog_backup_20240615.zip blog \ -x "*/node_modules/*" \ -x "*/.git/*" \ -x "*/logs/*" \ -x "*/tmp/*" \ -x "*/.env" \ -x "*.log" \ -x ".DS_Store"

这里除了node_modules.gitlogs,还加了几个有实际意义的排除项:

  • */tmp/*:临时目录,打包意义不大。
  • */.env:环境变量文件,通常包含数据库密码、第三方密钥等敏感信息。很多人打包时把.env一起发出去,后面出的是安全事件。
  • *.log:把所有散落在各处的日志文件都挡在包外,不限于 logs 目录下的那些。
  • .DS_Store:从 macOS 同步过来的隐藏文件,Windows 同事解压后完全用不上。

关于是否排除.env,不同团队有不同习惯。我个人的做法是:默认不打包,需要部署时通过专门的配置分发渠道传过去。如果确实有备份环境配置的需求,也应该单独加密存放,而不是混在代码压缩包里到处发。

3.3 排除规则太多时,用文件列表管理

项目规模一大,要排除的目录和文件种类经常超过十条。这时候一条命令写得又长又乱,维护起来很不方便。zip 支持从文件里读取排除规则,避免命令行过长:

zip -r blog_backup.zip blog -x@exclude.txt

注意是-x@加文件路径,中间没有空格。exclude.txt的内容每行一个模式:

*/node_modules/* */.git/* */.svn/* */logs/* */tmp/* */cache/* */.next/* */.nuxt/* */dist/* */__pycache__/* */.idea/* */.vscode/* */.env *.log *.tmp *.cache .DS_Store

把这份exclude.txt放在项目外(比如/opt/backup_config/里),备份脚本直接复用,新项目要加东西就改一行,非常方便。这也解决了另一个问题:手写一长串-x规则的时候,漏写一条你自己根本察觉不到,而用文件管理至少能看到完整的排除清单。

3.4 压缩完立刻自查,别等交付了再被打脸

打包命令执行完,不要急着走,立刻验证两个点:

# 看看压缩包有多大 ls -lh blog_backup_20240615.zip # 检查压缩包里是否还残留着不该出现的东西 unzip -l blog_backup_20240615.zip | grep -E "node_modules|\.env|\.git/" | head -20

如果第二条命令没有输出,说明排除规则全部生效了。如果还能看到相关路径,那就要回到上一节提到的匹配机制去排查问题。

再跑一次完整性校验:

unzip -t blog_backup_20240615.zip

zip 会逐个文件进行 CRC 校验,输出末尾显示No errors detected in compressed data才算真正完成。这一步在服务器磁盘空间紧张时尤其重要——如果压缩过程中磁盘被写满,zip 会中断并返回非零退出码,但很多人不会看退出码,直接把残缺的压缩包发出去了。

4. 排除失效、解压乱码、假损坏:排查链路与处理方案

4.1 “排除了为什么还在”——完整排查链路

我自己的项目里曾经出现过一次排除失败,现象很典型:命令里写了-x "node_modules/*",压缩包里node_modules却原封不动躺在里面。当时按这条链路一步步排查,最终定位到问题。

第一步,确认压缩包里的实际路径长什么样:

unzip -l backup.zip | grep node_modules | head -5

输出显示路径是app/node_modules/...这样的结构。这已经很能说明问题了:我的排除模式写的是node_modules/*,但实际匹配目标是app/node_modules/helmet/index.js。从路径开头匹配node_modules的话,app开头的那一段就让它匹配失败了。

第二步,检查当前工作目录:

pwd

确认执行 zip 命令时在/opt/app目录下,所以打包源路径是app,进包路径自然带了app/前缀。

第三步,把排除模式改成*/node_modules/*,重新打包,问题解决。

第四步,顺便检查了压缩包里有没有符号链接混进去。这是另一个隐蔽坑:如果项目目录里有指向外部目录的符号链接(比如logs -> /var/log/myapp),zip 默认会跟随符号链接,把链接指向的真实目录内容也拉进压缩包。哪怕你排除了*/logs/*,因为实际进入 zip 的路径可能是app/logs/...,这条规则应该能排除掉,但如果链接指向的目录很大,压缩过程中已经先占用了一段时间的 IO 和磁盘。这种情况建议打包时加-y选项,告诉 zip 只保留链接本身,不跟随链接指向的内容:

zip -r backup.zip app -y -x "*/node_modules/*"

4.2 中文字符文件名解压乱码:不是压缩包坏了,是编码问题

Windows 上用压缩软件打包的中文文件名 zip,拿到 Linux 下用unzip解压,经常出来一堆乱码。很多人第一反应是压缩包损坏,但unzip -t测出来是好的。

原因是 zip 格式本身没有强制规定文件名编码,Windows 简体中文环境下很多工具默认用 GBK 存储文件名,而 Linux 下的unzip默认按 UTF-8 解码,两边对不上就成了乱码。

Linux 环境下的处理方案,按优先级排序:

# 方案一:unzip 指定编码解压(部分发行版支持 -O 选项) unzip -O GBK file.zip # 方案二:用 7z 解压,对编码兼容性更好 7z x file.zip # 方案三:用 bsdtar 解压,libarchive 编码识别更智能 bsdtar -xvf file.zip

我自己在服务器上更常用方案三,因为bsdtar在处理不同来源的 zip 时,乱码出现概率明显比unzip低。如果发行版里没有,装一下libarchive-tools就有bsdtar了。

反过来,在 Linux 上用 zip 打包中文文件名文件传给 Windows 用户,也可能出现对方解压乱码的问题。目前比较稳妥的做法是:确保 Linux 系统 locale 是 UTF-8,然后用较新版本的 zip 工具打包,文件名会按 UTF-8 写入;Windows 10/11 自带的解压功能已经能正确识别 UTF-8 编码的 zip 文件。

4.3 压缩包“假损坏”:退出码、磁盘空间与测试

有一次帮同事排查,他反馈 zip 压缩包解压到一半报错,原话是“文件损坏了”。我先跑了unzip -t,输出明确提示bad CRC某几个文件。这就不是文件名编码问题,是文件内容确实有问题。

进一步查发现,执行压缩命令的服务器当时/tmp分区只剩不到 200M,而 zip 打包过程需要临时写入一部分数据,磁盘写满后命令直接中断。但同事没有看命令的退出码——如果仔细留意,echo $?会输出非 0 值,说明压缩并未成功完成。

这个案例给了两条教训:

  1. 压缩前确认磁盘余量,至少留出目标压缩包预估大小的两倍空间。
  2. 压缩命令执行后,立刻用$?检查退出码,并跑unzip -t做完整性验证。

操作很简单,但能把“发出去一个坏压缩包”这种低级事故彻底挡在门外面。

5. 把排除逻辑固化成脚本,形成肌肉记忆

5.1 一个可直接套用的备份脚本

到这一步,原理和坑都讲清楚了,最后落地一个可以放到生产环境直接用的备份脚本。这个脚本我自己的服务器上就在跑,核心逻辑就是“cd 到项目父目录,用相对路径打包,集中管理排除规则,最后校验完整性”。

#!/bin/bash # 项目目录备份脚本 # 使用前修改以下配置 BACKUP_DIR="/data/backups" PROJECT_DIR="/opt/app/blog" STAMP=$(date +%Y%m%d_%H%M%S) EXCLUDE_FILE="/opt/backup_config/blog_exclude.txt" # 确保备份目录存在 mkdir -p "$BACKUP_DIR" # 进入项目父目录,确保压缩包内路径是相对路径 cd "$(dirname "$PROJECT_DIR")" || exit 1 PROJECT_NAME="$(basename "$PROJECT_DIR")" # 执行压缩,排除规则从文件读取 zip -r "${BACKUP_DIR}/${PROJECT_NAME}_${STAMP}.zip" "$PROJECT_NAME" \ -x@"$EXCLUDE_FILE" if [ $? -eq 0 ]; then unzip -t "${BACKUP_DIR}/${PROJECT_NAME}_${STAMP}.zip" >/dev/null 2>&1 \ && echo "[OK] 压缩完成且校验通过: ${BACKUP_DIR}/${PROJECT_NAME}_${STAMP}.zip" \ || echo "[WARN] 压缩可能存在问题,请手动检查" else echo "[ERROR] 压缩失败,请检查磁盘空间和权限" exit 1 fi

加上定时任务,每天凌晨自动备份:

0 2 * * * /opt/scripts/backup_blog.sh >> /var/log/backup_blog.log 2>&1

5.2 常用排除项速查表

exclude.txt的时候,下面这份清单可以直接当参考。每一项都是实操中遇到过、确实会混进压缩包的“垃圾对象”。

排除对象推荐模式原因
Node 依赖目录*/node_modules/*体积巨大,随时可重装
Git 版本历史*/.git/*仓库历史不属于交付物
SVN 元数据*/.svn/*同上
日志目录*/logs/*越来越大,备份价值低
临时目录*/tmp/*通常无保留价值
缓存目录*/cache/*可再生成
构建产物*/dist/**/.next/**/.nuxt/*与源码备份需求冲突
Python 缓存*/__pycache__/*可自动生成
编辑器配置*/.idea/**/.vscode/*属于开发者本机配置
macOS 元数据.DS_Store基本是噪音
环境变量文件*/.env敏感信息,涉及安全
通用日志文件*.log覆盖未归类目录里的日志

表格里每一项都值得你根据自己的项目场景确认一下。比如dist目录,如果备份的目的是保留可部署产物,可能反而要保留;如果备份的是源码交付,那就要排除。规则没有绝对的正确答案,关键是你知道自己在做什么、为什么这样配。

5.3 和 tar 的 --exclude 做个对照,防止混淆

因为tar在 Linux 里同样高频,很多人会在两个命令之间来回切换,然后写混参数。对照一下:

# zip 排除文件 zip -r backup.zip app -x "*/node_modules/*" # tar 排除文件 tar -czf backup.tar.gz --exclude="*/node_modules" app

两者的逻辑完全一样,但语法不同。tar用的是--exclude,zip 用-x;tar 的排除模式对目录是否需要带/*要求没那么严格,但为了保险,同样建议写成"*/node_modules"这种形式(tar 的语义是排除该目录及其下所有内容)。两套命令差不多,但千万不能把参数给串了——我见过有人在tar命令里敲-x "*/node_modules/*",结果-x被 tar 解释成了“解压”,后面的命令行为完全走样。

压缩命令看似基础,但“排除不需要的文件”这个需求,在实际生产里踩坑的概率远比想象中高。从-x的匹配机制到引号问题,从符号链接到编码乱码,每一项都是真实环境里反复出现过的故障点。我个人在大量打包项目目录后形成的习惯是:先看目录结构再动手,排除规则固定成模板文件,打包完必跑一次unzip -t。这套流程看起来朴素,但它确实让我再没有犯过把node_modules发给别人这种尴尬错误。如果你在服务器上做备份也遇到类似困惑,照着上面的排查链路走一遍,大概率能把问题定位到根上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询