戰(zhàn)避開跨月與時(shí)區(qū)陷阱)
簡(jiǎn)介這份資源以PDF文檔形式收錄了一個(gè)Shell腳本遍歷日期范圍的具體實(shí)例適合需要在Linux/Unix環(huán)境下按日期批量處理數(shù)據(jù)的運(yùn)維人員、開發(fā)者和自動(dòng)化腳本初學(xué)者。文檔詳細(xì)講解了如何通過兩個(gè)命令行參數(shù)傳入開始與結(jié)束日期借助date -d將日期轉(zhuǎn)換為Unix時(shí)間戳再利用嵌套循環(huán)遞減輸出每一天并說明了外層循環(huán)邊界設(shè)定、日期格式化等關(guān)鍵細(xì)節(jié)。資源為單個(gè)PDF文件共1個(gè)文件大小僅27KB內(nèi)容緊湊便攜。已有2025人學(xué)習(xí)下載適合作為編寫定時(shí)任務(wù)、日志分析或批量數(shù)據(jù)抓取腳本時(shí)的參考。通過閱讀該實(shí)例可以掌握日期范圍遍歷的完整思路并理解如何與Python腳本聯(lián)動(dòng)為后續(xù)擴(kuò)展自動(dòng)化任務(wù)打下基礎(chǔ)。1. Shell腳本遍歷一個(gè)日期范圍一次循環(huán)省下三個(gè)通宵月度報(bào)表在凌晨?jī)牲c(diǎn)跑掛恢復(fù)時(shí)間只有六小時(shí)需要按天重跑七條任務(wù)日志抽取工具連著抽三天就漏了一天翻日志全靠肉眼。這種場(chǎng)景下Shell腳本遍歷一個(gè)日期范圍是最直接的止血手段把“從哪天到哪天”變成一段可重復(fù)執(zhí)行的循環(huán)邏輯讓補(bǔ)數(shù)、抽數(shù)、清理這些活不再靠人肉一條條復(fù)制命令。它不復(fù)雜一行 for 加一個(gè) date 就能啟動(dòng)但真正的坑都埋在格式、跨月和時(shí)區(qū)里。這篇文章寫給正在接觸 shell 腳本編程的運(yùn)維、數(shù)據(jù)工程師也寫給所有想把按天批處理自動(dòng)化的人。2. 為什么單靠 for 循環(huán)會(huì)翻車先把日期當(dāng)字符串而不是數(shù)字2.1 月份進(jìn)制與跨月為什么 i$(($i1)) 走不通不少第一次寫日期遍歷的人會(huì)直接走到“數(shù)字累加”這條路上去因?yàn)?shell 腳本 for 循環(huán)天生就是為數(shù)字準(zhǔn)備的。比如for i in $(seq 20250101 20250107)看著很順眼因?yàn)?20250101 到 20250107 確實(shí)像一串連續(xù)的整數(shù)。但問題出在月底今天是 20250131明天應(yīng)該是 20250201數(shù)字累加的結(jié)果卻是 20250132。這不是一個(gè)存在日期后續(xù)所有邏輯都會(huì)崩。根子在于日期不是十進(jìn)制。它內(nèi)部是“年、月、日”三層嵌套的進(jìn)制月有大小月二月還要看閏年如果哪天你接到一個(gè)需求要遍歷到 2 月 29 日純數(shù)字累加完全無法判斷當(dāng)年是不是閏年。另一個(gè)隱蔽問題是字符串長(zhǎng)度2025-1-1 和 2025-01-01 在字典序比較時(shí)結(jié)果完全不同2025-1-31 2025-02-01這個(gè)比較在字符串語(yǔ)境下是成立的但按真實(shí)日期排序它就錯(cuò)了。所以遍歷日期的第一原則永遠(yuǎn)不要把日期當(dāng)成普通整數(shù)來遞增要把變化動(dòng)作交給專門的命令。2.2 把變量交給 date 命令linux 的 shell 腳本變量與輸入習(xí)慣日期計(jì)算這件事Linux 上最可靠的只有date命令本身。它接受兩種典型輸入一種是-d 2025-01-01這種自然語(yǔ)言描述另一種是-d 時(shí)間戳這種 Unix 時(shí)間戳輸出格式完全由%Y%m%d這類占位符控制。寫 shell 腳本遍歷日期時(shí)循環(huán)變量存什么格式?jīng)Q定了腳本復(fù)雜度的天花板。我一般會(huì)把日期存成YYYY-MM-DD這種帶橫線的格式因?yàn)樗莇ate命令最不會(huì)誤判的輸入格式也符合字典序與時(shí)間序一致的特性。取當(dāng)天零點(diǎn)的寫法是today$(date %F)這條命令里%F等價(jià)于%Y-%m-%d輸出類似2025-04-11。如果你想取“昨天”就寫成yesterday$(date -d 2025-04-11 -1 day %F)凡是把變量喂給date -d的場(chǎng)景都建議在變量外加雙引號(hào)防止變量為空時(shí)命令被拆成兩個(gè)參數(shù)。這也是 linux 的 shell 腳本變量與輸入里最容易被忽略的細(xì)節(jié)變量沒加引號(hào)等于把控制權(quán)交了出去。在一臺(tái)干凈的 CentOS 或 Ubuntu 上這套寫法可以直接跑不依賴任何第三方工具。3. 從時(shí)間戳到純 date 遞進(jìn)兩個(gè)可直接運(yùn)行的遍歷腳本3.1 新手友好的 shell 腳本 for 循環(huán)寫法從時(shí)間戳開始要快速驗(yàn)證遍歷邏輯最省事的是把日期先轉(zhuǎn)成時(shí)間戳因?yàn)闀r(shí)間戳是一串連續(xù)整數(shù)可以直接放進(jìn) for 循環(huán)里。時(shí)間戳的跨度對(duì)應(yīng)真實(shí)的秒數(shù)跨月、跨年它自己會(huì)處理不用你操心大小月。#!/bin/bash start_ts$(date -d 2025-01-25 %s) end_ts$(date -d 2025-02-03 %s) for ((ts start_ts; ts end_ts; ts 86400)); do day$(date -d $ts %Y-%m-%d) echo processing $day done這段腳本的邏輯是先用date -d 2025-01-25 %s把起始日期轉(zhuǎn)成 Unix 時(shí)間戳得到一個(gè)很大的整數(shù)然后按一天 86400 秒逐步遞增循環(huán)體里再用date -d $ts %Y-%m-%d把時(shí)間戳轉(zhuǎn)回人類可讀的日期。前綴在 GNU date 里專門用來標(biāo)記時(shí)間戳輸入不要漏掉。%Y-%m-%d這里我故意不用%F是為了讓你看清格式占位符的組成兩者效果一樣。這個(gè)方案適合快速驗(yàn)證但有一個(gè)隱藏假設(shè)一天恰好等于 86400 秒。如果系統(tǒng)時(shí)區(qū)在春夏之交發(fā)生夏令時(shí)切換那一天實(shí)際只有 23 小時(shí)或 25 小時(shí)ts 86400就會(huì)偏移最終導(dǎo)致某天被跳過或重復(fù)執(zhí)行。國(guó)內(nèi)服務(wù)器一般不受影響但海外節(jié)點(diǎn)很常見。3.2 穩(wěn)妥的 shell 腳本 while 遍歷逐日加一天要繞開“一天多少秒”這個(gè)魔咒最好的辦法是不計(jì)算秒數(shù)直接讓date命令幫你把日期加一天。#!/bin/bash current2025-01-25 end2025-02-03 while [[ $current $end || $current $end ]]; do echo processing $current current$(date -d $current 1 day %F) donewhile 的循環(huán)條件做了兩件事如果當(dāng)前日期小于結(jié)束日期就繼續(xù)等于結(jié)束日期也執(zhí)行最后一次。很多人的第一版只寫了[[ $current $end ]]結(jié)果最后一天永遠(yuǎn)不會(huì)被處理因?yàn)槿掌谙嗟葧r(shí)循環(huán)已經(jīng)退出。date -d $current 1 day %F的意思是把$current當(dāng)作起點(diǎn)往后推一天輸出為YYYY-MM-DD。這里的$current 1 day是作為一個(gè)整體字符串傳給date的變量必須在引號(hào)內(nèi)否則空格會(huì)把參數(shù)拆開。這個(gè)版本的優(yōu)點(diǎn)是不關(guān)心每個(gè)月有多少天更不關(guān)心是不是閏年因?yàn)樗腥掌谕七M(jìn)邏輯都交給date去判斷。代價(jià)是循環(huán)次數(shù)多時(shí)每次迭代都要fork一個(gè)date進(jìn)程性能稍差但遍歷一年 365 天也沒感覺。比起性能我更建議在意格式統(tǒng)一current和end必須都是等長(zhǎng)字符串2025-1-25這種寫法會(huì)讓字典序比較失效。4. 跨越月份與年份動(dòng)態(tài)起止日期與邊界處理4.1 上個(gè)月與月初用 date 拼出調(diào)度區(qū)間真實(shí)需求里起止日期很少是寫死的。更多情況是“從本月 1 號(hào)到今天”“從上周一到上周日”“從 2024-01-01 到上個(gè)月末”。這些日期都能用一行date拼出來不需要你自己數(shù)天數(shù)。先看最常用的幾個(gè)# 本月 1 號(hào) first_of_month$(date -d $(date %Y-%m-01) %F) # 上個(gè)月 1 號(hào) first_of_last_month$(date -d $(date %Y-%m-01) -1 month %Y-%m-01) # 上個(gè)月最后一天 last_day_of_last_month$(date -d $(date %Y-%m-01) -1 day %F)核心技巧是date -d接受復(fù)合表達(dá)式。$(date %Y-%m-01)先拼出當(dāng)月的 1 號(hào)再交給外層date做加減。這里必須用命令替換把內(nèi)層結(jié)果先算出來不能寫成date -d %Y-%m-01 -1 day這種形式那是無效參數(shù)。取上個(gè)月最后一天的邏輯是先到本月 1 號(hào)再往前倒一天這樣天然處理了 2 月長(zhǎng)短和閏年。把這段邏輯接到遍歷循環(huán)里就形成了“跑上周一到上周日”的完整腳本#!/bin/bash start$(date -d last monday %F) current$start end$(date -d $start 6 day %F) while [[ $current $end || $current $end ]]; do echo processing $current current$(date -d $current 1 day %F) donedate -d last monday是 GNU date 對(duì)自然語(yǔ)言解析的支持它返回本周一或上周一取決于今天是不是周一。這類寫法可讀性很強(qiáng)但可移植性差BSD 版的date完全不支持。如果你的腳本只在 Linux 調(diào)度機(jī)上跑可以放心用。4.2 分區(qū)與文件名格式Y(jié)YYYMMDD、YYYY-MM-DD 與時(shí)間戳怎么選遍歷邏輯里日期格式不只是給人看的它直接對(duì)接下游的文件名、Hive 分區(qū)、API 參數(shù)。我建議在循環(huán)體里把三種格式都準(zhǔn)備好按需取用。格式示例適合場(chǎng)景注意點(diǎn)YYYY-MM-DD2025-04-11日志文件名、date 命令輸入字典序即時(shí)間序適合比較YYYYMMDD20250411Hive 分區(qū)、數(shù)據(jù)庫(kù)分區(qū)字段無分隔符注意補(bǔ)齊前導(dǎo)零時(shí)間戳1744329600時(shí)間范圍計(jì)算、API 簽名受時(shí)區(qū)與夏令時(shí)影響循環(huán)體里的標(biāo)準(zhǔn)做法是維護(hù)一個(gè)基準(zhǔn)日期變量其他格式都從它派生while [[ $current $end ]]; do part$(date -d $current %Y%m%d) echo partition$part path/data/log/$part current$(date -d $current 1 day %F) done這里在雙中括號(hào)[[ ]]里是合法的字符串比較操作符能同時(shí)覆蓋小于和等于比[[ $current $end || $current $end ]]更精簡(jiǎn)。注意[[ ]]是 bash 內(nèi)建語(yǔ)法不是所有 shell 都支持腳本首行要寫成#!/bin/bash而不是#!/bin/sh否則線上跑起來會(huì)報(bào)[[ not found。5. 遍歷日期時(shí)最常見的三類翻車現(xiàn)場(chǎng)與排查方法5.1 while 循環(huán)提前退出或完全沒進(jìn)循環(huán)先查字符串格式統(tǒng)一現(xiàn)象腳本不報(bào)錯(cuò)但循環(huán)只跑了一次就?;蛘叩谝恍卸紱]執(zhí)行。原因日期變量的格式混用。比如current初始值是2025-1-5end是2025-02-03字典序比較時(shí)2025-1-5和2025-02-03在第二個(gè)字符處就分出大小了1-5的-排在02的0之后于是條件判斷得到“當(dāng)前日期大于結(jié)束日期”循環(huán)直接被跳過。解決在腳本入口做一次格式化強(qiáng)制所有日期走同一套輸出。我習(xí)慣在拿到任何外部傳入日期后立刻執(zhí)行一次current$(date -d $current %F)這條命令會(huì)把2025-1-5規(guī)范成2025-01-05也能順手?jǐn)r截?zé)o效日期。如果你傳入的是20250411這種純數(shù)字格式date -d 20250411或date -d 2025-04-11都能解析但為了可讀性我統(tǒng)一在腳本內(nèi)部用%F。5.2 循環(huán)多跑一天或少跑一天時(shí)區(qū)與 86400 秒的陷阱現(xiàn)象用時(shí)間戳累加遍歷一個(gè)含月底、月初的區(qū)間結(jié)果某些天沒跑到或者某天被處理了兩次。日志里日期跳躍不規(guī)則。原因sleep 86400或ts 86400的假設(shè)失效。最常見的場(chǎng)景是服務(wù)器時(shí)區(qū)在夏令時(shí)切換例如美國(guó)東部時(shí)間 3 月的第二個(gè)周日當(dāng)天只有 23 小時(shí)11 月的第一個(gè)周日當(dāng)天有 25 小時(shí)。按 86400 秒推進(jìn)23 小時(shí)那天的循環(huán)會(huì)直接跳過一次迭代。解決不要在秒級(jí)別上推進(jìn)日期改用date -d $current 1 day這種日歷語(yǔ)義的推進(jìn)方式。另外在腳本頂部顯式聲明時(shí)區(qū)可以避免調(diào)度機(jī)環(huán)境變量不一致造成的干擾export TZAsia/Shanghai注意這個(gè)變量只影響當(dāng)前進(jìn)程及子進(jìn)程不會(huì)改系統(tǒng)配置是腳本級(jí)的“后悔藥”。如果團(tuán)隊(duì)里有人把TZ設(shè)成別的值再調(diào)試腳本也能按預(yù)期輸出。5.3 set -e 遇到無效日期導(dǎo)致整個(gè)腳本中斷加上手動(dòng)返回碼檢查現(xiàn)象腳本跑著跑著在某個(gè)日期突然退出沒有錯(cuò)誤信息只有非零退出碼。排查時(shí)發(fā)現(xiàn)循環(huán)體里的某個(gè)date命令吃到了非法日期。原因很多腳本模板里都有set -e作用是“任何命令非零退出就終止腳本”。date -d 2025-02-30 1 day這類非法日期會(huì)觸發(fā)非零返回set -e會(huì)毫不猶豫地掐死整個(gè)腳本。這在普通命令鏈里是保護(hù)機(jī)制但在循環(huán)里就成了殺手因?yàn)槌鲥e(cuò)時(shí)你沒有機(jī)會(huì)看是哪個(gè)日期出了問題。解決在關(guān)鍵的日期推進(jìn)命令后面加上顯式檢查不依賴隱式退出if ! next$(date -d $current 1 day %F); then echo error: invalid date - $current exit 1 fi current$nextif ! command的寫法把命令失敗當(dāng)成可預(yù)測(cè)的分支處理比set -e的“一刀切”更利于排錯(cuò)。輸出里帶上$current翻車時(shí)一眼就能定位到具體日期。這套排查方法值得刻進(jìn) shell 腳本基礎(chǔ)知識(shí)的肌肉記憶里別把腳本跑成黑匣子關(guān)鍵節(jié)點(diǎn)都要留痕。6. 讓批量遍歷真正好用并行執(zhí)行、日志與后悔藥6.1 按天循環(huán)改成并行執(zhí)行xargs -P 控制并發(fā)數(shù)按天遍歷的腳本一旦接到“補(bǔ)最近 90 天數(shù)據(jù)”的需求串行循環(huán)可能跑幾個(gè)小時(shí)。同步任務(wù)多的時(shí)候時(shí)間窗口根本不夠。常見做法是保留 while 循環(huán)生成日期列表但把處理邏輯交給xargs并發(fā)執(zhí)行seq 0 89 | xargs -P 4 -I {} bash -c day$(date -d 2025-01-01 {} days %F) bash /opt/tasks/run_daily.sh $day -P 4表示最多同時(shí)跑 4 個(gè)進(jìn)程-I {}把每一行的輸入0 到 89替換到后面的命令里date -d 2025-01-01 {} days按偏移量算出具體日期。這里要注意bash -c后面的腳本內(nèi)容要用單引號(hào)包住避免$day在父 shell 里提前展開。并發(fā)一旦起來如果腳本里有交互式輸入密碼的邏輯就會(huì)全部卡在等待輸入上所以并行前務(wù)必先確認(rèn)目標(biāo)腳本能非交互運(yùn)行比如依賴環(huán)境變量或已配置好的憑證文件。6.2 留好日志與標(biāo)記文件中斷重跑不再?gòu)牧汩_始遍歷腳本最怕的是跑到第 80 天程序被 kill重啟后又從頭開始。我的習(xí)慣是每處理完一個(gè)日期就生成一個(gè)空文件作為標(biāo)記重跑時(shí)先檢查標(biāo)記是否存在done_flag./done/$(date -d $current %Y%m%d).ok if [[ -f $done_flag ]]; then echo skip $current current$(date -d $current 1 day %F) continue fi bash /opt/tasks/run_daily.sh $current touch $done_flag current$(date -d $current 1 day %F)這份“后悔藥”成本極低但能讓你在凌晨三點(diǎn)放心地 CtrlC。我第一次寫這種循環(huán)時(shí)就是直接累加 86400 秒結(jié)果時(shí)區(qū)一切換就多跑了一天從那天起所有日期遍歷一律用date -d逐日推進(jìn)標(biāo)記文件也成了固定動(dòng)作。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取