1 datetime —— 时间对象是怎么「运转」的
是什么?为什么学?
datetime 模块是 Python 处理日期时间的官方标准库,核心有三个类:datetime(日期 + 时间)、date(只要日期)、timedelta(时间差)。日志时间戳、生日计算、订单超时判断、报表按天统计……凡是和时间有关的活儿都绕不开它。
底层原理
时间在 Python 里是对象,不是字符串。datetime 对象内部存着年、月、日、时、分、秒、微秒字段,datetime(2026, 8, 4, 9, 30, 45) 按位置传入即可构造;date 是它的「精简版」(没有时分秒)。**timedelta 是「时间差对象」**:datetime - datetime 得到 timedelta,datetime + 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),访问不存在的键时自动创建默认值并写回字典。namedtuple 是 tuple 的子类:不可变、能下标、能拆包,还多了属性名访问(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 循环配合 break。chain 把多个可迭代对象「首尾相接」;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 模块让你用一个模式字符串回答「这段文本里有没有电话号码 / 所有日期在哪 / 把数字换成 #」这类问题。字符串查找(in、find)只能匹配固定内容,正则能匹配一类内容。
底层原理
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()→ 问题:没匹配时m是None,报AttributeError→ 正确写法:先if m:判断再取值。
记忆口诀:\d 数字 \w 字,加 r 前缀别忘记;findall 找全部,search 取第一,sub 换 split 切,括号分组好提取。
5 动手实践:日期与文本处理工具
做一个工具脚本:从一段会议记录文本里提取所有日期(支持 2026-08-03 和 2026/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-DD,Counter 统计月份,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 object | re.sub / re.findall 第一个参数写成了列表 | 第一参数必须是正则字符串(r"..."),不要传列表 |