做数据报表、跑批任务、周环比统计的时候“获取上周最后一天”这种需求几乎躲不开。别看它一句话说得轻巧真正落地的时候能牵扯出时区、周起始日、数据库差异、跨年跨月一堆事。我最早写这个日期计算时以为拿当前日期减一天就行结果周一跑批时直接拿到了上周周一整个报表统计窗口错位被组里的人笑了一周。这篇就把我实际项目里怎么处理“获取上周最后一天”的过程掰开揉碎讲一遍Python、SQL、Shell 三种常见场景都覆盖顺便把踩过的坑一并放进来。适合正在写周期任务、数据清洗脚本或临时想核算周数据的同学参考。1. 思路拆解先想清楚“上周最后一天”到底是哪天1.1 周边界是个大坑业务规则不先定容易出事故很多人拿到这个需求第一反应是找“昨天”但“上周最后一天”不等于“昨天”。它取决于你所在系统的日历约定一周从周一开始还是从周日开始。国际标准化组织的 ISO 8601 里周一是一周的第一天周日是最后一天北美很多场景则沿用周日作为一周开始那周六就是最后一天。同一句“获取上周最后一天”在两种规则下可能差出整整一天。我建议动手前先问三个问题报表里“周”是怎么定义的调度平台的任务周期按星期几切分运营同事脑子里认为昨天还是上周日才是上周结束这些问题看似基础但问明白可以避免后续返工。比如我当时接的需求是要统计“上一个完整自然周”的订单量那在中文业务环境里通常就是周一到周日为一个完整周所以“上周最后一天”就是上个自然周的周日先把这个结论写进注释和设计文档里。1.2 时间基准和运行时刻会影响计算结果除了周起始日还必须关注代码运行时的“今天”由哪个时区决定。一台服务器如果用的是 UTC 时间业务在 UTC8北京时间已经过了零点服务器上却还是前一天那计算出来的上周日就可能偏移一天。我现在的习惯是任何代码里只要涉及“今天”都显式指定业务时区不依赖操作系统默认值。另外定时任务的实际执行时刻也很关键。周期任务在周一凌晨和周三上午跑计算逻辑虽然相同但“今天”不同。因此所有正确实现都要基于“当前日期相对偏移”而不是写死一个固定的减一减二。写到这一步你会发现这个需求真正考验的是“把一个语义明确的业务日期翻译成具体的代码表达式”的能力。2. 工具选型不同场景我用过哪些实现方案2.1 跑 Python 脚本时优先用 datetime 原生库在 Python 环境里标准库datetime完全够用不需要额外装包。核心 API 是date.weekday()它返回 0 到 6周一为 0周日为 6。基于这个返回值可以算出“从本周一到今天已经过了几天”再加 1 就是“从上周日到今天已经过了几天”用今天减去这个偏移量就得到上周日。这个方案的好处是标准库跨平台无依赖部署到任何 Python 环境都能跑。比如目标场景是“每周一早上生成上周的订单汇总文件”这段逻辑放进脚本里即可。代码风格上我会封装成一个函数方便后续复用和单测。有人会推荐dateutil.relativedelta它确实可以写得更语义化比如relativedelta(weekdaySU(-1))表示上一个周日但标准库方案不用引依赖我还是更偏向先写原生版。2.2 写 SQL 时按数据库类型做适配在数据仓库或在线库里处理“获取上周最后一天”SQL 方案要区分数据库。MySQL 里我常用WEEKDAY()函数它返回 0 到 6周一为 0周日为 6和 Python 的weekday()一致。用CURDATE() - INTERVAL (WEEKDAY(CURDATE()) 1) DAY就能得到上周日。PostgreSQL 则有两种思路一是用date_trunc(week, CURRENT_DATE)先把当前日期截断到本周一再减一天二是用EXTRACT(ISODOW FROM CURRENT_DATE)获取周几数字再往前减对应天数。SQL Server 会更绕一点因为DATEPART(WEEKDAY, ...)的返回值受DATEFIRST影响默认情况下周日为 1、周一为 2。所以我一般不写那种依赖会话设置的复杂表达式而是用DATEDIFF配合DATEADD把日期归零到某个基准日再向前推最后用CAST(... AS DATE)去掉时间部分。说到底SQL 每个方言的日期函数都有小脾气最稳的办法是每种数据库只保留一个经过测试的表达式别到处复制粘贴。2.3 Shell 脚本里用 GNU date 一行搞定如果只是临时在 Linux 服务器上想快速看一下上周日GNUdate非常方便。date -d last sunday %F一句就能输出形如2025-03-30的上周日。不过这个语法在 macOS 自带的 BSD date 上会失效跨平台就不可靠了。我更推荐用不依赖英文关键词的写法date -d today - $(date %u) days %F。其中%u表示今天是周几周一为 1周日为 7用今天减去对应天数自然落到上周日。如果你在容器或 cron 里调用 Shell建议把计算结果直接以YYYY-MM-DD格式作为环境变量传给下游任务不要传递带时间的完整字符串否则后面做日期比较时很容易踩类型坑。3. 实操过程一套可以直接抄作业的实现3.1 Python 标准库实现与测试用例先看我最常用的 Python 方案假设业务周从周一到周日from datetime import date, timedelta def get_last_sunday(today: date | None None) - date: today today or date.today() # weekday(): Monday0, Sunday6 offset today.weekday() 1 return today - timedelta(daysoffset) # 示例 print(get_last_sunday(date(2025, 4, 2))) # 2025-04-02 是周三2025-04-02 的weekday()返回 2代表周三偏移量是 3减 3 天得到 2025-03-30正是上一个周的周日。这个计算过程的关键点是偏移量的语义是“本周一到今天已经过了 N 天N1 就是从上周日到今天的间隔”。我建议多测几个关键日期周一当天、周三、周日当天、跨月、跨年。周一当天调用date(2025, 3, 31)weekday()返回 0偏移量 1得到 2025-03-30正好是上周日如果今天是周日比如date(2025, 4, 6)weekday()返回 6偏移量 7得到 2025-03-30。所以无论今天是不是周日这个函数返回的都是“上一个完整自然周的最后一天”不会把今天当成上周。如果团队里已经装了python-dateutil你还可以用相对差版本from datetime import date from dateutil.relativedelta import relativedelta, SU last_sunday date(2025, 4, 2) relativedelta(weekdaySU(-1))SU(-1)这层语义非常直白缺点是引入了额外依赖。我个人通常只会在项目里已经使用dateutil的时候顺手用否则纯原生版本更轻。3.2 MySQL 与 PostgreSQL 的 SQL 实现在 MySQL 中我常用的语句是这样的SELECT CURDATE() - INTERVAL (WEEKDAY(CURDATE()) 1) DAY AS last_sunday;假设今天是 2025-04-02WEEKDAY(CURDATE())返回 2括号整体是 3日期减 3 天得到 2025-03-30。注意这里不要写NOW()代替CURDATE()因为NOW()包含时分秒减INTERVAL之后仍然带时间下游接收时不方便。PostgreSQL 里我更推荐这样SELECT CAST(DATE_TRUNC(week, CURRENT_DATE) - INTERVAL 1 day AS DATE) AS last_sunday;date_trunc(week, ...)会自动把任意日期归到本周一再减一天就是上周日。PostgreSQL 的date_trunc本身返回带时间戳的类型所以最外层再用CAST(... AS DATE)去掉时间部分。还有一种写法是基于ISODOWSELECT CURRENT_DATE - CAST(EXTRACT(ISODOW FROM CURRENT_DATE) AS INTEGER) * INTERVAL 1 day AS last_sunday;这个版本靠数轴理解今天是周几就往前推几天。两者的结果一致我习惯用date_trunc版本因为可读性更好。3.3 Shell 一行命令的实测在 Linux 环境验证一下刚提到的方案today$(date %F) weekday$(date %u) last_sunday$(date -d $today - $weekday days %F) echo $last_sunday如果今天是 2025-04-02%u输出 3date -d 2025-04-02 - 3 days会输出 2025-03-30。这个写法比date -d last sunday好在不依赖“sunday”这个英文词也适合处理自定义的一周起始日。如果你需要上周六周日开始的一周把偏移改成weekday 2再模 7 就能得到但我不建议写在一条命令里逻辑容易乱封装成一个小脚本更合适。4. 容易翻车的细节这些坑我都替你踩过4.1 周一跑批时不要直接减一天新手最容易犯的错误是“拿今天减一天就是昨天昨天就是上周最后一天”。例如现在业务跑批时间设在周一早上 8 点你确实想要上周日而昨天恰好是周日看起来减一天是对的。可一旦任务在周二补跑减一天得到周一防不胜防。正确做法是始终基于“今天”这个锚点按照周几动态计算偏移。我后来把计算函数固定成“今天 weekday 加一”的偏移周一得到上周日周日也能正确落到更早的上周日无论补跑还是正常调度都能保持一致。4.2 时区不同步同一个脚本结果差一天我有一个项目数据库服务器使用 UTC业务团队在 UTC8。用 UTC 的“今天”去计算经常在早上 8 点前触发时落入前一天的日期。后来的解决办法是应用层通过ZoneInfo(Asia/Shanghai)显式取业务日期数据库查询则用SET TIME ZONE Asia/Shanghai统一会话时区。不要指望服务器默认时区恰好和业务一致越显式越安全。如果你在写 Python可以参考from datetime import datetime from zoneinfo import ZoneInfo today_in_cn datetime.now(ZoneInfo(Asia/Shanghai)).date()这样传给get_last_sunday的就是北京时间口径的今天。4.3 日期和时间戳混用导致结果对不上MySQL 的NOW()、PostgreSQL 的CURRENT_TIMESTAMP、SQL Server 的GETDATE()都带时分秒。计算“上周最后一天”时如果直接用这些值做日期减法结果往往带时间后续一步DATE()或CAST(... AS DATE)很容易漏。我见过最隐蔽的问题是上游输出2025-03-30 00:00:00下游拿它和业务时间2025-03-30 23:59:59比较时出现边界漏数。我现在的规范是日期边界计算全部使用日期类型常量比如CURDATE()、CURRENT_DATE如果真的只有时间戳必须在计算前先截断到日期再去做日期的加减。4.4 周起始日写了多个版本没人能维护团队里不同成员可能有不同习惯有人用MONDAY开头有人按SUNDAY开头代码里同时出现weekday()和isocalendar()。一旦“上周最后一天”的定义变了这些代码就要跟着改很容易漏掉一处。我在实际项目里会把这个日期定义抽成一个配置项比如WEEK_START MONDAY然后函数内部统一用同一个算法分支。这样新增业务不会出现“一个系统按周一算另一个按周日算”的混乱。5. 常见问题速查与排查思路记录我把实际操作中经常被问到的几个问题整理成了一张速查表方便你直接对照常见现象可能原因处理方式周一跑批时算出了上周周一直接用今天减一天改为按 weekday 动态计算偏移早上 8 点前结果比预期早一天系统时区是 UTC业务是 UTC8显式指定业务时区再取今天SQL 结果带了 00:00:00 时间用了 NOW() 或 CURRENT_TIMESTAMP改成 CURDATE() 或 CURRENT_DATE最后再 CAST AS DATE“上周最后一天”在周日和周一的定义对不上周起始日不统一先定业务规则把 WEEk_START 抽成配置PostgreSQL 算出来不是想要的周用了date_part而不是date_trunc用date_trunc(week, ...) - 1 dayShell 在 macOS 上报错BSD date 不支持-d last sunday用%u做数字偏移或改用 Python排查思路其实很固定第一步确认“今天”的口径第二步确认周起始日第三步再去看代码里的偏移量。不要一上来就怀疑数据库函数大部分问题都出在前两步。另外测试用例建议多覆盖几个极端日期比如 2024-12-30跨年周一、2024-12-29跨年前周日、2025-06-01周日确保跨年跨月不出错。我自己每次改成日期逻辑都会准备至少五个用例来跑一遍。6. 一点扩展思考这个函数还能怎么用派生需求经常出现不只上周最后一天还要上周最后一天往前推 N 天或者拿过去 4 个周日的列表做周趋势对比。基于上面的函数扩展起来很快from datetime import timedelta def get_last_week_end_list(n: int, todayNone) - list: last get_last_sunday(today) return [last - timedelta(weeksi) for i in range(n)] print(get_last_week_end_list(3, date(2025, 4, 2))) # [date(2025, 3, 30), date(2025, 3, 23), date(2025, 3, 16)]把这个函数和项目里的日历表配合还能进一步处理法定节假日导致的“自然周结束日”。比如某些行业习惯把周五作为统计周结束那你只需要把偏移逻辑从“减到上周日”改成“减到上周五”函数体内部改一行即可。我个人现在的习惯是把这类日期计算统一收进一个工具模块函数只接收today和week_end_weekday两个参数内部用同一种算法。这样一来业务上再怎么折腾周定义代码侧都能稳住。这个需求虽小但它直接影响到报表统计的正确性值得认真对待。