| Day 19 | 常用标准库:datetime / collections / itertools / re |

1 datetime —— 时间对象是怎么「运转」的

是什么?为什么学?

datetime 模块是 Python 处理日期时间的官方标准库,核心有三个类:datetime(日期 + 时间)、date(只要日期)、timedelta(时间差)。日志时间戳、生日计算、订单超时判断、报表按天统计……凡是和时间有关的活儿都绕不开它。

底层原理

时间在 Python 里是对象,不是字符串。datetime 对象内部存着年、月、日、时、分、秒、微秒字段,datetime(2026, 8, 4, 9, 30, 45) 按位置传入即可构造;date 是它的「精简版」(没有时分秒)。**timedelta 是「时间差对象」**:datetime - datetime 得到 timedeltadatetime + timedelta 得到新时间——时间运算的本质是对象加减。strftime(f = format)是「时间 → 字符串」,strptime(p = parse)是「字符串 → 时间」,方向相反。

生活类比

datetime电子日历上的一格date 像只写日期的纸质日历,timedelta撕日历的间隔——「今天加 7 天」就是往后翻 7 格,跨月、跨年、闰年自动处理,不用自己数天数。

注意事项总结

1 构造时间对象 + 日期加减 + 差值比较(一条龙)

from datetime import datetime, timedelta
dt = datetime(2026, 8, 4, 9, 30, 45)   # 年、月、日、时、分、秒
print(dt)
print(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
print(dt.strftime("%Y-%m-%d %H:%M:%S"))   # 格式化成字符串
d1 = datetime(2026, 8, 10, 9, 30, 45)
diff = d1 - dt
print("差多少天:", diff.days)
print("总秒数:", int(diff.total_seconds()))
print("一周前:", dt - timedelta(days=7))
print("d1 在 dt 之后:", d1 > dt)

运行结果:

2026-08-04 09:30:45
2026 8 4 9 30 45
2026-08-04 09:30:45
差多少天: 6
总秒数: 518400
一周前: 2026-07-28 09:30:45
d1 在 dt 之后: True

按「年月日时分秒」构造对象,.year 等属性取出各字段,strftime 变成字符串;d1 - dt 得到时间差对象,.days 取天数、.total_seconds() 换算总秒数;dt - timedelta(days=7) 往回翻 7 天(跨月自动处理);时间对象还能直接比大小——时间是对象:可构造、可计算、可排序,字符串只是它的「照片」

2 获取当前时间 + 常用格式符

from datetime import datetime, date, timedelta
now = datetime.now()          # 当前日期时间
today = date.today()          # 今天的日期
print(now.strftime("%Y-%m-%d %H:%M:%S"))
print("一周后:", today + timedelta(days=7))

常用格式符:%Y 四位年份、%m 两位月份、%d 两位日期、%H 小时、%M 分钟、%S 秒。

3 strptime 字符串 → 时间(格式必须严格对应)

from datetime import datetime
s = "2026-08-03 09:30:00"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")   # 格式必须和字符串严格对应
print(dt)
print(type(dt))

运行结果:

2026-08-03 09:30:00
<class 'datetime.datetime'>

易错点

  • 错误写法:strptime("2026-8-4", "%Y-%m-%d") → 问题:%m 要求两位数字,报 ValueError → 正确写法:字符串保持两位(2026-08-04),或改用正则解析。
  • 错误写法:自己写 if month == 12: year += 1... 处理跨年 → 问题:边界情况容易漏,闰年算错 → 正确写法:用 timedelta,内部处理所有日历规则。

记忆口诀:时间是对象,字符串是照片;加减用 timedelta,转换靠 f 和 p。

2 collections —— 更聪明的「容器三兄弟」

是什么?为什么学?

collections 模块提供比内置 list / dict / tuple 更高级的容器。今天学三个高频的:Counter(计数器)、defaultdict(带默认值的字典)、namedtuple(有名字的元组)——它们让你少写一半的 if 和循环

底层原理

Counter 本质是 dict 的子类——键是元素、值是次数;访问不存在的键返回 0 而不是报 KeyError(内部 __missing__ 兜底),most_common(n) 排序后取前 n 个。defaultdict 也是 dict 子类,构造时传「默认值工厂」(list / int),访问不存在的键时自动创建默认值并写回字典namedtupletuple 的子类:不可变、能下标、能拆包,还多了属性名访问(p.x)。

生活类比

Counter点餐记录本:每点一次划一笔,最后告诉你每种菜点了几个;defaultdict自动补货的储物柜:没有的格子打开时自动放一个空盒;namedtuple填写完整的快递单:每个栏位有名字,而不是一串不知道是什么的编号。

注意事项总结

1 Counter——统计词频,缺失键不报错

from collections import Counter
words = ["苹果", "香蕉", "苹果", "梨", "苹果", "香蕉"]
counts = Counter(words)
print(counts)
print("苹果:", counts["苹果"])
print("最多的两个:", counts.most_common(2))
print("没出现过的词:", counts["西瓜"])   # 缺失键返回 0,不报错

运行结果:

Counter({'苹果': 3, '香蕉': 2, '梨': 1})
苹果: 3
最多的两个: [('苹果', 3), ('香蕉', 2)]
没出现过的词: 0

Counter(words) 一行完成全部计数;关键:查没出现过的词返回 0 而不是 KeyError——手写 dict 计数必须先 if key in d 判断,麻烦得多。

2 defaultdict——分组收集,键不存在自动补默认值

from collections import defaultdict
scores = [("语文", 90), ("数学", 95), ("语文", 88), ("英语", 92)]
d = defaultdict(list)                 # 默认值工厂:list
for subject, score in scores:
    d[subject].append(score)
print(dict(d))
print(d["物理"])                      # 不存在的键得到空列表 []

运行结果:

{'语文': [90, 88], '数学': [95], '英语': [92]}
[]

defaultdict(list) 声明「访问不存在的键时自动放一个空列表」;循环里第一次遇到「语文」自动建空列表再追加,第二次直接追加——不用再写「键不存在就初始化」的分支。默认值是 int 时,d[key] += 1 可直接做计数。

3 namedtuple——有名字的元组

from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])   # 定义一种新"类型"
p = Point(3, 4)
print(p.x, p.y)        # 属性访问
print(p[0], p[1])      # 下标访问
x, y = p               # 还能拆包
print(x, y)

运行结果:

3 4
3 4
3 4

易错点

  • 错误写法:手写计数 if w in d: d[w] += 1 else: d[w] = 1 → 问题:啰嗦且容易漏分支 → 正确写法:用 Counter(words)defaultdict(int)
  • 错误写法:给 namedtuple 赋值 p.x = 5 → 问题:元组不可变,抛 AttributeError → 正确写法:重新构造 Point(5, 4)

记忆口诀:数数 Counter,分组 defaultdict,轻量结构 namedtuple;默认值工厂别忘了。

3 itertools —— 迭代器的「组合积木」

是什么?为什么学?

itertools 模块是一组「处理迭代器的工具函数」:合并(chain)、循环(cycle)、无限数数(count)、组合配对(product)、限量切片(islice)。它让你用一行替代手写循环,而且惰性(lazy)——元素「用到才算」,不会一次性全部造出来占内存。

底层原理

itertools 返回的都是迭代器对象(不是列表),遵循 Day 18 学的惰性求值:itertools.count(1) 理论上数到无穷大,但不会真的生成无穷个数字,只有每次 next() 或循环取一个才算一个。所以直接 list(itertools.count(1)) 会卡死,必须用 islice(限量取前几个)或 for 循环配合 breakchain 把多个可迭代对象「首尾相接」;product 计算笛卡尔积(所有配对),结果数 = 各序列长度相乘,注意别让量级爆炸。

生活类比

count无限长的取号机(要几个按几个);cycle循环播放的歌单chain把几根绳子接成一根product点套餐——主食 × 饮料的所有搭配;islice切蛋糕:只切第 2 到第 4 块。

注意事项总结

1 count + islice——从无限序列里「限量取」;cycle——循环播放

import itertools
for n in itertools.islice(itertools.count(1), 3):
    print("count:", n)
names = ["张三", "李四"]
for name in itertools.islice(itertools.cycle(names), 4):
    print("cycle:", name)

运行结果:

count: 1
count: 2
count: 3
cycle: 张三
cycle: 李四
cycle: 张三
cycle: 李四

itertools.count(1) 从 1 开始无限数数,islice(it, 3) 只取前 3 个;cycle(names) 把列表变成「循环播放」,配合 islice 取前 4 个。**绝不要直接 list(itertools.count(1))**——它没有尽头。

2 chain 合并 + product 笛卡尔积

import itertools
print(list(itertools.chain([1, 2], ["a", "b"])))   # 合并成一个序列
for combo in itertools.product([1, 2], ["a", "b"]):
    print(combo)

运行结果:

[1, 2, 'a', 'b']
(1, 'a')
(1, 'b')
(2, 'a')
(2, 'b')

chain 把两个列表首尾相接(合并列表不用写两层循环);product 生成所有 (数字, 字母) 配对——「遍历所有组合」就靠它(比如穷举 4 位数字密码)。

易错点

  • 错误写法:list(itertools.count(1)) → 问题:无限序列转列表,程序永远跑不完 → 正确写法:用 islice 限量取,或 for + break
  • 错误写法:同一个迭代器 for 两次 → 问题:迭代器是「一次性」的,第二次为空 → 正确写法:需要复用就转成 list() 存下来。

记忆口诀:count 无限数,cycle 循环播,chain 接绳子,product 全配对;无限序列别贪心,islice 限量取。

4 re —— 用「模式」而不是「代码」来找文本

是什么?为什么学?

正则表达式(regular expression,简称 regex)是一套「用符号描述文本模式」的迷你语言。re 模块让你用一个模式字符串回答「这段文本里有没有电话号码 / 所有日期在哪 / 把数字换成 #」这类问题。字符串查找(infind)只能匹配固定内容,正则能匹配一类内容。

底层原理

re 模块把模式字符串编译成状态机再扫描文本:findall 找出所有匹配子串、search第一个匹配(返回 match 对象)、sub 替换所有匹配、split 按匹配位置切分。常用符号:\d 数字、\w 字母数字下划线、\s 空白;. 任意字符、* 0 次或多次、+ 至少 1 次、? 0 次或 1 次;{n,m} 出现 n 到 m 次;[...] 字符集合;( ) 分组捕获;^ 开头、$ 结尾。**r 前缀(原始字符串)告诉 Python「别转义反斜杠」**,\d 才能原样交给正则引擎。

生活类比

正则像快递分拣规则:「把所有电话号(3-4-4 位数字带横杠)的包裹挑出来」——你描述规则,机器执行。find 像「找写着一模一样字样的包裹」,正则像「找符合规则的包裹」,一个认死理,一个认模式

注意事项总结

1 findall 找出所有匹配 + search 分组拆解

import re
text = "我的电话是 138-1234-5678,备用 010-12345678"
phones = re.findall(r"\d{3}-\d{4}-\d{4}", text)   # 3位-4位-4位
print(phones)
text2 = "今天是 2026-08-03,天气不错"
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", text2)  # search 找第一个
if m:
    print("整个匹配:", m.group(0))
    print("年:", m.group(1), "月:", m.group(2), "日:", m.group(3))

运行结果:

['138-1234-5678']
整个匹配: 2026-08-03
年: 2026 月: 08 日: 03

r"\d{3}-\d{4}-\d{4}" 表示「3 个数字-4 个数字-4 个数字」,findall 扫出所有匹配(010-12345678 是 3+8 位不符合结构所以没匹配上);( ) 把模式切成 3 组,search 找到第一个匹配后 m.group(0) 是整个匹配,group(1)~`group(3)` 是各组内容——提取年/月/日,拆组即可

2 split 按一类字符切分 + sub 替换

import re
text = "apple,banana;cherry orange"
print(re.split(r"[,; ]+", text))            # 按逗号/分号/空格切分
print(re.sub(r"\d+", "#", "abc123def456"))  # 所有数字替换成 #

运行结果:

['apple', 'banana', 'cherry', 'orange']
abc#def#

split(r"[,; ]+", text) 用「一个或多个逗号/分号/空格」做分隔符——str.split(",") 一次只能按一种分隔符强sub(r"\d+", "#", ...) 把连续数字整段替换成 #

易错点

  • 错误写法:正则字符串不写 r 前缀:"\d{3}" → 问题:\d 被当成 d,匹配全错 → 正确写法:写 r"\d{3}",原样传给正则引擎。
  • 错误写法:search 后不判断直接 m.group() → 问题:没匹配时 mNone,报 AttributeError → 正确写法:先 if m: 判断再取值。

记忆口诀:\d 数字 \w 字,加 r 前缀别忘记;findall 找全部,search 取第一,sub 换 split 切,括号分组好提取。

5 动手实践:日期与文本处理工具

做一个工具脚本:从一段会议记录文本里提取所有日期(支持 2026-08-032026/8/3 两种写法),转成统一格式,统计每月有几场会议,并计算每场距离今天还有几天。

# text_tools.py —— 日期与文本处理工具
import re
from datetime import datetime
from collections import Counter

def extract_dates(text):
    """提取两种格式的日期:2026-08-03 或 2026/8/3"""
    pattern = r"(\d{4})[-/](\d{1,2})[-/](\d{1,2})"
    results = []
    for year, month, day in re.findall(pattern, text):
        results.append(datetime(int(year), int(month), int(day)))
    return results

def format_report(dates):
    """统计每月会议数量,并计算每场距离今天的天数"""
    month_counts = Counter(d.month for d in dates)
    today = datetime.now().date()
    lines = []
    for d in dates:
        delta = (d.date() - today).days
        lines.append(f"{d.strftime('%Y-%m-%d')}(距今 {delta} 天)")
    lines.append("月份统计: " + ", ".join(f"{m}月×{n}" for m, n in sorted(month_counts.items())))
    return "\n".join(lines)

text = """
会议记录:
2026-08-03 项目启动会
2026/8/10 需求评审
2026-08-15 开发完成
2026/08/20 上线发布
"""

dates = extract_dates(text)
print(format_report(dates))

运行结果(今天是 2026-08-03 时):

2026-08-03(距今 0 天)
2026-08-10(距今 7 天)
2026-08-15(距今 12 天)
2026-08-20(距今 17 天)
月份统计: 8月×4

正则在 -/ 之间用 [-/] 字符集合兼容了两种写法,strftime 把格式统一成 YYYY-MM-DDCounter 统计月份,timedelta 的差值算出「距今几天」——今天学的四个模块在一处配合使用。

升级挑战:支持中文日期 2026年8月3日;再给工具加一个函数,用 re.sub 把文本里的日期全部替换成 [日期] 占位符。

今日总结

datetime 是对象不是字符串:构造、取属性、比大小、可排序;timedelta 自动处理跨月跨年(.days / .total_seconds()strftime 时间→字符串、strptime 字符串→时间,格式串严格对应 Counter 一键计数、缺失键返回 0、most_common(n) 取排行 defaultdict(list) 分组收集、defaultdict(int) 计数,不再手写 if 判断 namedtuple 有名字的元组:属性访问 + 下标 + 拆包,不可变 itertools 全是惰性迭代器:count 无限数、cycle 循环、chain 合并、product 配对,islice 限量取 正则符号:\d+{n,m}[...]( ) 分组,r 前缀保原样 re.findall 全部 / re.search 第一个(先判断再取)/ re.sub 替换 / re.split 切分 日期与文本处理工具运行正常:提取两种日期格式、统计月份、计算天数差

报错原因修复
ValueError: time data '2026-8-3' does not match format '%Y-%m-%d'strptime 的格式和字符串对不上,%m 要求两位数字让字符串保持两位(2026-08-03),或改用正则/手动拆分解析
KeyError: '英语'普通 dict 访问不存在的键换成 defaultdict(list),或先判断 if key in d
TypeError: 'itertools.count' object is not subscriptable对惰性迭代器用了 count[0] 这种下标itertools.islice(it, n) 限量取,或转成 list()
re.error: missing ), unterminated subpattern正则里圆括号没配对检查 ( ) 是否成对,数量对齐
TypeError: expected string or bytes-like objectre.sub / re.findall 第一个参数写成了列表第一参数必须是正则字符串(r"..."),不要传列表
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇