88
社区成员
发帖
与我相关
我的任务
分享学号:102400416
| 这个作业属于哪个课程 | https://bbs.csdn.net/forums/2601_CS_SE_FZU |
|---|---|
| 这个作业要求在哪里 | https://bbs.csdn.net/topics/620526318 |
| 这个作业的目标 | 以 AI(Claude Code)为结对伙伴,完成一个「计算机视觉学术热点追踪工具」,并如实记录人机协作的完整过程 |
| 其他参考文献 | arXiv API 官方文档、networkx 文档、matplotlib 文档 |
本次项目使用 Claude 作为人机结对编程辅助工具,全程参与项目开发与文档撰写。
以自然语言对话形式进行结对协作,通过精准提示词提出开发需求、修改要求与文档规范,由 Claude 输出初稿,我负责审核、校验、修改、最终决策。
Claude 负责快速产出代码、文档初稿与方案参考,我负责需求判断、代码审查、逻辑校验、迭代修改与最终负责,实现人机结对开发模式。
N(Need,需求)
计算机视觉(cs.CV)是近几年论文数量爆发式增长的领域(arXiv 上从 2016 年的约 3600 篇增长到 2025 年的约 35000 篇)。研究者、学生在选方向、读论文之前,常常需要快速回答两个问题:「这个领域最近在火什么?」「哪些子方向在上升、哪些在退潮?」。逐篇翻阅海量论文显然不现实,需要一个能自动汇总趋势的工具。
A(Approach,做法)
用 arXiv 官方开放 API 抓取 cs.CV 的论文元数据(标题、摘要、年份),然后:
B(Benefit,好处)
C(Competitors,竞品)
Semantic Scholar、Connected Papers 等工具也能做相似的事,但它们更重、偏通用,且可视化不可定制。本工具聚焦单一领域、代码开源可改,适合教学与二次开发。
D(Delivery,交付)
Python 脚本 + 生成的三类产出物(CSV、共现网络 PNG、两个 GIF),以及本报告。源码托管在代码仓库(链接见「成品展示」)。
系统信息架构(数据流)
flowchart LR
A[arXiv API] -->|fetch| B[crawler.py<br/>论文爬虫]
B -->|papers.csv| C[keyword_graph.py<br/>关键词图谱]
B -->|yearly_counts.csv| D[trend_animation.py<br/>热度动图]
C -->|keyword_graph.png| E[成品]
D -->|papers_per_year.gif<br/>keyword_race.gif| E
原型设计说明
本平台的原型分两个层次:
程序成品形态(命令行 + 静态产出物):python main.py 一键完成全流程,产出 5 个文件——① 论文数据表(CSV);② 关键词共现网络图;③ 两张热度动图。
墨刀高保真 Web 原型:见下文「墨刀原型设计」。
选用墨刀(MockingBot)这一专用原型设计工具,为平台设计了 5 个页面。
制作过程:
prototype_prompt.md),明确全局设计规范——深科技蓝主色 #2C6EF2、卡片白底圆角、顶部 5 菜单导航栏;

5 个页面一览:
| 页面 | 主要内容 |
|---|---|
| 首页 · 热门方向总览 | 3 个统计卡片(累计论文 1200 篇 / 年份 2016–2025 / 热门方向 40 个);左侧 Top 10 热门方向榜单;右侧关键词共现图谱(40 节点、173 条边,节点大小=词频、颜色=社区) |
| 热度走势对比页 | 左「逐年发文量增长」柱状动图(3631 → 34987);右「热门关键词条形图竞赛」;控制条(年份区间、播放/暂停、速度) |
| 论文列表管理页 | 搜索/筛选工具栏 + 数据表格(ID/标题/作者/年份/分类/操作),支持新增、编辑(弹窗表单)、删除(二次确认)、查询、分页 |
| 论文爬取/导入页 | 左「在线爬取」配置(年份区间、分类、采样数)+ 进度条 + 实时日志;右「本地导入」上传 CSV |
| 详情/关于页 | 项目简介、数据来源(arXiv API)、技术栈、功能模块、人机协作说明 |
页面流转:导航栏 5 菜单互通;首页图谱点击关键词 → 跳转热度走势页;爬取完成 → 自动跳转论文列表。
**原型截图:





prototype_prompt.md;另附可交互 HTML 原型 prototype/index.html(浏览器直接打开预览)。说明:以下是我与 AI 结对伙伴(Claude Code)在本次作业中的真实协作记录。关键提示词以代码块给出,AI 输出用摘要描述,并说明我采纳/拒绝的理由与修改。
我的提示词(核心):
帮我做一个「论文爬虫 + 关键词图谱 + 热度走势动图」的项目……
AI 输出摘要:AI 没有立刻动手写代码,而是先确认了两个关键决策——追踪的主题领域、论文数据源,并给出了每个选项的可行性(如知网反爬严格、arXiv 有免费开放 API)。
我的修改与理由:
AI 输出摘要:给出三模块划分(爬虫 / 关键词图谱 / 热度动图)与技术栈(requests + 标准库 xml 解析 + networkx + matplotlib),并强调关键词抽取用「停用词过滤 + 词频 + bigram 合并」的轻量方案而非重型 NLP 库。
我的修改与理由:
AI 输出摘要:检测到 Python 3.14.4,缺少依赖库,用 pip 安装了 requests、networkx、matplotlib、pandas、pillow。
我的修改与理由:直接采纳,无修改。
AI 输出摘要:一次性生成了 crawler.py、keyword_graph.py、trend_animation.py、main.py,并对每个函数写了中文 docstring 说明设计思路。
我的修改与理由:先做小规模测试(2 年 × 30 篇)验证全流程能跑通,再决定是否扩大规模——这是我自己加的「先小后大」的验证策略。
现象:完整运行时,抓到 2021 年时抛出 ReadTimeout,程序直接崩溃,且之前抓取的 5 年数据没有落盘。
AI 输出摘要:AI 定位为「缺重试机制 + 未增量落盘」,并修改了 fetch_page(加指数退避重试、超时 30s→60s)和 crawl(每抓完一年就写一次 CSV、某年失败只跳过不影响整体)。
我的修改与理由:
现象:重新出图后我发现,「逐年发文量」柱状图每个年份都是 120(因为每年均匀采样 120 篇),根本体现不出热度增长趋势,违背了「热度走势」的本意。
AI 输出摘要:AI 承认这是设计缺陷,提出用 arXiv 返回的 totalResults 字段直接读取每年「真实发文总数」(一次请求即可),而不是用采样数。
我的修改与理由:
现象:在 Windows 终端运行 python main.py 时,打印的中文日志(如 [crawl] 2016: 120 篇 中的「篇」字)显示成一串乱码,影响可读性。
AI 输出摘要:AI 定位为 Windows 终端默认编码(GBK)与 Python 输出 UTF-8 不一致,在入口处加 sys.stdout.reconfigure(encoding="utf-8") 修复。
我的修改与理由:
现象:我为 keyword_graph.py 补充了单元测试,运行后测试失败——本该被合并成 "object detection" 的 bigram 关键词,在结果里一个都没有。
我的定位:我逐行读代码找到根因——extract_keywords 里 solid 集合存的是元组(如 ("object", "detection")),但合并后的关键词是字符串("object detection"),最后的过滤条件 m in solid 拿字符串去和元组集合匹配,永远匹配不上,于是所有合并出的 bigram 都被过滤掉了。
AI 输出摘要:我向 AI 指出这个问题后,AI 确认是逻辑错误,并把过滤条件改成「含空格的字符串就是已合并的 bigram(" " in m)」,不含空格的单词才按词频阈值过滤。
我的修改与理由:
这里是与ai进行细节讨论的截图

crawler.py):用 requests 调 arXiv API,标准库解析 Atom XML,按年份抓取;keyword_graph.py):停用词过滤 + 词频统计 + bigram 合并抽取关键词,构建共现网络,networkx + matplotlib 出图;trend_animation.py):matplotlib.animation 逐帧更新,pillow 保存 GIF。这是本次作业最想强调的案例,包含四次「发现 AI 代码问题并纠正」:
ReadTimeout 直接崩溃,且不落盘导致数据丢失。我复现问题后,AI 补上重试 + 增量落盘。totalResults 真实发文量修复。sys.stdout.reconfigure(encoding="utf-8") 修复。keyword_graph.py 写单元测试时,测试失败暴露出一个隐蔽缺陷——solid 集合存的是元组、过滤时却拿字符串做 m in solid 匹配,导致所有合并出的 bigram(如 "object detection")都被丢弃。这是通过单元测试主动发现并纠正的 AI 代码错误。关键收获:AI 生成的代码不是「一次到位」的,需要人工 review、实际运行、质疑结果(甚至写测试去验证),才能发现并纠正其中的缺陷——这正是结对编程中「人」不可替代的价值。
按照作业要求,如实声明代码来源与我的理解程度。
| 文件 / 代码 | 来源 | 我是否理解其设计思路 |
|---|---|---|
crawler.py 整体框架、解析逻辑 | AI 生成 | 是(能解释 arXiv API 参数、Atom XML 解析、重试机制) |
crawler.py 的容错改进(重试/落盘) | AI 提出,我 review 后采纳 | 是 |
keyword_graph.py 关键词抽取与建图 | AI 生成 | 是(能解释停用词/bigram/共现的定义) |
trend_animation.py 动图逻辑 | AI 生成 | 是(能解释 FuncAnimation 逐帧更新的原理) |
main.py 参数与流程编排 | AI 生成 | 是 |
| 需求澄清、选题、数据源决策 | 我主导 | — |
| 发现趋势平线缺陷、bigram 合并缺陷 | 我主动发现(后者通过单元测试) | — |
运行 python main.py 后得到以下成品:
| 成品 | 说明 |
|---|---|
papers.csv | 1200 篇 cs.CV 论文元数据(2016–2025,每年 120 篇采样) |
yearly_counts.csv | 每年真实发文总数(3631 → 34987,近 10 倍增长) |
keyword_graph.png | 关键词共现网络图(40 个关键词、173 条边,按社区着色) |
papers_per_year.gif | 逐年发文量增长柱状动图(10 帧) |
keyword_race.gif | 历年热门关键词条形图竞赛(10 帧) |



讨论主要围绕三个问题展开,方式是我提问 / 质疑、AI 解释 / 修改:
整个讨论是「我提出需求与质疑 → AI 提出方案 → 我 review 并反馈 → AI 修改」的循环。
crawler.py 按年份构造 submittedDate 区间查询,翻页抓取,解析 Atom XML 得到标题/摘要/年份等字段;crawl_counts 读 totalResults 得到真实发文量。keyword_graph.py 用 extract_keywords 抽取关键词(含 bigram 合并),build_cooccurrence 统计两两共现,build_graph 建网络。trend_animation.py 用 FuncAnimation 逐帧生成两张动图;main.py 串联全流程并提供命令行参数。--no-crawl 复用已有数据,保证流程可断点续跑。项目按「数据获取 → 图谱分析 → 趋势可视化」三层拆成 4 个模块 + 1 组单元测试。下面分模块说明职责与关键设计点,并选录最能体现核心逻辑的关键代码(完整代码见仓库,均有中文注释)。
crawler.py职责:通过 arXiv 官方 API 抓取 cs.CV 论文元数据(标题/摘要/年份/作者/分类)存为 CSV,并抓取每年真实发文总数。
关键设计点:
REQUEST_DELAY),遵守 arXiv 限速要求;fetch_page 失败后按 5s、10s 退避重试,最多 3 次(对应迭代 5 的 ReadTimeout 崩溃);crawl 每抓完一年就写一次 CSV,某年失败只跳过不拖垮整体,中途崩溃也不丢已抓数据;fetch_year_count 只读 opensearch:totalResults(一次请求),得到每年真实总数(对应迭代 6 的「趋势平线」缺陷)。关键代码(重试退避):
def fetch_page(search_query, start=0, max_results=100,
sort_by="submittedDate", sort_order="ascending",
retries=3, timeout=60):
"""抓取一页结果,遇到瞬时网络错误会自动重试(退避 5s、10s)。"""
params = {"search_query": search_query, "start": start,
"max_results": max_results, "sortBy": sort_by,
"sortOrder": sort_order}
for attempt in range(retries):
try:
resp = requests.get(BASE_URL, params=params, timeout=timeout)
resp.raise_for_status()
root = ET.fromstring(resp.text)
return [parse_entry(e) for e in root.findall("a:entry", ATOM_NS)]
except (requests.exceptions.RequestException, ET.ParseError) as exc:
if attempt == retries - 1:
raise
wait = 5 * (attempt + 1) # 指数退避
print(f"[crawl] 请求失败({exc.__class__.__name__}),{wait}s 后重试...")
time.sleep(wait)
return []
关键代码(真实发文量):
def fetch_year_count(year, category="cs.CV", retries=3, timeout=60):
"""查询某一年论文的真实总数(读 opensearch:totalResults,一次请求即可)。"""
query = f"cat:{category} AND submittedDate:[{year}01010000 TO {year}12312359]"
params = {"search_query": query, "start": 0, "max_results": 1}
for attempt in range(retries):
try:
resp = requests.get(BASE_URL, params=params, timeout=timeout)
resp.raise_for_status()
root = ET.fromstring(resp.text)
node = root.find("o:totalResults",
{"a": ATOM_NS["a"], "o": OPENSEARCH_NS["o"]})
return int(node.text) if node is not None else 0
except (requests.exceptions.RequestException, ET.ParseError) as exc:
if attempt == retries - 1:
raise
time.sleep(5 * (attempt + 1))
return 0
keyword_graph.py职责:从论文标题抽取关键词,统计两两共现,构建 networkx 网络图并保存 PNG。
关键设计点:
关键代码:
def extract_keywords(titles, stopwords=STOPWORDS, min_freq=3):
"""从标题列表中抽取关键词,返回关键词列表、单词词频、二元组词频。"""
tokenized = [
[w for w in tokenize(t) if w not in stopwords and len(w) > 2]
for t in titles
]
word_freq = Counter(w for toks in tokenized for w in toks)
bigram_freq = Counter()
for toks in tokenized:
for a, b in zip(toks, toks[1:]):
bigram_freq[(a, b)] += 1
# 词频达到阈值的二元组,才认为是稳定搭配,可合并成关键词
solid = {bg for bg, c in bigram_freq.items() if c >= min_freq}
keywords_per_title = []
for toks in tokenized:
merged = []
i = 0
while i < len(toks):
if i + 1 < len(toks) and (toks[i], toks[i + 1]) in solid:
merged.append(toks[i] + " " + toks[i + 1])
i += 2
else:
merged.append(toks[i])
i += 1
# 只保留「高频单词」或「已合并的 bigram」:
# 含空格的字符串是已合并的 bigram;不含空格的单词按词频阈值过滤。
merged = [m for m in merged
if (" " in m) or word_freq.get(m, 0) >= min_freq]
keywords_per_title.append(merged)
return keywords_per_title, word_freq, bigram_freq
trend_animation.py职责:用 matplotlib.animation 生成两个 GIF——逐年发文量增长图、热门关键词条形图竞赛。
关键设计点:
FuncAnimation + update(i) 逐帧画第 i 年,pillow 保存 GIF;yearly_counts.csv 的真实发文总数,逐年累加呈现近 10 倍增长;关键代码:
def animate_keyword_race(df, out_gif="keyword_race.gif", top_k=10,
fps=2, min_freq=2):
"""动画 2:历年热门关键词的条形图竞赛。"""
years = sorted(df["year"].unique())
per_year = {}
for y in years:
titles = df[df["year"] == y]["title"].dropna().tolist()
kw_per_title, _, _ = extract_keywords(titles, min_freq=min_freq)
per_year[y] = Counter(k for kw in kw_per_title for k in kw)
pool = set()
for y in years:
pool.update(k for k, _ in per_year[y].most_common(top_k))
cmap = plt.cm.tab20
colors = {k: cmap(i % 20) for i, k in enumerate(sorted(pool))} # 统一配色
global_max = max(max(per_year[y].values(), default=0) for y in years) or 1
fig, ax = plt.subplots(figsize=(10, 6))
def update(i):
ax.clear()
y = years[i]
top = per_year[y].most_common(top_k)[::-1] # 倒序,最大的在最上面
labels = [t[0] for t in top]
vals = [t[1] for t in top]
ax.barh(labels, vals, color=[colors[l] for l in labels])
for idx, v in enumerate(vals):
ax.text(v, idx, f" {v}", va="center", fontsize=9)
ax.set_xlim(0, global_max * 1.15) # 固定横轴,避免跳动
ax.set_xlabel("Frequency")
ax.set_title(f"Top {top_k} Keywords in {y} (cs.CV)")
anim = animation.FuncAnimation(fig, update, frames=len(years),
interval=800, repeat=True)
anim.save(out_gif, writer="pillow", fps=fps)
plt.close()
main.py / config.py职责:命令行入口串联三个模块;config.py 集中管理抓取范围、采样数、阈值等参数。
关键设计点:
fix_console_encoding 用 sys.stdout.reconfigure(encoding="utf-8") 解决 Windows GBK 终端中文乱码(对应迭代 7);--years、--max-per-year、--no-crawl 支持不同规模和断点续跑;关键代码:
def fix_console_encoding():
"""让中文输出在各类终端(含 GBK 的 Windows 终端)都正常显示。"""
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except Exception:
pass
def main():
fix_console_encoding()
parser = argparse.ArgumentParser(description="CV 学术热点追踪工具")
parser.add_argument("--years", default=config.DEFAULT_YEARS)
parser.add_argument("--max-per-year", type=int, default=config.MAX_PER_YEAR)
parser.add_argument("--no-crawl", action="store_true")
args = parser.parse_args()
if args.no_crawl:
print("[main] 使用已有 CSV,跳过爬虫")
else:
start, end = map(int, args.years.split("-"))
years = range(start, end + 1)
crawler.crawl(years, max_per_year=args.max_per_year,
category=config.CATEGORY, out_csv=args.csv)
crawler.crawl_counts(years, category=config.CATEGORY,
out_csv="yearly_counts.csv")
keyword_graph.run(csv_path=args.csv, out_png="keyword_graph.png",
min_freq=config.MIN_FREQ, top_n=config.TOP_N)
trend_animation.run(csv_path=args.csv, counts_csv="yearly_counts.csv")
config.py 全文:
CATEGORY = "cs.CV" # arXiv 分类
DEFAULT_YEARS = "2016-2025" # 默认抓取年份区间
MAX_PER_YEAR = 120 # 每年最多采样论文数
MIN_FREQ = 3 # 关键词最低词频
TOP_N = 40 # 图谱保留的高频关键词数
tests/职责:对不依赖网络的纯函数做单元测试,验证解析与关键词抽取的正确性。
关键设计点:
<entry>,断言版本号被去掉、空白被压平、作者/分类解析正确。关键代码:
class TestExtractKeywords(unittest.TestCase):
def test_bigram_merge(self):
titles = [
"Object Detection with Neural Networks",
"Object Detection in Images",
"Object Detection Survey",
]
kw_per_title, _, _ = extract_keywords(titles, min_freq=2)
all_kw = {k for kws in kw_per_title for k in kws}
self.assertIn("object detection", all_kw) # 相邻 3 次,应合并
self.assertNotIn("neural", all_kw) # 低频词应被过滤
class TestParseEntry(unittest.TestCase):
def test_parse_title_year_and_version(self):
xml = """<entry xmlns="http://www.w3.org/2005/Atom">
<id>http://arxiv.org/abs/2101.12345v2</id>
<title> Deep Learning for Vision </title>
<published>2021-01-15T18:59:59Z</published>
<author><name>Alice</name></author>
<category term="cs.CV"/>
</entry>"""
p = parse_entry(ET.fromstring(xml))
self.assertEqual(p["arxiv_id"], "2101.12345") # 版本号 v2 被去掉
self.assertEqual(p["title"], "Deep Learning for Vision")
self.assertEqual(p["year"], "2021")
self.assertEqual(p["primary_category"], "cs.CV")
说实话,一开始我对「人机结对编程」的理解很肤浅——以为就是把需求丢给 AI、它写完我交上去。真正做起来才发现完全不是这么回事。
第一次被触动,是趋势图出问题的时候。AI 给的代码能跑、也能出图,但「逐年发文量」的柱状图每个年份都一样高——因为每年均匀采样 120 篇,画出来是一条「平线」,根本体现不了「热度走势」。那一刻我才意识到:AI 会自信地给你一个「能跑」的答案,但「能跑」不等于「对」。如果不是我自己盯着结果、觉得「这不对吧」,这个缺陷就会一路带进作业。
从那以后我养成了两个习惯:一是 AI 生成的代码我一定逐段读一遍,能用大白话讲出每段在干嘛才放行——这也是「禁止盲抄」逼我做的;二是先跑小规模、再放大,还给核心逻辑写了单元测试,结果测试真帮我抓出了 bigram 合并被误过滤的 bug。
最大的收获不是这个工具本身,而是理解了结对编程里「人」的位置:AI 擅长快速产出、解释、甚至诚实地承认错误,但它不会替你判断「结果合不合理、方向对不对」。判断力,仍然只能由人来提供。 另外,通过逐段读代码,我也真的学会了 arXiv API 的查询方式、关键词共现网络怎么建、matplotlib 动画的原理——这些本来会是我最想跳过、却最有价值的部分。
时间单位:分钟。(以下为回顾性估计,非逐分钟精确记录)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate 估计这个任务需要多少时间 | 60 | 40 | |
| Development | 开发 | ||
| · Analysis 需求分析(包括学习新技术) | 120 | 150 | |
| · Design Spec 生成设计文档 | 90 | 60 | |
| · Design Review 设计复审 | 60 | 45 | |
| · Coding Standard 代码规范 | 30 | 20 | |
| · Design 具体设计 | 120 | 100 | |
| · Coding 具体编码 | 360 | 420 | |
| · Code Review 代码复审 | 90 | 150 | |
| · Test 测试(自我测试,修改代码,提交修改) | 150 | 240 | |
| Reporting | 报告 | ||
| · Test Report 测试报告 | 60 | 40 | |
| · Size Measurement 计算工作量 | 30 | 20 | |
| · Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 | 60 | 90 | |
| 合计 | 1230 | 1375 |
从 PSP 表格能看出,我最初对时间的预估明显偏乐观,实际耗时超出预估约 2.4 小时,主要差在三处:
如果重来一次,我会在计划阶段就把「代码复审 + 测试」的预估调高一倍,并预留一整块时间专门做「怀疑 AI 输出」的验证——因为结对编程里,人的价值恰恰不在打字,而在挑错。
https://modao.cc/ai/share/6ab51dbc75d0275ef6fda70b



