【前情提要】
awk 练熟了,小林能从日志里按字段统计 Top IP、Top URL、状态码分布。老周说:“grep 筛,awk 算,但如果你想‘改’日志内容呢?比如脱敏 IP、提取时间段、删除空行。”小林打开 sed 文档,准备学第三把刀。但他不知道,s///、-i、-n、p、d,每一个都有讲究。
一、老周的第三把刀
早上到工位,老周丢过来一个任务:
“你博客的 access.log,里面有真实 IP。你把它脱敏,把 192.168.1.100 换成 ***.***.***.***。”
“用 awk 不行吗?”
“awk 能改,但改文本是 sed 的强项。”老周说,“grep 筛行,awk 算列,sed 改内容。这是日志分析的三把刀,各管一段。”
“那 sed 怎么用?”
“你先看它的基本语法。”
sed 's/旧内容/新内容/' 文件
“s 是 substitute,替换。/ 是分隔符。默认只替换每行第一个匹配。”
小林试:
echo "hello world" | sed 's/world/ubuntu/'
输出:
hello ubuntu
“成了。”小林说。
“但你注意,我让你脱敏 IP,不是只换一个。日志里每行都有 IP,你得全局替换。”
“加 g?”
“对。”
echo "192.168.1.100 192.168.1.100" | sed 's/192.168.1.100/***.***.***.***/g'
输出:
***.***.***.*** ***.***.***.***
“g 是 global,全局替换。不加 g,只替换每行第一个。”
小林记下:
sed 's/旧/新/':替换每行第一个sed 's/旧/新/g':全局替换
二、实战一:脱敏 IP
“现在正式脱敏。”老周说,“先看日志。”
sudo tail -5 /var/log/nginx/access.log
输出:
192.168.1.100 - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
192.168.1.101 - - [14/Sep/2026:10:00:02 +0800] "GET /posts/ HTTP/1.1" 200 2345 "-" "Mozilla/5.0"
192.168.1.100 - - [14/Sep/2026:10:00:03 +0800] "GET /api/health HTTP/1.1" 500 153 "-" "curl/7.81.0"
“你把所有 192.168.1.x 换成 ***.***.***.***。”
“我写 sed 's/192\.168\.1\.[0-9]*/***.***.***.***/g'?”
“为什么加点?”
“因为 . 在正则里是‘任意字符’,不转义的话,192x168x1x100 也会匹配。”小林说。
“对。”老周说,“sed 默认用基本正则,. 是元字符,要匹配真正的点,得写 \.。”
小林敲:
sudo sed 's/192\.168\.1\.[0-9]*/***.***.***.***/g' /var/log/nginx/access.log | head -5
输出:
***.***.***.*** - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
***.***.***.*** - - [14/Sep/2026:10:00:02 +0800] "GET /posts/ HTTP/1.1" 200 2345 "-" "Mozilla/5.0"
***.***.***.*** - - [14/Sep/2026:10:00:03 +0800] "GET /api/health HTTP/1.1" 500 153 "-" "curl/7.81.0"
“IP 没了。”小林说。
“但注意,你只是输出到屏幕,没改文件。”老周说,“要改文件,加 -i。”
sudo sed -i 's/192\.168\.1\.[0-9]*/***.***.***.***/g' /var/log/nginx/access.log
“但你先别改原文件。”老周拦住他,“-i 直接改文件,改错了没法恢复。你先输出到新文件。”
sudo sed 's/192\.168\.1\.[0-9]*/***.***.***.***/g' /var/log/nginx/access.log > /tmp/access_masked.log
“这是安全做法。”老周说,“sed 默认输出到屏幕,要改文件用 -i,但建议先输出到新文件确认。”
小林记下:
sed 's/正则/替换/g' file > newfile:输出到新文件,安全sed -i 's/正则/替换/g' file:直接改文件- 正则里
.要写\.
三、-n 和 p:只看你想看的行
“sed 还有两个常用参数:-n 和 p。”老周说。
“p 是 print?”
“对。默认 sed 会打印所有行。加 -n 取消默认打印,p 才会打印匹配的行。”
sudo sed -n '1,5p' /var/log/nginx/access.log
输出前 5 行。
“这跟 head -5 有啥区别?”
“head 只能看前几行,sed 能看任意范围。”
sudo sed -n '100,200p' /var/log/nginx/access.log
“第 100 到 200 行。”老周说,“你以后定位问题,先 grep -n 找到行号,再 sed -n '行号p' 看上下文。”
“那 -n 不加 p 呢?”
“-n 不加 p,什么都不输出。”老周说,“-n 是‘取消默认打印’,p 是‘打印’。两个一起用,才是‘只打印匹配行’。”
小林试:
sudo sed -n '1p' /var/log/nginx/access.log
输出第一行。
sudo sed -n '$p' /var/log/nginx/access.log
输出最后一行。
“$ 表示最后一行。”老周说,“sed -n '$p' 就是看最后一行。”
小林记下:
sed -n 'Np':打印第 N 行sed -n 'M,Np':打印第 M 到 N 行sed -n '$p':打印最后一行
四、实战二:提取时间段日志
“现在提取 10:00 到 10:05 的日志。”老周说。
“怎么写?”
“sed -n '/起始时间/,/结束时间/p'。”
sudo sed -n '/14\/Sep\/2026:10:00/,/14\/Sep\/2026:10:05/p' /var/log/nginx/access.log | head -20
“注意,日志里的 / 在 sed 里是分隔符,所以要转义成 \/。”老周说,“或者换个分隔符,比如 #。”
sudo sed -n '\#14/Sep/2026:10:00#,\#14/Sep/2026:10:05#p' /var/log/nginx/access.log | head -20
“用 # 做分隔符,就不用转义 / 了。”老周说,“sed 的分隔符可以是任意字符,选一个日志里不常见的,省得转义。”
“那如果我只想提取 10:00 这一分钟的日志呢?”
sudo sed -n '/14\/Sep\/2026:10:00/p' /var/log/nginx/access.log | head -20
“这是匹配包含 10:00 的所有行。”老周说,“但注意,10:00 也会匹配 10:00 到 10:00:59,不会匹配 10:01。sed 是按行匹配,不是按时间解析。”
小林记下:
sed -n '/起始/,/结束/p':提取范围- 日志里的
/要转义成\/,或用其他分隔符 sed -n '/时间/p':提取包含该时间的行sed不解析时间,只按文本匹配
五、实战三:删除空行和注释行
“你以后处理配置文件,经常要删空行和注释。”老周说。
“怎么删?”
“d 是 delete。”
sed '/^$/d' 文件
“^$ 表示空行,d 表示删除。”老周说,“你试试。”
小林创建了一个测试文件:
cat > /tmp/test.conf << 'EOF'
# 这是注释
server_name localhost;
# 这是另一条注释
listen 80;
root /var/www/blog;
EOF
删除空行:
sed '/^$/d' /tmp/test.conf
输出:
# 这是注释
server_name localhost;
# 这是另一条注释
listen 80;
root /var/www/blog;
删除注释行:
sed '/^#/d' /tmp/test.conf
输出:
server_name localhost;
listen 80;
root /var/www/blog;
“两个一起用。”
sed -e '/^$/d' -e '/^#/d' /tmp/test.conf
输出:
server_name localhost;
listen 80;
root /var/www/blog;
“-e 可以指定多个编辑命令。”老周说,“sed 的 d 是删除行,s 是替换,p 是打印。你以后处理配置,先删空行和注释,再看有效内容。”
小林记下:
sed '/^$/d':删除空行sed '/^#/d':删除注释行sed -e '/^$/d' -e '/^#/d':多个命令
六、sed -i:Mac 和 Ubuntu 的差异
“现在讲一个你之前踩过的坑。”老周说。
小林立刻想起来:“sed -i,Mac 要 -i '',Ubuntu 要 -i。”
“对。”老周说,“你在 Mac 上写脚本,如果用 sed -i,拿到 Ubuntu 跑,就会报错。Ubuntu 的 GNU sed,-i 后面直接跟命令;Mac 的 BSD sed,-i 后面必须跟一个备份后缀,空字符串也要写。”
“那我怎么写兼容脚本?”
“两种方式。”老周说。
方式一:判断系统。
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/old/new/g' file
else
sed -i 's/old/new/g' file
fi
方式二:用 -i.bak,两边都认。
sed -i.bak 's/old/new/g' file
“-i.bak 表示备份原文件为 file.bak,两边行为一致。”老周说,“但这样会多出备份文件,你得手动删。”
“那推荐哪个?”
“你主线在 Ubuntu 上写脚本,用 -i 就行。”老周说,“Mac 只做编辑器,不在 Mac 上跑 sed -i。这是最省事的。”
小林记下:
- Mac(BSD
sed):sed -i '' 's/old/new/g' file - Ubuntu(GNU
sed):sed -i 's/old/new/g' file - 兼容写法:
sed -i.bak 's/old/new/g' file - 主线在 Ubuntu 写脚本,别在 Mac 跑
sed -i
七、排错挑战:sed -i 把文件改坏了
下午,小林想批量修改 Nginx 配置,把 listen 80; 改成 listen 8080;。
他写:
sudo sed -i 's/listen 80;/listen 8080;/g' /etc/nginx/sites-available/blog
然后检查:
cat /etc/nginx/sites-available/blog
输出:
server {
listen 8080;
server_name localhost;
root /var/www/blog;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
“看起来没问题。”小林说。
然后他测试:
sudo nginx -t
输出:
nginx: configuration file /etc/nginx/nginx.conf test is successful
“通过了。”小林说。
“那你访问一下。”
小林在 Mac 浏览器访问 http://localhost:8080。
无法访问
“啥?”小林懵了,“配置没问题,Nginx 测试通过,为什么访问不了?”
他喊来老周。
老周看了一眼:“你改的是 listen 80 改成 listen 8080,但你的 Nginx 主配置里,有没有别的站点还在监听 80?如果有,两个站点冲突,Nginx 会拒绝启动或只加载一个。”
小林检查:
ls /etc/nginx/sites-enabled/
输出:
blog
default
“default 还在。”小林说,“它监听 80,blog 监听 8080,不冲突啊。”
“那 VMware Fusion 的端口转发呢?”老周问,“你之前配的是 Mac 8080 → Ubuntu 80。现在 Ubuntu 的 Nginx 监听 8080,你 Mac 访问 localhost:8080,转发到 Ubuntu 的 80,但 Ubuntu 的 80 没有服务在听。”
“那我得改端口转发?”
“对。”老周说,“要么把 Nginx 改回 80,要么把端口转发改成 Mac 8080 → Ubuntu 8080。你改了一个地方,得把整条链路都检查一遍。”
小林改回 80:
sudo sed -i 's/listen 8080;/listen 80;/g' /etc/nginx/sites-available/blog
sudo nginx -t
sudo systemctl reload nginx
再次访问 http://localhost:8080,博客回来了。
排错思路总结:
sed -i改配置,改完先nginx -t测试。- 测试通过但访问不了,检查整条链路:Nginx 监听端口 → 防火墙 → 端口转发 → 浏览器访问地址。
- 改端口时,所有依赖这个端口的配置都要同步改。
sites-enabled/下可能有多个站点,检查是否冲突。- 最稳的做法:改配置前先备份,
cp file file.bak。
“记住,”老周说,“sed -i 是直接改文件,改错了没法撤销。 你改配置前,先 cp 备份。cp /etc/nginx/sites-available/blog /etc/nginx/sites-available/blog.bak。改坏了,mv 回来就行。”
八、sed 常用命令速查
老周拿过小林的笔记本,写了一个表。
# 替换
sed 's/old/new/' file # 替换每行第一个
sed 's/old/new/g' file # 全局替换
sed -i 's/old/new/g' file # 直接改文件(GNU)
# 打印
sed -n 'Np' file # 打印第 N 行
sed -n 'M,Np' file # 打印第 M 到 N 行
sed -n '$p' file # 打印最后一行
sed -n '/pattern/p' file # 打印匹配行
# 删除
sed '/pattern/d' file # 删除匹配行
sed '/^$/d' file # 删除空行
sed '/^#/d' file # 删除注释行
# 多命令
sed -e 's/old/new/g' -e '/^$/d' file
# 范围提取
sed -n '/start/,/end/p' file
“这十几条,你背下来。”老周说,“以后处理日志和配置,80% 的场景都用这些。”
小林把速查表抄了一遍。
九、练习
- 练习一:用
sed 's/192\.168\.1\.[0-9]*/***.***.***.***/g'脱敏access.log,输出到/tmp/access_masked.log。用head -5确认 IP 被替换。 - 练习二:用
sed -n '/14\/Sep\/2026:10:00/,/14\/Sep\/2026:10:05/p'提取 10:00 到 10:05 的日志。用wc -l统计提取了多少行。然后用sed -n '100,200p'查看第 100 到 200 行。 - 练习三:创建一个测试配置文件,包含注释和空行。用
sed -e '/^$/d' -e '/^#/d'删除空行和注释,输出到新文件。对比原文件和新文件的行数。
参考答案
练习一:
sudo sed 's/192\.168\.1\.[0-9]*/***.***.***.***/g' /var/log/nginx/access.log > /tmp/access_masked.log
head -5 /tmp/access_masked.log
练习二:
sudo sed -n '/14\/Sep\/2026:10:00/,/14\/Sep\/2026:10:05/p' /var/log/nginx/access.log > /tmp/time_range.log
wc -l /tmp/time_range.log
sudo sed -n '100,200p' /var/log/nginx/access.log | head -10
练习三:
cat > /tmp/test.conf << 'EOF'
# 这是注释
server_name localhost;
# 这是另一条注释
listen 80;
root /var/www/blog;
EOF
wc -l /tmp/test.conf
sed -e '/^$/d' -e '/^#/d' /tmp/test.conf > /tmp/test_clean.conf
wc -l /tmp/test_clean.conf
cat /tmp/test_clean.conf
十、下集预告
sed 练熟了,小林能脱敏 IP、提取时间段、删除空行和注释了。老周说:“grep 筛,awk 算,sed 改,三把刀你都会了。但真正分析日志,得把它们串起来。”小林打开终端,准备学组合拳。但他不知道,grep | awk | sort | uniq -c | sort -rn | head 这条管道,每一步都有讲究。
下一集:《组合拳:grep + awk + sed 三件套》