python从入门到精通-第9章: 标准库精要 — 工业开发常用模块

3 阅读26分钟

第9章: 标准库精要 — 工业开发常用模块

Java/Kotlin 开发者习惯了"万物皆需依赖管理"——JSON 解析要引 Jackson,CSV 要引 Apache Commons,命令行解析要引 JCommander,连日志都要 SLF4J + Logback 两层抽象。Python 的哲学不同:标准库即开箱即用的工具箱。本章精选工业开发中最高频的 8 个标准库模块,每个都给出 Java/Kotlin 对比和可运行的 Python demo。掌握这些,你日常 80% 的编码任务不需要 pip install 任何第三方包。


9.1 pathlib: 面向对象的路径操作

Java/Kotlin 对比

// Java: java.nio.file.Path — 同样是面向对象,但 API 更冗长
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.file.Files;

Path path = Paths.get("/home/user/docs/report.txt");
Path parent = path.getParent();              // /home/user/docs
String filename = path.getFileName().toString(); // report.txt
String stem = filename.split("\\.")[0];       // report — 没有直接方法!
String ext = filename.substring(filename.lastIndexOf('.') + 1); // txt

Path resolved = path.resolveSibling("backup/report.txt"); // 路径拼接
boolean exists = Files.exists(path);           // 文件是否存在
String content = Files.readString(path);        // 读文件(Java 11+)

// 遍历目录
try (var stream = Files.list(Paths.get("/home/user/docs"))) {
    stream.filter(p -> p.toString().endsWith(".txt"))
          .forEach(System.out::println);
}
// Kotlin: 可以直接用 java.nio.file.Path,或 Okio 的 Path
val path = java.nio.file.Paths.get("/home/user/docs/report.txt")
val name = path.fileName.toString()   // report.txt
val parent = path.parent              // /home/user/docs
// Kotlin 没有自己独立的 Path 类,标准库依赖 Java NIO

Python 实现

from pathlib import Path

# === Path 创建 ===
# 纯路径(不访问文件系统)vs 具体路径(可访问文件系统)
p = Path("/home/user/docs/report.txt")     # PosixPath(Linux/Mac)
# p = Path("C:/Users/docs/report.txt")    # WindowsPath(Windows)
# 自动适配操作系统,不需要手动处理分隔符

# === Path 属性 ===
p.name           # 'report.txt'     文件名(含扩展名)
p.stem           # 'report'         文件名(不含扩展名)
p.suffix         # '.txt'           扩展名
p.suffixes       # ['.txt']         所有扩展名(如 .tar.gz → ['.tar', '.gz'])
p.parent         # PosixPath('/home/user/docs')  父目录
p.parents        # <PosixPath.parents>  所有祖先目录的序列
p.parts          # ('/', 'home', 'user', 'docs', 'report.txt')

# === 路径拼接 ===
docs = Path("/home/user/docs")
report = docs / "report.txt"              # / 运算符拼接(比 Java 的 resolve 更直观)
backup = docs / "backup" / "report.txt"   # 链式拼接
print(backup)  # /home/user/docs/backup/report.txt

# === 路径解析与规范化 ===
abs_path = Path("docs/report.txt").resolve()   # 转为绝对路径
print(abs_path)  # /workspace/docs/report.txt(基于当前工作目录)

rel_path = Path("/home/user/docs/report.txt").relative_to("/home/user")
print(rel_path)  # docs/report.txt

# exists() 检查路径是否存在
print(docs.exists())       # True/False
print(docs.is_dir())       # True
print(docs.is_file())      # False

# === 遍历目录 ===
# iterdir(): 列出直接子项
for child in Path("/home/user/docs").iterdir():
    print(f"{'DIR ' if child.is_dir() else 'FILE'} {child.name}")

# rglob(): 递归遍历(比 Java 的 Files.walk() 更简洁)
for py_file in Path("/home/user/project").rglob("*.py"):
    print(py_file)

# glob(): 非递归模式匹配
for md_file in Path("/home/user/docs").glob("*.md"):
    print(md_file)

# === 读写文件 ===
p = Path("/tmp/demo.txt")

# 写文件
p.write_text("Hello, pathlib!", encoding="utf-8")
p.write_bytes(b"binary data here")

# 读文件
content = p.read_text(encoding="utf-8")
data = p.read_bytes()

# === 文件操作 ===
new_path = Path("/tmp/demo_copy.txt")
p.copy2(new_path)     # 复制文件(保留元数据)
# p.rename(new_path)  # 重命名/移动
# p.unlink()          # 删除文件
# Path("/tmp/new_dir").mkdir(parents=True, exist_ok=True)  # 创建目录

# === .home(), .cwd() 常用类方法 ===
print(Path.home())   # 用户主目录 /home/user
print(Path.cwd())    # 当前工作目录

核心差异

特性Java NIO PathPython pathlib
路径拼接resolve() / resolveSibling()/ 运算符
获取文件名(不含扩展名)无直接方法,需手动 split.stem
获取扩展名无直接方法,需手动 substring.suffix
读文件Files.readString().read_text()
写文件Files.writeString().write_text()
递归遍历Files.walk().rglob()
模式匹配需 PathMatcher.glob() / .rglob()

常见陷阱

# 陷阱 1: / 运算符要求右边是 str 或 Path
# Path("/home") + "user"    # TypeError! 不能用 +
Path("/home") / "user"      # 正确

# 陷阱 2: Path 对象不是字符串
p = Path("/tmp/test.txt")
# open(p)                    # Python 3.6+ 可以,但旧版本不行
open(str(p))                 # 任何时候都安全
# f"file://{p}"              # TypeError!
f"file://{p}"                # Python 3.6+ 可以自动转换

# 陷阱 3: resolve() 的行为取决于路径是否存在
# 如果路径不存在,resolve() 不会解析符号链接
# 如果路径存在,resolve() 会解析符号链接到真实路径

# 陷阱 4: Windows 路径分隔符
# Path 会自动处理,不要手动用 "/" 或 "\\"
Path("docs") / "report.txt"  # 跨平台安全

何时使用

  • 永远优先用 pathlib,不要用 os.path。pathlib 是 Python 3.4+ 的官方推荐,API 更面向对象、更易读。
  • 需要跨平台路径操作时,pathlib 是唯一选择。
  • 与旧代码交互时,用 str(path) 转换。

9.2 os, sys, shutil: 系统操作

Java/Kotlin 对比

// Java: 系统操作分散在多个类中
import java.lang.System;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;

// 环境变量
String home = System.getenv("HOME");           // 单个
Map<String, String> env = System.getenv();     // 全部
System.setProperty("app.mode", "dev");         // JVM 属性(非环境变量)

// 命令行参数
public static void main(String[] args) { ... } // args 即命令行参数

// 文件操作
new File("/tmp/old.txt").renameTo(new File("/tmp/new.txt")); // 不推荐
Files.move(src, dst);  // 推荐 NIO 方式
Files.delete(path);
Files.copy(src, dst);

// 进程退出
System.exit(0);  // 终止 JVM

// 类路径
// Java 没有"运行时动态修改 classpath"的标准方式
// Kotlin: 直接用 Java API
val home = System.getenv("HOME")
// 或 Kotlin 扩展: ProcessBuilder().environment()

Python 实现

import os
import sys
import shutil
import tempfile

# === os.environ: 环境变量 ===
home = os.environ.get("HOME")          # 获取,不存在返回 None
home = os.environ["HOME"]              # 获取,不存在抛 KeyError
path = os.environ.get("PATH", "/bin")  # 获取,带默认值

# 设置环境变量(仅影响当前进程及子进程)
os.environ["MY_VAR"] = "hello"
del os.environ["MY_VAR"]               # 删除

# 遍历所有环境变量
for key, value in os.environ.items():
    print(f"{key}={value}")

# === os.path: 路径操作(旧式,推荐用 pathlib) ===
os.path.join("/home", "user", "docs")   # '/home/user/docs'
os.path.basename("/home/user/file.txt") # 'file.txt'
os.path.dirname("/home/user/file.txt")  # '/home/user'
os.path.exists("/tmp")                  # True/False
os.path.isfile("/tmp/file.txt")         # 是否为文件
os.path.isdir("/tmp")                   # 是否为目录
os.path.getsize("/tmp/file.txt")        # 文件大小(字节)

# === sys.argv: 命令行参数 ===
# python script.py arg1 arg2 --flag value
# sys.argv = ['script.py', 'arg1', 'arg2', '--flag', 'value']
print(sys.argv[0])   # 脚本名
print(sys.argv[1:])  # 除脚本名外的参数

# === sys.path: 模块搜索路径 ===
# Python 导入模块时搜索的路径列表
print(sys.path)
# 可以动态添加搜索路径(类似 Java 的 classpath)
sys.path.append("/my/custom/modules")

# === sys.exit(): 退出程序 ===
sys.exit(0)           # 正常退出
sys.exit(1)           # 异常退出
sys.exit("error msg") # 退出并打印消息

# === shutil: 高级文件操作 ===
src = Path("/tmp/source.txt")
dst = Path("/tmp/dest.txt")

# 复制
shutil.copy2(src, dst)           # 复制文件(保留元数据)
shutil.copytree("src_dir", "dst_dir")  # 递归复制整个目录

# 移动/重命名
shutil.move("old_location", "new_location")

# 删除
shutil.rmtree("dir_to_delete")    # 递归删除整个目录(危险!)

# 磁盘使用情况
usage = shutil.disk_usage("/tmp")
print(f"Total: {usage.total}, Used: {usage.used}, Free: {usage.free}")

# === os: 进程和系统信息 ===
print(os.name)           # 'posix' (Linux/Mac) 或 'nt' (Windows)
print(os.cpu_count())    # CPU 核心数
print(os.getpid())       # 当前进程 ID
print(os.getcwd())       # 当前工作目录
os.chdir("/tmp")         # 切换工作目录

# 创建临时文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
    f.write("temporary content")
    temp_path = f.name
print(f"Temp file: {temp_path}")

# === os: 执行外部命令 ===
# 注意: 推荐用 subprocess 模块,os.system 是最简方式
exit_code = os.system("ls -la")  # 返回退出码,输出直接打印到终端

核心差异

特性JavaPython
环境变量System.getenv()os.environ(字典接口)
命令行参数main(String[] args)sys.argv(列表)
模块搜索路径classpath(启动时确定)sys.path(运行时可修改)
递归删除目录需递归遍历 + Files.deleteshutil.rmtree() 一行搞定
递归复制目录需手动实现或用第三方库shutil.copytree()
临时文件Files.createTempFile()tempfile.NamedTemporaryFile()

常见陷阱

# 陷阱 1: os.environ 修改只影响当前进程
os.environ["MY_VAR"] = "value"
# 不会影响父进程(shell)的环境变量
# 也不会写入 ~/.bashrc 等配置文件

# 陷阱 2: sys.exit() 在 try/finally 中会被 finally 拦截
try:
    sys.exit(1)
finally:
    print("这行仍然会执行")  # finally 总是执行

# 陷阱 3: shutil.rmtree() 没有回收站!删除不可恢复
# 建议先用 dry_run 或确认路径
import os
target = "/tmp/some_dir"
if os.path.exists(target):
    shutil.rmtree(target)

# 陷阱 4: os.path.join 的行为
os.path.join("/home", "/absolute/path")
# 结果是 '/absolute/path'!绝对路径会丢弃前面的部分
# pathlib 的 / 也有同样行为

何时使用

  • pathlib: 路径操作的首选(见 9.1)
  • os.environ: 读取配置、Docker 环境变量
  • sys.argv: 简单脚本获取参数(复杂场景用 argparse,见 9.6)
  • sys.path: 插件系统、动态导入
  • shutil: 批量文件操作(复制、移动、删除目录树)
  • tempfile: 测试中创建临时文件

9.3 json, csv, tomllib: 数据序列化

Java/Kotlin 对比

// Java: JSON 需要 Jackson/Gson 等第三方库
import com.fasterxml.jackson.databind.ObjectMapper;

// 序列化
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);  // 对象 → JSON 字符串
mapper.writeValue(new File("user.json"), user); // 对象 → JSON 文件

// 反序列化
User user = mapper.readValue(jsonStr, User.class);       // JSON → 对象
Map<String, Object> map = mapper.readValue(jsonStr, Map.class); // JSON → Map

// CSV: Apache Commons CSV 或 OpenCSV
// TOML: 没有标准库支持,需要第三方库
// Kotlin: kotlinx.serialization
@Serializable
data class User(val name: String, val age: Int)

val json = Json.encodeToString(User("Alice", 30))
val user = Json.decodeFromString<User>(json)

Python 实现

import json
import csv
import io

# ============================================================
# json: Python 内置 JSON 支持,无需第三方库
# ============================================================

# === dumps / loads: 字符串 ↔ Python 对象 ===
data = {
    "name": "Alice",
    "age": 30,
    "skills": ["Python", "Java"],
    "address": {
        "city": "Beijing",
        "zip": "100000"
    },
    "active": True,
    "score": None  # None → JSON null
}

# 序列化: Python 对象 → JSON 字符串
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
# {
#   "name": "Alice",
#   "age": 30,
#   "skills": ["Python", "Java"],
#   "address": {"city": "Beijing", "zip": "100000"},
#   "active": true,
#   "score": null
# }

# 反序列化: JSON 字符串 → Python 对象
parsed = json.loads(json_str)
print(type(parsed))         # <class 'dict'>
print(parsed["name"])       # Alice
print(parsed["skills"][0])  # Python

# === dump / load: 文件读写 ===
with open("/tmp/data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

with open("/tmp/data.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

# === 自定义序列化 ===
from datetime import datetime

# 方式 1: default 参数
def custom_serializer(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

json_str = json.dumps({"time": datetime.now()}, default=custom_serializer)

# 方式 2: 继承 JSONEncoder
class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

json_str = json.dumps({"time": datetime.now()}, cls=CustomEncoder)

# === JSON 类型映射 ===
# JSON          Python
# object    →   dict
# array     →   list
# string    →   str
# number    →   int / float
# true      →   True
# false     →   False
# null      →   None


# ============================================================
# csv: 内置 CSV 读写
# ============================================================

# === 基础 reader/writer ===
rows = [
    ["name", "age", "city"],
    ["Alice", "30", "Beijing"],
    ["Bob", "25", "Shanghai"],
]

# 写 CSV
with open("/tmp/data.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(rows)     # 写入多行
    # writer.writerow(["Charlie", "28", "Guangzhou"])  # 写入单行

# 读 CSV
with open("/tmp/data.csv", "r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)  # ['name', 'age', 'city'] / ['Alice', '30', 'Beijing'] / ...

# === DictReader/DictWriter: 字典模式(更推荐) ===
data = [
    {"name": "Alice", "age": "30", "city": "Beijing"},
    {"name": "Bob", "age": "25", "city": "Shanghai"},
]

# 写
with open("/tmp/data_dict.csv", "w", newline="", encoding="utf-8") as f:
    fieldnames = ["name", "age", "city"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()       # 写入表头
    writer.writerows(data)     # 写入数据

# 读
with open("/tmp/data_dict.csv", "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["age"])  # Alice 30 / Bob 25
        # 注意: 所有值都是字符串!需要手动转换类型


# ============================================================
# tomllib: TOML 解析(Python 3.11+)
# ============================================================

# Python 3.11 内置 tomllib(只读)
# Python 3.10 需要第三方库: pip install tomli

import sys
if sys.version_info >= (3, 11):
    import tomllib
else:
    # 兼容 3.10 的写法
    try:
        import tomllib
    except ImportError:
        import tomli as tomllib  # pip install tomli

# TOML 是 Python 生态的配置文件标准(pyproject.toml)
toml_content = """
[database]
host = "localhost"
port = 5432
name = "myapp"

[database.pool]
min_size = 5
max_size = 20

[logging]
level = "INFO"
handlers = ["console", "file"]

[server]
hosts = ["0.0.0.0", "::"]
debug = false
"""

# 解析 TOML 字符串
config = tomllib.loads(toml_content)
print(config["database"]["host"])      # localhost
print(config["database"]["pool"]["min_size"])  # 5
print(config["logging"]["handlers"])   # ['console', 'file']

# 解析 TOML 文件
# with open("pyproject.toml", "rb") as f:
#     config = tomllib.load(f)

# 注意: tomllib 只有 loads/load(读取),没有 dumps/dump(写入)
# 写 TOML 需要第三方库: pip install tomli-w

核心差异

特性Java (Jackson)Python
JSON 支持第三方库内置 json
CSV 支持第三方库 (Apache Commons CSV)内置 csv
TOML 支持第三方库内置 tomllib (3.11+)
序列化自定义@JsonSerialize / JsonSerializerdefault 参数 / 继承 JSONEncoder
CSV 字典模式手动映射DictReader / DictWriter
日期序列化@JsonFormat / JavaTimeModule手动 default 函数

常见陷阱

# 陷阱 1: json.dumps 默认 ensure_ascii=True,中文会变成 \uXXXX
json.dumps({"name": "张三"})  # '{"name": "\\u5f20\\u4e09"}'
json.dumps({"name": "张三"}, ensure_ascii=False)  # '{"name": "张三"}'

# 陷阱 2: json.loads 返回的数字类型不固定
data = json.loads('{"int": 42, "float": 3.14}')
print(type(data["int"]))    # <class 'int'>
print(type(data["float"]))  # <class 'float'>
# 但: json.loads('{"big": 99999999999999999999}') → int(自动处理大数)

# 陷阱 3: CSV 所有值都是字符串
# reader 返回的每行都是字符串列表,不会自动转换类型
# "30" 不会变成 30,需要手动 int(row["age"])

# 陷阱 4: CSV 写文件必须 newline=""
# 否则 Windows 上会出现空行
with open("file.csv", "w", newline="") as f:  # 正确
    writer = csv.writer(f)

# 陷阱 5: tomllib 要求二进制模式打开文件
# with open("config.toml", "r") as f:    # 错误!
with open("config.toml", "rb") as f:      # 正确
    config = tomllib.load(f)

何时使用

  • json: API 数据交换、配置文件、日志结构化。Python 内置,性能足够大多数场景。
  • csv: 数据导入导出、Excel 兼容格式。简单场景用内置 csv,复杂场景(大数据量)考虑 pandas
  • tomllib: 读取 pyproject.toml 等配置文件。注意它只读,写 TOML 需要 tomli-w

9.4 re: 正则表达式

Java/Kotlin 对比

// Java: java.util.regex — 编译后使用
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// 编译正则
Pattern pattern = Pattern.compile("(?<name>[a-zA-Z]+)\\s+(?<age>\\d+)");
Matcher matcher = pattern.matcher("Alice 30");

if (matcher.find()) {
    String name = matcher.group("name");  // 命名捕获组
    String age = matcher.group("age");
}

// 替换
String result = Pattern.compile("\\d+").matcher("age: 30").replaceAll("N");
// Kotlin: 可用 Java API,或 Kotlin 扩展
val regex = Regex("""(?<name>[a-zA-Z]+)\s+(?<age>\d+)""")
val match = regex.find("Alice 30")
match?.groups?.get("name")?.value  // Alice

// Kotlin 三引号字符串不需要双反斜杠
// Java: "\\d+"  vs  Kotlin: """\d+"""

Python 实现

import re

# === re.compile: 编译正则表达式 ===
# Python 的 raw string (r"...") 不需要双反斜杠,和 Kotlin 的 """ """ 类似
pattern = re.compile(r"(?P<name>[a-zA-Z]+)\s+(?P<age>\d+)")

# === match: 从字符串开头匹配 ===
m = pattern.match("Alice 30 Bob 25")
if m:
    print(m.group("name"))  # Alice
    print(m.group("age"))   # 30
    print(m.group(0))       # Alice 30(整个匹配)
    print(m.group(1))       # Alice(第一个捕获组)
    print(m.groups())       # ('Alice', '30')(所有捕获组)
    print(m.groupdict())    # {'name': 'Alice', 'age': '30'}(命名捕获组)

# === search: 搜索第一个匹配(不要求从开头) ===
m = re.search(r"\d+", "Alice is 30 years old")
if m:
    print(m.group())  # 30

# === findall: 找到所有匹配 ===
numbers = re.findall(r"\d+", "Alice 30, Bob 25, Charlie 28")
print(numbers)  # ['30', '25', '28']

# 带捕获组的 findall: 返回捕获组内容
pairs = re.findall(r"(\w+)\s+(\d+)", "Alice 30, Bob 25")
print(pairs)  # [('Alice', '30'), ('Bob', '25')]

# === finditer: 迭代所有匹配对象(更灵活) ===
for m in re.finditer(r"(?P<name>\w+)\s+(?P<age>\d+)", "Alice 30, Bob 25"):
    print(f"{m.group('name')}: {m.group('age')}")
# Alice: 30
# Bob: 25

# === sub: 替换 ===
result = re.sub(r"\d+", "N", "Alice 30, Bob 25")
print(result)  # Alice N, Bob N

# 带函数的替换(比 Java 的 appendReplacement 更灵活)
def censor_age(match):
    age = int(match.group())
    return "***" if age < 18 else str(age)

result = re.sub(r"\d+", censor_age, "Alice 15, Bob 25, Charlie 12")
print(result)  # Alice ***, Bob 25, Charlie ***

# 限制替换次数
result = re.sub(r"\d+", "N", "a1 b2 c3 d4", count=2)
print(result)  # aN bN c3 d4

# === split: 正则分割 ===
parts = re.split(r"[,\s]+", "Alice,30 Bob 25 Charlie,28")
print(parts)  # ['Alice', '30', 'Bob', '25', 'Charlie', '28']

# === 标志位 ===
text = "Hello\nWorld"
re.findall(r"^hello", text)                    # [](不匹配)
re.findall(r"^hello", text, re.IGNORECASE)     # ['Hello']
re.findall(r"^hello", text, re.I | re.MULTILINE)  # ['Hello']

# 常用标志:
# re.IGNORECASE (re.I)   — 忽略大小写
# re.MULTILINE (re.M)    — ^ $ 匹配每行开头/结尾
# re.DOTALL (re.S)       — . 匹配换行符
# re.VERBOSE (re.X)      — 允许注释和空白

# === VERBOSE 模式: 写可读的正则 ===
pattern = re.compile(r"""
    (?P<protocol>https?)://   # 协议
    (?P<domain>[\w.]+)        # 域名
    (?::(?P<port>\d+))?       # 可选端口
    (?P<path>/\S*)?           # 可选路径
""", re.VERBOSE)

m = pattern.search("Visit https://example.com:8080/api/users")
if m:
    print(m.groupdict())
    # {'protocol': 'https', 'domain': 'example.com', 'port': '8080', 'path': '/api/users'}

# === 命名捕获组 ===
# Java/Kotlin 中也支持命名组,但 Python 语法更简洁
log_pattern = r'(?P<timestamp>\d{4}-\d{2}-\d{2}) (?P<level>\w+) (?P<message>.+)'
log_line = '2024-01-15 ERROR Database connection failed'
match = re.match(log_pattern, log_line)
if match:
    print(f"时间: {match.group('timestamp')}")  # 2024-01-15
    print(f"级别: {match.group('level')}")      # ERROR
    print(f"消息: {match.group('message')}")    # Database connection failed
    # 也可以用 match['timestamp'] 语法(更 Pythonic)
    print(f"时间(v2): {match['timestamp']}")

# 命名组替换
result = re.sub(
    r'(?P<name>\w+)=(?P<value>\w+)',
    lambda m: f"{m['name'].upper()}={m['value']}",
    'name=Alice age=30 city=Beijing'
)
print(f"替换结果: {result}")  # NAME=Alice AGE=30 CITY=Beijing

核心差异

特性Java MatcherPython re
命名捕获组语法(?<name>...)(?P<name>...)
不需要双反斜杠不支持(需要 "\\d+"raw string: r"\d+"
查找所有匹配需 while(matcher.find()) 循环findall() / finditer()
替换replaceAll() / appendReplacement()sub() 支持函数回调
可读正则不支持re.VERBOSE 支持注释
编译Pattern.compile()re.compile()

常见陷阱

# 陷阱 1: 命名捕获组语法不同!
# Java:    (?<name>...)
# Python:  (?P<name>...)   ← 注意大写 P

# 陷阱 2: match vs search 的区别
re.match(r"\d+", "abc 123")  # None!match 只从开头匹配
re.search(r"\d+", "abc 123") # <Match '123'> search 在任意位置匹配

# 陷阱 3: findall 带捕获组时行为变化
re.findall(r"\d+", "a1 b2")    # ['1', '2'] — 无捕获组,返回完整匹配
re.findall(r"(\d+)", "a1 b2")  # ['1', '2'] — 一个捕获组,返回捕获组
re.findall(r"(\d)(\d+)", "a12 b34")  # [('1', '2'), ('3', '4')] — 多个捕获组,返回元组

# 陷阱 4: 贪婪 vs 非贪婪
re.findall(r"<.+>", "<a> <b>")     # ['<a> <b>'] — 贪婪匹配
re.findall(r"<.+?>", "<a> <b>")    # ['<a>', '<b>'] — 非贪婪匹配(加 ?)

# 陷阱 5: re.compile 不是必须的
# re.search(r"\d+", text) 内部也会编译,但重复使用时 compile 更高效

何时使用

  • 简单字符串操作: 优先用 str.split(), str.replace(), str.startswith() 等字符串方法,正则表达式是最后手段。
  • 复杂模式匹配: 邮箱、URL、IP 地址等需要正则。
  • 数据清洗: 从非结构化文本中提取结构化数据。
  • 性能敏感: 预编译正则 re.compile(),避免重复编译。

9.5 logging: 结构化日志

Java/Kotlin 对比

// Java: SLF4J + Logback(两层抽象)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyApp {
    // 每个类一个 Logger 实例
    private static final Logger log = LoggerFactory.getLogger(MyApp.class);

    public void doWork() {
        log.debug("Processing item: {}", itemId);     // 占位符,延迟求值
        log.info("User {} logged in", username);
        log.warn("Retry attempt {}/{}", attempt, maxAttempts);
        log.error("Failed to connect", exception);    // 自动打印堆栈
    }
}

// 配置: logback.xml(XML 格式,功能强大但复杂)
/*
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>
*/
// Kotlin: 可用 SLF4J,或 kotlin-logging(更简洁)
import io.github.microutils.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}
// logger.info { "Processing $item" }  // lambda 延迟求值

Python 实现

import logging
import logging.config

# ============================================================
# 基础用法: basicConfig
# ============================================================

# 最简配置(适合脚本和小工具)
logging.basicConfig(
    level=logging.DEBUG,                    # 最低日志级别
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# 获取 Logger(通常以模块名为 name)
logger = logging.getLogger(__name__)  # __name__ = 当前模块名

# 日志级别(从低到高)
logger.debug("调试信息 - 变量值: x=%d", 42)      # 开发调试
logger.info("普通信息 - 服务启动完成")              # 关键业务流程
logger.warning("警告 - 磁盘空间不足 90%")          # 潜在问题
logger.error("错误 - 数据库连接失败")              # 错误但可恢复
logger.critical("严重 - 系统无法启动")             # 不可恢复的错误

# 异常日志(自动附带堆栈信息)
try:
    1 / 0
except ZeroDivisionError:
    logger.error("计算出错", exc_info=True)       # 方式 1: exc_info=True
    # logger.exception("计算出错")                # 方式 2: exception() 自动附带堆栈

# 占位符格式化(类似 SLF4J 的 {})
logger.info("User %s logged in from %s", "Alice", "192.168.1.1")
# 不要用 f-string!f-string 会立即求值,即使日志级别不够也会执行
# 错误: logger.debug(f"expensive call result: {expensive_function()}")
# 正确: logger.debug("expensive call result: %s", expensive_function())


# ============================================================
# 进阶用法: dictConfig(推荐用于正式项目)
# ============================================================

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,

    # 格式器
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
        "json": {
            # 生产环境推荐 JSON 格式,方便 ELK 等日志系统采集
            "format": '{"time":"%(asctime)s","level":"%(levelname)s","logger":"%(name)s","msg":"%(message)s"}',
        },
    },

    # 处理器
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "formatter": "standard",
            "stream": "ext://sys.stdout",  # 输出到标准输出
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "INFO",
            "formatter": "standard",
            "filename": "/tmp/app.log",
            "maxBytes": 10 * 1024 * 1024,  # 10MB 轮转
            "backupCount": 5,               # 保留 5 个备份
            "encoding": "utf-8",
        },
    },

    # Logger 配置
    "loggers": {
        # 第三方库日志级别调高,减少噪音
        "urllib3": {"level": "WARNING"},
        "requests": {"level": "WARNING"},
        # 业务模块
        "myapp": {"level": "DEBUG", "handlers": ["console", "file"], "propagate": False},
    },

    # 根 Logger
    "root": {
        "level": "WARNING",
        "handlers": ["console"],
    },
}

logging.config.dictConfig(LOGGING_CONFIG)

# 使用
app_logger = logging.getLogger("myapp.service")
app_logger.info("服务启动")
app_logger.debug("调试详情")

# ============================================================
# Logger 层级(类似 Java 的包层级)
# ============================================================
# logging.getLogger("myapp")           # 父 logger
# logging.getLogger("myapp.service")   # 子 logger,继承父的配置
# logging.getLogger("myapp.dao")       # 子 logger

# propagate=False 表示不向父 logger 传播(类似 Java 的 additivity=false)

核心差异

特性SLF4J/LogbackPython logging
Logger 命名类全限定名模块名 __name__
延迟求值{} 占位符%s 占位符
配置方式XML (logback.xml)dictConfig (Python 字典) / basicConfig
日志级别TRACE < DEBUG < INFO < WARN < ERRORDEBUG < INFO < WARNING < ERROR < CRITICAL
异常日志log.error("msg", exception)logger.error("msg", exc_info=True)logger.exception()
日志轮转<rollingPolicy>RotatingFileHandler / TimedRotatingFileHandler
过滤第三方日志<logger level="WARN">loggers 字典配置

常见陷阱

# 陷阱 1: basicConfig 只在第一次调用时生效
# 如果其他库先调用了 basicConfig,你的配置会被忽略
# 解决: 用 dictConfig,或在程序入口最早调用 basicConfig

# 陷阱 2: 不要用 f-string 做日志
logger.debug(f"user: {user}")          # 错误!即使 DEBUG 级别被过滤,f-string 也会执行
logger.debug("user: %s", user)         # 正确!只有 DEBUG 级别启用时才会格式化

# 陷阱 3: 模块级 logger vs 函数内 logger
# 正确: 模块顶层定义(只创建一次)
logger = logging.getLogger(__name__)

# 错误: 函数内定义(每次调用都创建)
def bad():
    logger = logging.getLogger(__name__)  # 虽然返回同一实例,但没必要

# 陷阱 4: logging.getLogger() 返回的是单例
# 同名 getLogger() 返回同一个 Logger 对象
# 不要在循环中反复创建 Handler,否则日志会重复输出

# 陷阱 5: 根 logger 的消息会传播到所有 handler
# 如果子 logger 和根 logger 都配置了 console handler,日志会打印两次
# 解决: 设置 propagate=False

structlog: 现代结构化日志

工业级项目推荐使用 structlog 替代标准 logging。详见 12.7 可观测性

# pip install structlog
# structlog 让日志输出结构化 JSON,便于 ELK/Grafana 等工具消费
import structlog

logger = structlog.get_logger()

# 自动输出结构化日志
logger.info("user_login", user_id=42, ip="192.168.1.1")
# 输出: user_id=42 ip=192.168.1.1 event=user_login

# 与标准 logging 集成
structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)

何时使用

  • 脚本/工具: basicConfig 足够,一行配置搞定。
  • 正式项目/服务: dictConfig 是唯一推荐,支持 Handler、Formatter、Logger 层级、日志轮转。
  • JSON 日志: 生产环境推荐 JSON 格式,方便 ELK/Grafana Loki 采集。可用 python-json-logger 库。
  • 不要 print(): 永远不要用 print() 做日志输出,它没有级别、没有时间戳、无法被日志系统采集。

9.6 argparse: 命令行解析

Java/Kotlin 对比

// Java: JCommander 或 picocli(第三方库)
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;

@Parameters(commandDescription = "Database migration tool")
public class MigrateCommand {
    @Parameter(names = {"--host"}, description = "Database host", required = true)
    private String host;

    @Parameter(names = {"--port"}, description = "Database port")
    private int port = 5432;

    @Parameter(names = {"--dry-run"}, description = "Dry run mode")
    private boolean dryRun = false;
}
// JCommander jcommander = new JCommander(migrateCommand, args);
// jcommander.parse();
// Kotlin: kotlin-cli 或 argparse4j
// 同样需要第三方库

Python 实现

import argparse

# ============================================================
# 基础用法
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="数据库迁移工具",       # 程序描述
        epilog="示例: python migrate.py --host localhost --port 5432 up",
    )

    # === 位置参数(必填) ===
    parser.add_argument(
        "command",                          # 参数名
        choices=["up", "down", "status"],   # 限定取值
        help="迁移命令: up | down | status",
    )

    # === 可选参数 ===
    parser.add_argument(
        "--host", "-h",                     # 长选项 + 短选项
        default="localhost",
        help="数据库主机地址 (默认: localhost)",
    )
    parser.add_argument(
        "--port", "-p",
        type=int,                           # 自动类型转换
        default=5432,
        help="数据库端口 (默认: 5432)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",                # 布尔开关,出现即为 True
        help="试运行模式,不实际执行",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="count",                     # 计数器,-v=1, -vv=2, -vvv=3
        default=0,
        help="详细程度: -v, -vv, -vvv",
    )
    parser.add_argument(
        "--tables",
        nargs="+",                          # 接受一个或多个值
        help="要迁移的表名(空格分隔)",
    )

    args = parser.parse_args()

    # 使用解析结果
    print(f"命令: {args.command}")
    print(f"主机: {args.host}")
    print(f"端口: {args.port}")
    print(f"试运行: {args.dry_run}")
    print(f"详细级别: {args.verbose}")
    print(f"表: {args.tables}")

# 运行示例:
# python migrate.py up --host 192.168.1.1 --port 3306 --dry-run -vv --tables users orders
# 命令: up
# 主机: 192.168.1.1
# 端口: 3306
# 试运行: True
# 详细级别: 2
# 表: ['users', 'orders']


# ============================================================
# 子命令(类似 git 的 git add / git commit)
# ============================================================

def create_parser():
    parser = argparse.ArgumentParser(description="项目管理工具")
    subparsers = parser.add_subparsers(dest="subcommand", help="子命令")

    # === 子命令: build ===
    build_parser = subparsers.add_parser("build", help="构建项目")
    build_parser.add_argument("--release", action="store_true", help="发布构建")
    build_parser.add_argument("--target", choices=["web", "mobile", "desktop"], default="web")

    # === 子命令: deploy ===
    deploy_parser = subparsers.add_parser("deploy", help="部署项目")
    deploy_parser.add_argument("environment", choices=["dev", "staging", "prod"])
    deploy_parser.add_argument("--skip-tests", action="store_true")
    deploy_parser.add_argument("--version", required=True, help="部署版本号")

    return parser

# 使用
parser = create_parser()
args = parser.parse_args(["deploy", "prod", "--version", "1.2.3", "--skip-tests"])
# args.subcommand  = 'deploy'
# args.environment = 'prod'
# args.version     = '1.2.3'
# args.skip_tests  = True

# argparse 自动生成 --help 信息
# python script.py --help
# python script.py deploy --help

核心差异

特性JCommander (Java)argparse (Python)
依赖第三方库内置标准库
参数定义注解 @Parameteradd_argument() 调用
布尔开关boolean = true 属性action="store_true"
类型转换自动(基于字段类型)type=int/float/... 参数
子命令@Parameters 嵌套add_subparsers()
自动帮助@Parameter(help=...)help= 参数,自动生成 --help
必填参数required = truerequired=True

常见陷阱

# 陷阱 1: action="store_true" 的参数不能有 default=True
# --dry-run 出现 → True,不出现 → False(默认就是 False)
parser.add_argument("--dry-run", action="store_true")  # 正确

# 陷阱 2: nargs="?" vs nargs="+"
# nargs="?" — 0 个或 1 个参数
# nargs="+" — 1 个或多个参数(至少 1 个)
# nargs="*" — 0 个或多个参数

# 陷阱 3: parse_args() 默认读取 sys.argv
# 测试时可以手动传入参数列表
args = parser.parse_args(["--host", "localhost", "up"])  # 不依赖命令行

# 陷阱 4: 互斥参数
group = parser.add_mutually_exclusive_group()
group.add_argument("--verbose", action="store_true")
group.add_argument("--quiet", action="store_true")
# 不能同时指定 --verbose 和 --quiet

何时使用

  • 任何需要命令行参数的脚本: argparse 是 Python 标准答案,不需要第三方库。
  • 简单脚本: 1-2 个参数,直接 add_argument
  • 复杂 CLI 工具: 子命令 + 互斥参数 + 类型转换。
  • 替代方案: click(第三方库,装饰器风格更优雅)、typer(基于 click,类型提示驱动)。

9.7 sqlite3: 内置数据库

Java/Kotlin 对比

// Java: JDBC + SQLite 驱动(需要第三方依赖)
import java.sql.*;

// 1. 加载驱动(新版 JDBC 自动加载)
// Class.forName("org.sqlite.JDBC");

// 2. 建立连接
String url = "jdbc:sqlite:/tmp/myapp.db";
Connection conn = DriverManager.getConnection(url);

// 3. 创建语句
Statement stmt = conn.createStatement();
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");

// 4. 参数化查询(防 SQL 注入)
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name, age) VALUES (?, ?)");
pstmt.setString(1, "Alice");
pstmt.setInt(2, 30);
pstmt.executeUpdate();

// 5. 查询
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
    String name = rs.getString("name");
    int age = rs.getInt("age");
}

// 6. 事务
conn.setAutoCommit(false);
try {
    // ... 多个操作
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

// 7. 关闭资源(Java 7+ try-with-resources)
try (Connection c = DriverManager.getConnection(url);
     Statement s = c.createStatement()) {
    // ...
}
// Kotlin: Exposed 框架或直接用 JDBC
// 同样需要第三方依赖

Python 实现

import sqlite3

# ============================================================
# 连接和基本操作
# ============================================================

# 连接数据库(文件不存在会自动创建)
conn = sqlite3.connect("/tmp/myapp.db")

# 使用 row_factory 让查询返回字典(默认返回元组)
conn.row_factory = sqlite3.Row

# 获取游标
cursor = conn.cursor()

# 创建表
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id    INTEGER PRIMARY KEY AUTOINCREMENT,
        name  TEXT NOT NULL,
        age   INTEGER,
        email TEXT UNIQUE
    )
""")

# ============================================================
# 参数化查询(防 SQL 注入 — 永远不要用字符串拼接!)
# ============================================================

# 插入数据 — 使用 ? 占位符
cursor.execute(
    "INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
    ("Alice", 30, "alice@example.com")
)
cursor.execute(
    "INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
    ("Bob", 25, "bob@example.com")
)

# 批量插入 — executemany
users = [
    ("Charlie", 28, "charlie@example.com"),
    ("Diana", 35, "diana@example.com"),
    ("Eve", 22, "eve@example.com"),
]
cursor.executemany(
    "INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
    users
)

# 提交事务(修改操作必须 commit)
conn.commit()

# ============================================================
# 查询
# ============================================================

# fetchone: 获取一行
cursor.execute("SELECT * FROM users WHERE name = ?", ("Alice",))
row = cursor.fetchone()
if row:
    # row_factory=sqlite3.Row 时,可以用列名访问
    print(row["name"])   # Alice
    print(row["age"])    # 30
    print(row["id"])     # 1
    print(dict(row))     # {'id': 1, 'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}

# fetchall: 获取所有行
cursor.execute("SELECT name, age FROM users WHERE age > ?", (25,))
rows = cursor.fetchall()
for row in rows:
    print(f"{row['name']}: {row['age']}")
# Alice: 30
# Diana: 35

# 迭代游标(大数据量时更省内存)
cursor.execute("SELECT * FROM users")
for row in cursor:
    print(row["name"], row["age"])

# ============================================================
# 事务
# ============================================================

try:
    cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, "Alice"))
    cursor.execute("UPDATE users SET age = ? WHERE name = ?", (26, "Bob"))
    conn.commit()     # 成功则提交
except sqlite3.Error as e:
    conn.rollback()   # 失败则回滚
    print(f"事务失败: {e}")

# ============================================================
# 上下文管理器(自动 commit/rollback)
# ============================================================

# with conn:  — 成功自动 commit,异常自动 rollback
with conn:
    conn.execute("UPDATE users SET age = age + 1 WHERE name = ?", ("Alice",))

# ============================================================
# 关闭连接
# ============================================================
conn.close()

# ============================================================
# 最佳实践: 使用上下文管理器
# ============================================================

def query_users(db_path: str, min_age: int) -> list[dict]:
    """查询年龄大于 min_age 的用户"""
    with sqlite3.connect(db_path) as conn:
        conn.row_factory = sqlite3.Row
        cursor = conn.execute(
            "SELECT name, age, email FROM users WHERE age > ? ORDER BY age",
            (min_age,)
        )
        return [dict(row) for row in cursor]

results = query_users("/tmp/myapp.db", 25)
for user in results:
    print(user)

核心差异

特性JDBC (Java)sqlite3 (Python)
依赖第三方 JDBC 驱动内置标准库
连接DriverManager.getConnection()sqlite3.connect()
参数占位符?:name?(仅支持 ?
结果集ResultSet(需 while(rs.next()))fetchone() / fetchall() / 迭代
字典访问rs.getString("name")row["name"](需设置 row_factory)
资源关闭try-with-resourceswith 上下文管理器
事务setAutoCommit(false) + commit/rollbackwith conn: 自动管理

常见陷阱

# 陷阱 1: 忘记 commit!
# Python 的 sqlite3 默认需要手动 commit,修改不会自动持久化
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
conn.commit()  # 必须!或者用 with conn: 自动提交

# 陷阱 2: 不要用字符串拼接 SQL(SQL 注入风险)
# 错误!
name = "Alice"
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# 正确!
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

# 陷阱 3: 占位符只支持 ?,不支持命名参数
# Java: "WHERE name = :name"
# Python: "WHERE name = ?"  ← 只能用 ?

# 陷阱 4: SQLite 的类型是动态的(弱类型)
# 可以在 INTEGER 列中存字符串,SQLite 不会报错
# 类型检查需要应用层来做

# 陷阱 5: 并发写入
# SQLite 不支持并发写入。多线程/多进程写入需要:
# - 使用 WAL 模式: conn.execute("PRAGMA journal_mode=WAL")
# - 或使用连接池 + 超时: sqlite3.connect(db, timeout=30)

何时使用

  • 原型开发/测试: SQLite 零配置,适合快速验证。
  • 嵌入式应用: 桌面应用、移动应用、CLI 工具的本地存储。
  • 小型 Web 应用: 读多写少、单机部署的场景。
  • 不适合: 高并发写入、需要水平扩展的场景(用 PostgreSQL/MySQL)。

9.8 datetime, zoneinfo: 日期时间

Java/Kotlin 对比

// Java: java.time API (Java 8+) — 设计优秀
import java.time.*;
import java.time.format.DateTimeFormatter;

// 当前时间
LocalDateTime now = LocalDateTime.now();            // 2025-01-15T10:30:00
LocalDate today = LocalDate.now();                  // 2025-01-15
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

// 创建
LocalDate date = LocalDate.of(2025, 1, 15);
LocalTime time = LocalTime.of(10, 30, 0);
LocalDateTime dt = LocalDateTime.of(date, time);

// 时区转换
ZonedDateTime tokyo = beijing.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

// 格式化
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(fmt);                  // 2025-01-15 10:30:00

// 解析
LocalDateTime parsed = LocalDateTime.parse("2025-01-15 10:30:00", fmt);

// 时间差
Duration d = Duration.between(start, end);           // 秒/纳秒级
Period p = Period.between(date1, date2);             // 年/月/日级
// Kotlin: 直接用 java.time,或 kotlinx-datetime
// kotlinx-datetime 提供跨平台支持
import kotlinx.datetime.*
import kotlinx.datetime.Clock.System.now

val now: Instant = Clock.System.now()
val localDt = now.toLocalDateTime(TimeZone.of("Asia/Shanghai"))

Python 实现

from datetime import datetime, date, time, timedelta, timezone
from zoneinfo import ZoneInfo  # Python 3.9+

# ============================================================
# 获取当前时间
# ============================================================

# 无时区(naive)— 不要在服务端使用!
now_naive = datetime.now()
print(now_naive)  # 2025-01-15 10:30:00.123456

# 有时区(aware)— 永远优先使用!
now_utc = datetime.now(timezone.utc)
print(now_utc)  # 2025-01-15 02:30:00.123456+00:00

now_beijing = datetime.now(ZoneInfo("Asia/Shanghai"))
print(now_beijing)  # 2025-01-15 10:30:00.123456+08:00

# 日期和时间分开
today = date.today()           # 2025-01-15
current_time = datetime.now().time()  # 10:30:00.123456

# ============================================================
# 创建日期时间
# ============================================================

d = date(2025, 1, 15)                          # 日期
t = time(10, 30, 0)                            # 时间
dt = datetime(2025, 1, 15, 10, 30, 0)          # 日期时间
dt_aware = datetime(2025, 1, 15, 10, 30, 0, tzinfo=ZoneInfo("Asia/Shanghai"))

# 从时间戳创建
dt_from_ts = datetime.fromtimestamp(1705282200, tz=ZoneInfo("Asia/Shanghai"))

# ============================================================
# 日期时间属性
# ============================================================

now = datetime.now(ZoneInfo("Asia/Shanghai"))
now.year          # 2025
now.month         # 1
now.day           # 15
now.hour          # 10
now.minute        # 30
now.second        # 0
now.microsecond   # 123456
now.weekday()     # 2 (周一=0, 周日=6)
now.isoweekday()  # 3 (周一=1, 周日=7)
now.isoformat()   # '2025-01-15T10:30:00.123456+08:00'

# ============================================================
# 时间计算(timedelta)
# ============================================================

# timedelta: 天、秒、微秒
delta = timedelta(days=7, hours=3, minutes=30)
print(delta)  # 7 days, 3:30:00

# 加减
tomorrow = date.today() + timedelta(days=1)
yesterday = date.today() - timedelta(days=1)
next_week = datetime.now() + timedelta(weeks=1)
two_hours_later = datetime.now() + timedelta(hours=2)

# 计算两个日期的差
d1 = date(2025, 1, 15)
d2 = date(2025, 3, 20)
diff = d2 - d1  # datetime.timedelta
print(diff.days)  # 64

dt1 = datetime(2025, 1, 15, 10, 0)
dt2 = datetime(2025, 1, 15, 14, 30)
diff = dt2 - dt1
print(diff.total_seconds())  # 16200.0(4.5 小时 = 16200 秒)

# ============================================================
# 时区转换
# ============================================================

# 创建带时区的时间
beijing = datetime(2025, 1, 15, 10, 30, tzinfo=ZoneInfo("Asia/Shanghai"))

# 转换到其他时区
tokyo = beijing.astimezone(ZoneInfo("Asia/Tokyo"))
print(tokyo)  # 2025-01-15 11:30:00+09:00

new_york = beijing.astimezone(ZoneInfo("America/New_York"))
print(new_york)  # 2025-01-14 21:30:00-05:00

utc = beijing.astimezone(timezone.utc)
print(utc)  # 2025-01-15 02:30:00+00:00

# ============================================================
# 格式化与解析
# ============================================================

now = datetime.now(ZoneInfo("Asia/Shanghai"))

# strftime: datetime → 字符串
print(now.strftime("%Y-%m-%d %H:%M:%S"))       # 2025-01-15 10:30:00
print(now.strftime("%Y年%m月%d日"))              # 2025年01月15日
print(now.strftime("%A, %B %d, %Y"))            # Wednesday, January 15, 2025
print(now.strftime("%Y%m%dT%H%M%SZ"))           # ISO 8601(需手动加 Z)

# 常用格式码:
# %Y  四位年份    %m  月份(01-12)  %d  日(01-31)
# %H  小时(00-23) %M  分钟(00-59)  %S  秒(00-59)
# %f  微秒        %A  星期全名     %a  星期缩写
# %B  月份全名    %b  月份缩写     %Z  时区名
# %z  UTC偏移     %j  一年中第几天  %W  一年中第几周

# strptime: 字符串 → datetime
s = "2025-01-15 10:30:00"
parsed = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
print(parsed)  # 2025-01-15 10:30:00(naive,无时区)

# 解析带时区的字符串
s = "2025-01-15 10:30:00+08:00"
parsed = datetime.strptime(s, "%Y-%m-%d %H:%M:%S%z")
print(parsed)  # 2025-01-15 10:30:00+08:00(aware)

# ISO 格式(推荐)
iso_str = now.isoformat()            # 2025-01-15T10:30:00.123456+08:00
parsed = datetime.fromisoformat(iso_str)  # 自动解析

# ============================================================
# 时间戳(Unix timestamp)
# ============================================================

import time

# datetime → 时间戳
ts = datetime.now(timezone.utc).timestamp()  # 1705282200.123456

# 时间戳 → datetime
dt = datetime.fromtimestamp(ts, tz=ZoneInfo("Asia/Shanghai"))

# 当前时间戳
print(time.time())  # 1705282200.123456

# ============================================================
# 常见场景
# ============================================================

# 判断是否工作日
import calendar
def is_workday(d: date) -> bool:
    return d.weekday() < 5  # 周一=0, 周五=4

# 获取某月的天数
_, days_in_month = calendar.monthrange(2025, 2)  # 28(2025年2月)
_, days_in_month = calendar.monthrange(2024, 2)  # 29(闰年)

# 计算年龄
def calculate_age(birth_date: date) -> int:
    today = date.today()
    return today.year - birth_date.year - (
        (today.month, today.day) < (birth_date.month, birth_date.day)
    )

age = calculate_age(date(1990, 6, 15))
print(age)  # 34(假设今天是 2025-01-15)

核心差异

特性Java Time APIPython datetime
当前时间LocalDateTime.now()datetime.now()
时区支持ZonedDateTimedatetime + ZoneInfo
时区转换.withZoneSameInstant().astimezone()
时间差Duration / Periodtimedelta(统一)
格式化DateTimeFormatterstrftime()
解析LocalDateTime.parse()strptime() / fromisoformat()
ISO 格式.toString().isoformat() / fromisoformat()
naive vs aware编译期区分类型运行时属性 tzinfo

常见陷阱

# 陷阱 1: naive vs aware datetime 不能直接比较或运算!
naive = datetime(2025, 1, 15, 10, 0)
aware = datetime(2025, 1, 15, 10, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
# naive == aware  # TypeError!
# naive < aware   # TypeError!

# 解决: 统一为 aware
naive = naive.replace(tzinfo=ZoneInfo("Asia/Shanghai"))

# 陷阱 2: datetime.now() 返回的是本地时区,不是 UTC
datetime.now()                    # 本地时区(naive)
datetime.now(timezone.utc)        # UTC(aware)— 推荐
datetime.now(ZoneInfo("Asia/Shanghai"))  # 指定时区(aware)

# 陷阱 3: strftime 的格式码和 Java 的 DateTimeFormatter 不同
# Java:   yyyy-MM-dd HH:mm:ss
# Python: %Y-%m-%d %H:%M:%S

# 陷阱 4: timedelta 不支持年/月(因为它们的长度不固定)
# timedelta(days=365)  # 不精确,闰年有 366 天
# 需要用 dateutil.relativedelta 处理年月
from dateutil.relativedelta import relativedelta  # pip install python-dateutil
next_month = date.today() + relativedelta(months=1)

# 陷阱 5: fromisoformat 在 3.10 和 3.11+ 的行为不同
# Python 3.11+ 支持更多 ISO 8601 格式
# Python 3.10 只支持 datetime.isoformat() 输出的格式

# ⚠️ 陷阱: datetime.now() vs datetime.utcnow()
# datetime.utcnow() 在 Python 3.12 中已被废弃!
from datetime import datetime, timezone

# 错误(已废弃):
# now = datetime.utcnow()  # DeprecationWarning in 3.12+

# 正确: 使用时区感知的 datetime
now = datetime.now(timezone.utc)
print(f"UTC now: {now.isoformat()}")

# ⚠️ 陷阱: 朴素 datetime 和时区感知 datetime 不能比较
naive = datetime(2024, 1, 1)  # 朴素(无时区)
aware = datetime(2024, 1, 1, tzinfo=timezone.utc)  # 时区感知
# naive == aware  # TypeError! Python 3.x 中不允许

# 正确: 统一使用时区感知 datetime

何时使用

  • datetime: 所有日期时间操作的基础。服务端代码永远用 aware datetime(带时区)。
  • date: 只需要日期(生日、截止日期等)。
  • timedelta: 时间加减、计算间隔。
  • ZoneInfo (3.9+): 时区处理,替代已废弃的 pytz
  • time.time(): 需要高精度时间戳时(性能测量、缓存过期)。
  • 第三方库: 复杂日期逻辑(工作日计算、 recurrence rule)用 python-dateutil;大量日期数据处理用 pendulumarrow

总结: 标准库选型速查

场景Python 标准库Java/Kotlin 对应
路径操作pathlibjava.nio.file.Path
环境变量os.environSystem.getenv()
命令行参数argparseJCommander / picocli
JSONjsonJackson / Gson
CSVcsvApache Commons CSV
TOMLtomllib (3.11+)无标准方案
正则表达式rejava.util.regex
日志loggingSLF4J / Logback
嵌入式数据库sqlite3JDBC + SQLite 驱动
日期时间datetime + zoneinfojava.time
文件复制/删除shutilFiles.copy/move/delete
临时文件tempfileFiles.createTempFile()

核心原则: Python 标准库覆盖了工业开发 80% 的需求。先查标准库,再考虑第三方包。pip install 之前,先问自己:标准库能不能做?