linux shell

关注公众号 jb51net

关闭
首页 > 脚本专栏 > linux shell > Linux uniq去除重复行

Linux使用uniq命令去除重复行的技巧分享

作者:Jinkxs

在 Linux 的世界里,文本处理是日常运维、数据分析、日志排查等工作中最频繁的操作之一,而 uniq 命令作为 GNU coreutils 中的一员,虽然看似简单,却蕴藏着强大的去重能力,本文将带你深入掌握 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 经常与 sortawkgrepcut 等组合使用,构建强大的文本处理流水线。

案例:找出访问最频繁的 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 强大,但它也有几个明显的短板:

  1. 依赖排序:必须先 sort,否则无法正确去重。
  2. 内存限制:超大文件排序可能耗尽内存。
  3. 不支持正则匹配:无法按模式模糊去重。
  4. 单行比较:无法跨行或结构化比较。

对于更复杂的去重需求,我们可以转向脚本语言如 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 命令通常更快,因为:

但在复杂业务逻辑、结构化解析、多条件过滤等场景下,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. 读取第一行,设为“当前行”
  2. 读取下一行,与“当前行”比较
  3. 若相同 → 计数器+1(若启用 -c),继续
  4. 若不同 → 输出“当前行”及计数,更新“当前行”为新行,计数器重置为1
  5. 循环直到 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

测试你的理解:小测验

  1. uniq 能否直接对无序文件去重?
  2. uniq -d 输出的是什么?
  3. 如何让 uniq 忽略大小写?
  4. sort file.txt | uniq -c | sort -nr 的作用是什么?
  5. 为什么 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)也能做 DISTINCTGROUP BY 去重,但 uniq 优势在于:

例如:

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
# 若有输出,则存在重复依赖,需人工干预

性能优化建议

  1. 大文件先压缩再传输gzip + zcat 减少 I/O
  2. 使用 LC_ALL=C sort 加速排序(禁用本地化)
  3. 避免管道嵌套过深:可考虑临时文件缓存中间结果
  4. 并行处理parallel + split 分片加速

示例:

export LC_ALL=C
sort bigfile.txt | uniq -c > result.txt

跨平台兼容性

虽然 uniq 是 POSIX 标准命令,但在 macOS、BSD、Linux 上行为略有差异。例如:

建议在脚本头部加入版本检测:

if ! command -v uniq &> /dev/null; then
    echo "uniq could not be found"
    exit 1
fi

深入源码:GNU uniq 的实现思路

如果你想阅读 uniq 的 C 源码,可从 GNU Coreutils 项目入手(虽不能提供 GitHub 链接,但可通过官网下载 tarball)。其核心逻辑围绕:

精简伪代码:

char *prev = NULL;
int count = 0;

while (read_line(&current)) {
    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 中传入比较器即可灵活扩展。

教学案例:课堂练习题

让学生完成以下任务:

  1. 创建包含重复行的文本文件
  2. 使用 sort + uniq 去重
  3. 使用 uniq -c 统计频次
  4. 使用 uniq -d 找出高频词
  5. 用 Java 实现相同功能
  6. 对比两者性能(time 命令)

学习路径建议

如果你刚接触 Linux 文本处理,建议按此顺序学习:

  1. cat, head, tail —— 基础查看
  2. grep —— 搜索过滤
  3. sort —— 排序
  4. uniq —— 去重计数
  5. awk, sed —— 高级处理
  6. xargs, find —— 批量操作
  7. Shell 脚本编程 —— 自动化

工具链整合:日志分析系统雏形

结合 uniqcronmail 可构建简易监控系统:

#!/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 中国等都有丰富讨论。

小测验答案

  1. ❌ 不能,必须先排序。
  2. ✅ 仅显示重复的行(出现 ≥2 次)。
  3. ✅ 使用 -i 选项,且通常配合 sort -f
  4. ✅ 按出现频次从高到低排序显示。
  5. ✅ 因为排序代价高,且有时用户只需相邻去重;分开设计更灵活高效。

总结:uniq 的哲学

uniq 体现了 Unix 工具设计的核心哲学:

Do One Thing and Do It Well.

它不做排序,不做复杂匹配,只专注于“相邻行去重”。通过管道与其他工具组合,却能解决千变万化的问题。这正是 Linux 命令行的魅力所在 —— 简单、组合、强大

掌握 uniq,不仅是学会一个命令,更是理解了如何用最小工具构建最大价值的思维方式。

下一步行动建议

  1. 打开终端,创建测试文件实操一遍
  2. 用 Java 实现完整功能并添加单元测试
  3. 尝试在真实日志中应用 uniq 分析
  4. 编写自动化脚本提升工作效率
  5. 探索 awksed 进阶文本处理

记住:The shell is your superpower. 

以上就是Linux使用uniq命令去除重复行的技巧分享的详细内容,更多关于Linux uniq去除重复行的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文