
在 Linux 下用 zip 命令压缩文件几乎是所有人接触服务器后的第一堂必修课。但“压缩时怎么排除不需要的文件”这件事却很少有人一次讲明白。你很可能遇到过明明只想打个项目包结果 .git、node_modules、logs 全被卷进去压缩包体积暴涨发给别人还显得特别不专业。这篇文章不绕圈子直接把 Linux 里 zip 压缩时排除不需要文件的方法、通配符写法、典型场景和踩坑案例全部拆开适合刚学 Linux 的新人也适合想把备份流程做得更规范的运维朋友。1. 为什么压缩时总想把某些文件“踢出去”1.1 不设排除规则打包会发生什么先还原一下最常见的场景。你在一台服务器上维护某个项目目录结构大概是这样的project/ ├── .git/ ├── src/ ├── logs/ ├── uploads/2024/ ├── node_modules/ ├── config.env └── readme.md很多人第一次打包就会直接执行zip -r project.zip project/看起来没问题但 zip 的默认行为是“把目录里所有能扫描到的内容全部写进包”包括隐藏文件。于是压缩完之后你用unzip -l一看包里的实际内容变成了$ unzip -l project.zip | head -30 Archive: project.zip Length DateTime Name --------- ---------- ---------- 0 01-01-2025 10:00 project/.git/ ... 0 01-01-2025 10:01 project/node_modules/ 12345678 01-01-2025 10:10 project/node_modules/some-dependency/dist/index.js ... 4096 01-01-2025 10:12 project/logs/app.log.20250101这时候压缩包已经不是一个“项目代码的快递包”而是一份“整目录全量快照”。.git 这种版本控制目录占了几百 MBnode_modules 这种依赖目录更是几 GB 起步收包的人还要花时间解压一堆根本用不上的文件。最危险的是另一种情况如果目录里有 config.env、.env、xxx.pem 这类敏感配置你没有排除就直接打包发给别人等于把生产环境的配置、数据库地址、甚至密钥一起交给了对方。我在实际工作中见过不止一次因为打包不过滤导致的“信息顺带外流”所以现在凡是做线上备份或对外交付一定会带上一套固定的排除规则。1.2 通用的排除清单怎么定排除清单会随着用途变化但有几类文件是几乎每次都要排除的。我把它整理成一份常用清单第一次写命令时可以直接套分类典型文件或目录排除原因版本控制目录.git/、.svn/、.hg/仓库元数据接收方不做 git 操作时毫无用途依赖目录node_modules/、vendor/、.venv/、venv/可通过安装命令重新生成体积大得离谱日志文件logs/、*.log、*.log.*日志是时点性数据归档时旧日志往往不需要临时与缓存*.tmp、*.swp、.cache/、__pycache__/、.DS_Store可再生成进了包只会污染内容敏感文件.env、config.local.php、*.pem、*.key这是安全底线建议永远排除大型产物dist/、build/、*.zip、*.tar.gz压缩嵌套大文件会浪费时间包体也臃肿建立清单意识比记住命令更重要。因为一旦没有固定清单打包时就会临时凭感觉决定“这个目录太大好像可以不带”结果漏掉真正不该带的文件等包传出去再补救就非常被动了。2. 排除文件的核心-x 参数怎么用2.1 一条最简单的带排除压缩命令zip 命令本身提供了-x参数全称--exclude作用就是“排除后面跟着的一串路径模式”。最简单的用法是zip -r project.zip project/ -x project/logs/*这条命令会在打包过程中跳过所有匹配project/logs/下面内容。-x后面可以跟一个模式也可以空格分隔跟多个zip -r project.zip project/ -x project/logs/* project/tmp/*你还可以连续多次写-xzip -r project.zip project/ -x project/logs/* -x project/tmp/*这两种写法我都试过效果基本一致。我建议保持统一的命令顺序zip -r 压缩包名 要压缩的目录 -x 排除模式1 排除模式2这个顺序最不容易错换到不同机器上执行也不容易出问题。还有人喜欢把-x放在压缩包名前面zip 某些版本也允许但可读性差一旦模式写多了后面的人很难看懂这条命令到底排了什么。注意zip -r里的-r一定不能省。zip 对目录默认不递归少了-r它只会在压缩包里留下一个空目录引用目录下的文件根本不会被压进去。2.2 通配符规则让排除模式跨目录生效排除模式支持通配符这块反而是很多人的易错点。zip 的通配符和 shell 的 glob 规则“看起来像又不完全像”。*匹配任意长度的任意字符包括路径分隔符/。这一点和 tar、find 的常见行为不太一样find 的*默认不跨目录层级但 zip 的*可以直接斜杠通吃。举个例子你想排除所有层级的 node_modules用zip -r app.zip app/ -x *node_modules*这个模式能匹配到app/node_modules、app/src/node_modules、app/packages/module_a/node_modules。因为*把目录层级整个包进去了。这也是 zip 排除模式里最好用的点。?只匹配单个字符。比如*.log.???能匹配app.log.001、app.log.bak但不会匹配app.log.0012。[]字符集合也能用比如排除图片文件时写*.[jJ][pP][gG]可以匹配.jpg和.JPG但这类用法在排除场景里相对少能看懂就行。这里还涉及一个很重要的底层逻辑zip 的排除模式匹配的是“压缩包内部的虚拟路径”不是压缩前机器上的绝对路径。同样一份project用zip -r a.zip project/压缩后包内路径会带project/前缀如果用zip -r a.zip .在当前目录执行包内路径可能是./src/index.js或src/index.js不同 zip 版本的表现还有细微差异。为了绕开前缀问题一个特别稳的写法是排除模式尽量写成*/关键字/*这种宽松格式。比如要排除.git不要只写.git/*而是写*/.git/*和*/.git一起上要排除日志直接写*.log让*跨目录去匹配这样不管你在哪个目录下执行结果大概率是一致的。2.3 排除目录本身 vs 只排目录内容这里有个很多人不知道的细节。排除一个目录有两种理解一种是把整个目录连同目录项一起拿掉另一种是把目录里的内容拿掉但那个空目录还留在压缩包里。比如执行zip -r backup.zip data/ -x data/cache/*压缩结束后你可能会在 zip 包里看到data/cache/这个空目录。原因就是排除模式只匹配了cache/下面的内容没有匹配目录项本身。如果不想看到空目录需要把目录项也加入排除列表zip -r backup.zip data/ -x data/cache data/cache/*总结一下希望“目录内容全部不要但目录结构保留一个占位”写dir/*就够了希望“目录和目录里的内容全都不要”建议同时写dir与dir/*。这个细节最容易在“明明排了目录压缩包里怎么还有一个空文件夹”的疑问里被注意到。下次遇到先别怀疑 zip 有 bug多半是只匹配了内容、没匹配目录项。3. 实战五个高频场景的完整命令3.1 打包项目代码排除 .git、node_modules、开发产物从仓库拉了一份代码现在要打包发给别人或上传到内部环境推荐这样写cd project/ zip -r ../project-release.zip . -x */node_modules/* */.git/* */.git */__pycache__/* *.log拆开解释一下思路*/node_modules/*匹配任意层级的依赖目录内容*/.git/*和*/.git一起用确保 .git 目录项本身也被拿掉*/__pycache__/*给 Python 项目准备避免 .pyc 缓存混入*.log排除任意层级的日志文件。如果项目里有.env这类敏感配置别忘记加一条zip -r ../project-release.zip . -x */node_modules/* */.git/* */.git */__pycache__/* *.log .env *.env有人会问.env和*.env有什么区别前者更偏向匹配根目录下的.env后者匹配任意位置所有以.env结尾的文件。两个一起写覆盖面更完整。3.2 备份日志与运行目录排除旧日志、pid、临时文件服务器上做日志归档时经常只想保留当前运行状态下的必要文件cd /var/log/app/ zip -r /backup/log-$(date %Y%m%d).zip . -x *.log.* *.pid *.tmp *.swp这里*.log.*排除的是带有日期后缀的滚动日志*.log可能需要保留最新一份如果不分新旧都要排除直接写*.log即可。这种带日期的压缩包命名方式很实用执行$(date %Y%m%d)后文件名会自动变成log-20250120.zip这种格式。配合 crontab 定时跑日志归档基本能做到无人值守。3.3 打包前用 find 和 rsync 先过滤再交给 zip绝大多数场景用-x就够了但有一种情况我推荐换思路排除条件过于复杂比如“排除两天前的日志”“排除大于 100MB 的文件”“排除名字带某关键字的文件”。这种需求用-x写模式会非常痛苦筛还容易漏。我看到很多运维喜欢用 find 先筛文件列表cd /srv/www/myapp/ find . -type f -mtime -2 -print0 | xargs -0 zip -r app_$(date %Y%m%d).zip这条命令只把最近两天修改过的文件打包旧文件全部绕开。但这里有个坑用 find 生成的列表直接交给 zip压缩包里的目录结构不一定完整而且如果文件路径里带空格没有-print0配套-0参数就会出错。我自己更偏爱另一种思路清晰的做法先用 rsync 做一次带排除的复制再对预检目录打包rsync -a --exclude*.log --excludecache/ /srv/www/myapp/ /tmp/myapp_prep/ zip -r myapp_$(date %Y%m%d).zip /tmp/myapp_prep/*这样排除规则是 rsync 的打包逻辑是 zip 的两边互不干扰。虽然多了一次磁盘 I/O但中小目录完全可接受而且排错特别直观一看/tmp/myapp_prep/里的内容就知道规则有没有生效不用反复解压压缩包。3.4 用脚本变量维护排除模式避免每回重写如果你已经需要写 shell 脚本强烈建议把排除模式收进数组统一管理#!/usr/bin/env bash EXCLUDES( */.git/* */.git */node_modules/* logs/* *.log *.tmp .env ) zip -r $1 . ${EXCLUDES[]}调用时这样用./backup.sh /backup/site_$(date %Y%m%d).zip以后增加一个排除目录只需要在数组里加一行。整个命令的可读性和可维护性都会有质的提升再也不用翻历史命令回忆上次用了哪些模式。3.5 打包完才想起来漏排了用 zip -d 补救偶尔会碰上“压缩包已经生成突然发现某个目录不该打进去”的情况。重新打包可能耗时很长这时候可以用zip -d从已有压缩包里删除目标文件zip -d app.zip *.log */.git/* */.git-d参数同样支持通配符匹配逻辑和-x基本一致。不过有一点要心里有数对大压缩包执行-d时zip 会重写整个压缩包相当于重新压缩一遍并没有想象中那么快。所以它更适合应急救火正常流程还是建议在打包前就用-x把规则定好。4. 排除不生效的常见坑与排查思路4.1 通配符没加引号被 shell 提前展开这是最典型的翻车现场。你写zip -r backup.zip . -x *.log由于没有加引号shell 会在执行 zip 前先把当前目录下所有满足*.log的普通文件名展开成参数列表。如果当前目录下恰好有access.log、error.logzip 实际收到的就不是*.log这个模式而是几个具体文件名结果是子目录里的日志依然全进来了排除等于没排。正确写法是把所有带通配符的模式用单引号或双引号包起来zip -r backup.zip . -x *.log凡是包含*、?、[]的排除模式都建议用引号包住。这不是 zip 的 bug而是 shell 的 glob 展开机制在作怪。4.2 路径前缀不一致模式匹配不上前面提过zip 排除模式匹配的是压缩包内部路径而你写命令时的路径不一定能和它完全对上。比如cd /data/ zip -r backup.zip project/ -x project/cache/*这条一般没问题因为压缩包内路径就是project/cache/...。但如果你换个执行方式cd /data/project/ zip -r backup.zip . -x project/cache/*压缩包里的实际路径已经变成cache/或./cache/了project/cache/*根本匹配不上cache 目录就这样悄悄进了包。遇到这种情况不用猜直接先看一眼包里到底长什么样zipinfo -1 backup.zip | head -20确认了包内真实路径格式之后再照着写排除模式。这也是我看路径错误的习惯动作比反复试命令效率高得多。4.3 目录排除了却又留下空目录问题另一个高频现象是“排是排掉了但压缩包里多了一个空目录项”。原因在 2.3 小节已经讲过。如果你发现空目录很扎眼就用dir加dir/*并排解决。我这里再补充一个容易误伤的写法zip -r backup.zip . -x *cache*这个模式看起来没问题但*cache*会匹配所有路径里带cache的内容包括cache_data这种名字里含 cache 的普通目录误伤范围非常大。更精确的写法应该是zip -r backup.zip . -x */cache */cache/*排目录时宁可多写一条精确的也不要图省事用一个宽泛到可能误伤的模糊模式。4.4 检测压缩包内容的快捷姿势压缩完成后做一次“体检”是排除规则生效的最可靠保障。我常用的检查姿势如下unzip -l archive.zip列出完整文件列表和总体积。如果被打包目录有 200MB压出来只有 2KB说明模式太宽把不该排的都排了。zipinfo -1 archive.zip只输出文件名方便直接交给 grep 做检查。zipinfo -v archive.zip查看单个文件的详细属性常用于确认软链接和权限位。unzip -t archive.zip测试压缩包完整性确认没有文件损坏。每次生产一个交付包我基本都会跑一条 grep 检查zipinfo -1 archive.zip | grep -E (node_modules|\.git|\.env|\.log) echo need check || echo clean输出是 clean才放心把包发出去。5. 把“排除文件”升级成自动化备份流程5.1 一个可直接落地的备份脚本模板如果每天都要做同样的事手敲命令终究不是办法。这里给你一个可以改改就用的脚本#!/usr/bin/env bash set -euo pipefail SRC_DIR${1:?请指定要打包的目录} OUT_DIR${2:-/backup} DATE$(date %Y%m%d_%H%M%S) ARCHIVE${OUT_DIR}/$(basename ${SRC_DIR})_${DATE}.zip EXCLUDES( */.git/* */.git */node_modules/* */__pycache__/* *.log *.tmp *.swp .env *.pem ) mkdir -p ${OUT_DIR} cd ${SRC_DIR} zip -r ${ARCHIVE} . ${EXCLUDES[]} # 压缩完立刻自检确认排除规则生效 if zipinfo -l ${ARCHIVE} | grep -Eq /(\.git|node_modules)/|\.env$; then echo [WARN] 排除规则未完全生效请检查参数。 else echo [OK] ${ARCHIVE} fi脚本里的set -euo pipefail我建议保留它能在变量未定义、命令出错、管道出错时马上中断执行避免“看起来成功、实际没跑对”的假象。自检那段是关键。压缩后立刻用zipinfo -l检查关键字一旦发现不该出现的内容立刻报警。这个环节我已经用它拦截过好几次因为路径前缀变化导致的误打包成本几乎为零却很管用。5.2 什么时候改用 tar、rsync 更合适zip 不是唯一能做排除压缩的工具说实话当排除规则复杂到用数组维护都费劲时就该考虑换工具了。维度ziptar gziprsync跨平台直接解压Windows/macOS 双击可开需要额外解压工具只同步不生成包排除语法-x配合通配符--exclude配合通配符--exclude/--exclude-from增量差异备份不支持弱强最合适场景对外交付包、上传对象存储服务器本地归档目录镜像、定时同步备份zip 最大的优势是跨平台兼容性好交付给不熟悉命令行的同事、上传到对象存储后让其他人直接下载解压它是默认选择。但做长期保留、定时镜像、增量同步这类备份任务时tar 与 rsync 明显更成熟排除规则也可以单独写进文件维护。5.3 给新手的几条打包习惯建议踩过那么多坑之后我给自己定了几条铁律分享给你参考第一排除模式一律加引号这是成本最低的防翻车手段。第二执行前先想清楚包内路径格式不确定就用zipinfo -1看两眼。第三压缩完成后别急着发先用 grep 做一次关键字检查。第四把常用的排除模式沉淀成脚本或数组不要每次临时发挥。最后也是最重要的一点敏感文件例如.env、key、pem 这类不管什么场景都进排除列表宁可漏打业务文件也不要冒风险把敏感信息打包发出去。我在实际使用中踩得最深的一次就是最开始把排除模式里的*忘了加引号导致排了半天日志文件压缩包里却一个不少。从那以后我给自己定了前两条规矩所有模式用单引号包住压缩完立刻跑一遍zipinfo -1 | grep检查。这两秒钟的操作帮我省下了无数次重新打包的时间。如果你还没试过把排除模式放进脚本变量我建议下一次打包时就尝试一次。把通配符、目录项、敏感文件都收进一个数组用不了几分钟但以后每次打包都会轻松很多。