重影卷轴
Hoppy 走到档案馆试炼的第二道门,看到桌上摆着一卷让人头疼的抄录记录:同一批卷轴被记了不止一次,像是留下了几层重影。档案官轻声提醒他,最危险的不是“看起来有点乱”,而是这些重复行会悄悄把后面的统计带偏。
所以这节课的重点不是再学一个新容器,而是开始更稳地做判断:什么该拿来记“见过没有”,什么该保留第一次出现的顺序,什么该负责做次数统计。Chapter 6 到这里,会更像一次真正的小任务,而不是单点按钮练习。
先想清楚:你到底想保留什么
遇到重复记录时,常见的失误不是不会写循环,而是把所有东西都塞进同一种结构里。可在这种任务里,不同结构负责的事情其实不一样:set 很适合回答“这个名字我见过吗”,list 适合保留第一次出现的顺序,dict 则适合把“位置 -> 次数”这种统计结果收起来。
sample_titles = ["Moon Thread", "Ember Ink", "Moon Thread", "Fern Seal"]
seen_titles = set()
ordered_unique = []
for title in sample_titles:
if title not in seen_titles:
seen_titles.add(title)
ordered_unique.append(title)
print(ordered_unique)
这个玩具例子只演示一件事:set 不是拿来展示顺序的,而是帮你快速记住“看过了”;真正按第一次出现顺序留下来的,是 ordered_unique 这个 list。等会儿进入 starter 时,你还会再加上一份 dict 统计,三种结构就会一起工作。
今天的任务:把重影卷轴收成唯一档案
starter 已经帮你读好了 duplicate_scroll.txt 和 shelf_map.json。你要先完成 clean_line(raw_line) 和 build_record(cleaned_line),再把步骤组织起来:做出 cleaned_lines、all_records,然后用 seen_scrolls 和 unique_records 去掉重影,最后用 wing_counts 和 archive_summary 收出统计结果。
每一行都带着同样的噪声:前缀 "## "、被 "~" 挤在一起的名字,以及尾巴上的 "??"。先把这些动作收进 clean_line(raw_line)。
在 build_record(cleaned_line) 里,先拆出 scroll_name、shelf_code 和 status,再用 shelf_map[shelf_code] 补上 wing_name 和 keeper_name。
用 set 记录哪些卷轴名已经见过,用 list 保留第一次出现的完整记录顺序,再用 dict 统计每个档案翼区留下了多少份唯一卷轴。这里的重点不是代码越花越好,而是结构职责要清楚。
最后整理 archive_summary,让它至少告诉你:原始行数、唯一卷轴数、删掉了多少重影行、唯一且 ready 的卷轴有多少,以及 wing_counts。这就是一份很典型的中型整理任务收口。
我们不是在追求最炫的去重写法,也不是要把任务做成复杂优化题。真正想练的是:你能不能在一个稍微完整的任务里,把 set、list、dict 放到它们各自最合适的位置上。
参考答案点击展开点击收起
import json
with open("duplicate_scroll.txt", "r", encoding="utf-8") as file:
scroll_text = file.read().strip()
with open("shelf_map.json", "r", encoding="utf-8") as file:
shelf_map = json.load(file)
print("Duplicate scroll text:")
print(scroll_text)
print("Shelf map:", shelf_map)
scroll_lines = scroll_text.splitlines()
print("Scroll lines:", scroll_lines)
def clean_line(raw_line):
return raw_line.strip().replace("## ", "").replace("~", " ").replace("??", "")
def build_record(cleaned_line):
parts = cleaned_line.split(" | ")
scroll_name = parts[0].split("=")[1]
shelf_code = parts[1].split("=")[1]
status = parts[2].split("=")[1]
shelf_record = shelf_map[shelf_code]
return {
"scroll_name": scroll_name,
"shelf_code": shelf_code,
"status": status,
"wing_name": shelf_record["wing_name"],
"keeper_name": shelf_record["keeper_name"],
}
cleaned_lines = []
for raw_line in scroll_lines:
cleaned_lines.append(clean_line(raw_line))
all_records = []
for cleaned_line in cleaned_lines:
all_records.append(build_record(cleaned_line))
seen_scrolls = set()
unique_records = []
for record in all_records:
scroll_name = record["scroll_name"]
if scroll_name not in seen_scrolls:
seen_scrolls.add(scroll_name)
unique_records.append(record)
wing_counts = {}
for record in unique_records:
wing_name = record["wing_name"]
if wing_name not in wing_counts:
wing_counts[wing_name] = 0
wing_counts[wing_name] += 1
ready_unique_count = 0
for record in unique_records:
if record["status"] == "ready":
ready_unique_count += 1
archive_summary = {
"raw_row_count": len(all_records),
"unique_scroll_count": len(unique_records),
"duplicate_row_count": len(all_records) - len(unique_records),
"ready_unique_count": ready_unique_count,
"wing_counts": wing_counts,
}
print("Cleaned lines:", cleaned_lines)
print("All records:", all_records)
print("Seen scrolls:", seen_scrolls)
print("Unique records:", unique_records)
print("Wing counts:", wing_counts)
print("Archive summary:", archive_summary)高级技巧想更进一步?点击展开点击收起
这节课最值得带走的,不是“去重”两个字本身,而是你开始会分工:set 管见过没有,list 管保留顺序,dict 管统计结果。结构一旦选对,步骤就会自然稳定下来。
下一课会换一种方式检验这些能力:不是让你从零搭,而是让你修一份出问题的小脚本。到那时,你会更明显地感受到,真正学会结构选择的人,也更容易看懂并修正别人的数据处理流程。