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