字符串与文件读写 (Strings & Files)
章节概述
C 程序员最头疼的两件事:字符串操作和文件 I/O。C 的字符串是 char[]——需要手动管理内存、用 strlen/strcpy/strcat 做操作、时刻警惕缓冲区溢出。C 的文件读写需要 fopen/fread/fwrite,出错时检查 errno。本章展示 Python 如何让这两件事变得”无痛”:字符串是内建对象(带 40+ 种方法),文件操作通过 with 上下文管理器自动关闭,pathlib 让路径拼接从字符串拼凑变成对象操作。
核心理念:C 字符串是”字节数组”,Python 字符串是”不可变 Unicode 字符序列”。C 文件 I/O 返回的是”原始字节流”,Python 文件 I/O 返回的是”带自动缓冲和编码解码的高级对象”。这六层抽象的差异,让 Python 的字符串和文件操作在 C 程序员看来几乎像魔法。
第一节:Python str —— 富文本处理
1.1 字符串创建与编码
python -c "
# 多种字符串字面量
s1 = 'single quoted'
s2 = \"double quoted\"
s3 = '''triple quoted
can span
multiple lines'''
s4 = 'unicode: 中文, emoji: '
# 原始字符串(不处理反斜杠转义)
path = r'C:\Users\name\Documents' # 类似 C 的原始字符串字面量
print('raw path:', path)
# f-string(格式化字符串,Python 3.6+)
name = 'Alice'
age = 30
print(f'{name} is {age} years old')
print(f'Next year: {age + 1}')
print(f'Hex: {255:#x}') # 支持格式说明符
"对比 C 语言:
// C: 字符串就是字节数组
char s1[] = "Hello"; // 6 字节(含 '\0')
char s2[100];
strcpy(s2, s1); // 手动复制
strcat(s2, " World"); // 手动拼接(危险!注意缓冲区大小)
// C 没有 f-string 等价物
int age = 30;
char buf[100];
snprintf(buf, sizeof(buf), "Age: %d", age); // 最接近 f-string1.2 字符串常用方法
python -c "
s = ' Hello, World! '
print('strip(): |', s.strip(), '|')
print('lstrip(): |', s.lstrip(), '|')
print('rstrip(): |', s.rstrip(), '|')
print('upper(): ', s.upper())
print('lower(): ', s.lower())
print('replace(): ', s.replace('World', 'Python'))
print('split(): ', s.strip().split(','))
print('find(): ', s.find('World')) # 返回索引,未找到返回 -1
print('index(): ', s.index('World')) # 未找到抛出 ValueError
print('count(): ', s.count('l'))
print('startswith():', s.strip().startswith('He'))
print('endswith(): ', s.strip().endswith('!'))
print('isdigit(): ', '123'.isdigit())
print('isalpha(): ', 'abc'.isalpha())
print('isalnum(): ', 'abc123'.isalnum())
"对比 C 语言等价操作:
// C: 每个操作都需要专用函数或手工实现
#include <string.h>
#include <ctype.h>
char *str = "Hello, World!";
strlen(str); // len()
strchr(str, 'W'); // find() 的简化版
strstr(str, "World"); // find() 的子串版本
strcmp(a, b); // == 运算符(但 Python 直接用 == 比较字符串内容)
// Python 的 startswith()、split()、strip() 在标准 C 库中都没有直接等价物
// 需要手动遍历实现第二节:编码与字节 —— str vs bytes
2.1 Unicode 字符串 vs 原始字节
python -c "
# str: Unicode 字符序列(内部用变长编码存储)
s = '你好 world '
print('len(s):', len(s)) # 11 个字符
print('s.encode(\"utf-8\"):', s.encode('utf-8'))
print('utf-8 bytes:', len(s.encode('utf-8'))) # 22 字节(中文每字 3 字节)
# bytes: 原始字节序列
b = s.encode('utf-8')
print('type(b):', type(b))
print('b:', b)
print('b.decode(\"utf-8\"):', b.decode('utf-8'))
# 从六进制字符串构建 bytes
hex_bytes = bytes.fromhex('48656c6c6f')
print('fromhex:', hex_bytes, '→', hex_bytes.decode('ascii'))
"C 语言的字符串处理:
// C: 字符串是 char*,编码取决于系统和源码编码
char *s = "你好"; // 源码是 UTF-8 则 s 是 UTF-8 字节序列
printf("len: %zu\n", strlen(s)); // 返回字节数而非字符数
// C 没有内建编码转换——需要 iconv 或系统 API
// iconv_open("UTF-8", "GBK"); ...2.2 编码错误处理
python -c "
# 错误字节序列
broken = b'Hello \xff World'
# strict: 抛异常
try:
broken.decode('utf-8')
except UnicodeDecodeError as e:
print(f'strict error: {e}')
# ignore: 静默跳过
print('ignore:', broken.decode('utf-8', errors='ignore'))
# replace: 用 U+FFFD 替换
print('replace:', broken.decode('utf-8', errors='replace'))
# 编码时遇到不可编码字符
s = 'café'
print('ascii ignore:', s.encode('ascii', errors='ignore'))
print('ascii replace:', s.encode('ascii', errors='replace'))
print('ascii xmlcharref:', s.encode('ascii', errors='xmlcharrefreplace'))
"2.3 字符码位操作
python -c "
# ord: 字符 → Unicode 码位
print('ord(\"A\"):', ord('A')) # 65
print('ord(\"中\"):', ord('中')) # 20013
print('ord(\"\"):', ord('')) # 128013
# chr: 码位 → 字符
print('chr(65):', chr(65))
print('chr(20013):', chr(20013))
print('chr(128013):', chr(128013))
# 十六进制表示
print('hex(ord(\"中\")):', hex(ord('中'))) # 0x4e2d
print('\"\\u4e2d\":', '\u4e2d') # Unicode 转义
"C 语言中使用
wchar_t和L"..."来处理宽字符,但不同平台的wchar_t大小不一致(Windows 2 字节,Linux 4 字节),且缺乏内建的正规化(normalization)支持。Python 的str统一了这一切。
第三节:文件读写 —— 从 fopen 到 with open
3.1 文件读取
# 创建测试文件
echo 'Line 1
Line 2
Line 3' > /tmp/test.txt
python -c "
# 基本读取方式
with open('/tmp/test.txt', 'r', encoding='utf-8') as f:
content = f.read() # 读取全部内容
print('Read all:')
print(repr(content))
"
python -c "
with open('/tmp/test.txt') as f:
lines = f.readlines() # 读取为行列表
print('readlines:', lines)
"
python -c "
with open('/tmp/test.txt') as f:
for i, line in enumerate(f, 1): # 逐行迭代(内存友好)
print(f'Line {i}: {line.rstrip()}')"对比 C 语言:
// C: 文件操作需要显式打开、检查、关闭
#include <stdio.h>
int main() {
FILE *f = fopen("test.txt", "r");
if (!f) {
perror("fopen");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("%s", line);
}
if (fclose(f) != 0) {
perror("fclose");
}
return 0;
}Python 的 with 语句在离开代码块时自动关闭文件(即使发生异常),无需手工 fclose。
3.2 文件写入
python -c "
lines = ['Hello\n', 'World\n', 'Python\n']
with open('/tmp/output.txt', 'w', encoding='utf-8') as f:
f.write('Single line\\n')
f.writelines(lines)
# 追加模式
with open('/tmp/output.txt', 'a') as f:
f.write('Appended line\\n')
"
python -c "
with open('/tmp/output.txt') as f:
print(f.read())
"文件打开模式对比:
| Python 模式 | C 模式 | 含义 |
|---|---|---|
'r' | "r" | 只读,文件必须存在 |
'w' | "w" | 只写,覆盖已有文件 |
'a' | "a" | 追加,文件不存在则创建 |
'x' | — | 排他创建,文件存在则失败 |
'r+' | "r+" | 读写,文件必须存在 |
'w+' | "w+" | 读写,覆盖已有文件 |
'b' 后缀 | "b" 后缀 | 二进制模式 |
't' 后缀 | — | 文本模式(默认) |
3.3 二进制文件与结构体读写
python -c "
import struct
# 将 C struct 格式写入文件
# 打包:类似 C 的 struct write
data = struct.pack('i f 5s', 42, 3.14, b'hello')
print('packed:', data)
with open('/tmp/struct.bin', 'wb') as f:
f.write(data)
# 解包:类似 C 的 struct read
with open('/tmp/struct.bin', 'rb') as f:
raw = f.read()
i, f_val, s = struct.unpack('i f 5s', raw)
print(f'unpacked: int={i}, float={f_val}, str={s}')
"对比 C 语言等价操作:
// C: 直接写入/读取结构体
struct Data {
int i;
float f;
char s[5];
};
struct Data d = {42, 3.14, "hello"};
fwrite(&d, sizeof(d), 1, f); // 写入
fread(&d, sizeof(d), 1, f); // 读取Python 的
struct模块专门为 C 结构体的序列化设计——格式字符串'i f 5s'直接指定了 C 级别的内存布局。当你需要与 C 程序交换二进制数据时,struct是必经之路。
第四节:pathlib —— 面向对象的文件路径
4.1 路径操作:从字符串拼凑到对象方法
python -c "
from pathlib import Path
# 创建 Path 对象
p = Path('/home/user/docs/report.pdf')
print('name: ', p.name) # report.pdf
print('stem: ', p.stem) # report(不含后缀)
print('suffix: ', p.suffix) # .pdf
print('parent: ', p.parent) # /home/user/docs
print('parts: ', p.parts) # ('/', 'home', 'user', 'docs', 'report.pdf')
print('is_absolute:', p.is_absolute()) # True
# 路径拼接(用 / 运算符!)
base = Path('/home/user')
full = base / 'docs' / 'file.txt'
print('joined: ', full)
"C 语言中的路径操作需要手动拼接字符串:
// C: 路径操作是字符串操作
char path[256];
snprintf(path, sizeof(path), "%s/%s/%s", "/home/user", "docs", "file.txt");
// 容易引入缓冲区溢出、多余斜杠、平台分隔符不一致等问题4.2 文件系统操作
python -c "
from pathlib import Path
import os
# 当前目录
print('cwd:', Path.cwd())
# 遍历目录(glob 模式)
p = Path('/tmp')
print('txt files:', list(p.glob('*.txt')))
# 检查存在性
f = Path('/etc/hosts')
print('exists:', f.exists())
print('is_file:', f.is_file())
print('is_dir:', f.is_dir())
# 读取和写入
out = Path('/tmp/pathlib_demo.txt')
out.write_text('Hello from pathlib!\\n')
print('read:', out.read_text())
# 创建目录
d = Path('/tmp/test_dir/subdir')
d.mkdir(parents=True, exist_ok=True)
print('dir exists:', d.exists())
"4.3 确保文件原子写入
python -c "
from pathlib import Path
def atomic_write(path, content):
'''原子写入:先写临时文件,再原子重命名'''
tmp = path.with_suffix(path.suffix + '.tmp')
tmp.write_text(content)
tmp.rename(path) # POSIX rename 是原子操作
atomic_write(Path('/tmp/atomic.txt'), 'Important data\\n')
print(Path('/tmp/atomic.txt').read_text())
"这个模式与 C 的
rename()调用完全等价——利用 POSIXrename的原子性来确保文件内容的一致性。Python 的优势在于不需要手动管理临时文件的fd。
练习
以下题目用于验证本章所学内容:
| 题号 | 题目 | 链接 | 涉及知识点 |
|---|---|---|---|
| P1010 | 幂次方 | https://www.luogu.com.cn/problem/P1010 | 函数、递归 |
| P1012 | 拼数 | https://www.luogu.com.cn/problem/P1012 | 字符串、排序 |
| P1014 | Catalan数 | https://www.luogu.com.cn/problem/P1014 | 数学、递推 |