
做运维和开发这行日志分析是日常逃不掉的活。每次拿到一堆日志文件最常见也最烦的操作就是把重复的行梳理出来或者统计每种记录到底出现了多少次。这种场景下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 到 -z2.1 去重的核心参数-c、-d、-uuniq最常用的三个参数是-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 出现了三次只是它们不相邻而已。加一个 sortsort 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?id1而你只想按/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 Nuniq -c的输出有个特点前面那一列计数是数字而且默认是右对齐、前面带空格。比如2 /api/user/info 1 /api/order/list这时候直接sort -rn是可以的因为sort的默认分隔符就是空白数字在第一个字段。但如果你后面还要用awk取第二列就要注意计数字段前面有空格直接取$2是取不到的。常见的处理办法是再包一层awksort access_tmp | uniq -c | awk {print $1, $2}这里的小坑在于uniq输出的计数列不是左边贴墙的有空格填充。大多数情况下用sort -rn排序没问题但如果你要让结果进表格软件处理建议加个sed把开头空格压缩掉... | uniq -c | sed s/^[ \t]*// | sort -rn还有一件事sort -rn里的-r是逆序-n是按数字大小排序。如果忘了加-nsort会按字符串字典序排那么 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_ALLCLC_ALLC sort log.txt | LC_ALLC uniq -cLC_ALLC会强制按字节逐个比较结果稳定、可预期。如果你确实希望按语言习惯来比如让大小写和重音符合某种规则那就别设 C用系统默认的 UTF-8 locale 就好。多字节字符还有一个副作用如果用-w限制比较字符数有的实现按字节数有的按字符数环境不同结果也不同。所以我处理含中文的文本时基本不用-w这种半吊子参数要么全行比较要么先用awk做字段切割。5.3 大文件性能经验uniq本身跑得快真正的性能瓶颈基本都出现在sort上。sort面对超大文件时如果内存放不下会在/tmp下创建临时文件磁盘 IO 就成了决定因素。加上/tmp空间不足sort会直接失败报错信息还不算友好。我的几个经验总结如下一是尽量先缩小数据。grep裁剪行、awk提取字段让进入sort的数据量尽可能小。二是给sort指定足够的内存上限和并发数比如sort -S 2G --parallel4会让大文件排序快不少。三是临时目录如果紧张可以临时指定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方案更吃内存。具体用哪种看你的数据特征和机器配置。最后我说一点个人习惯在写管道前我喜欢先在几万行的样本上试跑一遍确认输出格式符合预期再对整个大文件执行。这种方式帮我躲掉了很多次“跑了大半天发现命令写错”的惨案。日志分析这事慢不怕错才可怕。