ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux uniq命令详解:从sort配合到日志去重实战

Linux uniq命令详解:从sort配合到日志去重实战

1. 先搞清楚uniq到底在做什么

用Linux的人迟早都会遇到uniq,但很多人第一次用它的经历都不太愉快——明明是想把重复行去掉,结果文件一模一样,一行都没少。这不是uniq没干活,而是它天生就是个“挑食”的工具:uniq只处理相邻的重复行。换句话说,如果相同的行没有挨在一起,它就当没看见。这一点是理解uniq所有行为的地基。

打个比方,uniq就像一个只认前后邻居的保安。你在队伍里连续排了三个同一个人,他能帮你揪出来;但如果同一个人分别站在队头、队中、队尾,他就完全无感了。所以“配合sort使用”几乎是uniq的标配姿势:sort先把相同内容行排到一起,uniq再逐个压掉重复。

在处理日志去重、统计访问来源、清洗配置文件这类场景时,uniq + sort的组合能解决80%以上的文本去重需求。而且它跟awk、sed、cut这些命令搭配起来,能玩出很多花活。下面我直接把常用参数和实操案例展开讲,每一段都能直接在你的终端里跑。

2. 七个必会参数,逐个拆解

2.1 -c:让每个重复项都带上“计数”

uniq -c是统计类需求中使用频率最高的参数。它的作用是在每行前面加一个出现次数,格式上默认是右对齐的:先是空格,然后是数字,再接一个空格,最后是原来的行内容。

$ cat test.txt apple banana apple apple cherry $ sort test.txt | uniq -c 3 apple 1 banana 1 cherry

注意我在这里先执行了sort,因为原始文件里两个apple中间隔着banana,直接跑uniq -c只会统计到第一行的apple和第二行连着的apple,第三行单独的apple会漏掉。这是uniq新手最容易踩的坑。

-c的实际价值不只是告诉你每个值出现几次。在日志分析里,它往往是流水线的第一站。比如你拿到一堆访问日志,想知道哪些IP最活跃,先把IP字段抽出来排序,再用uniq -c计数,最后按数字倒序排一下,直接就能看到排名。

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

这条命令我几乎每周都用。sort -rn里的-r是倒序,-n是按数值而不是字典序排。如果不加-n,10会排在2前面,因为字典序是“10” < “2”。这个细节很多人栽过。

2.2 -d 和 -D:只看重复项,还是连组看

默认情况下,uniq把重复行只保留一行。但有些时候你偏偏想知道哪些行是重复的,或者想看重复行到底长什么样。

uniq -d表示只输出重复的行(每组重复只输出一行),uniq -D表示把所有的重复行都输出(一组也不折叠)。配合之前的test.txt:

$ sort test.txt | uniq -d apple $ sort test.txt | uniq -D apple apple apple

这里的区别很实用。比如你怀疑配置文件里某个参数被重复声明了,-d能快速定位到哪个参数重复了;但如果你想进一步分析重复的上下文,或者要验证重复项是不是内容完全一致,-D把所有重复行都铺开更直观。

还有一个值得记的变形:uniq -cd。这个组合可以把每个重复项的出现次数也带出来,适合做“哪些值重复出现超过N次”的筛选。比如找出现超过2次的重复IP:

$ awk '{print $1}' access.log | sort | uniq -cd | awk '$1 > 2 {print}'

这里第一段sort | uniq -cd先统计每个IP次数,第二段awk按数字过滤。两层管道一接,问题就解决了。

2.3 -u:把“只出现一次的行”捞出来

-u和-d正好相反:它只输出那些在文件中没有重复的行。这个参数在找“孤立项”时很好用。

比如你有一份设备序列号清单,想知道哪些序列号只在列表里出现一次(可能是登记异常或漏号),直接排序后加-u:

$ sort serials.txt | uniq -u

-u配合其他命令还能做集合运算。比如有两个文件A和B,想找在A中但不在B中的行,可以先合并排序再处理。当然专门的comm命令做集合对比更强,但sort fileA.txt fileB.txt | uniq -u在行内容不重复的前提下也能起到排查差异的作用,胜在命令短、直觉好记。

2.4 -i:忽略大小写去重

日志、配置、尤其是手工录入的数据里,大小写不统一是常态。FOX、Fox、fox在uniq看来是三行不同的内容,但业务层面它们可能是同一个值。

uniq -i会让uniq做比较时忽略大小写差异。注意:它影响的是“是否认为两行重复”,输出内容仍然保留第一次出现的那一行的原始大小写形态。

$ cat mixed.txt Error ERROR error Warning $ sort mixed.txt | uniq -i Error Warning

这里有个实际经验:-i和sort的配合有个先后讲究。sort默认按ASCII排序,大写字母排在小写前面,所以Error、ERROR、error会被排成ERROR、Error、error。此时uniq -i会把这三行都归为重复,输出时保留第一行,也就是全大写的ERROR。如果你想要保留小写形式,得先考虑好排序规则,或者用sort -f把大小写忽略后再排,这样输出的保留形态会不一样。

2.5 -w和-s:精准控制比较范围

有这样一种场景:每一行的前一部分是编号/时间戳,后一部分才是业务内容。你希望忽略前几位字符,只比较后面的内容;或者只比较前N个字符。

-s N表示跳过前N个字符开始比较,-w N表示只比较前N个字符。两个参数可以组合,控制比较的范围是一个“滑动窗口”。

举个例子,有三行日志:

2024-01-01 ERROR: disk full 2024-01-02 ERROR: disk full 2024-01-03 WARN: cpu load high

如果直接用uniq,三行互不相同。但我想按“错误类型”去重,也就是说想忽略开头的日期部分,拿第12个字符之后的内容做比较。首行是10位日期加一个空格,要跳过前11个字符:

$ uniq -s 11 messages.log 2024-01-01 ERROR: disk full 2024-01-03 WARN: cpu load high

第二行的ERROR: disk full因为和第一行除日期外完全相同,被判定为重复行直接压掉了。

反向场景:只需要比较每行前固定长度的前缀。比如一批数据行,前缀是状态码,后面跟详细描述,我想按状态码去重:

$ uniq -w 3 status.log

如果前缀包含空格或字段结构复杂,直接用-f参数会更优雅,见下一节。

2.6 -f:跳过N个字段

-f N表示在比较时跳过前N个字段。这里的“字段”默认由空白字符(空格或Tab)分隔,跟awk默认的字段分隔方式一致。

典型场景是处理“用户名 + 邮箱”这种两列数据。比如passwd文件里每行是用户名:x:UID:GID:描述:家目录:Shell,你只想比较UID那一列,就能写出类似这样的命令:

$ sort -t: -k3,3n passwd | uniq -t: -f2

不对,这里要纠正。uniq本身不支持-t指定分隔符,它只认固定的“空白字符作为字段分隔”。所以如果文件不是用空白分隔的,就没办法直接用-f,得先用tr或awk转换。

-f最顺手的使用场景是处理空格分隔的文本。比如一份记录文件,每行是日期 用户名 操作,想按“用户名”去重,即同一用户只保留一条操作记录:

$ sort -k2,2 data.txt | uniq -f1

-f1表示跳过第一个字段(日期),从第二个字段(用户名)开始比较。这里注意sort也按第二个字段排序,保证同一用户的记录相邻,否则uniq照样“看不见”。

2.7 参数组合:-c -d -i -s一起上

实际工作中很少只用一个参数,组合起来才能解决真实问题。举一个常见的混合场景:分析一批大小写混乱、带有重复前缀的统计文本。

$ cat raw.log 2024-05-01 Error: timeout 2024-05-01 ERROR: timeout 2024-05-02 error: timeout 2024-05-03 warn: retry $ sort -f raw.log | uniq -cid -s 11 3 error: timeout

我解释一下这条命令的执行过程:sort -f先忽略大小写排序;uniq -c要求带计数;-i表示比较时忽略大小写;-d表示只显示重复项;-s 11跳过每行前11个字符。最终输出把三个“不同日期但同样错误信息”的行合并成一组,并给出次数3。这样既忽略了前置日期,又忽略了大小写,还只保留了重复内容,一行命令就把多维度的处理都做完了。

3. 四个真实场景实操复盘

3.1 场景一:网站访问日志IP热度统计

从Nginx或Apache日志里统计访问最多的IP,是uniq最经典的应用。日志大概长这样:

192.168.1.10 - - [01/May/2024:12:00:01 +0000] "GET /index.html HTTP/1.1" 200 512 192.168.1.20 - - [01/May/2024:12:00:03 +0000] "GET /about.html HTTP/1.1" 200 340 192.168.1.10 - - [01/May/2024:12:00:05 +0000] "GET /images/logo.png HTTP/1.1" 304 0

完整统计命令是:

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20

四段管道各干一件事:awk抽IP列;sort把相同IP排成相邻;uniq -c给每个IP计数;sort -rn按次数从高到低排;head只取前20名。

输出的样例是:

1023 192.168.1.10 876 203.0.113.5 512 198.51.100.7

这里有个性能细节值得说:当access.log非常大时,管道里的中间数据都会经过排序,内存占用和磁盘IO都不小。这时有一个替代方案,直接用awk做统计,不排序,速度反而更快:

$ awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head -20

awk用关联数组做计数,不需要排序相同行,省掉了一大块开销。但代价是:如果IP种类极多(比如百万级),awk也会占用大量内存。对于一般规模的日志,两种方案都够用;我个人的习惯是日志文件在几百MB以内用sort + uniq,超过这个量级就改用awk。

3.2 场景二:两份列表求交集差集

假设你有两个文件,一个是本周活跃用户ID,一个是上月活跃用户ID,都是每行一个ID。想找出“本月新出现的用户”,即在本月列表中但不在上月列表中的。

一个直观做法:

$ cat this_week.txt last_month.txt | sort | uniq -u

等等,这有个逻辑问题。求差集不能简单地用uniq -u,因为如果两个文件里都有同一个ID,它会出现两次,uniq -u会直接把它丢弃(因为有重复),但我们要找的是“只在本月,不在上月”的ID,正确做法是把两个文件打标区分。

正确的姿势是用comm,或者给文件加标记:

$ awk 'NR==FNR {seen[$1]=1; next} !seen[$1]' last_month.txt this_week.txt

这条awk命令干的事是:读第一个文件last_month.txt时记录所有ID到seen数组;读第二个文件this_week.txt时,如果某行ID没出现在seen里,就输出。这是真正意义上的差集。

那uniq在这种场景里还有用吗?有。如果只是想看“两个文件合并后哪些元素不重复”,sort file1 file2 | uniq -u是合理的。它表达的是:集合里孤零零出现一次的元素。这个语义在“找两边都没配对的数据”时很有用。做数据校验时,我经常拿它快速扫一眼合并结果里的异常孤立值。

3.3 场景三:配置项重复声明检查

曾经处理过一份服务配置文件,几千行,里面有可能存在重复的配置项声明。排查方法是把配置项名称(每行第一个字段)抽出来排序,再统计重复项。

$ awk '{print $1}' app.conf | sort | uniq -d

如果发现输出里有配置项名称,再用grep -n定位每个重复声明的具体行号:

$ grep -n '^timeout' app.conf

这种“先用uniq找异常,再用grep定位”的模式排查文本问题特别顺手。uniq -d在这里相当于一个探测器,只报异常不报正常,输出量小,人眼扫起来不累。

3.4 场景四:脚本里的标志物去重

写Shell脚本时,经常会遇到一段文本里出现了某个标志多次,需要合并成一次。比如把多个来源的IPv4地址合并后去重,生成防火墙白名单:

$ cat ip_list_*.txt | sort -u > whitelist.txt

等一下,这里我用了sort -u而不是sort | uniq。原因在于sort -u在排序的同时直接去重,一步到位,效率更高,而且语义更清晰——“排完顺便去个重”。那uniq的定位在哪?uniq的优势在于“我已经有了一段排好序的数据,只想做轻量去重”,或者需要计数、保留重复组等sort -u做不到的操作。

脚本里比较经典的uniq用法是处理“连续重复标记”。比如一个上报系统会连续输出多行相同的状态信息,脚本里要去掉连续重复只保留变化点,就用:

$ uniq monitor.log

这里故意不排序,因为我们要的恰恰是“相邻去重”这个语义,保留时间线的变化,不把历史记录全部打乱。这说明uniq即使不配合sort也有自己的用武之地,关键看你想表达什么逻辑。

4. 进阶玩法:uniq与兄弟命令的协同

4.1 “两步走”和“一步到位”:sort -u vs sort | uniq

很多初学者会问:sort -u不是更简单吗?为什么还要分开写sort | uniq?

答案是:sort -u只解决“去重”一个需求;而uniq有计数、保留重复组、忽略大小写、跳过字段等一系列附加能力。当需求从“去重”变成“去重并统计次数”时,sort | uniq -c是唯一的选择。sort -u做不到计数。

另外还有一个性能细节。sort -u在排序过程中去重,内存消耗通常比sort | uniq略低一点,因为它在合并阶段就直接丢弃重复项,减少了最终输出和后续管道的负载。从执行结果看两者完全一样,但如果你在管道中间还有其他处理,分开写反而更灵活。

4.2 去掉uniq -c输出的前导空格

uniq -c输出的计数默认右对齐,前面带了若干空格。这在人眼阅读时很整齐,但如果你想把它喂给其他程序,或者转成CSV,前面的空格就成了干扰。

一个常见的清理手法是用sed或awk:

$ sort test.txt | uniq -c | awk '{$1=$1; print}'

awk '{$1=$1; print}'的作用是强制awk重算整行,把字段之间的多个空格压缩成单个空格,并且去掉行首空格。输出变成:

3 apple 1 banana 1 cherry

如果你想把计数和内容调换位置,比如变成内容,计数的CSV格式,可以这样:

$ sort test.txt | uniq -c | awk '{print $2, $1}' | tr ' ' ',' apple,3 banana,1 cherry,1

4.3 和cut结合:按字段抽完再统计

有一个场景:日志文件是空格分隔的,你想统计第二列里的值分布。比如:

$ cut -d' ' -f2 access.log | sort | uniq -c | sort -rn

cut先把字段抽出来,后续流程和之前一样。这个组合在“字段名称已知、结构固定”的文件里非常顺手,比awk省一点心智负担。

值得注意的是,如果文件字段之间是多个连续空格,cut -d' '可能会抽出空字段。这种时候用awk按默认空白分隔反而更稳。我个人的经验是:结构规整的空格分隔文件用cut;字段分隔不规整的用awk,尽量别纠结。

4.4 处理Windows换行符的坑

从Windows传过来的文本文件,每行结尾是\r\n而不是\n。用uniq去重时,因为每一行末尾都粘着\r,看似内容相同的两行可能实际上是apple\r和apple\r——这种就算相同了。但更多时候是内容里有不同,比如一行是apple\r,另一行是apple,肉眼一样,uniq判定为不同行。

这不是uniq的问题,是数据源的编码问题。处理办法是先把\r去掉:

$ sed 's/\r$//' windows.txt | sort | uniq -c

每次从同事那边拿到Windows生成的日志,我都会习惯性先清理换行符。否则统计结果可能莫名其妙多出许多“看似重复实际不重复”的数据。

4.5 用uniq辅助做“批次边界检测”

假设你的程序每输出1000行日志会打一个特殊标记行---BATCH---,你想确认标记之间是否重新计数。把标记行也拉出去重,看是否有意外重复的批次标记:

$ grep 'BATCH' app.log | uniq -d

如果有输出,说明标记行重复了,可能是程序逻辑出了问题。这个用法利用的正是uniq“相邻重复判断”的特性,不需要排序,直接沿着时间线检查“连续出现两次相同标记”的异常。这类场景在监控脚本里能省不少事。

5. 常见问题与排查实录

在终端里跑uniq踩过的坑,我整理成了速查表,基本覆盖我日常遇到的大部分问题:

现象原因解决办法
uniq执行后文件一行没变相同行不相邻,uniq只处理连续重复先sort再uniq,或改用sort -u
计数结果右对齐,空格太多uniq -c默认输出格式如此用awk '{$1=$1; print}'清理空格
重复行没被识别行尾有\r(Windows换行符)先执行sed 's/\r$//'再处理
大小写不同的行被当不同行uniq默认区分大小写加-i参数
想去掉前几位时间戳再判断日期/前缀干扰了比较用-s N跳过前N个字符
字段分隔符不是空格uniq -f只认空白字符先用tr或awk转换分隔符
去重后顺序全变了排序步骤导致确认需求,需保留顺序时改用awk维护数组
大文件统计卡顿sort排序开销大改成awk直接用关联数组计数
只看到重复项里的一行-d和-D语义不同需要全部行时改用-D
中文内容识别异常locale排序环境影响设置LC_ALL=C或LC_ALL=zh_CN.UTF-8后重试

这里重点说一下中文排序环境的坑。当文本包含中文时,sort的排序结果受locale环境影响很大。在某些locale下,中文并不会按你预期的“拼音顺序”或“Unicode顺序”排列,而是可能按字节序排。这本身不影响uniq判断“是否相同”,但会影响哪些相同的行能被排到一起。如果中文数据去重结果不对劲,先把LC_ALL设置成一致的环境:

$ LC_ALL=C sort data.txt | uniq -c

设置为Clocale后,排序依据是字节序,结果确定性强,不容易受当前系统环境干扰。这个方法在和其他同事协作、复现问题时尤其重要。

还有一个常见误操作,是直接把uniq的结果重定向回原文件:

$ sort test.txt | uniq -c > test.txt # 危险

因为Shell的>重定向会在命令执行前就清空原文件,等管道里的命令真正开始运行时,文件内容已经没了。正确做法是先输出到临时文件,再覆盖原文件:

$ sort test.txt | uniq -c > tmp.txt && mv tmp.txt test.txt

这个坑我在刚学Shell时踩过,弄丢过一份还没备份的数据,从那以后凡是要覆盖原文件,我都坚持“先写临时文件再mv”的流程。多说一句:sort -o test.txt test.txt这种写法是安全的,因为sort -o对输入输出同一文件做了特殊处理,但uniq没有对应的-o参数,所以别指望它。

6. 用过的都说好:留下几句实在话

uniq这个命令表面上简单,但它真正考验的是你对“相邻”这两个字的理解。所有诡异表现,只要你记住它的工作前提是“只处理连续出现的重复行”,就都能解释通了。使用uniq超过半年后,我养成了一个习惯:凡是涉及文本去重的任务,先问自己三个问题——数据是否有序?比较时要忽略哪些维度?输出时要保留哪些字段?把这三个问题想清楚,命令基本一次就能写对。

最常用的组合里,sort | uniq -c是绝对的榜首,几乎任何“统计出现次数”的需求都能套这个模板。其次就是sort | uniq -d,定位重复项时输出干净利落。真正让我离不开uniq的,反而不是这些常规用法,而是它在管道里那种“轻量、专注、只做一件事”的气质。处理文本时,一个命令只解决一个问题,管道再长也清晰可控。

最后分享一个我从某个维护老系统的同事那里学到的技巧:把sort -u写成Shell函数,加一个交互确认,防止误覆盖数据文件。工具本身是死的,但使用习惯是活的,防御性地处理数据文件,什么时候都不吃亏。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进