做运维和开发这行,日志分析是日常逃不掉的活。每次拿到一堆日志文件,最常见也最烦的操作,就是把重复的行梳理出来,或者统计每种记录到底出现了多少次。这种场景下,uniq是出场率极高的命令之一。但这名字起得有点迷惑性——它并不是“把整份文件里重复出现的行全部合并”,而是只处理相邻的重复。很多第一次用它的人,栽的第一个跟头就在这里。
这篇文章不打算做参数手册式的罗列,而是把uniq从原理、参数到日志实战里的用法,掰开揉碎了讲一遍,中间穿插一些我自己踩过的坑和排查思路。适合刚接触 Linux 日常命令的新手,也适合已经会用但老觉得哪里不对劲的朋友。看完你会明白:uniq为什么叫“相邻去重”,在什么场景下不能直接用,以及怎么组合sort、awk和管道去处理真实日志。文里的命令我都实际跑过,不是从资料里抄来的。
1. 先搞清楚 uniq 的原理和基本行为
1.1 uniq 到底在比较什么
先说原理。uniq读入数据之后,是一行一行往下扫的,比较的对象不是“整份文件里所有行”,而是相邻的两行。它判断“当前这一行和下一行是否相同”,相同就视为一组,默认只保留这一组的第一行;如果碰到不同的行,就以为新的一组开始了。这里的关键是“相邻”。也就是说,如果相同的内容分散在文件不同位置,中间还夹着别的行,uniq根本照顾不到。
这个设计的底层逻辑其实很明确:uniq是一个流式工具,它没必要把整个文件读进内存做全局哈希,只需要维护很小的缓冲区,记住上一行的内容做比较基准。好处是内存占用极低,处理超大日志时特别稳。几十个 GB 的文件,只要磁盘读得动,uniq这边不会成为瓶颈。代价就是它对输入数据的“排布”有要求——重复项必须在相邻位置。
用生活里的例子来理解,就像你整理一叠扑克牌:先把相同的牌归拢到一起,再数每种牌有几张,非常轻松;但如果牌是散乱的,你每次只看眼前这一张和上一张是否一样,自然数不对。所以你会发现,uniq在现实场景中几乎总是和sort一起出现,sort负责“归拢”,uniq负责“统计和去重”,分工明确。
有个细节很多人会忽略:uniq默认的输出策略是“每组保留第一行”。哪怕某一行在这份文件里出现了十万次,只要这些行是连续的,uniq默认也只给你留一个代表。如果你以为不加参数就是“所有内容只出现一次”,那你就会在结果里看到重复的痕迹,从而误判数据。这个感知要先建立起来,后面讲参数的时候你会理解得更透。
1.2 一个最小例子:从文件到终端
直接跑一个最小例子。假设有个 test.txt,内容如下:
apple banana apple apple banana执行最简单的命令:
uniq test.txt输出是:
apple banana apple banana看仔细了:第三行和第四行是两个相邻的 apple,被合并成了一个;但第一行和第三行的 apple 中间隔了个 banana,没有相邻,所以没被合并。最后的 banana 也是同理。很多人第一次跑完这个例子,才恍然大悟uniq原来是这样工作的。
这个例子虽然基础,但它已经把uniq的脾气暴露得很清楚:你让它“去除重复”,它实际上是“合并相邻的相同行”。所以日常工作中,如果直接拿uniq去处理没有排序过的文本,结果往往会让你怀疑人生。
注意:
uniq后面可以直接跟文件名,也可以不给参数直接读标准输入。也就是说,cat file | uniq和uniq file效果一样,但前者多了一次管道读写的开销,文件大的时候没必要这么用。
2. 核心参数详解:从 -c 到 -z
2.1 去重的核心参数:-c、-d、-u
uniq最常用的三个参数是-c、-d、-u,我一个个说。
-c是给每一组统计出现次数。输出格式是在行首补一个计数,然后一个空格,再接原来的内容。比如还是刚才那个 test.txt,执行:
uniq -c test.txt输出:
1 apple 1 banana 2 apple 1 banana看到没有,这里有两组 apple,各自计数,第一组 1 个,第二组 2 个,因为它们在文件里不是连续的。你要是想统计“整个文件里 apple 一共出现几次”,这里是不行的,得先排序。这个放到第三节讲。
-d的意思是只输出重复过的组,每组只打印一行代表。换句话说,把文件里那些“有邻居”的行挑出来。还是用 test.txt 举例,uniq -d只会输出:
apple因为只有第二组 apple 是重复的。注意,这个参数不会告诉你重复了几次,也不会把每一行都打出来。如果你想把重复组里每一行都显示出来,可以用-D,这在 GNU 版本的uniq里是单独的选项。
-u和-d正好相反,只输出“完全没有重复过”的行。在 test.txt 里,三个孤零零的单行组都会被挑出来,所以-u的结果是:
apple banana banana第一行的 apple 是一个独立组,第二行和第五行的 banana 也各自是独立组,而第三第四行那两个连续的 apple 是重复组,不输出。这个行为和很多人的直觉不一样,但习惯之后就非常好用——你想知道哪些内容只出现了一次,直接-u就能筛出来。
为了方便查,我把这些参数的效果整理成一张小表:
| 参数 | 含义 | 输出效果 |
|---|---|---|
| -c | 统计每组出现次数 | 每行前加计数 |
| -d | 只显示重复过的组 | 每组输出一行 |
| -D | 显示重复组里的每一行 | 输出重复组的完整内容 |
| -u | 只显示没有重复过的行 | 每组输出一行 |
2.2 控制比较范围:-f、-s、-w
这三个参数影响的是“比较时应该看哪些内容”,而不是最终输出的内容。
-f N是跳过每行前 N 个字段再比较。字段默认以空白分隔。举个例子,日志行是这样:
2025-06-01 10:00:01 ERROR disk full 2025-06-01 10:00:02 ERROR disk full如果我们只想看ERROR disk full部分,不想让前面的时间戳影响去重,可以:
uniq -f 2 log.txt它会跳过每行的前两个字段,也就是日期和时间,再去比较后面的内容。像“同一句话在短时间内反复刷屏”这类日志,用-f非常适合。
-s N则是跳过每行的 N 个字符,然后才开始比较。它和-f的区别是-f按字段跳,-s按字符跳。比如行首固定有一串流水号,长度固定,你想忽略它,就用-s加长度。
-w N正好相反,它只比较每行前 N 个字符。比如你只想关心行首的错误码,不想管后面的一大段描述:
uniq -w 5 log.txt只要前五个字符相同,就当作同一行。这三个参数可以组合使用,但说实话,组合的场景不多,多数时候用其中一个就够了。
有个值得提醒的点:-f、-s、-w影响的是“比较范围”,不影响输出内容。也就是说,即使跳过了时间戳,输出的还是完整行,时间戳依然在。别人拿结果去看,一眼能看出原始数据,这点是好的。
2.3 容易被忽略的 -i、-z
-i是忽略大小写。比如Apple和apple默认被当成两行,加了-i就会视为重复。这个参数在统计用户输入、或者合并大小写不一致的数据时非常有用。
-z是处理的一种特殊分隔方式:不用换行符,而是用空字符(null)来分隔每一行。啥时候需要呢?举个典型场景:find命令配合-print0输出文件名,文件名里如果带了换行符,正常的uniq会被换行符卡断,用-z就能正确处理:
find /data -type f -print0 | uniq -z但要注意,-z输出的也是 null 分隔,你最好不要直接拿到终端当文本看,否则一眼看去全是乱糟糟的一坨。它更多的用途是在脚本和管道里和其他支持类似参数的命令配合,比如xargs -0。
还有个实际坑:早期有些版本的uniq不支持-z,或者不同系统的行为不一致。常见的 Linux 发行版基本都支持了,但如果你在某个精简的嵌入式环境里跑,命令行报不支持是很正常的,不用太惊讶。
3. 为什么 uniq 要配 sort,以及具体怎么配
3.1 相邻去重的坑:一个真实的翻车现场
说一个我自己的经历。有一次排查线上接口错误,从网关日志里手工提取了一些状态码,内容长这样:
200 500 200 404 200 503我想统计每个状态码出现的次数,很自然地写了:
uniq -c status.txt结果输出:
1 200 1 500 1 200 1 404 1 200 1 503看起来好像每个错误码都只出现过一次,如果我不小心,就会得出“没有大量重复错误”的结论。但事实是 200 出现了三次,只是它们不相邻而已。加一个 sort:
sort status.txt | uniq -c输出:
3 200 1 404 1 500 1 503这才是正确的统计结果。这次翻车让我印象特别深:日志分析里,凡是遇到uniq,先问自己一句“输入排好序了吗”。没排序就用uniq,跟闭着眼睛数扑克牌差不多。
3.2 sort 和 uniq 的经典组合拳
所以最经典的用法就是两个字:先排再去。网上百分之九十的教程里,uniq出现的位置都长这样:
sort access.log | uniq -c | sort -rn第一段sort把重复行归拢到相邻,中间uniq -c完成分组计数,最后再用反向数字排序,把出现次数最多的组放到最上面。整个链条非常顺,而且全程没有中间垃圾文件。
到这里会有个疑问:那sort本身有-u参数,sort -u file直接就能去重,为什么还要sort | uniq?其实两者结果在多数情况下是一样的,差别在于sort -u是在排序过程中直接去掉重复,而sort | uniq -c能得到每组计数。如果你想输出“每种内容出现几次”,就一定得用uniq -c。反过来,如果只是单纯要一个不重复列表,sort -u更省事。
另外提醒一个细节:sort的去重比较和uniq的去重比较,在 locale 不同的环境下可能有一点点差异,极端情况下sort -u的结果和sort | uniq不完全一致。遇到这种怪事,先怀疑 locale,后面第五节还会展开说。
3.3 不想动原文件?用管道就地解决
所谓的“就地解决”,是我个人比较喜欢的一种工作方式:不生成中间结果文件,全部用管道串在一起。这样做的好处是省磁盘、省时间,而且命令写完后可以当成一个整体复用到别的日志上。
举个例子,我想看看日志里哪些接口被调用次数最多:
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20这一行里没有任何中间文件。如果你的日志特别大,还可以在第一段sort前先做个裁剪,只提取你关心的字段。比如先grep出错误请求,再提取接口路径,最后统计:
grep ' 500 ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20管道命令多了以后,排错要有点耐心。我习惯是先把管道分段测试:先跑awk '{print $7}'看看输出字段是否合理,再跑sort,再跑uniq -c,一段一段验证。每次都把整串命令丢上去跑,出了错你根本不知道是第几段的问题。
4. 实战:日志分析里的 uniq 用法
4.1 统计接口访问次数
假设访问日志每行是标准的添加型格式,字段以空格分隔,类似这样:
192.168.1.10 - - [01/Jun/2025:10:00:01 +0800] "GET /api/user/info HTTP/1.1" 200 5321 192.168.1.11 - - [01/Jun/2025:10:00:02 +0800] "GET /api/order/list HTTP/1.1" 200 3210 192.168.1.10 - - [01/Jun/2025:10:00:03 +0800] "GET /api/user/info HTTP/1.1" 200 5321在这种标准格式里,请求路径刚好是第七个字段,所以提取路径并统计次数的命令就是:
awk '{print $7}' access.log | sort | uniq -c | sort -rn用上面的样例,会得到:
2 /api/user/info 1 /api/order/list这一步就完成了“谁最热”的初步观察。如果接口名本身带查询参数,比如/api/user/info?id=1,而你只想按/api/user/info统计,就得提前把问号后面的内容截掉。这时候用awk的split或者sed都可以:
awk '{print $7}' access.log | sed 's/\?.*//' | sort | uniq -c | sort -rn这种“先裁剪、再归拢、后统计”的思路,在日志分析里特别常用。数据量越大,提前裁剪收益越明显,因为进入sort的数据越小,排序越快。
4.2 抓出重复的错误码
还有一种场景是抓应用错误日志里反复出现的信息。应用日志一般有一定格式,比如:
[java] 2025-06-01 10:00:01 ERROR 数据库连接超时 [java] 2025-06-01 10:00:01 ERROR 数据库连接超时 [java] 2025-06-01 10:00:02 WARN 磁盘空间不足 [java] 2025-06-01 10:00:03 ERROR 数据库连接超时想找出哪些错误信息刷得最凶,我的做法是:
grep 'ERROR' app.log | sort | uniq -c | sort -rn | head -10这里的关键是grep之后别忘了sort。如果日志里的 ERROR 行是穿插着不同内容出现的,直接把grep结果喂给uniq -c,又会出现我在 3.1 里翻车的情况。
排序之后你会得到一组带次数的错误信息,基本一眼就能看出哪条报错最猖獗。我经常在排查线上故障时用这条命令快速定位“当前影响面最大”的错误类型,比一把一把翻日志高效得多。想看得更细,还可以把head后面的数字调大,或者用grep把次数的阈值卡住,都是顺手的事。
4.3 按次数排序并截取 Top N
uniq -c的输出有个特点:前面那一列计数是数字,而且默认是右对齐、前面带空格。比如:
2 /api/user/info 1 /api/order/list这时候直接sort -rn是可以的,因为sort的默认分隔符就是空白,数字在第一个字段。但如果你后面还要用awk取第二列,就要注意计数字段前面有空格,直接取$2是取不到的。常见的处理办法是再包一层awk:
sort access_tmp | uniq -c | awk '{print $1, $2}'这里的小坑在于uniq输出的计数列不是左边贴墙的,有空格填充。大多数情况下,用sort -rn排序没问题,但如果你要让结果进表格软件处理,建议加个sed把开头空格压缩掉:
... | uniq -c | sed 's/^[ \t]*//' | sort -rn还有一件事:sort -rn里的-r是逆序,-n是按数字大小排序。如果忘了加-n,sort会按字符串字典序排,那么 9 会排在 10 前面,统计结果就乱了。这三个字符r、n一块儿用,基本上是我的肌肉记忆。
5. 常见坑与排查心得
5.1 头尾空格导致去重失效
这是新手最容易碰到的一类问题:明明两行内容看起来一模一样,uniq却就是不去重。原因往往是行首或行尾藏了不可见的空格。比如从某个老系统导出的文件,行尾带着\r回车符;或者数据在导出时,自动补了若干空格对齐。uniq是逐字节比较的,多一个空格就不是同一行了。
排查方法是把文件内容可视化。用cat -A可以看到行尾的$和 Tab 的表示;sed -n '1,5l'也能把不可见字符显示出来。确认有空格后再清洗:
sed -i 's/[ \t]*$//' file.txt如果还有 Windows 换行符,顺手一起处理:
sed -i 's/\r$//' file.txt清理完之后再排序去重,结果就正常了。这个坑我栽的次数不少,尤其是从别人那里拿到的导表文件,十次里能有一半带着尾部空格。你可以在写脚本时直接把这步清洗放在uniq之前,一劳永逸。
5.2 中文字符和 locale 问题
涉及到中文字符,uniq的行为和环境变量的 locale 有关系。locale 设置不同,比较规则也会变。像以前遇到过,在某个系统的默认 locale 下,两个看似不同的中文行被uniq当成重复了,或者反过来。对于纯 ASCII 的日志,这个问题一般不会暴露,但一旦日志里有中文、日文等多字节字符,就可能出现怪现象。
我给出的建议是:如果只是为了做机械去重,不希望受 locale 影响,可以在命令前统一设置LC_ALL=C:
LC_ALL=C sort log.txt | LC_ALL=C uniq -cLC_ALL=C会强制按字节逐个比较,结果稳定、可预期。如果你确实希望按语言习惯来,比如让大小写和重音符合某种规则,那就别设 C,用系统默认的 UTF-8 locale 就好。
多字节字符还有一个副作用:如果用-w限制比较字符数,有的实现按字节数,有的按字符数,环境不同结果也不同。所以我处理含中文的文本时,基本不用-w这种半吊子参数,要么全行比较,要么先用awk做字段切割。
5.3 大文件性能经验
uniq本身跑得快,真正的性能瓶颈基本都出现在sort上。sort面对超大文件时,如果内存放不下,会在/tmp下创建临时文件,磁盘 IO 就成了决定因素。加上/tmp空间不足,sort会直接失败,报错信息还不算友好。
我的几个经验总结如下:一是尽量先缩小数据。grep裁剪行、awk提取字段,让进入sort的数据量尽可能小。二是给sort指定足够的内存上限和并发数,比如sort -S 2G --parallel=4,会让大文件排序快不少。三是临时目录如果紧张,可以临时指定TMPDIR到空间充足的盘上:
TMPDIR=/data/tmp sort -S 2G access.log | uniq -c | sort -rn如果文件的重复度特别高,但你又只需要“每种内容出现一次”,那用awk配合关联数组可能比sort | uniq更快,因为awk只需要过一遍文件:
awk '!seen[$0]++' big.txt这个写法对“保留首次出现顺序、全局去重”特别有用,是uniq做不到的。但如果是统计次数,awk的内存会随不重复行数增长,大文件下可能比sort方案更吃内存。具体用哪种,看你的数据特征和机器配置。
最后我说一点个人习惯:在写管道前,我喜欢先在几万行的样本上试跑一遍,确认输出格式符合预期,再对整个大文件执行。这种方式帮我躲掉了很多次“跑了大半天发现命令写错”的惨案。日志分析这事,慢不怕,错才可怕。