返回博客

43. 常用内建模块(上):datetime、collections、hashlib

43. 常用内建模块(上):datetime、collections、hashlib

第四十三篇:常用内建模块(上)—— datetime、collections、hashlib

前言

Python 的内建模块是日常开发中最频繁使用的工具库。datetime 处理日期时间,collections 提供高级容器类型,hashlib 负责消息摘要和加密哈希。本文将系统讲解这三大核心模块的用法与实战场景。

datetime —— 日期与时间处理

基本日期操作

from datetime import datetime, date, time, timedelta

# 获取当前日期和时间
now = datetime.now()
today = date.today()
current_time = time(14, 30, 0)

print(f"当前时间: {now}")        # 2026-09-09 14:30:00.123456
print(f"当前日期: {today}")      # 2026-09-09
print(f"星期: {today.weekday()}")  # 0=Monday ... 6=Sunday
print(f"ISO 格式: {today.isoformat()}")  # 2026-09-09

# 日期加减
tomorrow = today + timedelta(days=1)
yesterday = today - timedelta(days=1)
next_week = now + timedelta(weeks=1)
three_hours_ago = now - timedelta(hours=3)

print(f"明天: {tomorrow}")
print(f"一周后: {next_week}")
print(f"三小时前: {three_hours_ago}")

# 计算两个日期之间的差值
start_date = date(2026, 1, 1)
end_date = date(2026, 12, 31)
days_between = (end_date - start_date).days
print(f"全年天数: {days_between} 天")  # 364

日期格式化与解析

from datetime import datetime

dt = datetime(2026, 9, 9, 14, 30, 45)

# 格式化:datetime → 字符串
formats = {
    "标准格式": dt.strftime("%Y-%m-%d %H:%M:%S"),
    "短日期": dt.strftime("%Y/%m/%d"),
    "中文格式": dt.strftime("%Y年%m月%d日 %H时%M分%S秒"),
    "ISO 8601": dt.isoformat(),
    "RFC 2822": dt.strftime("%a, %d %b %Y %H:%M:%S +0000"),
}

for name, formatted in formats.items():
    print(f"{name}: {formatted}")
# 标准格式: 2026-09-09 14:30:45
# 短日期: 2026/09/09
# 中文格式: 2026年09月09日 14时30分45秒
# ISO 8601: 2026-09-09T14:30:45
# RFC 2822: Wed, 09 Sep 2026 14:30:45 +0000

# 解析:字符串 → datetime
date_strings = [
    "2026-09-09",
    "2026/09/09 14:30",
    "Sep 9, 2026",
]

parsers = [
    ("%Y-%m-%d", "%Y-%m-%d"),
    ("%Y/%m/%d %H:%M", "%Y/%m/%d %H:%M"),
    ("%b %d, %Y", "%b %d, %Y"),
]

for text, fmt in zip(date_strings, parsers):
    parsed = datetime.strptime(text, fmt[0])
    print(f"'{text}' → {parsed}")

strftime / strptime 格式化代码速查

代码 含义 示例
%Y 四位年份 2026
%y 两位年份 26
%m 月份(01-12) 09
%d 日期(01-31) 09
%H 小时(00-23) 14
%M 分钟(00-59) 30
%S 秒(00-59) 45
%A 完整 weekday Wednesday
%a 缩写 weekday Wed
%B 完整 month September
%b 缩写 month Sep
%p AM/PM PM

时区处理

from datetime import datetime, timezone, timedelta

# UTC 时间
utc_now = datetime.now(timezone.utc)
print(f"UTC 时间: {utc_now}")  # 2026-09-09 06:30:00+00:00

# 北京时区(UTC+8)
beijing_tz = timezone(timedelta(hours=8))
beijing_now = datetime.now(beijing_tz)
print(f"北京时间: {beijing_now}")  # 2026-09-09 14:30:00+08:00

# 时区转换
utc_dt = datetime(2026, 9, 9, 14, 30, tzinfo=timezone.utc)
beijing_dt = utc_dt.astimezone(beijing_tz)
print(f"UTC 14:30 → 北京时间: {beijing_dt}")  # 2026-09-09 22:30:00+08:00

# 使用 zoneinfo(Python 3.9+,推荐)
try:
    from zoneinfo import ZoneInfo
    
    tokyo_tz = ZoneInfo("Asia/Tokyo")
    ny_tz = ZoneInfo("America/New_York")
    
    london_now = datetime.now(ZoneInfo("Europe/London"))
    tokyo_now = london_now.astimezone(tokyo_tz)
    ny_now = london_now.astimezone(ny_tz)
    
    print(f"伦敦: {london_now}")
    print(f"东京: {tokyo_now}")
    print(f"纽约: {ny_now}")
except ImportError:
    print("zoneinfo 需要 Python 3.9+")

实战:日历生成器

import calendar
from datetime import date

def generate_calendar(year, month):
    """生成指定年月日历"""
    first_day = date(year, month, 1)
    last_day = date(year, month, calendar.monthrange(year, month)[1])
    
    month_label = f"{'='*30}\n{year}年 {month}月\n{'='*30}"
    header = f"{'Sun':>6} {'Mon':>6} {'Tue':>6} {'Wed':>6} {'Thu':>6} {'Fri':>6} {'Sat':>6}"
    
    lines = [month_label, header]
    indent = " " * (first_day.weekday() * 7 + 2)
    lines.append(indent)
    
    for day in range(1, calendar.monthrange(year, month)[1] + 1):
        current_date = date(year, month, day)
        day_str = f"{day:>6}" if day != last_day.day else f"{day:<6}"
        
        if current_date.weekday() >= 5:
            lines[-1] += day_str
            lines.append("")
        else:
            lines[-1] += day_str
        
        if len(lines[-1].strip()) >= 42:
            lines.append("")
    
    return "\n".join(lines)

print(generate_calendar(2026, 9))

collections —— 高级容器

namedtuple —— 命名元组

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y)          # 3 4
print(p[0], p[1])         # 3 4
print(p._asdict())       # OrderedDict([('x', 3), ('y', 4)])
print(p._replace(x=10))  # Point(x=10, y=4)

# 实用示例:表示坐标点
class Circle(namedtuple('CircleBase', ['center', 'radius'])):
    @property
    def area(self):
        import math
        return math.pi * self.radius ** 2

c = Circle(Point(0, 0), 5)
print(f"圆心: ({c.center.x}, {c.center.y}), 半径: {c.radius}")
print(f"面积: {c.area:.2f}")  # 面积: 78.54

deque —— 双向队列

from collections import deque
import time

# 高效的双端插入和弹出
dq = deque(maxlen=5)  # 固定大小的滑动窗口
for item in [1, 2, 3, 4, 5, 6, 7]:
    dq.append(item)
    print(f"添加 {item}: {list(dq)}")
# 添加 1: [1]
# 添加 2: [1, 2]
# ...
# 添加 5: [1, 2, 3, 4, 5]
# 添加 6: [2, 3, 4, 5, 6] ← maxlen 限制自动丢弃左侧元素
# 添加 7: [3, 4, 5, 6, 7]

# 性能对比:列表 vs 队列做 FIFO 队列
def list_queue(n):
    q = []
    for i in range(n):
        q.append(i)
    while q:
        q.pop(0)  # O(n) 复杂度!

def deque_queue(n):
    q = deque(range(n))
    while q:
        q.popleft()  # O(1) 复杂度

n = 100_000
start = time.perf_counter()
list_queue(n)
list_time = time.perf_counter() - start

start = time.perf_counter()
deque_queue(n)
deque_time = time.perf_counter() - start

print(f"list pop(0): {list_time:.2f}s")
print(f"deque popleft: {deque_time:.2f}s")
print(f"deque 比 list 快约 {list_time/deque_time:.0f} 倍!")

defaultdict —— 默认值字典

from collections import defaultdict

# 避免 KeyError — 自动初始化默认值
word_count = defaultdict(int)
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
for word in words:
    word_count[word] += 1
print(dict(word_count))
# {'apple': 3, 'banana': 2, 'cherry': 1}

# 多级嵌套字典(免去繁琐的空值检查)
groups = defaultdict(list)
students = [
    ("Alice", "math"), ("Bob", "science"),
    ("Charlie", "math"), ("Diana", "art"),
    ("Eve", "science"), ("Frank", "math"),
]
for name, subject in students:
    groups[subject].append(name)

for subject, members in sorted(groups.items()):
    print(f"{subject}: {', '.join(members)}")
# art: Diana
# math: Alice, Charlie, Frank
# science: Bob, Eve

# 计数器统计
char_count = defaultdict(lambda: 0)
text = "hello world hello python"
for char in text:
    char_count[char] += 1
print(dict(sorted(char_count.items())))

Counter —— 计数器

from collections import Counter

# 词频统计
text = "the quick brown fox jumps over the lazy dog the fox"
word_counter = Counter(text.split())
print(word_counter)
# Counter({'the': 3, 'fox': 2, 'quick': 1, 'brown': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1})

# 最常见的 N 个元素
print(word_counter.most_common(3))
# [('the', 3), ('fox', 2), ('quick', 1)]

# 数学运算
c1 = Counter(a=3, b=1, c=2)
c2 = Counter(a=1, b=2, d=5)

print(c1 + c2)   # Counter({'a': 4, 'b': 3, 'c': 2, 'd': 5}) 相加
print(c1 - c2)   # Counter({'a': 2, 'c': 2}) 相减(只保留正值)
print(c1 & c2)   # Counter({'a': 1, 'b': 1}) 交集(取最小值)
print(c1 | c2)   # Counter({'a': 3, 'b': 2, 'c': 2, 'd': 5}) 并集(取最大值)

# 实际用例:分析日志级别分布
logs = ["ERROR", "INFO", "DEBUG", "ERROR", "WARNING", "INFO", "ERROR", "INFO"]
log_counter = Counter(logs)
total = sum(log_counter.values())
for level, count in log_counter.most_common():
    percentage = count / total * 100
    bar = "█" * int(percentage / 5)
    print(f"{level:8s} {count:3d} ({percentage:5.1f}%) {bar}")

OrderedDict —— 有序字典

from collections import OrderedDict

menu = OrderedDict([
    ("starter", "沙拉"),
    ("main", "牛排"),
    ("dessert", "提拉米苏"),
    ("drink", "红酒"),
])

menu.move_to_end("starter")
print(menu)
# OrderedDict([('main', '牛排'), ('dessert', '提拉米苏'), ('drink', '红酒'), ('starter', '沙拉')])

# FIFODict 实现 — 缓存淘汰策略
class FIFODict(OrderedDict):
    def __init__(self, max_size):
        super().__init__()
        self.max_size = max_size
    
    def __setitem__(self, key, value):
        if key in self:
            self.move_to_end(key)
        elif len(self) >= self.max_size:
            self.popitem(last=False)  # 删除最老的
        super().__setitem__(key, value)

cache = FIFODict(max_size=3)
cache["a"] = 1
cache["b"] = 2
cache["c"] = 3
print(cache)  # {'a': 1, 'b': 2, 'c': 3}
cache["d"] = 4  # 淘汰 'a'
print(cache)  # {'b': 2, 'c': 3, 'd': 4}

ChainMap —— 多重映射组合

from collections import ChainMap

defaults = {"theme": "light", "language": "zh", "notifications": True}
user_prefs = {"theme": "dark"}
system_env = {"language": "en", "debug": False}

config = ChainMap(user_prefs, defaults, system_env)
print(config["theme"])       # dark(用户偏好优先)
print(config["language"])    # zh(用户偏好优先)
print(config["debug"])       # False(来自环境变量)
print(config["notifications"])  # True(来自默认值)

# 合并多个字典
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
merged = ChainMap(dict1, dict2)
print(dict(merged))  # {'a': 1, 'b': 2, 'c': 3, 'd': 4}

hashlib —— 消息摘要与加密哈希

基本哈希算法

import hashlib

data = "Hello, World!"

# MD5(不推荐用于安全用途,仅用于校验和)
md5_hash = hashlib.md5(data.encode()).hexdigest()
print(f"MD5: {md5_hash}")
# 例: e59ff97941044f85df5297e1c302d260

# SHA-1(已不安全,不建议使用)
sha1_hash = hashlib.sha1(data.encode()).hexdigest()
print(f"SHA-1: {sha1_hash}")

# SHA-256(推荐的安全哈希)
sha256_hash = hashlib.sha256(data.encode()).hexdigest()
print(f"SHA-256: {sha256_hash}")

# SHA-512
sha512_hash = hashlib.sha512(data.encode()).hexdigest()
print(f"SHA-512: {sha512_hash}")

# 查看所有支持的算法
algorithms = hashlib.algorithms_available
print(f"可用算法: {sorted(algorithms)}")

HMAC — 消息认证码

import hmac
import hashlib

def sign_request(secret_key, message):
    """使用 HMAC-SHA256 签名请求"""
    mac = hmac.new(
        secret_key.encode(),
        msg=message.encode(),
        digestmod=hashlib.sha256
    )
    return mac.hexdigest()

api_key = "my-secret-key-12345"
request_body = '{"action":"login","user":"alice"}'

signature = sign_request(api_key, request_body)
print(f"签名: {signature}")

# 验证签名
is_valid = hmac.compare_digest(sign_request(api_key, request_body), signature)
print(f"签名验证结果: {is_valid}")

# ⚠️ 始终使用 hmac.compare_digest 而不是 == 比较!
# compare_digest 防时序攻击

文件校验和

import hashlib
import os

def compute_file_hash(filepath, algorithm="sha256", chunk_size=8192):
    """计算文件的哈希值"""
    hash_func = hashlib.new(algorithm)
    
    with open(filepath, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            hash_func.update(chunk)
    
    return hash_func.hexdigest()

# 演示用模拟数据
demo_data = b"This is a demo file for hashing purposes."
md5_demo = hashlib.md5(demo_data).hexdigest()
sha256_demo = hashlib.sha256(demo_data).hexdigest()

print(f"演示数据哈希:")
print(f"数据长度: {len(demo_data)} bytes")
print(f"MD5:    {md5_demo}")
print(f"SHA256: {sha256_demo}")

密码存储最佳实践

import hashlib
import os
import time

def hash_password(password, salt=None, iterations=100_000):
    """
    安全的密码哈希(PBKDF2-HMAC-SHA256)
    
    Args:
        password: 明文密码
        salt: 随机盐值(如果为 None 则自动生成)
        iterations: PBKDF2 迭代次数(越高越安全,但越慢)
    
    Returns:
        编码后的存储字符串格式: pbkdf2_sha256$salt$iterations$hash
    """
    if salt is None:
        salt = os.urandom(32)  # 生成 256 位随机盐
    
    dk = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        iterations
    )
    
    stored = f"pbkdf2_sha256${salt.hex}${iterations}${dk.hex()}"
    return stored

def verify_password(stored_password, provided_password):
    """验证密码"""
    parts = stored_password.split('$')
    if len(parts) != 4:
        raise ValueError("无效的存储格式")
    
    algorithm, salt_hex, iterations, expected_hash = parts
    salt = bytes.fromhex(salt_hex)
    iterations = int(iterations)
    
    dk = hashlib.pbkdf2_hmac(
        algorithm,
        provided_password.encode('utf-8'),
        salt,
        iterations
    )
    
    computed_hash = dk.hex()
    return hmac.compare_digest(computed_hash, expected_hash)

# 测试
password = "my_secure_password_123!"
print("正在哈希密码(可能需要几秒钟)...")
start = time.perf_counter()
hashed = hash_password(password)
elapsed = time.perf_counter() - start
print(f"哈希耗时: {elapsed:.2f}s")
print(f"存储格式: {hashed[:60]}...")

print(f"验证正确密码: {verify_password(hashed, password)}")  # True
print(f"验证错误密码: {verify_password(hashed, 'wrong_password')}")  # False

进度条可视化

def progress_bar(current, total, width=50, prefix="", suffix=""):
    """简易文本进度条"""
    length = width
    filled = round(length * current / max(total, 1))
    bar = "█" * filled + "-" * (length - filled)
    percent = 100 * current / max(total, 1)
    elapsed_str = f"[{current}/{total}] [{percent:5.1f}%]"
    print(f"\r{prefix} {elapsed_str} |{bar}| {suffix}", end="", flush=True)
    if current >= total:
        print()

# 模拟文件下载进度
import random
import time

total_size = 1_000_000  # 1MB
downloaded = 0

while downloaded < total_size:
    chunk_size = random.randint(1000, 50000)
    downloaded = min(downloaded + chunk_size, total_size)
    progress_bar(downloaded, total_size, prefix="下载中", suffix=f"{downloaded//1024}KB")
    time.sleep(0.05)

最佳实践总结

✅ 推荐做法

  1. 日期计算用 timedelta — 不要用手动加减秒数来计算天/周/月
  2. 字符串转 datetime 用 strptime — 不要直接用 eval 或正则解析日期字符串
  3. Counter 用于频率统计 — 比手写的 defaultdict 简洁得多
  4. defaultdict(int) — 替代 if key in dict: else 的空值检查
  5. 密码哈希用 pbkdf2 — 不要用简单的 MD5/sha1 存密码
  6. HMAC 比较用 compare_digest — 防时序攻击

❌ 避免做法

import datetime

# ❌ 用时间戳换算天(容易出错)
days = timestamp // (24 * 60 * 60)

# ✅ 用 timedelta
from datetime import timedelta
delta = timedelta(seconds=timestamp)
days = delta.days

# ❌ 在循环中重复导入模块
for _ in range(1000000):
    import json
    data = json.loads(...)

# ✅ 在文件顶部统一导入
import json
for _ in range(1000000):
    data = json.loads(...)

# ❌ 用 MD5 验证密码完整性
stored_md5 = hashlib.md5(password.encode()).hexdigest()

# ✅ 使用 PBKDF2/HMAC-SHA256
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)

总结

本文系统讲解了 Python 三大核心内建模块:

  1. datetime — 日期时间创建、timedelta 运算、strftime 格式化、strptime 解析、时区处理
  2. collections — namedtuple/deque/defaultdict/Counter/OrderedDict/ChainMap 的使用场景
  3. hashlib — MD5/SHA/SHA-256/SHA-512、HMAC 消息认证、文件校验和、PBKDF2 密码哈希
  4. 实战应用 — 日历生成器、FIFO 缓存、进度条、密码安全存储

下一篇文章将深入讲解常用内建模块(下):itertools、contextlib、urllib。


本文是 Python 教程系列的第四十三篇。