Bash 脚本逐行详解:一个HTTP请求与文件下载的示例
📋 目录
- 脚本整体结构概览
- 逐行逐函数详解
- 核心知识点汇总
- 二次开发指南
- 常见问题与调试技巧
一、脚本整体结构概览
这个脚本是一个交互式数据获取器,用于演示如何通过命令行工具获取和下载网络资源。整体流程如下:
┌─────────────────────────────────────────────────────────┐ │ 1. 创建下载目录 │ │ 2. 进入无限循环(while true) │ │ ├─ 输入关键词(输入 q 退出) │ │ ├─ 输入子关键词 │ │ ├─ 搜索资源 → 获取资源ID │ │ ├─ 用ID获取真实下载链接 │ │ ├─ 提取文件扩展名 │ │ ├─ 下载文件 │ │ ├─ 检查HTTP状态码 │ │ └─ 移动到指定目录 │ │ 3. 退出循环,显示结束信息 │ └─────────────────────────────────────────────────────────┘二、逐行逐函数详解
🔹 第 1 行:#!/bin/bash
#!/bin/bash这行代码称为shebang,告诉操作系统脚本应该由哪个解释器来执行。此处指定使用bash。
🔹 第 3 行:创建下载目录
mkdir-p/storage/emulated/0/下载的资源mkdir: 创建目录的命令。-p: 如果父目录不存在,则一并创建,且如果目录已存在,不报错。
🔹 第 12 行:变量拼接
name="${artist}-${song}"| 代码段 | 说明 |
|---|---|
name= | 定义变量name |
"${artist}-${song}" | 字符串拼接。将artist和song用-连接起来 |
${...} | 变量引用的标准写法,比直接$artist更安全 |
示例:
artist="示例歌手"song="示例歌曲"name="示例歌手-示例歌曲"🔹 第 13-25 行:函数get_resource_search_results()
get_resource_search_results(){curl-s-XGET"https://api.example.com/resource/search?keyword=${artist}-${song}&page=1"\-H'User-Agent: Mozilla/5.0 (Linux; Android 14; Redmi K30 Pro Build/UKQ1.230804.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'\-H'Accept: */*'\--compressed}📌 函数定义语法
函数名(){# 函数体}📌curl命令详解
curl是一个强大的命令行网络请求工具,相当于命令行版的浏览器。
| 参数 | 全称 | 说明 |
|---|---|---|
-s | silent | 静默模式,不显示进度条和错误信息 |
-X GET | 指定请求方法为 GET | GET 是获取数据的标准方法 |
-H | Header | 添加 HTTP 请求头,模拟浏览器行为 |
--compressed | 接受压缩响应 | 告诉服务器可以发送 gzip 压缩的内容 |
📌 URL 解析
https://api.example.com/resource/search?keyword=示例歌手-示例歌曲&page=1| 部分 | 说明 |
|---|---|
https:// | 协议,加密的超文本传输协议 |
api.example.com | 服务器域名(教学示例) |
/resource/search | 接口路径 |
?keyword=示例歌手-示例歌曲 | 查询参数:搜索关键词 |
&page=1 | 查询参数:页码(page number)= 1 |
📌 HTTP 请求头解析
请求头是浏览器/客户端发给服务器的“自我介绍”。
| 头信息 | 说明 |
|---|---|
User-Agent | 最重要的头!伪装成 Android 手机浏览器,让服务器以为是正常用户 |
Accept: */* | 接受任何类型的响应内容 |
🔹 第 27-30 行:函数extract_rid()
extract_rid(){echo"${1}"|awk-F'"''/\"id\"/{print $4; exit}'}📌 函数参数
${1}是函数的第一个参数。
📌awk命令解析
假设搜索返回的 JSON 类似这样:{"status":1,"data":[{"id":"12345","name":"示例"}]}
-F'"': 将"作为字段分隔符。'/\"id\"/{...}': 匹配包含"id"的行。print $4: 打印第4个字段(即id的值)。
🔹 第 31-37 行:函数get_resource_url()
get_resource_url(){curl-s-XGET"https://api.example.com/resource/download?id=${1}"\-H'User-Agent: Mozilla/5.0 (Linux; Android 14; Redmi K30 Pro Build/UKQ1.230804.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'\--compressed}这个函数和get_resource_search_results()几乎一样,区别是:
| 比较项 | 搜索函数 | 下载URL获取函数 |
|---|---|---|
| URL | search?keyword=... | download?id=... |
| 参数 | 关键词 | 资源ID |
| 返回 | 搜索结果列表 | 真实下载链接 |
流程关系:
搜索(关键词) → 获取资源ID → 用ID获取真实下载链接🔹 第 39-44 行:函数get_file_extension()
get_file_extension(){case"${1}"in*.*)echo"${1##*.}";;*)echo'';;esac}📌case语句
case"变量"in模式1)命令;;模式2)命令;;*)默认命令;;esac📌${1##*.}—— 参数扩展
这是 bash 的高级字符串操作:
| 语法 | 说明 |
|---|---|
${var##*.} | 从变量开头删除最长匹配的*.部分,保留剩余内容 |
示例:
url="https://example.com/resource/file.pdf"${url##*.}# 结果是:pdf函数逻辑:
- 如果 URL 包含
.→ 提取扩展名(pdf/txt/json 等) - 如果 URL 不包含
.→ 返回空字符串
🔹 第 46-50 行:函数download_resource()
download_resource(){echo"开始下载${name}.${2}..."curl-# ${1} >"${name}.${2}"echo"下载完成${name}.${2}."}| 代码段 | 说明 |
|---|---|
${name} | 使用外层定义的name变量(关键词-子关键词) |
${1} | 第一个参数:下载链接 |
${2} | 第二个参数:文件扩展名 |
curl -# | -#显示简单的进度条(# 号) |
> | 重定向符号,把 curl 的输出保存到文件 |
🔹 第 52-55 行:主执行流程(调用函数)
search_result=$(get_resource_search_results)RID=$(extract_rid"${search_result}")resource_url=$(get_resource_url"${RID}")file_extension=$(get_file_extension"${resource_url}")📌$(命令)—— 命令替换
$(命令)会执行括号内的命令,把输出结果作为字符串返回。
逐行解析:
| 代码 | 说明 |
|---|---|
search_result=$(get_resource_search_results) | 调用搜索函数,把返回的 JSON 保存到search_result |
RID=$(extract_rid "${search_result}") | 把搜索结果传给extract_rid,提取资源ID |
resource_url=$(get_resource_url "${RID}") | 用资源ID获取真实下载链接 |
file_extension=$(get_file_extension "${resource_url}") | 从链接中提取文件扩展名 |
数据流:
关键词 → 搜索结果(JSON) → 资源ID → 下载URL → 扩展名🔹 第 58-64 行:下载并检查状态
# 开始下载download_resource"${resource_url}""${file_extension}"# 检查下载进度,直到进度条达到100%progress=$(curl-s-o/dev/null-w"%{http_code}""${resource_url}")while["$progress"!="200"];dosleep1progress=$(curl-s-o/dev/null-w"%{http_code}""${resource_url}")done📌curl -s -o /dev/null -w "%{http_code}"
| 参数 | 说明 |
|---|---|
-s | 静默模式 |
-o /dev/null | 把响应体丢弃(/dev/null是“黑洞”) |
-w "%{http_code}" | write out,只输出 HTTP 状态码 |
📌 这个循环的问题说明(教学重点)
这段代码存在以下逻辑问题,是为了教学演示而保留的:
- 顺序错误:先执行了
download_resource下载文件,然后才检查状态码。如果下载时出错,文件可能已经损坏。 - 无限循环风险:如果服务器一直返回非200状态码,这个循环会永远执行下去。
正确的做法应当是:先检查状态码,确认可以下载后再开始下载;或者给循环加上最大重试次数。
🔹 第 66 行:移动文件
mv-f./{*.pdf,*.txt,*.json}/storage/emulated/0/下载的资源| 代码段 | 说明 |
|---|---|
mv | move,移动文件 |
-f | force,强制覆盖,不提示 |
./ | 当前目录 |
{*.pdf,*.txt,*.json} | 花括号扩展,匹配当前目录下所有 pdf/txt/json 文件 |
目标路径 | /storage/emulated/0/下载的资源 |
四、二次开发指南
🛠️ 改进方向 1:添加重试次数限制
# 改进后的下载检查(最多重试 5 次)max_retries=5retry_count=0progress=$(curl-s-o/dev/null-w"%{http_code}""${resource_url}")while["$progress"!="200"]&&[$retry_count-lt$max_retries];dosleep2progress=$(curl-s-o/dev/null-w"%{http_code}""${resource_url}")retry_count=$((retry_count+1))echo"第${retry_count}次重试..."doneif["$progress"=="200"];thendownload_resource"${resource_url}""${file_extension}"elseecho"下载失败,已重试${max_retries}次"continuefi🛠️ 改进方向 2:支持批量下载(从文件读取列表)
创建列表文件resources.txt:
示例歌手-示例歌曲 另一作者-另一作品 第三作者-第三作品改进脚本:
#!/bin/bashmkdir-p/storage/emulated/0/下载的资源# 从文件读取,每行格式:关键词-子关键词whileIFS=read-rline||[-n"$line"];do# 跳过空行和注释行[-z"$line"]&&continue[["$line"=~^# ]] && continue# 用 - 分割主关键词和子关键词artist="${line%%-*}"song="${line#*-}"name="${line}"echo"正在处理:${name}"# ... 原来的搜索、下载逻辑 ...done<"resources.txt"🛠️ 改进方向 3:添加下载进度显示
download_resource(){echo"开始下载${name}.${2}..."curl--progress-bar-o"${name}.${2}""${1}"echo""echo"下载完成${name}.${2}."}🛠️ 改进方向 4:添加日志记录
LOG_FILE="download.log"log(){echo"[$(date'+%Y-%m-%d %H:%M:%S')]$1"|tee-a"$LOG_FILE"}# 使用方式log"开始下载:${name}"log"下载失败: HTTP${progress}"| 代码段 | 说明 |
|---|---|
date '+%Y-%m-%d %H:%M:%S' | 获取当前时间 |
tee -a | 同时输出到屏幕和追加到文件 |
🛠️ 改进方向 5:支持自定义下载目录
read-p"请输入下载目录(默认 /storage/emulated/0/下载的资源):"download_dirdownload_dir="${download_dir:-/storage/emulated/0/下载的资源}"mkdir-p"$download_dir"# 修改移动命令mv-f./{*.pdf,*.txt,*.json}"$download_dir"🛠️ 改进方向 6:使用更健壮的 JSON 解析
原代码的awk方法不够健壮,推荐安装jq工具:
# 安装 jq(需要管理员权限)# apt install jq# 改进后的 extract_ridextract_rid(){echo"${1}"|jq-r'.data[0].id // empty'}🛠️ 改进方向 7:添加文件存在性检查
download_resource(){localfilename="${name}.${2}"if[-f"$filename"];thenread-p"文件${filename}已存在,是否覆盖?(y/n): "confirmif["$confirm"!="y"];thenecho"跳过下载"return1fifiecho"开始下载${filename}..."curl-# "${1}" > "$filename"echo"下载完成${filename}."}| 代码段 | 说明 |
|---|---|
[ -f 文件 ] | 检查文件是否存在 |
local | 定义局部变量(只在函数内有效) |
return 1 | 返回错误码(非 0 表示出错) |
📝 完整改进版脚本示例
#!/bin/bash# ========== 配置区 ==========DOWNLOAD_DIR="/storage/emulated/0/下载的资源"MAX_RETRIES=3LOG_FILE="download.log"# ===========================# 创建下载目录mkdir-p"$DOWNLOAD_DIR"# 日志函数log(){echo"[$(date'+%Y-%m-%d %H:%M:%S')]$1"|tee-a"$LOG_FILE"}log"========== 资源下载器启动 =========="whiletrue;doread-p"请输入主关键词(退出输入 q ):"artist["$artist"=="q"]&&breakread-p"请输入子关键词(空格输入 _ ):"songname="${artist}-${song}"log"开始搜索:${name}"# 搜索资源search_result=$(curl-s-XGET"https://api.example.com/resource/search?keyword=${artist}-${song}&page=1"\-H'User-Agent: Mozilla/5.0 (Linux; Android 14; Redmi K30 Pro) AppleWebKit/537.36 Chrome/117.0.0.0 Mobile Safari/537.36'\-H'Accept: */*'\--compressed)# 提取资源IDRID=$(echo"${search_result}"|awk-F'"''/\"id\"/{print $4; exit}')if[-z"$RID"];thenlog"错误: 未找到资源${name}"continuefilog"获取到资源ID:${RID}"# 获取下载链接resource_url=$(curl-s-XGET"https://api.example.com/resource/download?id=${RID}"\-H'User-Agent: Mozilla/5.0 (Linux; Android 14; Redmi K30 Pro) AppleWebKit/537.36 Chrome/117.0.0.0 Mobile Safari/537.36'\--compressed)if[-z"$resource_url"];thenlog"错误: 无法获取下载链接"continuefilog"下载链接:${resource_url}"# 提取扩展名if[["$resource_url"==*.*]];thenfile_extension="${resource_url##*.}"elsefile_extension="txt"fifilename="${name}.${file_extension}"# 检查文件是否已存在if[-f"$filename"];thenread-p"文件${filename}已存在,是否覆盖?(y/n): "confirm["$confirm"!="y"]&&continuefi# 下载文件(带重试)log"开始下载:${filename}"retry_count=0download_success=falsewhile[$retry_count-lt$MAX_RETRIES];doifcurl-fSL-o"$filename""$resource_url";thendownload_success=truebreakfiretry_count=$((retry_count+1))log"下载失败,第${retry_count}次重试..."sleep2doneif["$download_success"=true];thenmv-f"$filename""$DOWNLOAD_DIR"log"下载成功:${filename}"elselog"下载失败:${filename}(已重试${MAX_RETRIES}次)"rm-f"$filename"fidonelog"========== 资源下载器结束 =========="echo"感谢使用!"