Linux使用uniq命令去除重复行的技巧分享
在 Linux 的世界里,文本处理是日常运维、数据分析、日志排查等工作中最频繁的操作之一。而 uniq 命令作为 GNU coreutils 中的一员,虽然看似简单,却蕴藏着强大的去重能力。很多人误以为 uniq 可以直接“智能”地删除所有重复行——其实不然!它要求输入必须已排序才能正确识别相邻重复项。本文将带你深入掌握 uniq 的各种用法,并通过 Java 实现对比其逻辑,辅以图表和实用链接,让你彻底吃透这个经典命令。
什么是 uniq?
uniq 是一个用于报告或忽略文件中重复行的命令行工具。它的核心作用是:仅对连续重复的行进行去重或计数。这意味着如果你有一个未排序的文件,直接使用 uniq 并不能达到全局去重的效果!
关键前提:输入必须有序!
$ cat unsorted.txt apple banana apple cherry banana $ uniq unsorted.txt apple banana apple # ← 仍然出现!因为不连续 cherry banana # ← 仍然出现!
只有当数据经过 sort 排序后,uniq 才能发挥真正作用:
$ sort unsorted.txt | uniq apple banana cherry
正确姿势:sort file.txt | uniq
uniq 常用选项详解
| 选项 | 说明 |
|---|---|
-c | 在每行前显示该行重复次数(count) |
-d | 仅显示重复的行(至少出现两次) |
-u | 仅显示唯一的行(只出现一次) |
-i | 忽略大小写比较 |
-f N | 忽略前 N 个字段(以空白分隔) |
-s N | 忽略前 N 个字符 |
-w N | 仅比较前 N 个字符 |
实战演练:基础去重
假设我们有一个日志文件 access.log,内容如下:
GET /home POST /login GET /home GET /profile POST /login GET /home
我们想统计每个请求路径的访问频次:
$ sort access.log | uniq -c
3 GET /home
1 GET /profile
2 POST /login
输出结果清晰展示了每个唯一行及其出现次数。
高级技巧:忽略字段与字符
有时我们并不关心整行是否相同,而是希望基于部分字段或字符进行去重。
示例:忽略前两个字段
假设我们有如下数据:
2024-05-01 user123 login_success 2024-05-02 user123 login_failed 2024-05-03 user123 login_success 2024-05-04 user456 login_success
我们只想根据“用户名 + 状态”去重,忽略日期:
$ sort data.txt | uniq -f 1 2024-05-01 user123 login_success 2024-05-02 user123 login_failed 2024-05-04 user456 login_success
-f 1 表示跳过第一个字段(即日期),从第二个字段开始比较。
结合其他命令使用
uniq 经常与 sort、awk、grep、cut 等组合使用,构建强大的文本处理流水线。
案例:找出访问最频繁的 IP 地址
假设 nginx.log 格式为:
192.168.1.1 - - [01/May/2024:10:00:00] "GET /index.html" 192.168.1.2 - - [01/May/2024:10:01:00] "GET /about.html" 192.168.1.1 - - [01/May/2024:10:02:00] "GET /contact.html"
提取 IP 并统计:
$ awk '{print $1}' nginx.log | sort | uniq -c | sort -nr
2 192.168.1.1
1 192.168.1.2
再加一步取 Top 3:
$ awk '{print $1}' nginx.log | sort | uniq -c | sort -nr | head -3
数据可视化:mermaid 图表展示处理流程

这个流程图清晰展示了从原始数据到分析结果的完整链路,适用于大多数日志分析场景。
uniq 的局限性
尽管 uniq 强大,但它也有几个明显的短板:
- 依赖排序:必须先
sort,否则无法正确去重。 - 内存限制:超大文件排序可能耗尽内存。
- 不支持正则匹配:无法按模式模糊去重。
- 单行比较:无法跨行或结构化比较。
对于更复杂的去重需求,我们可以转向脚本语言如 Python、Java 或使用数据库。
Java 实现 uniq 逻辑
下面我们用 Java 编写一个简易版的 uniq 工具,支持 -c、-d、-u 三个常用参数。
Step 1: 定义命令行参数解析
import java.util.*;
public class SimpleUniq {
private boolean count = false;
private boolean showDuplicatesOnly = false;
private boolean showUniqueOnly = false;
public void parseArgs(String[] args) {
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "-c":
count = true;
break;
case "-d":
showDuplicatesOnly = true;
break;
case "-u":
showUniqueOnly = true;
break;
default:
// 假设后续是文件名,这里简化处理
break;
}
}
}
}
Step 2: 实现核心去重逻辑
public void processLines(List<String> lines) {
if (lines.isEmpty()) return;
Map<String, Integer> lineCount = new LinkedHashMap<>();
String prevLine = null;
// 模拟 uniq 的“相邻去重”逻辑
for (String line : lines) {
if (!line.equals(prevLine)) {
lineCount.put(line, lineCount.getOrDefault(line, 0) + 1);
} else {
// 如果和上一行相同,增加计数
int currentCount = lineCount.get(line);
lineCount.put(line, currentCount + 1);
}
prevLine = line;
}
// 输出结果
for (Map.Entry<String, Integer> entry : lineCount.entrySet()) {
String line = entry.getKey();
int cnt = entry.getValue();
if (showDuplicatesOnly && cnt < 2) continue;
if (showUniqueOnly && cnt > 1) continue;
if (count) {
System.out.printf("%6d %s%n", cnt, line);
} else {
System.out.println(line);
}
}
}
注意:上述实现模拟的是“相邻重复”的行为,而不是全局去重。真正的 uniq 不会跨行累计计数,除非重复行连续出现。
Step 3: 支持全局去重(类似 sort + uniq)
如果我们希望实现“全局去重”,可以修改逻辑:
public void globalDedup(List<String> lines) {
Map<String, Integer> globalCount = new HashMap<>();
for (String line : lines) {
globalCount.put(line, globalCount.getOrDefault(line, 0) + 1);
}
for (Map.Entry<String, Integer> entry : globalCount.entrySet()) {
String line = entry.getKey();
int cnt = entry.getValue();
if (showDuplicatesOnly && cnt < 2) continue;
if (showUniqueOnly && cnt > 1) continue;
if (count) {
System.out.printf("%6d %s%n", cnt, line);
} else {
System.out.println(line);
}
}
}
Step 4: 主函数整合
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
public class SimpleUniq {
// ... 上面定义的字段和方法 ...
public static void main(String[] args) throws IOException {
SimpleUniq app = new SimpleUniq();
app.parseArgs(args);
if (args.length == 0) {
System.err.println("Usage: java SimpleUniq [-c] [-d] [-u] [file]");
return;
}
List<String> lines;
if (args.length == 1 || (args.length > 1 && !args[args.length - 1].startsWith("-"))) {
String filename = args[args.length - 1];
lines = Files.readAllLines(Paths.get(filename));
} else {
// 从标准输入读取
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
lines = reader.lines().collect(Collectors.toList());
reader.close();
}
// 是否启用全局去重?这里默认使用相邻去重(模拟原生 uniq)
// 如需全局去重,可先排序:
// Collections.sort(lines);
// 然后调用 globalDedup
Collections.sort(lines); // 模拟 sort | uniq
app.globalDedup(lines);
}
}
Java vs Shell 性能对比
虽然 Java 实现功能强大、可扩展,但在处理纯文本时,Shell 命令通常更快,因为:
sort和uniq是高度优化的 C 程序- 流水线操作避免了中间文件 I/O
- 系统调用开销小
但在复杂业务逻辑、结构化解析、多条件过滤等场景下,Java 更具优势。
实际应用场景举例
场景一:清理重复的配置项
你有一个 .env 文件,不小心复制粘贴导致重复:
DB_HOST=localhost API_KEY=abc123 DB_HOST=localhost DEBUG=true API_KEY=abc123
执行:
$ sort .env | uniq > .env.clean
得到干净版本:
API_KEY=abc123 DB_HOST=localhost DEBUG=true
场景二:分析用户行为路径
用户访问记录:
userA -> home userB -> product userA -> cart userA -> home userC -> home
你想知道哪些用户访问了多次首页:
$ grep "home" user_actions.log | cut -d' ' -f1 | sort | uniq -d userA
场景三:合并多个日志文件并去重
$ cat log1.txt log2.txt log3.txt | sort | uniq > merged_unique.log
进阶思考:uniq 的算法本质
uniq 的底层算法非常简单:
- 读取第一行,设为“当前行”
- 读取下一行,与“当前行”比较
- 若相同 → 计数器+1(若启用
-c),继续 - 若不同 → 输出“当前行”及计数,更新“当前行”为新行,计数器重置为1
- 循环直到 EOF
这种“滑动窗口”式的相邻比较,时间复杂度为 O(n),空间复杂度为 O(1),效率极高。
uniq 与其他去重工具对比
| 工具 | 是否需要排序 | 是否支持全局去重 | 是否支持计数 | 适用场景 |
|---|---|---|---|---|
uniq | ✅ 是 | ❌ 否 | ✅ 是 | 简单相邻去重 |
awk '!a[$0]++' | ❌ 否 | ✅ 是 | ✅ 是(需手动) | 全局去重,灵活 |
sort -u | ✅ 是 | ✅ 是 | ❌ 否 | 快速全局去重 |
perl -ne 'print unless $seen{$_}++' | ❌ 否 | ✅ 是 | ❌ 否 | 脚本化去重 |
| Java/Python | ❌ 否 | ✅ 是 | ✅ 是 | 复杂逻辑、大数据、结构化 |
自动化脚本示例
编写一个 Shell 脚本 dedup.sh,自动对指定文件去重并备份原文件:
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 <filename>"
exit 1
fi
FILE=$1
BACKUP="${FILE}.bak"
if [ ! -f "$FILE" ]; then
echo "Error: File '$FILE' not found."
exit 1
fi
echo "Backing up $FILE to $BACKUP..."
cp "$FILE" "$BACKUP"
echo "Deduplicating and sorting $FILE..."
sort "$BACKUP" | uniq > "$FILE"
echo "Done. Original saved as $BACKUP"
赋予执行权限:
chmod +x dedup.sh ./dedup.sh data.txt
测试你的理解:小测验
uniq能否直接对无序文件去重?uniq -d输出的是什么?- 如何让
uniq忽略大小写? sort file.txt | uniq -c | sort -nr的作用是什么?- 为什么
uniq不设计成自动排序?
批量处理多个文件
有时我们需要对目录下所有 .log 文件分别去重:
for file in *.log; do
echo "Processing $file..."
sort "$file" | uniq > "${file%.log}.clean.log"
done
${file%.log} 是 Bash 参数扩展,用于移除后缀。
构建自己的 uniq 工具箱
你可以创建别名或函数,简化常用操作:
# ~/.bashrc or ~/.zshrc # 全局去重并计数 alias uniqc='sort | uniq -c' # 显示重复行 alias uniqd='sort | uniq -d' # 显示唯一行 alias uniqu='sort | uniq -u' # 忽略大小写去重 alias uniqi='sort -f | uniq -i'
然后 source ~/.bashrc 生效。
使用示例:
$ cat mixed.txt Apple apple Banana banana Cherry $ cat mixed.txt | uniqi Apple Banana Cherry
常见错误与避坑指南
错误 1:忘记排序
$ uniq data.txt # 无效!重复行仍存在
✅ 正确做法:
$ sort data.txt | uniq
错误 2:误用-u和-d
$ echo -e "a\na\nb" | sort | uniq -u b
很多人期望 -u 是“去重后的所有行”,其实它是“只出现一次的行”。
错误 3:字段分隔符误解
-f N 是以空白字符(空格、制表符)为分隔符跳过字段。如果数据用逗号分隔,需先转换:
$ sed 's/,/ /g' data.csv | sort | uniq -f 1
或使用 awk 更灵活处理:
$ awk -F',' '{print $2,$3}' data.csv | sort | uniq
uniq 的创意用法
1. 检测文件是否完全相同
$ md5sum file1 file2 | sort | uniq -w 32
如果输出只有一行,说明两个文件内容一致。
2. 找出两个文件的差异部分
$ sort file1 file2 | uniq -u
输出的是只在一个文件中出现的行。
3. 生成唯一 ID 列表
$ awk '{print $2}' users.log | sort | uniq
提取第二列(假设是用户ID),去重排序。
日志轮转中的应用
在系统维护中,常需清理重复日志条目以节省空间:
# 清理并压缩旧日志 zcat access.log.1.gz | sort | uniq | gzip > access.log.1.uniq.gz
uniq 与数据库去重对比
虽然数据库(如 MySQL、PostgreSQL)也能做 DISTINCT 或 GROUP BY 去重,但 uniq 优势在于:
- 无需数据库环境
- 启动快,零配置
- 适合一次性、临时性任务
- 可无缝接入 Shell 脚本自动化
例如:
SELECT url, COUNT(*) FROM access_log GROUP BY url ORDER BY COUNT(*) DESC;
等价于:
awk '{print $7}' access.log | sort | uniq -c | sort -nr
uniq 在 DevOps 中的角色
在 CI/CD 流水线中,常需检查依赖列表、镜像标签、部署配置是否有重复冲突:
# 检查 requirements.txt 是否有重复包 sort requirements.txt | uniq -d # 若有输出,则存在重复依赖,需人工干预
性能优化建议
- 大文件先压缩再传输:
gzip+zcat减少 I/O - 使用
LC_ALL=C sort加速排序(禁用本地化) - 避免管道嵌套过深:可考虑临时文件缓存中间结果
- 并行处理:
parallel+split分片加速
示例:
export LC_ALL=C sort bigfile.txt | uniq -c > result.txt
跨平台兼容性
虽然 uniq 是 POSIX 标准命令,但在 macOS、BSD、Linux 上行为略有差异。例如:
- macOS 的
sort默认不支持--parallel - 某些选项(如
-w)在旧版系统中可能缺失
建议在脚本头部加入版本检测:
if ! command -v uniq &> /dev/null; then
echo "uniq could not be found"
exit 1
fi
深入源码:GNU uniq 的实现思路
如果你想阅读 uniq 的 C 源码,可从 GNU Coreutils 项目入手(虽不能提供 GitHub 链接,但可通过官网下载 tarball)。其核心逻辑围绕:
readline()逐行读取strcmp()比较字符串prevline缓存上一行- 计数器控制输出格式
精简伪代码:
char *prev = NULL;
int count = 0;
while (read_line(¤t)) {
if (prev == NULL || strcmp(prev, current) != 0) {
if (prev != NULL) output(prev, count);
prev = strdup(current);
count = 1;
} else {
count++;
}
}
output(prev, count); // flush last line
Java 版本增强:支持自定义比较器
我们可以让 Java 版本支持忽略大小写、忽略前缀等功能:
@FunctionalInterface
interface LineComparator {
boolean isEqual(String a, String b);
}
// 默认精确匹配
LineComparator defaultComparator = String::equals;
// 忽略大小写
LineComparator ignoreCaseComparator = (a, b) -> a.equalsIgnoreCase(b);
// 忽略前 N 字符
LineComparator ignorePrefixComparator(int n) {
return (a, b) -> {
if (a.length() <= n || b.length() <= n) return false;
return a.substring(n).equals(b.substring(n));
};
}
在 processLines 中传入比较器即可灵活扩展。
教学案例:课堂练习题
让学生完成以下任务:
- 创建包含重复行的文本文件
- 使用
sort+uniq去重 - 使用
uniq -c统计频次 - 使用
uniq -d找出高频词 - 用 Java 实现相同功能
- 对比两者性能(time 命令)
学习路径建议
如果你刚接触 Linux 文本处理,建议按此顺序学习:
cat,head,tail—— 基础查看grep—— 搜索过滤sort—— 排序uniq—— 去重计数awk,sed—— 高级处理xargs,find—— 批量操作- Shell 脚本编程 —— 自动化
工具链整合:日志分析系统雏形
结合 uniq、cron、mail 可构建简易监控系统:
#!/bin/bash
LOG="/var/log/app/error.log"
REPORT="/tmp/daily_report.txt"
echo "=== Daily Error Summary ===" > "$REPORT"
echo "" >> "$REPORT"
zcat "$LOG".*gz 2>/dev/null | cat - "$LOG" | \
grep "$(date -d yesterday +%Y-%m-%d)" | \
awk '{print $5}' | sort | uniq -c | sort -nr >> "$REPORT"
mail -s "Daily Error Report" admin@example.com < "$REPORT"
每天自动发送错误类型统计邮件。
社区与讨论
虽然不能提供具体论坛链接,但你可以在主流技术社区搜索 “Linux uniq tutorial” 或 “uniq command examples”,通常 Stack Overflow、Reddit 的 r/linuxquestions、Linux 中国等都有丰富讨论。
小测验答案
- ❌ 不能,必须先排序。
- ✅ 仅显示重复的行(出现 ≥2 次)。
- ✅ 使用
-i选项,且通常配合sort -f。 - ✅ 按出现频次从高到低排序显示。
- ✅ 因为排序代价高,且有时用户只需相邻去重;分开设计更灵活高效。
总结:uniq 的哲学
uniq 体现了 Unix 工具设计的核心哲学:
Do One Thing and Do It Well.
它不做排序,不做复杂匹配,只专注于“相邻行去重”。通过管道与其他工具组合,却能解决千变万化的问题。这正是 Linux 命令行的魅力所在 —— 简单、组合、强大。
掌握 uniq,不仅是学会一个命令,更是理解了如何用最小工具构建最大价值的思维方式。
下一步行动建议
- 打开终端,创建测试文件实操一遍
- 用 Java 实现完整功能并添加单元测试
- 尝试在真实日志中应用
uniq分析 - 编写自动化脚本提升工作效率
- 探索
awk和sed进阶文本处理
记住:The shell is your superpower.
以上就是Linux使用uniq命令去除重复行的技巧分享的详细内容,更多关于Linux uniq去除重复行的资料请关注脚本之家其它相关文章!
相关文章
shell的条件测试,变量测试,表达式中的0和1,数值判断,字符串判断
本文主要介绍了shell的条件测试,变量测试,表达式中的0和1,数值判断,字符串判断,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2024-01-01
shell脚本中main函数中$#获取不到脚本传入参数个数浅析
Linux的shell脚本,有时候我们在运行shell脚本时会给脚本传入参数,出于逻辑上的严谨,在脚本中可能会做一些逻辑判断或处理,例如判断脚本传入参数的个数,这篇文章主要介绍了shell脚本中main函数中$#获取不到脚本传入参数个数浅析,需要的朋友可以参考下2024-03-03


最新评论