Skip to content

第2章 Shell 三剑客与 Python ​

对应简历:技能第2条「熟练编写 Shell 脚本(grep / sed / awk)实现日志分析、批量部署、数据备份等自动化任务」;技能第3条「熟悉 Python 基础与自动化脚本编写,正在自学 FastAPI 框架进行运维工具开发」;项目一「给数据面节点写批量 docker load 镜像加载脚本」。 口径提醒:Python 部分按简历口径"基础 + 自动化脚本 + FastAPI 自学中",项目以"课程实验 + 理解"讲述,不编造生产级 Python 项目。 使用提醒:三剑客的每条追问答案都能独立成段口述;手写题模板(2.12)要求能默写。

2.1 Shell 变量与特殊变量($@ $# $* $?)【必背】 ​

标准回答:Shell 变量的规则先说清楚:name=value 直接赋值,等号两边绝对不能有空格——这是和 Python 最不一样的地方;取值用 $name,export 把变量升级为全局让子进程可见,unset 删除。引号语义三分:单引号原样不解析变量、双引号解析变量、反引号或 $() 执行命令替换,我统一用 $() 因为可读且能嵌套。

特殊变量是必背的一组:$0 脚本名、$1 到 $n 位置参数、$# 参数个数、$* 所有参数合成一个整体、$@ 每个参数各自独立、$? 上一条命令的退出状态码(0 成功非 0 失败)、$$ 当前脚本 PID、$! 最近一个后台进程的 PID。脚本容错的基石就是判断 $?:关键命令之后 [ $? -eq 0 ] || exit 1,让脚本"失败即停"而不是带着错误状态往下跑。

银行项目的批量加载脚本里,$? 判断 docker load 成败决定是否继续,特殊变量是每个脚本的骨架。

追问链:

  • 追问1:$* 和 $@ 区别? → 答:不加引号时两者行为一样,都是拆开遍历。区别在加引号的时刻:"$*" 把所有参数拼成一个整体字符串,相当于 "a b c" 一项;"$@" 保持每个参数独立,相当于 "a" "b" "c" 三项。写遍历参数的循环必须用 for arg in "$@",这样带空格的参数才不会被拆碎——比如文件路径里有空格的场景。这是脚本正确性的细节分水岭,我写脚本统一 "$@"。
  • 追问2:怎么把变量变成全局? → 答:当前进程和子进程的范围里,export VAR=value 让变量进入子进程环境;要让每次登录都有,就写进 /etc/profile(全员)或 ~/.bashrc(单用户),然后 source 一下立即生效。两者的区别要分清:export 管的是"进程间传递",profile 文件管的是"会话持久化"——面试里把这两个层次分开讲会显得很清楚。
  • 追问3:read、declare 和数组? → 答:read -p "提示语" var 做交互输入,-p 给提示、-t 可加超时;declare -i 声明整数变量、declare -r 只读;数组用 arr=(a b c) 一次赋值,取单个 ${arr[0]}、取全部 ${arr[@]}、取长度 ${#arr[@]},遍历 for i in "${arr[@]}"。数组我在处理"IP 列表 + 状态结果"这类配对数据时用过,比一堆散变量干净。
  • 追问4(深挖):运算怎么做? → 答:首选 $((1+2)) 算术展开,语法最干净;let 等价于它;expr 是老写法,参数间要空格还容易被通配符坑;要小数运算就上 bc:echo "scale=2; 10/3" | bc。另外条件判断要区分:单括号 [ 是 test 命令,数值比较用 -eq/-lt、字符串用 =;双括号 [[ ]] 是关键字,支持 &&、|| 和 =~ 正则匹配,更现代更安全——我的新脚本一律用 [[ ]]。

关联话术:"我的每个生产脚本开头都是固定结构:shebang、set 安全项、参数检查($# 不够就 usage 退出)、变量区。这套骨架是在银行客户现场被'脚本跑一半挂了没人知道'教育出来的——特殊变量不是考点,是脚本可信度的地基。"

实操模板:

bash
#!/bin/bash
echo "脚本名: $0"
echo "参数个数: $#"
echo "所有参数(@): $@" && echo "所有参数(*): $*"
ls /not_exist >/dev/null 2>&1
echo "上条命令状态: $?"      # 非零
echo "当前PID: $$"

2.2 条件、循环、函数与调试骨架 【理解】 ​

标准回答:条件判断用 if [[ 条件 ]]; then ... elif ... else ... fi,条件类型三组:数值 -eq、-ne、-gt、-lt,文件 -f 普通文件、-d 目录、-e 存在、-s 非空,字符串 =、!=、-z 空判断。多分支匹配用 case,case "$1" in start) ... ;; stop) ... ;; esac,写启停脚本的天然结构。

循环四种:for i in 列表 遍历、C 风格 for ((i=0;i<10;i++)) 计数、while read line 逐行读文件(必须配 done < file 的重定向写法)、until 条件为假才停。函数用 function name() { ...; } 或直接 name() { ...; },调用时写名字不带括号;要特别注意 return 只能返回 0-255 的状态码,想"返回值"要用 echo 输出配合命令替换 result=$(func) 接。

break 和 continue 控制流程。启停脚本的通用骨架我脱口而出:case 分支 + start/stop/status 三个函数 + 进程检查,完整版在 2.12 模板。

追问链:

  • 追问1:现写一个启动管理脚本? → 答:结构上 case $1 匹配 start/stop/restart/status 四个分支:start 先 pgrep -f 应用名 判断已在跑避免重复启动,再 nohup 后台拉起并落日志;stop 用 pgrep 拿 PID 先 kill -15 等 5 秒再补 -9;status 输出进程和端口占用;restart 顺序调用 stop 和 start。骨架我会先写在白板上,再往里填判断——完整模板在 2.12 模板三的位置,面试手写这结构就够拿分,细节参数如实说"生产版我按模板抄,不会记错"。
  • 追问2:while read 和 for 读文件的区别? → 答:for 循环按空白符(空格、制表、换行)切分,一行里有空格的记录会被拆成多项,结构信息丢失;while read line 一次读一整行到变量,保结构。还有一个隐蔽坑:while 接在管道后面时运行在子 shell 里,循环内修改的变量出了循环就没了,必须用 while read line; do ...; done < file 的重定向写法把循环留在当前 shell。这两个点我说得出踩坑经历,可信度高。
  • 追问3:批量 ping 100 个 IP 输出结果怎么写? → 答:一行核心是 for ip in $(cat ip.txt); do ping -c1 -W1 $ip &>/dev/null && echo "$ip ok" || echo "$ip fail"; done > ping.txt。两个要点:-c1 -W1 限制只发一个包、等一秒就超时,不然 100 台能把脚本拖死;cmd && a || b 的三元结构按退出码分流,是 Shell 里最优雅的条件表达式。这个结构稍加改造就是我的批量巡检脚本雏形。

关联话术:"我三年里写的脚本 90% 是'for 循环 + ssh/scp + 判断'的组合:批量部署、批量巡检、批量加载镜像——骨架全是这一节的内容。面试官让我现场写脚本我不慌,因为我真的天天在写,只是每个脚本都会比课堂版多三样东西:日志、防重、失败处理。"

2.3 grep 高频用法与正则基础 【理解】 ​

标准回答:grep 是行过滤工具,按"把匹配的行挑出来"工作。常用参数一组:-i 忽略大小写、-v 反选不匹配的行、-n 显示行号、-w 按单词精确匹配、-c 只计数、-r 递归整个目录、-o 只输出匹配到的部分、-A/-B/-C N 显示匹配行的后/前/上下文 N 行——排障看报错上下文我最常用 grep -C 3 'error',因为一行报错往往看不出调用链。

正则基础必须分两档说清楚:基础正则 BRE 有 ^ 行首、$ 行尾、. 任意单字符、* 前一字符零次或多次、[] 字符集、[^] 取反;扩展正则 ERE 增加了 + 一次以上、? 零或一次、{n,m} 次数区间、| 或、() 分组。grep 默认用 BRE,要 ERE 就加 -E(等价于 egrep),sed 要加 -r, awk 默认就是 ERE——"哪个工具默认哪档正则"是这题的加分细节。

追问链:

  • 追问1:统计 nginx 日志里 502 的总次数? → 答:第一种 grep -c ' 502 ' access.log,502 两侧带空格是为了按字段精确匹配状态码那一段,避免匹配到 URL 里恰好含 502 的请求;第二种更严谨的做法是 awk '$9==500 || $9==502' access.log | wc -l,按第九列状态码字段的值判断,不受字段内容干扰。我日常两种都用,需要精确字段语义时上 awk,快速粗估时 grep -c 一把梭。
  • 追问2:找出凌晨 4 点到 6 点的日志? → 答:按日志时间字段做正则:grep -E '24/Oct/2026:0[45]:|24/Oct/2026:06:' access.log,把 04、05 整个小时和 06 点用或条件拼起来。另一种思路是 sed 的范围匹配 sed -n '/起始时间/,/结束时间/p' app.log,从匹配起始行一直打印到结束行——但前提是日志严格连续有序。两种方法我都能写,面试说出来还能对比适用条件,比只背一个强。
  • 追问3:过滤注释行和空行看配置? → 答:grep -Ev '^#|^$' /etc/ssh/sshd_config——-v 反选,^# 注释行、^$ 空行。这是我看任何服务配置文件有效内容的第一条命令,sshd_config、sysctl.conf、fstab 都适用。日常高频到肌肉记忆的程度,改配置前先跑一遍看"真正生效的是什么",避免被一堆注释干扰判断。

关联话术:"日志排障我每天用 grep -C 3 看报错上下文;审计客户环境配置时'grep -Ev ^#|^$' 是我的开场动作。这两个小习惯我在面试里说出来,面试官一般都能感受到'这人真的天天在干活'——工具不在多,在用出效率。"

2.4 sed 增删改查高频用法 【必背】 ​

标准回答:sed 是流式行编辑器,我的记忆口诀是"查 p、删 d、改 s、增 i/a"四类操作。查:sed -n '10p' 打印第 10 行,sed -n '/error/p' 打印匹配行,-n 抑制默认输出、和 p 动作配对使用,不加 -n 会每行都输出一遍。删:sed '3,5d' 删行号范围、sed '/^$/d' 删空行、sed '/^#/d' 删注释行。

改:sed 's/old/new/g' 全局替换,不加 g 只替换每行第一处;sed -i 直接修改源文件,生产规范是先 sed -i.bak 让它自动留备份再落盘。增:sed '2i\text' 在第 2 行前插入、sed '/pattern/a\text' 在匹配行后追加。定位方式四种可以组合:具体行号、$ 末行、/正则/、范围 /start/,/end/。sed 的价值在于非交互、可批量、可进管道,是自动化改配置的主力。

追问链:

  • 追问1:修改配置文件里某个参数,命令怎么写? → 答:简单场景 sed -i 's/^Port 22/Port 2222/' /etc/ssh/sshd_config。但真实配置文件经常是"有的行注释了有的没注释",这时用反向引用:sed -ri 's/^#?(Port ).*/\1 2222/' sshd_config——#? 匹配可能存在的注释符,(Port ) 分组用 \1 引用回来,一条命令同时覆盖注释与未注释两种形态。这招是我批量改 15 台国产化虚拟机配置时的主力写法。
  • 追问2:sed -i 有什么风险? → 答:-i 直接改写源文件,正则写错就可能把配置改坏,而且没有撤销。我的规范两步走:要么 sed -i.bak 让 sed 自动生成 .bak 备份,要么先不加 -i 跑一遍看标准输出的结果,确认替换符合预期再落盘。批量操作前先在一台上验证、diff 对比,再推广——这在银行客户环境是变更纪律,不是可选项。
  • 追问3:sed 和 vim 改文件的区别?为什么脚本里用 sed? → 答:vim 是交互式编辑器,需要人在终端上操作,没法进自动化流程;sed 是流式的,输入可以来自管道、输出可以进管道,天然适合脚本批量处理几十上百台机器。另外 sed 处理超大文件也稳,因为它逐行流式处理不整读内存。一句话:改一台用人机交互的 vim,改一批用可编程的 sed。
  • 追问4(深挖):sed 的工作原理? → 答:sed 逐行处理:把当前行读进"模式空间",对模式空间执行脚本里的命令,处理完默认把模式空间打印出来,再读下一行,直到文件结束;-n 就是关掉这个默认打印,所以必须配 p 显式输出。还有"保持空间"可以做暂存交换实现倒序这类高级操作。理解模式空间这个模型,就能解释 sed 为什么是流式、为什么 -n 和 p 必须配对——知其然也知其所以然。

关联话术:"批量改集群配置——替换 yum 源地址、统一改 sshd 参数、批量开关 SELinux——我就是 sed -i.bak 加 grep 验证一把梭,银行项目 15 台国产化虚拟机的配置统一就是这么做下来的。这题我可以现场手写带反向引用的版本,是最能证明'真用过'的题型之一。"

实操模板:

bash
sed -n '/ERROR/p' app.log              # 查
sed -i.bak 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config   # 改+备份
sed -ri 's/^#?(Port ).*/\1 2222/' sshd_config   # 反向引用处理注释行
sed '/^$/d;/^#/d' conf > conf.clean    # 删空行+注释行
sed -n '/2026-10-24 04:/,/2026-10-24 06:/p' app.log   # 时间段范围取日志

2.5 awk 取列、统计与数组 【必背】 ​

标准回答:awk 是一门文本分析语言,运维视角必须掌握它的三个核心能力。第一取列:awk '{print $1,$7}' file 按默认空白分隔取第一、七列;-F: 把分隔符改成冒号,比如 awk -F: '{print $1}' /etc/passwd 取用户名;$NF 是最后一列,NF 是当前行的字段数、NR 是当前行号。

第二条件过滤:awk '$9==500' access.log 按字段值筛选,awk '/error/' 按正则筛行,模式和动作可以组合。第三关联数组统计——这是 awk 的灵魂:awk '{ip[$1]++} END{for(i in ip) print i,ip[i]}' access.log,逐行时以第一列为下标给数组计数,END 块里遍历输出,一个数组就完成了分组统计。

执行模型是 BEGIN{预处理} 模式{逐行处理} END{收尾} 三段式,BEGIN 适合打印表头、END 适合输出汇总。printf 能格式化对齐输出报表。日志统计题的万能钥匙就是"数组下标换成你要统计的维度"。

追问链:

  • 追问1:统计访问最多的前 10 个 IP? → 答:两种写法我都能默写。管道流:awk '{print $1}' access.log | sort | uniq -c | sort -nr | head,取列、排序、去重计数、按数值倒序、截取,五个基础工具串起来,好记;纯 awk 版:awk '{ip[$1]++} END{for(i in ip) print ip[i],i}' access.log | sort -nr | head,少两个进程效率略高。面试现场我一般写管道版——因为它同时展示了 sort/uniq 的功底,写错的风险也更低。
  • 追问2:统计每个 URL 的访问量并取 TOP10? → 答:把数组下标从 IP 换成 URL 字段就行:awk '{url[$7]++} END{for(i in url) print url[i],i}' access.log | sort -nr | head。统计状态码分布就把下标换成 $9,统计每分钟流量就换成时间字段截断——同一把钥匙开所有统计题,这就是掌握关联数组的意义:不用背 N 道题,只背一个模式。
  • 追问3:取 /etc/passwd 里 UID 大于 1000 的用户名? → 答:awk -F: '$3>1000{print $1}' /etc/passwd。-F: 改分隔符、第三列做数值条件、命中后打印第一列,一条命令把"改分隔符 + 条件 + 取列"三个能力全用上。我盘点系统普通用户、排查可疑账号就用这条,加个 $3==0 反过来还能查非 root 的 UID0 账号——安全巡检常用。
  • 追问4(深挖):awk 怎么算和与平均值? → 答:累加器模式:awk '{sum+=$5} END{print sum, sum/NR}' file——每行把第五列累进 sum,END 块里 sum 是总和、sum/NR 是平均值(NR 累计了行数)。这个结构我做监控数据汇总时常用:把采样 CSV 的某列聚合出均值和总量,一条 awk 就出结果,不用再写程序。

关联话术:"日志分析是简历技能第 2 条的原文场景:nginx 日志 TOP IP、URL 统计、错误状态码分布,三件套(awk 取列、sort/uniq 计数、head 截取)是我给业务出访问报告的标准组合。我还能接一句:'后来我把这套统计逻辑用 Python 重写过做固定报表,两边效率差异我也对比过'——把 2.11 的内容自然带出来。"

实操模板:

bash
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10   # TOP10 IP
awk '{s[$9]++} END{for(c in s) print c,s[c]}' access.log | sort -k2 -nr  # 状态码分布
awk '$4>"[24/Oct/2026:16" && $4<"[24/Oct/2026:18"' access.log | wc -l   # 时段流量
awk -F: '$3>=1000{print $1,$7}' /etc/passwd                            # 普通用户

2.6 sed 与 awk 的区别与选型 【理解】 ​

标准回答:一句话分工:sed 擅长"行级编辑",awk 擅长"列级分析"。再展开一层原理:sed 是流编辑器,把每行放进模式空间做替换、删除、追加后输出,它本质上在"改文本的形状",没有运算能力;awk 是编程语言,自动把每行按分隔符切成字段($1、$2),有变量、数组、循环、条件,能做算术和统计报表,它是在"理解文本的结构并计算"。

选型原则:改配置文件、清洗文本格式、批量替换,用 sed;日志统计、取列、按字段过滤、出报表,用 awk;范围不确定时先用 grep 把数据缩小再交给后两者。三剑客的经典组合是流水线:grep 负责找到、sed 负责改掉、awk 负责算清楚——这句也是我给团队新人讲三剑客时的开场白。

追问链:

  • 追问1:都能干的事为什么选一个? → 答:比如取第 10 行,sed -n '10p' 和 awk 'NR==10' 完全等价,但 sed 更短、语义更直接;反过来一旦涉及"列"或"算"——比如取第三列并求和——sed 就写不动了,awk 一行搞定。我的选型口诀是"改形状用 sed、看结构用 awk、找内容用 grep",按最小工具原则选,脚本才好维护。面试里能主动对比工具边界,比背十个命令更能体现理解。

关联话术:"我给面试官的总结版就是那句话:'grep 负责找到、sed 负责改掉、awk 负责算清楚'。然后我会举一个具体例子:分析数币平台 nginx 日志时,先用 grep -E ' 50[0-9] ' 抓 5xx,再 sed 提取时间字段归一化,最后 awk 按分钟数组统计出故障时段的请求量曲线——三板斧串成一条管道,这是我真实的日常工作流。"

2.7 crontab 五段式与经典坑 【必背】 ​

标准回答:crontab 表达式五段:分、时、日、月、周,取值分别是 0-59、0-23、1-31、1-12、0-7(0 和 7 都是周日);五个符号:* 任意、*/n 每隔 n、- 范围、, 列表、具体数字。示例背三个:30 3 * * * 每天凌晨 3:30;*/5 * * * * 每 5 分钟;0 0 * * 0 每周日零点。

操作命令:crontab -e 编辑、-l 列出、-r 全删(危险操作,没有确认,误敲就清空,我从来只敢用 -l 和 -e);系统级任务写 /etc/crontab 或 /etc/cron.d/ 下,格式多一列执行用户。

四个经典坑必须会讲:第一,% 在 crontab 里是特殊字符表示换行,date 命令里的 % 必须写成 \%,否则命令被截断——这是我真实踩过然后写进规范的坑;第二,cron 的 PATH 环境变量极小,脚本内外全部用绝对路径;第三,cron 是非登录 shell,不加载 profile,需要的环境变量要在脚本里自己 source 或 export;第四,排查分两步——/var/log/cron(Ubuntu 是 syslog)确认任务有没有触发,"没触发查 cron,触发了没结果查脚本日志"。

追问链:

  • 追问1:每天凌晨 3:30 清空 /tmp? → 答:课程版答案是 30 3 * * * /usr/bin/rm -rf /tmp/* &>/dev/null,能说但不完美;我的生产版是 30 3 * * * /usr/bin/find /tmp -type f -mtime +0 -delete——find 按修改时间只删文件不动目录结构,比裸 rm 可控得多,误删风险和破坏面都小。面试时把两个版本对比着说,体现的是"会答题更会生产"的差别。
  • 追问2:怎么确认定时任务执行了? → 答:三步验证法:第一步看 /var/log/cron 的触发记录,有记录说明 cron 层面正常;第二步看脚本自己的日志,我在每个 cron 任务里都强制加 >> /var/log/xxx.log 2>&1 重定向,把 stdout 和 stderr 都落盘;第三步看产物,备份文件、报表的时间戳对不对。经验之谈:cron 排查 50% 的问题第一步就是"任务行里加了重定向吗",没有日志的 cron 任务等于盲跑。
  • 追问3:日和周同时指定会怎样? → 答:这是 cron 的一个语义陷阱:日和周同时指定时是"或"的关系,任一条件满足就执行,不是"与"。比如 0 0 1 * 1 是每月 1 号或每个周一都跑,而不是"每月 1 号且是周一"。要实现与逻辑,只能在脚本里自己判断,比如开头加 [ "$(date +%d)" -eq 01 ] || exit 0。知道这个坑的存在,比背答案重要,因为排障时它会以"任务为什么多跑了"的形态出现。
  • 追问4(深挖):秒级任务怎么办? → 答:cron 最小粒度是分钟,秒级有三条路:一是常驻脚本里 while true; do ...; sleep 5; done,配 systemd service 加 Restart=always 托管——我在生产上用的就是这个方案,稳且简单;二是 systemd timer,OnCalendar 支持到秒级,还能看 journal 日志;三是专门的调度系统。我的选型顺序是 systemd 常驻优先,因为复用了我全部的服务管理经验。

关联话术:"备份、日志清理、巡检三类脚本在两个项目里都靠 crontab 调度:银行环境按客户审计要求放 /etc/cron.d 并指定运行用户和权限;移动云用应用账号的 crontab 加全量日志重定向。我的固定口号是'脚本 + cron + 日志留痕'是自动化三板斧,缺一个都不算交付完成。"

实操模板:

bash
# 每天凌晨2点数据库备份并记日志(%已转义示范)
0 2 * * * /opt/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
# 文件名带日期必须转义 % 或在脚本内生成
30 3 * * * /opt/scripts/clean.sh > /dev/null 2>&1
# 排查三板斧
crontab -l; tail -50 /var/log/cron; cat /var/log/mysql_backup.log

2.8 set -euo pipefail 与脚本调试 【理解】 ​

标准回答:脚本健壮性三件套 set -euo pipefail,我放在每个生产脚本的 shebang 下一行。逐个解释:-e 是任何命令返回非零就立即退出脚本,防止"前面失败了后面还接着跑"的带病执行;-u 是引用未定义变量直接报错,防手误打错变量名导致空变量扩散;-o pipefail 是管道中任何一环失败都算整条管道失败——默认 Shell 只看管道最后一个命令的退出码,前面挂了都不知道。

调试工具两件:bash -x script.sh(或脚本内 set -x)打开执行回显,每条命令带展开过程打印出来,是查变量值和执行路径的第一利器,set +x 关闭;bash -n script.sh 只做语法检查不执行,发布前必跑。生产脚本规范我还有四条:shebang 固定 #!/bin/bash、变量引用加双引号、关键操作前有条件判断、日志重定向加幂等设计——重复执行不产生副作用,这是交付脚本可信的底线。

追问链:

  • 追问1:#!/bin/bash 的作用? → 答:它告诉内核用 /bin/bash 解释执行这个脚本,与调用方当前用什么 shell 无关。没有这行时脚本由调用方的 shell 解释,如果作者是 bash 环境而执行环境是 dash 或 sh,bashism 语法——[[ ]]、数组、==——就会报错。Ubuntu 的 sh 默认链接到 dash 是这个坑的高发场景,所以我的规范是脚本必须写 shebang 且 chmod +x,用 ./script 方式执行。
  • 追问2:set -e 有什么例外? → 答:-e 在 if 条件判断里、&& || 链中、while 条件里不触发退出,因为这些位置的失败是"被预期"的。由此产生一个真实坑:某个我"允许失败"的命令放在普通位置,-e 让整个脚本意外中断了。解法是显式声明意图:cmd || true 或者用 if 包起来。这个坑我踩过一次,之后团队规范里就加了"允许失败的命令必须显式 || true"这一条。
  • 追问3:怎么调试"cron 里跑不起来、手动正常"的脚本? → 答:这是环境差异问题,按三步收窄:第一步 bash -x script.sh 看完整执行过程,对比手动和 cron 的输出差异在哪一步分岔;第二步重点查环境差异——cron 的 PATH 很小、不加载 profile,检查脚本里的绝对路径和环境变量;第三步在 crontab 行加日志重定向,让 cron 执行时的 stderr 也落盘对比。九成的原因最后都落在路径和环境变量上,这套流程能把排查时间压到几分钟。

关联话术:"我交付的脚本——初始化脚本、批量加载脚本——全部带 set -euo pipefail 加 exec 日志重定向,失败即停、现场可查。银行客户现场'脚本可信'的口碑就是这么来的:客户自己拿脚本去重跑,结果和交付时完全一致,这是我认为运维脚本最重要的品质。"

2.9 Shell 与 Python 选型对比 【理解】 ​

标准回答:我的选型原则很明确。简单命令编排、文件与文本处理、系统管理类任务,用 Shell:一行管道能解决的事不值得写程序,而且 Shell 无依赖、任何一台服务器开箱即用,这在批量运维场景是巨大优势。复杂逻辑、数据结构、跨平台、需要调 HTTP 接口/数据库/第三方库的任务,用 Python:有列表字典这些结构、有异常处理、函数可复用,代码量大了以后的维护性完全不同。

我的经验判断口诀是三条线:脚本超过 100 行、if 嵌套超过 3 层、要处理 JSON 或调 API——踩到任何一条就该换 Python。我两边都真实在写:批量部署、日志清理、巡检用 Shell;日志统计分析、资源采集这类要算数据、出固定格式报表的用 Python——简历上"Shell 熟练、Python 基础"的口径就是我真实的分工状态。

追问链:

  • 追问1:为什么运维还要学 Python? → 答:因为运维的进阶方向是运维开发:CMDB 资产平台、工单系统、巡检平台、把命令行工具 API 化,这些都需要 Web 框架和数据处理能力,Shell 管机器,Python 做平台。我正在自学 FastAPI 就是往这个方向走——结合我主导公司 AI 工具落地的经历,学新框架的速度和坚持度我都不担心。这个回答把"为什么学"和"我正在学什么"接起来,比空谈趋势有说服力。
  • 追问2:Python 调 Shell 命令怎么做? → 答:规范方式是 subprocess:subprocess.run(['df','-h'], capture_output=True, text=True),返回对象里有 returncode 判断成败、stdout 拿输出,安全且信息完整;os.system 是老接口,只能拿退出码拿不到输出,我不在新代码里用。周边生态还有三个我常用的库:paramiko 批量 SSH、requests 调 HTTP 接口、psutil 采系统资源——Python 强在生态,这四个库组合起来就能写采集和巡检工具。

关联话术:"面试被问'你为什么两个都学',我的答案是分工论:'Shell 扛日常自动化,因为快、到处能跑;Python 做要算数据、要出报表、要调接口的活,因为可维护。判断标准我有三条——超百行、嵌套三层、碰 JSON 就换 Python。'然后我可以现场举例:nginx 日志 TOP10 我 Shell 一条管道、Python 十行,两版都能写,差别我亲测过。"

2.10 Python 基础语法要点(运维够用版)【理解】 ​

标准回答:按运维使用的频度过要点。数据结构四种:list 可变序列、dict 键值对(我用得最多,配置和统计天然是字典)、tuple 不可变(可哈希能当 dict 的 key)、set 去重集合。语法特征:缩进就是语法块,强制代码整洁;f-string 格式化 f"{name}" 是现代写法;文件操作用 with open(path) as f 上下文管理器,出了 with 块自动关闭,不怕忘。

函数用 def 加默认参数;异常用 try/except Exception as e/finally,运维脚本里网络和命令调用的异常必须接住不然半夜崩给你看。工程化:模块 import、虚拟环境 python3 -m venv venv 隔离依赖、pip 装包(内网用镜像源参数)。

运维常用库清单:requests 调 HTTP、paramiko 做 SSH、psutil 采资源、subprocess 调命令、json 和 csv 处理数据格式。脚本骨架固定:参数解析 + 循环采集或处理 + 异常捕获 + 输出日志——我的两个小工具(2.11)都是这个骨架。

追问链:

  • 追问1:列表和元组区别?字典怎么遍历? → 答:list 可变、tuple 不可变;不可变带来的额外能力是可哈希,能当 dict 的 key 或进 set,list 不行。字典遍历标准写法 for k, v in d.items() 同时拿键值;取值用 d.get(k, 默认值),键不存在时返回默认值而不是抛 KeyError——采集脚本里字段可能缺失,get 是防崩的第一选择。这两点是我写统计脚本每天用的,顺口就能答。
  • 追问2:Python 执行 Shell 命令拿结果? → 答:subprocess.run(['df','-h'], capture_output=True, text=True)——列表传参避免 Shell 注入,capture_output 把 stdout/stderr 收进返回对象,text=True 直接给字符串;用 .returncode 判断成败、.stdout 拿内容。我的资源采集脚本底层就是这个调用:subprocess 执行命令、psutil 直接读 API,两层结合。这里能带出安全意识"参数用列表不用字符串拼接",是加分细节。
  • 追问3:深拷贝浅拷贝、GIL 了解吗? → 答:浅拷贝 copy() 只复制第一层,嵌套的内层对象还是引用同一份;深拷贝 deepcopy() 递归复制出完全独立的副本。GIL 是 CPython 的全局解释器锁,同一时刻只允许一个线程执行字节码,所以 CPU 密集型任务多线程不会提速,要用多进程;IO 密集型因为线程会在 IO 等待时释放 GIL,多线程仍然有效。这层我如实定位为"知道边界":我的生产脚本规模没被 GIL 卡过,异步编程的细节还在学。

关联话术:"这一节我的口径是诚实加扎实:'基础语法我过关,采集和统计脚本独立能写;装饰器、协程这些进阶我还在系统学。'然后马上补一个实证——把 2.11 的 nginx 统计脚本逻辑现场讲一遍,用能力证明代替形容词,比吹'精通'安全得多。"

2.11 用 Python 做过什么 / FastAPI 能聊的程度 【必背】 ​

标准回答:我用 Python 做过两类东西,一类是课程实验加工作延伸的日志统计,一类是系统采集。第一个是 Nginx 日志统计脚本:读 access.log,逐行拆字段,用 dict/Counter 统计 PV、UV、TOP10 IP、状态码分布,最后输出格式化报表——本质是把 awk 那套统计逻辑用 Python 重写,但好处是能直接产出固定格式报表、容易扩展成定时任务。

第二个是系统资源采集脚本:psutil 定时采 CPU、内存、磁盘、网络指标写进 CSV 或日志,超阈值输出告警行,放 crontab 调度。

FastAPI 我正在自学,能聊的程度是这样:它是基于 Starlette 和 Pydantic 的现代异步 Web 框架,三大卖点我理解——路由用装饰器声明(@app.get("/status"))、利用类型注解自动生成交互式 API 文档(/docs 的 Swagger 页面)、原生 async/await 支持高并发 IO。目前我能独立写健康检查、资源查询这类简单接口,目标是把巡检和资产查询做成运维小 API。不冒充熟练,这是我的诚实边界。

追问链:

  • 追问1:现场写个 Python 小脚本? → 答:我能手写"统计文件里某关键字出现次数"这个级别的十行脚本:open 读文件、循环内 count 累加或用 Counter、最后 print 结果——模板在本章实操区。复杂逻辑我讲思路分步说,不确定的 API 我会直接讲"这个函数我记得用途,具体参数我到时查文档",比硬编一个错误签名好。这种诚实反而让面试官放心。
  • 追问2:为什么选 FastAPI 不是 Flask/Django? → 答:三个理由:第一异步性能好,基于 asyncio,IO 密集的运维接口场景正合适;第二自动文档,类型注解直接生成 Swagger 交互页,省掉一半接口文档工作;第三类型校验强,Pydantic 把参数校验做在框架层。对比着看:Django 是全家桶太重,我的场景不需要 ORM 和后台;Flask 同步模型在 IO 密集下弱一档。这是我自学选型时实际对比过的结论,不是背的。
  • 追问3:Python 和 Shell 统计日志哪个好? → 答:按场景分。一次性临时分析,Shell 三剑客快——一条管道十秒出结果,写完即弃;要反复跑、输出固定报表、结果要入库或推送的,Python 好——逻辑可维护、格式可控、好扩展告警。我在课程里把 nginx TOP10 用两种语言各写过一遍,体会很具体:Shell 版短但改需求痛苦,Python 版长但第二次改需求十分钟搞定——这个对比我面试常讲。
  • 追问4(深挖):FastAPI 的异步原理? → 答:底层是 asyncio 事件循环:async def 定义的协程在遇到 IO 时主动让出控制权,事件循环切换去处理其他就绪任务,单线程就能维持大量并发连接,不像传统每请求一线程那样吃内存。再往下到 uvicorn 的事件循环实现细节、ASGI 协议规范这层,我还在学——我的口径是"自学中、能写会查、不深吹",面试官一般都认可这个边界感。

关联话术:"简历上写的是'正在自学 FastAPI',面试我就按这个口径走:'已经能写简单接口、知道它解决什么问题;把它放进我的成长路线——结合我主导公司 AI 工具评估落地的经历(阿里云百炼 API、Cherry Studio、Coze 工作流都在用),API 和工作流这套我上手很快。'这样把诚实和能力证明捆在一起,比单说'在学'有力得多。"

实操模板(Python 两个小脚本,可背):

python
#!/usr/bin/env python3
# 1) nginx 日志 TOP10 IP(dict 统计版)
from collections import Counter
import re
cnt = Counter()
with open('/var/log/nginx/access.log') as f:
    for line in f:
        ip = line.split()[0]
        cnt[ip] += 1
for ip, n in cnt.most_common(10):
    print(f"{n:>8}  {ip}")
python
#!/usr/bin/env python3
# 2) 系统资源采集(psutil)
import psutil, datetime, csv
row = [datetime.datetime.now().strftime('%F %T'),
       psutil.cpu_percent(interval=1),
       psutil.virtual_memory().percent,
       psutil.disk_usage('/').percent]
with open('/var/log/res_collect.csv', 'a', newline='') as f:
    csv.writer(f).writerow(row)
if row[1] > 90: print(f"CPU HIGH: {row[1]}%")

2.12 必背脚本模板库(现场手写五个)【必背】 ​

标准回答:我准备了五个手写模板覆盖高频手写题:第一 Nginx 日志 TOP10 IP,考三剑客管道;第二 MySQL 全量加增量备份,考数据库加脚本工程;第三 7 天前日志清理,考 find 时间参数;第四服务存活检测自愈,考判断和 systemd;第五批量主机巡检,考 for 加 ssh。

手写的结构要求统一:shebang、set 安全项、变量区、主逻辑、日志留痕五段式——宁可短而完整,不可长而残缺。每个模板的生产化改进点我也准备在追问里讲:备份要防锁表、巡检要防单台失败拖死全局、自愈要有升级告警边界、清理要先预览后删除。这五个模板是我把两年现场工作提炼出来的,全部在真实或课程环境跑过。

追问链:

  • 追问1:备份脚本怎么保证不锁表、不影响业务? → 答:四个手段叠加:mysqldump 对 InnoDB 加 --single-transaction,用一致性快照读,全程不锁表;时间选凌晨低峰(我配 2:00)把 IO 影响降到最低;如果有从库,备份放从库跑,主库零压力;脚本层面输出全部重定向到日志、失败时 echo 错误行方便接告警。四个手段分别解决锁、负载、资源和可观测四个维度——这样分维度答,比背参数有层次。
  • 追问2:巡检脚本几十台机器,有一台 SSH 不通怎么办? → 答:核心原则是"一台失败不能拖死全局"。两个实现:ssh 加 -o ConnectTimeout=5 限制等待时间,避免默认超时挂很久;命令包在 || echo "$host unreachable" 里,失败就记录一行继续下一台。产出统一 tee 到带日期的日志文件留档。我在银行项目跑批量巡检时,个别节点关机是常态,这个设计保证了报告永远能出来、还能顺手暴露失联节点。
  • 追问3:自愈脚本重启也拉不起来怎么办? → 答:自愈必须有边界:重启后二次检测,还是不通就写 FAILED 日志并触发告警——mail 或者 curl 一个 webhook。"只尝试一次,失败交给人"是我的原则,因为无限重启反而掩盖故障、刷爆日志,还可能把"配置错误"伪装成"随机故障"。这道题真正考的是运维的故障升级意识,不是脚本语法。
  • 追问4(深挖):怎么防止脚本被重复执行(上一次没跑完又触发)? → 答:两种方案:一是 flock 文件锁,crontab 行写成 flock -n /tmp/xxx.lock -c '/opt/scripts/xxx.sh',-n 拿不到锁就直接退出,上一轮还在跑时新触发自动放弃;二是脚本开头 pgrep -f "$0" | grep -v $$ 检查自身是否已有实例在跑。备份脚本最需要这个——全量备份跑 40 分钟,cron 每 10 分钟触发一次没有锁就是灾难。这个细节在面试里说出来很加分,因为它是真实事故才会教给你的。

实操模板一:Nginx 日志 TOP10 IP(三剑客综合)

bash
#!/bin/bash
LOG=${1:-/var/log/nginx/access.log}
[ -f "$LOG" ] || { echo "no log: $LOG"; exit 1; }
awk '{print $1}' "$LOG" | sort | uniq -c | sort -nr | head -10 \
  | awk '{printf "%-6s %s\n", $2, $1"次"}'

实操模板二:MySQL 全量+增量备份(crontab 配套)

bash
#!/bin/bash
# mysql_backup.sh —— 每天 2:00 全量(mysqldump) + 滚动 binlog 增量,保留7天
set -euo pipefail
BAK=/data/backup/mysql; DATE=$(date +%F)
USER=root; PASS='xxxx'; SOCK=/tmp/mysql.sock
mkdir -p $BAK/full $BAK/binlog
# 全量:--single-transaction 对InnoDB快照不锁表; --master-data=2 记录binlog位点
mysqldump -u$USER -p"$PASS" -A -R --triggers --single-transaction \
  --master-data=2 --flush-logs | gzip > $BAK/full/${DATE}_all.sql.gz
# flush-logs 已切出新binlog,把旧binlog拷走即增量
cp -a /data/mysql/binlog.* $BAK/binlog/ 2>/dev/null || true
find $BAK -type f -mtime +7 -delete        # 保留7天
echo "$(date +%F_%T) backup done" >> $BAK/backup.log
# 恢复思路: 全备导入 -> mysqlbinlog 从位点截取增量 -> 重放

实操模板三:7 天前日志清理

bash
#!/bin/bash
# clean_log.sh —— 清理 7 天前的日志,先预览后删除
DIR=${1:-/var/log/app}
find "$DIR" -type f -name "*.log" -mtime +7 -ls        # 预览
find "$DIR" -type f -name "*.log" -mtime +7 -delete    # 确认后删除
# 大量文件时用 xargs 更稳: find ... -print0 | xargs -0 rm -f

实操模板四:服务存活检测自愈

bash
#!/bin/bash
# check_app.sh —— 端口不通则尝试拉起并记录,crontab */2 * * * *
APP="nginx"; PORT=80; LOG=/var/log/appguard.log
if ss -lnt | grep -q ":$PORT "; then
  exit 0
fi
echo "$(date +%F_%T) $APP down, restarting" >> $LOG
systemctl restart $APP
sleep 3
if ss -lnt | grep -q ":$PORT "; then
  echo "$(date +%F_%T) $APP recovered" >> $LOG
else
  echo "$(date +%F_%T) $APP restart FAILED" >> $LOG   # 此处可接告警:mail/curl webhook
fi

实操模板五:批量主机巡检

bash
#!/bin/bash
# batch_check.sh —— 批量巡检:负载/内存/磁盘/失败服务
LIST=/opt/scripts/iplist.txt     # 每行: ip 或 ip:user
for host in $(cat $LIST); do
  echo "===== $host ====="
  ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no $host \
    "hostname; uptime | awk -F'load average:' '{print \$2}'; \
     free -m | awk '/Mem/{printf \"mem %s%%\n\", \$3/\$2*100}'; \
     df -h | awk '\$6==\"/\"{print \$6, \$5}'; systemctl --failed --no-legend"
done | tee /var/log/patrol_$(date +%F).log

2.13 脚本三种执行方式 source / bash / ./ 的区别 【了解】 ​

标准回答:三种执行方式的本质区别是"在哪个进程里跑"。bash script.sh 新开一个子 shell 进程执行,脚本里的 cd、export、变量修改都随子进程结束而消失,不影响当前终端——隔离性好,适合跑任务。./script.sh 按 shebang 指定的解释器在子进程执行,效果同上,但前提是文件有执行权限(chmod +x),报 Permission denied 就是没有 x 权限。

source script.sh(等价写法 . script.sh)在当前 shell 进程里执行,脚本内的变量定义、cd、函数都会留在当前会话——所以改了 /etc/profile 要 source 才生效、部署 Java 前要 source 环境变量文件加载 JDK 路径。

我的习惯用法:任务类脚本用 bash 或 ./ 跑保证隔离,环境加载类文件用 source 灌进当前会话,两者切换是每天的日常。

追问链:

  • 追问1:执行脚本报 Permission denied? → 答:说明文件没有执行权限,两条路:正规做法 chmod +x script.sh 然后 ./script.sh;或者绕开权限要求直接 bash script.sh,因为解释器执行不需要脚本本身有 x 位。反过来讲,如果脚本不该被执行(比如配置文件),去掉 x 权限就是一道防线。这个报错我在给客户交付时见过太多次,答案已经条件反射了。

关联话术:"这个知识点我按'用途'讲:部署数币平台后端 jar 之前,一定要先 source 环境变量文件把毕昇 JDK 的路径加载进来,否则 java 命令都找不到;而初始化脚本、部署脚本一律用 bash 跑,避免污染当前会话。'加载用 source、跑任务用 bash'这句话就是我的使用口诀。"

2.14 现场手写题应对策略 【了解】 ​

标准回答:现场手写脚本我有固定的四步应对法。第一步复述需求确认边界:输入是什么(文件、参数、列表)、输出到哪(屏幕、文件、日志)、失败怎么办(跳过还是终止)——这一步既是确认,也给自己争取构思时间,还展示了工程素养。第二步先写固定骨架:shebang、set 安全项、变量区、main 逻辑的空壳,把结构分先拿到手。

第三步往里填关键命令,用最熟的:pgrep、ss、find、awk 管道这些高频组合;拿不准的参数写思路注释,不硬编。第四步主动讲生产化改进:"生产版我会加日志、加 flock 防重复执行、加告警"——这一句话往往比脚本本身更值钱,因为它证明你写过生产脚本。高频手写题就五类:启停脚本、日志统计、备份脚本、清理脚本、批量巡检,全部覆盖在 2.12 的模板里,考前把五个模板各默写一遍就有底气。

追问链:

  • 追问1:写不出来完整版怎么办? → 答:策略是降级而不是空白:先写伪代码框架——循环、判断、调用的位置用注释标出来,把结构分拿到;然后填自己确定的部分;剩下不确定的命令参数,直接说"这个参数我记得大概,生产里我会 man 确认"。面试官考手写本质是验证"有没有真写过",一个结构完整、注释清楚、诚实标注疑点的版本,远好过一个编造参数的"完整"版本——这个判断我在几次模拟面试里都得到了验证。

关联话术:"我的手写策略本身就是话术:'拿到题我先确认输入输出和失败处理,然后写骨架再填命令,最后我一定补一句生产版会加日志和锁。'这三句话一说,面试官就知道我是按交付标准写脚本的——手写题考的从来不是背命令,是工程习惯。"

2.15 Shell 传参与 shift、getopts 【理解】 ​

标准回答:脚本的专业度很大程度体现在参数处理上。基础层:$1 到 $9 直接取参数,第 10 个开始必须用 ${10}(不加花括号 $10 会被解释成 $1 拼一个 0——经典坑);shift 把参数列表左移一位,shift 之后的 $1 变成原来的 $2,配合 while 循环可以处理任意数量参数,是"不知道有几个参数"场景的标准解法。

进阶层:getopts 是 Shell 内置的参数解析器,支持短选项和标志位,while getopts "u:p:h" opt; do case $opt in u) USER=$OPTARG;; ... esac; done 这样的结构能把脚本做成 ./xxx.sh -u admin -p xxx 的专业形态,还自带 -h 帮助分支。

更复杂的选项需求(长选项 --user)要外部的 getopt 或直接换 Python 的 argparse——这也呼应我"超百行换 Python"的选型线。

我的交付脚本都带 usage 函数:参数不合法就打印用法退出,绝不带空变量往下跑。

追问链:

  • 追问1:shift 的典型使用场景? → 答:最典型的是"参数是命令 + 参数列表"的包装脚本:脚本先取 $1 当命令,shift 掉,剩下的 "$@" 原样传给命令执行——比如 start|stop 之后的额外参数透传。另一个场景是遍历不定长参数列表:while [ $# -gt 0 ]; do 处理 $1; shift; done,比 for 循环灵活,因为可以在循环里按条件 shift 多个。这两种写法我在批量操作脚本里都用过。
  • 追问2:getopts 和 getopt 什么区别? → 答:getopts 是 Shell 内置的,只支持短选项(-u),但使用简单、跨 shell 行为一致,我的运维脚本用它就够了;getopt 是外部命令(util-linux),支持长选项(--user)和选项重排,但用法繁琐、不同发行版行为有差异。我的选型:简单脚本 getopts,参数复杂到需要长选项时说明脚本已经复杂到该换 Python argparse 了——工具选择和技术栈选择是同一个逻辑。

关联话术:"我的批量加载脚本就是'命令 + 参数列表'形态:脚本本身管循环和判断,镜像目录作为参数传入、$# 校验不为空、加载结果逐项校验——参数处理不是炫技,是让脚本能交给别人安全使用的护栏。这也是我交付的脚本客户敢直接重跑的原因之一。"

2.16 字符串处理与判断(截取/替换/长度)【理解】 ​

标准回答:Shell 字符串操作有一套 ${} 语法家族,我按使用频率讲。取长度:${#var}——统计字符串长度。截取子串:${var:起点} 从第几位取到尾、${var:0:8} 取前 8 位——最常用的场景是取日期字符串的一部分,比如 date +%Y%m%d 再截 小时分钟。

按模式删除:${var#*/} 从头最短匹配删到第一个 /、${var##*/} 最长匹配删到最后一个 /(等价 basename 拿文件名)、${var%/*} 从尾最短删(等价 dirname 拿目录)、${var%%.*} 删到第一个点之前(拿不含扩展名的文件名)。替换:${var/old/new} 替换第一处、${var//old/new} 替换全部。

这套语法在处理文件路径和版本号时比反复调 awk/cut 高效——比如从镜像全名 仓库/项目/名字:tag 里剥离出名字和 tag,两条 ${} 搞定。判断配套:[[ $var == abc* ]] 通配匹配、[[ $var =~ ^[0-9]+$ ]] 正则判断纯数字——表单校验类逻辑的标配。

追问链:

  • 追问1:##*/ 和 %%.* 组合能干什么? → 答:拆解文件名是它们的黄金组合:f="/data/images/kuboard-v3.5.2.tar.gz",${f##*/} 得到 kuboard-v3.5.2.tar.gz(去目录),再 ${name%%.tar*} 得到 kuboard-v3.5.2(去后缀)——两步拿到干净的名称。我在批量镜像加载脚本里就是用这类方法从文件名提取镜像名和版本做核对清单的;比 awk -F 分割更直观,且不产生子进程,循环里大规模调用时性能优势明显。
  • 追问2:字符串判空的几种写法和区别? → 答:[ -z "$var" ] 判空、[ -n "$var" ] 判非空,注意变量必须加双引号——不加时变量为空会导致 [ -n ] 参数塌缩成单参数,判断结果错误,这是历史事故级别的坑;更稳的写法是 [[ -n $var ]],双括号对空变量有保护。还有防御性默认值语法 ${var:-默认值}——变量为空就用默认值,我在脚本里给路径类变量统一加这层保护,避免空变量拼出 rm -rf / 这种灾难路径。

关联话术:"${var##*/} 剥文件名、${var%%.*} 去后缀这一组,是我处理镜像包和日志文件名的日常动作;加上 ${var:-默认值} 的防御默认值,我的脚本从不依赖'调用者一定记得传参'——这叫脚本的自卫能力,是我从批量部署的坑里攒出来的写法习惯。"

2.17 sort / uniq / cut / tr 管道家族 【理解】 ​

标准回答:三剑客之外的文本管道家族,配合起来覆盖大量统计场景。sort 排序:-n 按数值(不加它 10 会排在 9 前面)、-r 倒序、-k2 按第二列、-u 去重、-t: 指定分隔符——日志统计的"sort | uniq -c | sort -nr"铁三角里它出现两次,一次按内容排序让相同行相邻(uniq 的前提),一次按计数倒序取 TOP。uniq 去重计数:uniq -c 输出"次数 内容",它只合并相邻行所以必须先 sort——这个依赖关系是高频追问。

cut 按列切:cut -d: -f1 按分隔符取第一列、-c1-10 按字符位置截取,简单场景比 awk 轻量。tr 字符转换:tr 'a-z' 'A-Z' 大小写、tr -d '\r' 删 Windows 换行的 \r(处理客户传来的脚本文件必用)、tr -s ' ' 压缩连续空格。

家族组合的招牌菜就是 TOP N 统计:awk '{print $1}' log | sort | uniq -c | sort -nr | head——每个工具只做一件事、串起来完成统计,这就是 Unix 哲学。

追问链:

  • 追问1:为什么 uniq 前必须 sort? → 答:uniq 的实现是"只比较相邻行"——相同内容的行不相邻就不会被合并,计数也是按相邻段算的。所以管道里 sort 的作用不仅是排序,更是把相同行聚到一起为 uniq 创造前提。漏掉 sort 的现象很有迷惑性:uniq -c 输出会出现多个"1 相同IP"的行,统计结果错误但表面看命令都执行成功了——这个坑我在代码评审时给开发指出来过。
  • 追问2:cut 和 awk 取列怎么选? → 答:cut 胜在轻和快,但能力有限:分隔符只能是单字符、不能处理连续不定长空格(awk 按空白自动分列能处理)。所以我的选择逻辑:格式规整(冒号、逗号分隔的 passwd/csv 类)用 cut;日志这种空白不定的用 awk。另外 awk 能带条件和计算(awk '$3>1000{print $1}'),cut 纯切片——需要条件过滤时没得选,一定是 awk。

关联话术:"这个家族是我做访问报告的完整流水线:grep 按时间过滤 → awk 取 IP 列 → sort 加 uniq -c 计数 → sort -nr 排 TOP → head 截取,五连管道一行命令出报表。我在面试里手写这条管道不用思考,因为写了不下一百遍——这是最真实的'肌肉记忆'证明。"

2.18 日志分析实战五道经典题(面试手写高频)【必背】 ​

标准回答:日志分析是手写题最高频的题型,我把五道经典题的答案要点全部准备好。题一 TOP10 IP:awk '{print $1}' access.log | sort | uniq -c | sort -nr | head——考管道铁三角。题二 每个状态码的次数并找 5xx:awk '{s[$9]++} END{for(c in s) print c,s[c]}' 关联数组版,再 awk '$9>=500' | wc -l 数 5xx 总量。

题三 统计 4 点到 6 点的请求量:grep -E '24/Oct/2026:0[45]:|24/Oct/2026:06:' | wc -l 或者 awk 对时间字段做字符串比较(时间字段字典序可比较是关键洞察)。题四 每分钟错误趋势:grep 'error' log | awk '{print substr($4,2,17)}' | sort | uniq -c——截取到分钟粒度再计数,substr 精确切出时间片。

题五 找访问量最高的 URL:把铁三角的取列对象换成 $7(request 的 URL 字段)。五道题的公共骨架就两个:铁三角管道和关联数组——我面试时先说骨架再展开,显示的是"掌握模式"而不是"背题"。

追问链:

  • 追问1:日志字段按空格切分有坑吗? → 答:有,两个经典坑:一是 request 字段 "GET /path HTTP/1.1" 本身含空格,导致 URL 不是固定的第几列——$7 之所以能用,是因为默认日志格式里 request 前面恰好固定 5 个字段;改用双空格分隔或自定义格式时列号全变,稳妥做法是先统一 log_format 再写分析脚本。二是有些字段(如 user-agent)含大量空格,从后往前数要用 $NF。所以批量分析前先 head -1 看一眼真实格式再定列号——这是我的第一动作。
  • 迭问2:日志有几十 G,这些命令还能直接跑吗? → 答:能跑但要讲方法:awk/sort 都是流式或分块处理,几十 G 慢但可行;加速手段三个:先用 grep -E 按时间或状态码预过滤把量级降下来(这是收益最大的一步);用 zcat 直接处理压缩的历史日志不落地解压;split 分片并行处理后合并计数。我在银行项目处理 BCS 的大日志就是"先过滤再统计"的路子——任何时候都先想清楚要什么,再决定扫多少数据。

关联话术:"这五道题我在课程实验里全部实际写过并验证过输出,awk 关联数组版和管道版我各写一遍对比;面试手写我会主动写管道版(出错率低),然后口头补'纯 awk 版少两个进程'——两个版本都拿得出手,这题就稳了。"

2.19 Shell 脚本陷阱清单(血泪教训版)【理解】 ​

标准回答:我把踩过和见过的脚本陷阱整理成清单,每条都对应一类真实事故。一、变量不加引号:rm -rf $DIR 在变量为空时变成 rm -rf,灾难级——正确是 rm -rf "$DIR",路径有空格也靠引号保命。二、空变量扩散:不校验 $# 就使用 $1,脚本带着空值跑出不可预期结果——入口必须参数校验。三、[ 里用 >:< 在单括号里是重定向符不是比较符,会静默创建文件——比较用 -gt/-lt 或 [[ ]]。

四、遍历参数不用 "$@":用 $* 或不带引号的 $@,带空格参数被拆碎。五、cron 里的 % 不转义:命令被截断(2.7 的真实案例)。六、管道内修改变量无效:cat f | while read 的循环体在子 shell 里,count 出不来——用重定向写法或 awk 完成统计。七、find 结果直接进 for:文件名带空格会断开——用 find -print0 | xargs -0 或 while read。

八、脚本无锁可重入:上一次没跑完又被 cron 触发,两个实例同时写——flock(2.12 追问 4)。九、无日志静默失败:出了问题没现场——所有输出重定向落盘。这九条是我写脚本的自检清单,交付前逐条过。

追问链:

  • 迭问1:哪一条你觉得最致命? → 答:变量不加引号排第一,因为它和 rm -rf 组合时是数据毁灭级的。我的习惯防线有两层:所有变量展开一律双引号包裹;危险命令(rm、kubectl delete)的路径变量先 [ -n "$DIR" ] || exit 1 非空校验再加 -- 分隔(rm -rf -- "$DIR" 防止变量以 - 开头被当选项)。这两层成本几乎为零,收益是杜绝一整类事故——安全性和复杂度不一定正相关,这是脚本设计里最有性价比的投资。

关联话术:"这份清单我给团队做过一次内部分享,标题就叫'脚本事故九式',每条配一个真实或推演的翻车现场。面试里被问'你写脚本有什么讲究',我直接报这九条再展开两三条细节——比说'我代码规范'这种空话有说服力得多。"

2.20 expect 与批量交互场景 【了解】 ​

标准回答:expect 是处理交互式命令的自动化工具:有些命令(ssh 首次连接的 yes 确认、su 切换密码、某些设备初始化向导)必须人工输入,expect 用"等待特定提示 → 自动发送响应"的模式模拟人。核心四要素:spawn 启动交互进程、expect 匹配提示关键字、send 发送输入(\r 是回车)、interact 交还控制权。

一个骨架例:spawn ssh user@host,expect "password:",send "密码\r"。运维的典型用法是配合 for 循环批量初始化一批需要交互的设备。

但我要主动讲它的局限和替代:ssh 批量场景更好的方案是免密钥(ssh-keygen 加 ssh-copy-id)或 pssh/ansible——expect 传明文密码不安全也难维护;所以 expect 在我的工具箱里是"兜底件",只在密钥方案不可用(客户环境限制)时上场。移动云和银行的批量操作我走的都是密钥加脚本,expect 属于课程实验加理解口径。

追问链:

  • 迭问1:批量 ssh 为什么优先密钥而不是 expect? → 答:三个理由:安全——expect 脚本里是明文密码,落盘即泄密,密钥是免密的;可靠——expect 依赖提示符的精确匹配,系统提示语一变(中英文、版本差异)脚本就失效,密钥方案没有这个脆弱点;生态——密钥之上能直接跑 scp/rsync/ansible 全家桶,expect 每个命令都要写一套匹配。我的实践顺序:先申请密钥分发(金融环境走变更流程),实在不行才 expect 临时顶一下并限期替换。

关联话术:"'expect 我会用但尽量不用'——这句话我在面试里说过,它表达的是安全意识和方案选型能力:工具会得多不厉害,知道什么时候不该用才厉害。配合我 Ansible 的简历技能(第 17 条 CI/CD 与 Ansible),批量管理的正道我是清楚的。"

2.21 xargs 与并行批处理 【理解】 ​

标准回答:xargs 解决的问题是"把上游的输出变成下游命令的参数"——管道默认只能传标准输入流,而 rm、scp 这些命令要的是参数列表,xargs 就是这个转换器。基础用法:cat ip.txt | xargs -n1 ping -c1(-n1 每次取一个参数执行一次)、find /log -name "*.gz" | xargs rm -f 批量删除。两个安全细节必须会讲:一是文件名带空格——find 输出按空白切分会被拆碎,规范写法是 find ... -print0 | xargs -0 rm(用 null 分隔切割,null 不会出现在文件名里,绝对安全);二是参数上限——一次传参有 ARG_MAX 限制,xargs 默认自动分批,也可 -n 100 手工控批。并行是加分项:-P 8 起八个进程并发执行,批量 scp、批量探测这类无依赖任务能提速数倍。

我在批量操作里的固定流程是"两段式":先 dry-run 只打印将要执行的命令让人确认,确认后再真正执行——批量删除类操作这是保命流程,写进了我给团队的建议里。

追问链:

  • 迭问1:xargs 和 find -exec 怎么选? → 答:功能上等价,差别在效率和控制力:find -exec {} ; 对每个文件 fork 一个子进程,一万个文件一万次 fork,慢;xargs 把参数攒成批再执行(rm 一次删几百个),进程开销小一个量级,还有 -P 并行和 -n 控批能力。所以我的默认是 find -print0 | xargs -0,exec 只在简单场景顺手用。能讲出"批量化执行减少 fork"这个原理,这题就不是背参数。
  • 迭问2:批量执行有一条失败想继续并知道是谁,怎么办? → 答:两层处理:xargs 默认子命令失败不会中断后续批次(退出码反映最后一批),符合"继续跑完"的期望;关键是结果可观测——我给每条命令拼 && echo "$item ok" || echo "$item FAIL",输出统一 tee 到带日期的日志,跑完 grep FAIL 一眼得到失败清单再做二次处理。批量任务的完成度必须可验证——"跑完了"和"都成功了"是两件事,这是批量运维的纪律。

关联话术:"批量加载镜像、批量传证书这类'列表 → 动作'的任务,我的固定形态是 for 循环或 xargs 两种:小列表用 for(能加复杂判断),长列表用 xargs(高效可并行),日志全落盘。面试被问批量操作,我把两种形态的差异和选择标准讲出来,就是实战者的答案。"

2.22 Shell 与 Ansible 的边界:什么时候上自动化平台 【理解】 ​

标准回答:Shell 脚本和 Ansible(简历技能第 17 条)是规模分层关系不是替代关系。Shell 的特点:直接、零依赖(任何服务器都有 bash)、适合单机任务和"ssh 循环 + 命令"式的简单批量;但它的批量是"推命令"模式,没有状态管理——节点多了之后"哪些成功哪些失败、配置有没有漂移"就管不住了,而且跨发行版差异(yum vs apt、sshd vs ssh)要自己在脚本里写兼容。Ansible 的核心价值三个:声明式幂等——playbook 描述"应该是什么状态",执行时自动比对收敛,跑十遍结果一样;inventory 分组——按角色批量管理节点;模块化——package、service、template 等模块把跨发行版差异吸收掉。我的边界判断:三五台的重复任务用脚本;十台以上、多环境、要长期维护一致性的,上 Ansible。

移动云 7 台节点我用的是"脚本做初始化 + Kuboard 做日常"的组合;Ansible 属于我技能清单里持续深化的方向——这个口径诚实,同时展示了认知框架。

追问链:

  • 迭问1:Ansible 的幂等是怎么保证的? → 答:两层机制:模块层面,核心模块都设计成"先检查再执行"——package 模块先判断是否已安装、service 先查状态,已是目标状态就返回 ok 不做变更;声明层面,playbook 写 state: present、enabled: true,描述终态而非动作序列。对比 Shell 要自己写"存在就跳过"的判断(我的脚本用幂等设计模拟这个效果),Ansible 把幂等做进了框架——这是声明式和命令式的本质区别,和 K8s"调谐到期望态"是同一个哲学,我能把这三个体系串起来讲。

关联话术:"'脚本管机器、平台管状态'是我对工具箱的定位。我在两个项目的批量工作用 Shell 全流程扛下来,同时清楚十倍规模时 Ansible 的价值点(inventory、幂等、跨发行版模块)。面试聊自动化我不会把 Ansible 吹成生产主力,而是讲清边界和我的演进计划——诚实加认知框架,比吹嘘安全得多。"

2.23 日期与数值处理(date / bc / awk 算数)【了解】 ​

标准回答:运维脚本的日期处理有一套高频组合。date 取值:date +%F 年月日、date +%F_%T 带时间、date -d "yesterday" +%F 昨天、date -d "7 days ago" +%F 七天前——find -mtime 清理和备份文件名全靠它。嵌套坑要再强调一次:date 里的 % 在 crontab 里要转义(2.7 的经典坑),在脚本内生成则无碍,所以我的规范是"文件名在脚本内生成、crontab 只调脚本"。数值运算三件套:$((i+1)) 整数算术展开(首选,最干净);bc 算小数——echo "scale=2; 10/3" | bc 得 3.33;awk 在管道里边算边格式化——echo "95 100" | awk '{printf "%.1f%%\n", $1/$2*100}' 直接输出百分比。整数给双括号、小数给 bc 或 awk,这个分工我在写巡检报表时天天用:磁盘使用率、内存占比都是 awk printf 一行出结果。

追问链:

  • 迭问1:date 做时间差计算怎么做? → 答:用时间戳相减:start=$(date +%s) 记开始秒数,任务结束 end=$(date +%s),echo $((end-start)) 秒——我的备份和巡检脚本都用这个模式统计耗时写进日志,报表里"备份耗时趋势"就是这行代码攒出来的。要"X 分 Y 秒"的显示再做除模运算。这个小技巧写一次用三年,也是脚本"自带可观测性"的最小实现。

关联话术:"我的巡检和备份脚本里的文件名、耗时统计、百分比输出全是这一节的组合——'日期做文件名、时间戳算耗时、awk 出小数报表'。面试手写脚本时这些细节信手拈来,就是'天天写'和'考前突击'的区别。"

2.24 正则进阶:贪婪与非贪婪、分组与引用 【理解】 ​

标准回答:正则的进阶三点是区分"会写"和"写对"的分水岭。第一贪婪与非贪婪:量词(*、+、{n,m})默认贪婪——尽可能多匹配;加 ? 变非贪婪——尽可能少匹配。经典案例:提取 HTML 标签内容,<.*> 贪婪版会从第一个 < 一路吃到最后一个 >(把中间的标签全吞了),<.*?> 非贪婪版只吃到最近的 >——处理"成对分隔符"内容必须非贪婪。sed/awk/grep -E 都遵守这个规则。第二分组与引用:(abc) 捕获分组,\1 反向引用第一个分组(sed 里替换用 \1,扩展正则可写成 (\w+) \1 找重复单词);(?:...) 非捕获分组只要分组效果不要引用。2.4 的 sed -ri 's/^#?(Port ).*/\1 2222/' 反向引用就是分组应用的实战。第三边界:^ $\b 锚定行首行尾和单词边界——grep -w error 之所以精确就是 \b 的效果;多行模式(sed 按"行"处理天然带边界)和跨行匹配(要先 tr '\n' 合并或用 awk RS)的差异要心里有数。我的建议是把这三个点各配一个自己踩过的例子,面试时"正则怎么样"就能答成小亮点。

追问链:

  • 迭问1:给你一段日志让你提取字段,你的正则设计思路? → 答:三步法:先看"定界"——字段之间是固定分隔符(空格、竖线、逗号)还是上下文依赖;能用 -F 或 cut 按分隔符切的绝不用正则(简单工具优先);必须用正则时(比如"提取 URL 里最后一段路径"),先写死一条真实样本,从两端锚定(^ 或已知前缀),量词优先非贪婪,最后用 grep -o 或 sed -n 's/.(pattern)./\1/p' 提取并用两三条样本验证——包括一条"不该匹配"的负样本。设计正则的方法论比背语法更值钱。

关联话术:"正则我按'三层'自评:基础匹配和量词熟练、分组引用在 sed 批量改配置里天天用、复杂前瞻后顾这类我承认要现查文档——边界说清楚,反而显得整体可信。我还有一个习惯是任何正则改动先用 3 条样本验证再上生产,这是我改 15 台节点配置没出过事的底层原因。"

2.25 脚本质量保障:shellcheck 与 dry-run 文化 【了解】 ​

标准回答:脚本的质量保障我有两层工具和一层文化。工具第一层 shellcheck:静态分析器,不改脚本只挑毛病——未加引号的变量、可能为空的展开、-e 下的隐蔽分支、废弃语法,每条告警都带编号(SC2086 这种)和解释;我的习惯是新脚本交付前过一遍 shellcheck,把 warning 级清零——它抓出来的问题多数是"平时不炸、特定输入才炸"的隐患,性价比极高。工具第二层 dry-run:脚本带 --dry-run 或 DRYRUN=1 模式,只打印将执行的命令不真执行——我在批量删除、批量变更类脚本里必做这个模式(2.21 的两段式就是它)。文化层是"评审 + 留痕":重要脚本互相 review(重点看 rm、delete、写操作),所有变更脚本归档到统一目录带版本——出问题能定位到"哪一版脚本、谁执行的"。这三层不是形式主义:我在银行环境交付的脚本客户自己要反复重跑,质量标准是被客户逼出来的。

追问链:

  • 迭问1:shellcheck 报的 SC2086 是什么问题? → 答:SC2086 是"变量未加双引号"——rm -rf $DIR 在 DIR 含空格时被拆成多个参数、为空时变成裸 rm -rf,正是 2.19 陷阱清单的第一条。shellcheck 把这类问题做成静态可检的规则,修复就是加引号 "$DIR"。举这个例子的价值在于说明 shellcheck 不是玄学——它检查的每一条都对应真实事故模式,我能在陷阱清单和它的告警之间建立对应关系,说明两个工具我都真用过。
  • 迭问2:批量部署脚本怎么设计"节点清单"? → 答:我的固定格式是"每行一个节点:ip 或 ip:user:port",脚本逐行解析——好处是清单即文档,加节点改一行;进阶版按组分段([master]、[worker]),不同组执行不同动作。这个设计受了 Ansible inventory 的启发,也让我在多人协作时"谁在清单里、用什么账号"一目了然。清单文件和脚本一起进 git,历史可查——批量操作的"配置和逻辑分离"就是这么一小步,但省掉的是"改脚本里的 IP 列表忘了备份"这类低级事故。

关联话术:"'脚本交付三件套:shellcheck 过、dry-run 模式、归档带版本'——这句话我在面试里说,面试官一般都会追问细节,正好把 2.19 的陷阱清单和 2.21 的两段式串起来讲。质量意识是运维从'能干'到'可信'的分水岭,这是我三年驻场最大的体会之一。"

2.26 本章案例 ​

  • 【真实项目】(银行)数据面节点批量镜像加载脚本:背景是银行内网环境完全无法访问海外镜像仓库,9 个 BCS 数据面节点需要各加载几十个 arm64 镜像 tar 包,逐台手工敲命令既慢又容易漏。任务由我负责把镜像高效准确地铺满全部数据面节点。行动:我写了一个 for 循环遍历镜像目录的脚本,用 case 语句排除 edge-manager 开头的管理面专用镜像(那些是管理面节点加载的,数据面不需要),其余逐个 cat $f | docker load 导入并 echo 进度,每一步用 $? 判断成败、失败即停。结果:9 个节点一次跑通,BCS 数据面纳管如期完成,脚本和镜像清单一起沉淀进《智能合约引擎安装部署指南》——这是简历技能第 2 条"批量部署自动化"最实证的一条。
  • 【课程项目】MySQL 定时备份全流程:按 2.12 模板二完成"全量 mysqldump --single-transaction + --flush-logs 切 binlog 做增量 + find -mtime +7 滚动清理 + crontab 2:00 调度"的完整闭环,并做了恢复演练:全备导入测试库、mysqlbinlog 从记录位点截取增量重放、核对数据。这个实验把第 3 章的备份理论变成了肌肉记忆——面试聊备份时我说的每个参数都有实操支撑。
  • 【经典坑】crontab 里的 % 转义:备份文件名要带日期 date +%F,直接写在 crontab 命令行里,任务"日志显示触发了却没有任何产物"。定位:查 cron 日志确认任务执行了,再看脚本输出发现命令在 % 处被截断——% 在 crontab 里是特殊字符,代表换行,后面的内容被当成了标准输入。解决:把 % 转义为 \%,或者更彻底地把整个逻辑封装进脚本、crontab 行只写脚本绝对路径。从此我所有 cron 任务遵守一条铁律:"crontab 只调脚本,不写长命令"——这条规范后来写进了交付文档。

章末自测清单(快问快答) ​

  1. $#、$?、$$ 分别是什么? → 参数个数、上条命令退出状态、当前脚本 PID。
  2. $* 和 $@ 加引号后的区别? → "$*" 一个整体字符串,"$@" 各参数独立。
  3. 怎么判断上条命令成功? → [ $? -eq 0 ],或直接 cmd && echo ok || echo fail。
  4. 删配置文件注释行和空行一条命令? → sed '/^#/d;/^$/d' file 或 grep -Ev '^#|^$' file。
  5. 统计 nginx TOP10 IP 一条管道? → awk '{print $1}' access.log | sort | uniq -c | sort -nr | head。
  6. awk 关联数组统计的写法? → awk '{ip[$1]++} END{for(i in ip) print i,ip[i]}'。
  7. sed 直接改文件且自动备份? → sed -i.bak 's/a/b/g' file。
  8. crontab 五段顺序? → 分 时 日 月 周。
  9. crontab 里 % 为什么要转义? → % 是 cron 特殊字符表示换行,会截断命令;转义 % 或封装进脚本。
  10. set -euo pipefail 三个选项各管什么? → -e 出错即停、-u 未定义变量报错、pipefail 管道任一环失败整体算失败。
  11. bash -x 的作用? → 打印每条命令展开执行过程,调变量值利器;bash -n 只查语法。
  12. source 和 bash 执行脚本的区别? → source 在当前 shell 执行(变量/cd 留存),bash 开子 shell 隔离。
  13. Shell 和 Python 怎么选? → 命令编排/文本处理用 Shell;复杂逻辑/数据结构/API 调用用 Python。
  14. FastAPI 三大卖点? → 装饰器路由、类型注解自动生成 Swagger 文档、原生 async 异步高性能。
  15. 备份脚本里 --single-transaction 的作用? → InnoDB 快照备份不锁表(一致性读)。