CS1602计算导论
Lab 12Part 5 健壮性与真实世界AI Level 1

真实文本数据处理

这次实验给你一份故意做脏的数据——有编码问题、有格式不一致的行、有缺失字段。你的任务是把它处理干净,并且不崩溃。

截止:小作业 4 · 12 月 8 日 周二 23:59
本页目录

本次目标

  • 用 with 和显式编码安全地读写文件
  • 处理格式不规整的真实数据,跳过坏行而不崩溃
  • 用正则从半结构化文本里提取字段
  • 让随机实验可复现

准备数据

先运行这段代码生成本次实验用的文件:

import random
from pathlib import Path

random.seed(1602)          # 固定种子,保证所有人拿到同一份数据

Path("/tmp/lab12").mkdir(exist_ok=True)

names = ["alice", "bob", "carol", "dave", "eve"]
levels = ["INFO", "WARN", "ERROR"]
lines = []
for i in range(200):
    ts = f"2026-11-{random.randint(1, 28):02d} {random.randint(0,23):02d}:{random.randint(0,59):02d}:{random.randint(0,59):02d}"
    lvl = random.choices(levels, weights=[6, 3, 1])[0]
    user = random.choice(names)
    ms = random.randint(1, 3000)
    lines.append(f"{ts} {lvl} user={user} action=login duration={ms}ms")

# 故意插入 12 行坏数据
for _ in range(12):
    bad = random.choice([
        "",                                     # 空行
        "### 这是一行注释",                       # 注释
        "2026-11-05 时间戳后面什么都没有",          # 字段缺失
        "2026-11-06 12:00:00 DEBUG user= action=login duration=abc",   # 用户名空、时长非数字
    ])
    lines.insert(random.randint(0, len(lines)), bad)

Path("/tmp/lab12/app.log").write_text("\n".join(lines), encoding="utf-8")
print("已生成 /tmp/lab12/app.log,共", len(lines), "行")

题目

12-1 编码实验

写一段代码回答下面三个问题,把代码和输出都放进提交说明:

  1. "计算导论" 用 UTF-8 编码是几个字节?用 GBK 是几个字节?为什么不一样?
  2. 把 UTF-8 编码的 "你好" 用 latin-1 解码,会得到什么?把结果再用 latin-1 编码回去,能还原成原来的字节吗?
  3. 把 UTF-8 编码的 "你好" 用 ascii 解码会怎样?和第 2 问的失败方式有什么不同?

12-2 安全地读文件

实现:

def read_lines(path: str) -> list[str]:
    """读入文件的所有行,去掉行尾换行符。
    文件不存在时抛出 FileNotFoundError,并附上有用的信息。
    """

要求:用 with、显式 encoding="utf-8"、不要用 readlines() 之后再处理(直接逐行遍历)。

在提交说明里回答:如果这个文件有 10GB,你的实现会有什么问题?

12-3 解析日志

实现:

def parse_line(line: str) -> dict[str, str] | None:
    """解析一行日志,返回 {date, time, level, user, action, duration} 的字典。
    无法解析时返回 None。
    """

日志的正常格式是:

2026-11-05 14:23:45 INFO user=alice action=login duration=1234ms

用正则加命名分组实现。duration 去掉 ms 后缀,作为字符串返回即可。

然后:

def load_log(path: str) -> tuple[list[dict[str, str]], list[tuple[int, str]]]:
    """返回 (成功解析的记录列表, [(行号, 原始内容)] 的坏行列表)。"""

在提交说明里报告:总共多少行?成功解析多少行?坏行有哪些?

12-4 统计

基于 12-3 的结果,实现三个统计函数:

def count_by_level(records: list[dict[str, str]]) -> dict[str, int]: ...
def busiest_user(records: list[dict[str, str]]) -> str: ...
def average_duration(records: list[dict[str, str]]) -> float: ...

average_duration 要注意:duration 是字符串,转换可能失败。失败的记录该算进平均还是跳过? 你决定,但要在提交说明里说明。

12-5 输出成 CSV

把 12-3 解析出的记录写成一个 CSV 文件 /tmp/lab12/parsed.csv,含表头。

要求用 csv 模块(不是手写 join(",")),并且加 newline=""。

然后再用 csv.DictReader 读回来,验证记录数一致。把验证代码和输出贴进提交说明。

12-6 正则提取

给定下面这段文本,写正则提取三类信息:

text = """
联系方式:张三 zhangsan@sjtu.edu.cn 13812345678
李四的邮箱是 lisi.wang@example.com,电话 15900001111
王五:wangwu@qq.com / 13700002222
无效的:not-an-email@ 和 123456
"""
  1. 所有邮箱地址
  2. 所有 11 位手机号(1 开头)
  3. 所有邮箱的域名部分(@ 后面的)

在提交说明里写出你的三个正则,并说明你怎么确认它没有误匹配”无效的”那一行。

12-7 可复现的模拟

写一个函数模拟”生日悖论”:

def birthday_collision(n_people: int, trials: int, seed: int | None = None) -> float:
    """模拟 trials 次,每次随机给 n_people 个人分配生日(1-365),
    返回出现至少两人同生日的比例。
    """

用它算出 n_people 从 20 到 30 时的碰撞概率,找出概率首次超过 50% 的人数。

要求:同一个 seed 必须给出完全相同的结果。 在提交说明里贴两次运行的输出证明这一点。

提交前自查

  • 12-1 三个问题都有代码和输出
  • 12-2 用了 with 和显式编码,回答了 10GB 的问题
  • 12-3 用正则命名分组,报告了坏行
  • 12-3 程序在整份脏数据上没有崩溃
  • 12-4 说明了失败记录的处理策略
  • 12-5 用了 csv 模块和 newline=""
  • 12-6 三个正则都有,并说明了怎么排除误匹配
  • 12-7 贴了两次运行结果证明可复现
  • AI 使用声明写了
  • 个人项目 B 已提交