88
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2601_FZU_SE · 软件工程实践 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业——与AI结对编程(顶会热词统计) |
| 这个作业的目标 | 与 AI 结对完成需求分析(NABCD)、原型设计、编程实现与云端部署,并完整记录人机协作过程;掌握 Git 分支与 Release 协作 |
| 其他参考文献 | 《构建之法(第四版)》第3、4、8章;DBLP 公开数据;OpenAlex API;ECharts 文档 |
小刚是一个电影迷,因为一部电影里的机器人能快速分类视野中的物体,对计算机视觉产生了兴趣。但 CVPR、ICCV、ECCV 三大顶会论文数量极其庞大——据会议官网,仅 CVPR 2025 就有 13008 篇投稿、2878 篇被录取——靠人工逐篇阅读来总结研究热点几乎不可行。
本次作业要求与 AI 结对,设计并实现一个"顶会热词统计平台",至少包含 5 项基础功能:论文信息爬取、论文列表管理、热门方向分析、关键词图谱、热度走势对比(动图),并部署到华为云服务器。
我做的平台:一个纯前端(HTML + CSS + 原生 JavaScript + ECharts)的顶会热词统计平台,数据持久化在浏览器 localStorage,无需后端即可运行与部署。
| 项目 | 链接 |
|---|---|
| CodeArts 仓库(学号命名) | https://devcloud.cn-north-4.huaweicloud.com/codehub/project/19c4f4b120bc43f7a93cda0c3fc8e4d2/codehub/3087051/repo |
| 代码规范 | 仓库内 codestyle.md |
| 部署访问地址 | |
| 原型发布链接 |
| 工具 | 版本 / 模型 | 主要用途 |
|---|---|---|
| DeepSeek | deepseek-v4-flash | 需求分析、信息架构设计、功能模块编码、代码审查与 Bug 排查、文档撰写 |
本次结对全程使用 DeepSeek 作为 AI 编程助手,协作方式为:我提出需求 → AI 产出方案或代码 → 我审查、实测、修改 → 再反馈给 AI 迭代。
analytics.js 统计函数、charts.js 的 ECharts 配置、crawler.js 初稿、style.css 视觉样式。| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 35 |
| • Estimate | • 估计这个任务需要多少时间 | 30 | 35 |
| Development | 开发 | 1290 | 1580 |
| • Analysis | • 需求分析(包括学习新技术) | 120 | 150 |
| • Design Spec | • 生成设计文档 | 60 | 70 |
| • Design Review | • 设计复审 | 30 | 40 |
| • Coding Standard | • 代码规范 | 40 | 45 |
| • Design | • 具体设计 | 120 | 140 |
| • Coding | • 具体编码 | 600 | 720 |
| • Code Review | • 代码复审 | 120 | 180 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 200 | 235 |
| Reporting | 报告 | 240 | 300 |
| • Test Report | • 测试报告 | 60 | 70 |
| • Size Measurement | • 计算工作量 | 30 | 25 |
| • Postmortem & Process Improvement Plan | • 事后总结,并提出过程改进计划 | 150 | 205 |
| 合计 | 1560 | 1915 |
预估与实际偏差分析:
按照《构建之法》第 8 章的 NABCD 模型,从需求(Need)、做法(Approach)、好处(Benefit)、竞争(Competitors)、推广(Delivery) 五个维度分析。
用户:以"小刚"为代表的计算机视觉方向初学者 / 低年级研究生 / 想快速了解领域现状的开发者。
痛点(我先让 AI 从不同角色视角列举,再人工筛选):
| 痛点 | 说明 | 我的判断 |
|---|---|---|
| 论文量太大 | CVPR 2025 投稿 13008 篇,人工逐篇读不可行 | ✅ 核心痛点 |
| 不知道从哪看起 | 没有"热门方向"的量化参考,容易在冷门方向浪费时间 | ✅ 核心痛点 |
| 检索工具不面向"趋势" | Google Scholar / DBLP 面向"找某篇论文",不面向"看领域趋势" | ✅ 核心痛点 |
| 想知道方向是升温还是降温 | 只看某年的榜单无法判断趋势 | ✅ 我补充的痛点 |
| 想快速跳到原文 | 需要摘要 + 原文链接一站式获取 | ✅ 基础需求 |
我的判断与取舍:AI 一开始还列了"论文笔记管理""引用关系分析"等需求。我拒绝了——理由是这些需求偏离了作业"热词统计"的核心,且实现成本高、与"帮助小刚了解热点"这一目标关联弱。需求分析要做减法。
整体做法:构建一个"采集 → 管理 → 统计 → 可视化"的流水线。
timeline 时间轴动图,把统计结果变成可交互、可播放的图表。关键技术决策:
| 决策 | 选择 | 理由 |
|---|---|---|
| 前端还是全栈 | 纯前端 | 作业明确允许;无需服务器与数据库依赖,部署成本最低,平台兼容性最好 |
| 持久化方案 | localStorage | 作业允许;刷新不丢数据,满足"论文列表管理"需求 |
| 关键词口径 | 文档频次(同篇论文内去重) | 简单、可解释、可比较;避免长摘要论文因篇幅长而被高估 |
| 关键词归一化 | 领域词典映射到 canonical | "monocular depth"与"depth estimation"必须合并,否则热点被稀释、趋势不可比 |
| 数据源 | 多源降级链 | 单一数据源不可靠(实测 DBLP 已无法在浏览器调用),降级保证功能始终可用 |
index.html 即可运行,部署到任意静态托管即可上线。source 标签(OpenAlex / CrossRef / DBLP / 内置 / 模拟),绝不把模拟数据伪装成真实爬取结果。| 竞品 | 优势 | 劣势(本平台的切入点) |
|---|---|---|
| Google Scholar | 数据全、引用统计强 | 面向"找具体论文",不提供热词排行与趋势动图;国内访问不稳定 |
| DBLP | 权威题录、有公开 API | 只给题录,不给摘要与关键词;无任何可视化;且已启用反爬,浏览器端难以直接调用 |
| Connected Papers | 引用关系图谱直观 | 图谱是论文关系而非关键词热词;无中文;无趋势对比 |
| CNKI / 万方 | 中文友好 | 面向中文文献,不覆盖 CVPR/ICCV/ECCV 三大顶会;需付费 |
| 会议官网 | 数据最权威 | 只有论文列表,无任何聚合分析 |
本平台的差异化优势:
诚实地说:本平台的数据量与权威性远不如 Google Scholar(45 篇示例数据 vs 数万篇真实论文)。它的价值在于验证"热词统计"这一产品形态,而不是替代专业检索工具。
使用 墨刀(Modao) 设计平台全部页面原型。
原型发布链接:【待填:墨刀分享链接】
| 页面 | 核心内容 | 交互逻辑 |
|---|---|---|
| 首页/热门方向总览 | Top10 热门方向柱状图 + 关键词共现图谱 + 统计卡 + 会议/年份分布 | 点击柱子或图谱节点 → 弹出相关论文列表;顶部按钮跳转爬取页/走势页 |
| 论文爬取/导入 | 单个标题输入框 + 批量文本框 + 文件导入 + 结果日志 | 提交后逐条爬取,实时输出日志与来源标签 |
| 论文列表管理 | 查询工具栏 + 论文表格 | 模糊/精确查询联动;无结果时显示"联网爬取"提示条;查看/编辑/删除弹出模态框 |
| 热度走势对比 | timeline 动图 + 单热词折线 + 统计口径说明 | 时间轴自动播放;切换关键词下拉 → 折线图重绘 |
| 了解更多 | 三大顶会背景 + 数据来源 + 统计口径 | 静态内容页 |
【截图占位:墨刀原型 5 个页面截图,建议并排 2×3 展示】
内置示例数据 ──┐
├──> localStorage(论文列表)──> 统计分析层 ──> 图表层(首页/走势页)
爬取/导入 ─────┘ ↑
│
增删改查(列表页)──> 未命中时触发联网爬取 ──┘
关键点:所有页面共享同一份 localStorage 数据源,因此在爬取页新增论文后,首页 Top10 与走势动图会自动反映新数据——这条数据流转关系是我在原型阶段就必须理清的,否则会出现"爬了论文但统计没变"的体验断裂。
顶会热词统计平台
├── 功能1:论文信息爬取
│ ├── 单个题目爬取
│ ├── 批量导入(换行/逗号/分号/顿号)
│ ├── txt 文件导入
│ └── 多源降级链(OpenAlex → CrossRef → DBLP → 模拟)
├── 功能2:论文列表管理
│ ├── 新增 / 删除 / 修改
│ ├── 精确查询(题目完全匹配)
│ ├── 模糊查询(编号/关键词/作者/会议/年份/摘要)
│ └── 未命中时联网爬取并入库
├── 功能3:热门方向分析
│ ├── Top10 热门方向(文档频次)
│ ├── 年份增长率
│ └── 涉及会议标注
├── 功能4:关键词图谱
│ ├── 关键词共现网络(力导向)
│ └── 点击关键词 → 相关论文列表
├── 功能5:热度走势对比动图
│ ├── timeline 时间轴自动播放(多年间 × 三大顶会)
│ └── 单热词年度折线走势
└── 扩展:了解更多(顶会背景 / 统计口径 / 数据来源)
应用层 app.js 路由、交互、渲染、模态框
↓
可视化层 charts.js ECharts 五种图表封装
↓
统计层 analytics.js Top10 / 共现 / 热度走势 / 增长率
↓
爬取层 crawler.js 多源降级链 + 关键词抽取与清洗
↓
存储层 store.js localStorage 增删改查 + 精确/模糊查询
↓
数据层 data.js 内置示例论文数据(44 篇)
分层的意义:上层依赖下层,各层职责单一。例如要把数据源从 OpenAlex 换成会议官网,只需改 crawler.js,其余各层不受影响。
| # | 问题 | 定位过程 | 解决方式 |
|---|---|---|---|
| 1 | 爬取功能形同虚设:AI 初稿调用 DBLP API,但浏览器里永远拿不到数据,全部静默降级为模拟数据 | 联网实测发现 DBLP 返回 HTML 验证页而非 JSON | 改为多源降级链,主源换为支持 CORS 的 OpenAlex |
| 2 | 批量导入中文标点失效:粘贴中文列表时整段被当成一个标题 | 单元测试覆盖多种分隔符时发现 | 修正分隔符正则,补齐 ;、、 |
| 3 | 热词统计被噪声污染:while 等停用词、OpenAlex 的 Cartography/Shot (pellet) 等无关词进入 Top10 | 打印爬取结果的关键词时肉眼发现明显不合理的词 | 补全停用词表 + 新增三层关键词校验 |
| 4 | 切换页面时动图重新开始播放 | 交互走查发现 hash 变化触发重复渲染 | hashchange 增加"页面已激活则跳过"判断 |
【截图/动图占位:至少 10 张,建议按下列顺序,每张配一句文字说明】
本次是人机结对而非人人结对。我的协作模式是:
我提需求 → AI 给方案/代码 → 我审查逻辑 → 我实测验证 → 发现问题 → 带着具体现象反馈给 AI → 迭代
↑
这一步是传统结对里"驾驶员/领航员"互查的等价物
关键体会:AI 在"写出看起来对的代码"上极强,但在"这段代码在真实环境里能不能跑通"上几乎不主动验证——而这恰恰是结对编程中"领航员"该干的事。所以我把人类角色定位在验证者与质疑者,而不是打字员。
提示词(Prompt):
我要做一个"CVPR/ICCV/ECCV 顶会热词统计平台",
用户是刚接触计算机视觉的学生,需求是快速了解研究热点。
请从「初学者 / 研究生 / 开发者」三个不同角色视角,
分别列出他们可能的使用场景和痛点,并给出竞品分析要点。
AI 输出摘要:AI 从三个角色给出了 12 条痛点,并列出 Google Scholar、DBLP、Connected Papers 等竞品。但它额外建议了"论文笔记管理""引用关系分析""团队协作"三个功能。
我的采纳与拒绝:
| AI 建议 | 我的决定 | 理由 |
|---|---|---|
| 多角色痛点列举 | ✅ 采纳 | 视角全面,帮我发现了"想知道方向是升温还是降温"这个我自己没想到的痛点 |
| 竞品分析框架 | ✅ 采纳并扩写 | 我补充了 CNKI/万方、会议官网两个本土竞品 |
| 论文笔记管理 | ❌ 拒绝 | 偏离"热词统计"核心,且与目标用户痛点无关 |
| 引用关系分析 | ❌ 拒绝 | 实现成本高(需引用数据),作业范围内收益低 |
| 团队协作 | ❌ 拒绝 | 单人课程作业场景不成立 |
| 深色科技风配色 | ⚠️ 修改后采纳 | 改为浅色 + 蓝色主色——学术数据展示需要凸显图表,深色背景长时间阅读易疲劳 |
这次协作让我明白:AI 倾向于"多给",而人类的价值在于"敢砍"。需求分析的核心能力是做减法。
【截图占位:案例 A 的 Prompt 与 AI 回复截图】
提示词(Prompt):
功能4 需要"关键词图谱,点击某个关键词可展现相关论文"。
我的数据是每篇论文带 keywords 数组。
请用 ECharts 实现一个力导向图,节点是关键词、大小按词频,
连线表示两个关键词出现在同一篇论文里,点击节点回调关键词。
AI 输出摘要:AI 给出了 ECharts graph + layout: 'force' 的完整配置,包括 symbolSize 计算、force.repulsion 参数、click 事件回调。
我做的修改:
value * 3),导致高频词节点过大、低频词几乎看不见。我改为 14 + Math.sqrt(value) * 9,用开方压缩让节点大小差异更柔和、层次更清晰。const key = ks[i] < ks[j] ? ks[i] + "||" + ks[j] : ks[j] + "||" + ks[i];
edgeMap[key] = (edgeMap[key] || 0) + 1;
tooltip 对边和节点用了同一个 formatter,导致悬停连线时显示 undefined。我改为按 p.dataType 分支处理。【截图占位:案例 B 的 Prompt、AI 生成代码、我的修改对比截图】
这个案例最能说明"人机结对"里人类审查的价值。
现象:功能1 代码逻辑读起来完全正确——调用 DBLP API、resp.json() 解析、取 hits.hit[0].info。但实测时**每条爬取都降级成了"模拟数据"**。
我的排查过程:
text/html,内容是 Making sure you're not a bot!;Access-Control-Allow-Origin —— 即使解决了反爬,浏览器端 fetch 也无法跨域读取。我把这两个事实反馈给 AI:
反馈提示词:
你写的 DBLP 爬取在浏览器里拿不到数据。我实测:
1) GET https://dblp.org/search/publ/api?q=...&format=json 返回的是
text/html,内容是 "Making sure you're not a bot!"(Anubis 反爬挑战)
2) 该响应没有 Access-Control-Allow-Origin 头
请分析这对纯前端方案意味着什么,并给出替代数据源建议。
AI 输出摘要:AI 承认初稿假设了"DBLP API 可直接调用"这一前提,而该前提已不成立;并建议评估 OpenAlex、CrossRef、Semantic Scholar。
我的处理:我没有直接采信 AI 的建议,而是亲自对四个数据源做了联网实测:
| 数据源 | CORS | 提供摘要 | 实测结论 |
|---|---|---|---|
| OpenAlex | ✅ | ✅ | 选为主源 |
| CrossRef | ✅ | ❌ | 备用源 |
| DBLP | ❌ | ❌ | 仅兜底 |
| Semantic Scholar | — | ✅ | 无 Key 时高频返回 429,不采用 |
最终实现四级降级链,并实测验证拿到真实数据:爬取 CLIP 论文成功返回 1202 字符的真实摘要与关键词 vision-language | representation learning | zero-shot learning。
这个案例的意义:AI 的代码"语法正确、逻辑自洽、注释完整",但前提假设是错的(认为第三方 API 永久可用且支持跨域)。如果不实测,这个 Bug 会一直隐藏到演示时才暴露。**"读懂 AI 的代码"不等于"验证了 AI 的代码"。**
【截图占位:AI 初稿代码、我的实测证据(命令行返回 HTML)、修改后的降级链代码】
现象:编写测试用例覆盖多种分隔符时,发现输入 Paper B,Paper C;Paper D 只解析出 2 条而不是 3 条。
原因:AI 写的正则只考虑了英文标点 /[\r?\n|,|;]/,漏掉了中文输入法下的全角 ; 和 、。
修复:
.split(/\r?\n|[,,;;、]/)
反思:这是典型的"AI 按理想化英文场景写代码"。中文用户实际粘贴的列表几乎必然带全角标点,这类问题只有站在真实用户角度测试才会发现。
现象:爬取 ViT 论文后,输出的关键词是 vision transformer | transformer | while | image (mathematics) | cartography | electrical engineering | geography。
问题:while 是漏网的停用词;后面几个是 OpenAlex 自动生成的通用概念关键词,与论文主题无关。它们会进入 Top10 和走势统计,直接污染功能3、功能5 的结果。
我的修复(三层过滤):
修复后:ViT 的关键词变为干净的 vision transformer | transformer。
【截图占位:案例 C 的 Prompt、报错/异常输出、修复前后对比截图】
以下摘取项目关键代码并解释设计思路(约 300 行)。项目分层:data → store → crawler → analytics → charts → app。
/**
* 查询:支持精确 / 模糊
* - 精确查询:title 完全等于 query(忽略首尾空格、大小写不敏感)
* - 模糊查询:在标题、关键词、编号、作者、摘要中做子串匹配
*/
find(query, exact) {
const papers = this.getAll();
const q = (query || "").trim().toLowerCase();
if (!q) return papers;
if (exact) {
return papers.filter((p) => p.title.trim().toLowerCase() === q);
}
return papers.filter((p) => {
// 把一条论文的所有可检索字段拼成一个"大字符串",再做子串匹配。
// 这样用户输入编号/关键词/作者/年份都能命中,而无需为每种字段写一套逻辑。
const hay = [
p.title, p.id, p.conference, String(p.year),
p.abstract || "", (p.keywords || []).join(" "), (p.authors || []).join(" ")
].join(" ").toLowerCase();
return hay.indexOf(q) !== -1;
});
}
思路:作业要求"输入论文题目精确查询,也支持模糊查询(输入论文编号、关键词等基本信息)"。精确查询用全等比较;模糊查询我采用字段拼装 + 子串匹配,好处是新增检索维度(比如以后加"机构")只需往数组里加一项,不需要改动匹配逻辑。
一个细节:**先 trim() 再 toLowerCase()**,保证用户输入首尾空格、大小写不一致时都能正确命中。
const SOURCES = [
{ name: "openalex", label: "OpenAlex", run: fromOpenAlex },
{ name: "crossref", label: "CrossRef", run: fromCrossRef },
{ name: "dblp", label: "DBLP", run: fromDblp }
];
async function crawlByTitle(title) {
const q = (title || "").trim();
if (!q) return { paper: null, status: "empty", message: "标题不能为空" };
// 1) 本地已存在 → 不重复爬取
const existing = window.PaperStore.find(q, true);
if (existing.length > 0) {
return { paper: existing[0], status: "exists", message: "本地已存在该论文,未重复爬取" };
}
// 2) 依次尝试各数据源(降级链)
const errors = [];
for (let i = 0; i < SOURCES.length; i++) {
const src = SOURCES[i];
try {
const paper = await src.run(q);
if (paper && paper.title) {
return { paper, status: src.name, message: "已从 " + src.label + " 获取论文信息" };
}
errors.push(src.label + ": 未检索到匹配结果");
} catch (e) {
// 网络失败 / CORS 被拦截 / 反爬验证 / 解析失败 → 尝试下一个源
console.warn("数据源 " + src.label + " 失败,尝试下一个:", e);
errors.push(src.label + ": " + e.message);
}
}
// 3) 全部失败 → 模拟数据兜底,保证功能可用且来源可追溯
const paper = {
title: q, authors: [], conference: "CVPR",
year: new Date().getFullYear(),
abstract: "(模拟数据)所有数据源均不可用,此为根据标题生成的演示记录……",
keywords: mergeKeywords(extractKeywords(q), [], q, 8),
url: "https://api.openalex.org/works?search=" + encodeURIComponent(q),
source: "simulated"
};
return { paper, status: "simulated", message: "数据源均不可用,已生成模拟记录(" + errors.join(";") + ")" };
}
思路:这是本项目最关键的改动。原始设计只调 DBLP,而实测发现 DBLP 的接口在浏览器端已经不可用。降级链的设计要点:
for 循环 + try/catch,任一源成功即返回,避免单点故障;errors 数组记录每个源失败的原因,最终展示给用户,让失败原因可见,而不是静默吞掉;source: "simulated" 的记录,界面上用橙色标签明示为模拟数据——绝不让用户误以为这是真实爬取结果;/**
* 领域词典:把常见表述归一化到规范关键词。
* 命中任一 pattern 就输出 canonical。
*/
const CV_TERMS = [
{ canonical: "object detection", patterns: ["object detection", "detector", "detect"] },
{ canonical: "segmentation", patterns: ["segmentation", "segment anything", "mask"] },
{ canonical: "diffusion model", patterns: ["diffusion", "denoising diffusion", "ddpm"] },
{ canonical: "depth estimation", patterns: ["depth estimation", "monocular depth", "metric depth"] },
// ... 共 40 余条,覆盖检测/分割/Transformer/扩散/三维/多模态等方向
];
function extractKeywords(text) {
const t = " " + String(text || "").toLowerCase() + " ";
const found = [];
CV_TERMS.forEach((term) => {
const hit = term.patterns.some((p) => t.indexOf(p.toLowerCase()) !== -1);
if (hit && found.indexOf(term.canonical) === -1) found.push(term.canonical);
});
return found;
}
思路:为什么一定要归一化? 如果直接把 "monocular depth"、"depth estimation"、"metric depth" 当作三个不同关键词,同一个研究方向会被拆成三份计数,Top10 排名失真、走势曲线断裂。归一化到唯一 canonical 后,不同论文的同一方向才能被合并统计——这是热词排行可比较的前提。
/**
* 数据源关键词校验:OpenAlex 的自动关键词噪声大,
* 会出现 "image (mathematics)"、"cartography"、"shot (pellet)"
* 这类与论文主题无关的通用概念,会污染 Top10 与热词走势。
* 过滤规则:
* 1) 含括号的(多为消歧义通用概念)丢弃;
* 2) 命中通用词黑名单的丢弃;
* 3) 未真实出现在标题/摘要中的丢弃;
* 4) 单个单词需至少出现 2 次,避免 generality/usability 这类偶然词。
*/
function mergeKeywords(dictKeywords, apiKeywords, text, limit) {
const out = dictKeywords.slice();
const t = String(text || "").toLowerCase();
(apiKeywords || []).forEach((kw) => {
const k = String(kw || "").toLowerCase().trim();
if (!k || k.length < 3) return;
if (k.indexOf("(") !== -1 || k.indexOf(")") !== -1) return; // 规则1
if (GENERIC_KEYWORDS.has(k)) return; // 规则2
const occurrences = countOccurrences(t, k);
if (occurrences === 0) return; // 规则3
if (k.indexOf(" ") === -1 && occurrences < 2) return; // 规则4
if (out.indexOf(k) === -1) out.push(k);
});
// 词典与数据源都无结果时,才退回高频实词
if (out.length === 0) {
frequentWords(text, 2).forEach((w) => out.push(w));
}
return out.slice(0, limit || 8);
}
思路:规则3(必须真实出现在正文中)是最有效的一条——它一句话就过滤掉了 cartography、geography、electrical engineering 这类"OpenAlex 认为相关但论文根本没提"的词。规则4 则解决了抽象名词的误入。
/** 关键词 -> 文档频次(同一篇论文内重复出现只计 1 次) */
function keywordFrequency(papers) {
const freq = {};
papers.forEach((p) => {
(p.keywords || []).forEach((k) => {
freq[k] = (freq[k] || 0) + 1;
});
});
return freq;
}
/** Top N 关键词 */
function topKeywords(papers, n) {
const freq = keywordFrequency(papers);
return Object.keys(freq)
.map((k) => ({ name: k, value: freq[k] }))
.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name))
.slice(0, n || 10);
}
思路:词频统计用文档频次(一篇论文内同一关键词只算 1 次)。这个口径的选择理由:如果用"出现总次数",摘要写得长的论文会天然占优,热度会偏向"话多的论文"而非"热门的方向"。sort 里加了 || a.name.localeCompare(b.name) 是为了让同分词频的结果稳定排序,避免每次刷新 Top10 顺序抖动。
/**
* 热度走势数据(功能5)
* 返回 { years, keywords, counts: { 关键词: { 会议: [每年词频] } } }
*/
function heatTrend(papers, topN) {
const ys = years(papers);
const kws = topKeywords(papers, topN || 8).map((k) => k.name);
const counts = {};
kws.forEach((k) => {
counts[k] = {};
CONFERENCES.forEach((c) => (counts[k][c] = ys.map(() => 0)));
});
// 把论文"投射"到 关键词 × 会议 × 年份 的三维计数矩阵上
papers.forEach((p) => {
const yi = ys.indexOf(p.year);
if (yi === -1) return;
(p.keywords || []).forEach((k) => {
if (counts[k] && counts[k][p.conference]) {
counts[k][p.conference][yi] += 1;
}
});
});
return { years: ys, keywords: kws, counts };
}
思路:先把计数矩阵按年份数初始化全 0,再遍历论文累加。这样做的好处是即使某个关键词在某年某会议没有论文,矩阵里也有对应的 0,保证 ECharts 的 series 数据长度一致——否则时间轴播放到某一年时图表会因为数据长度不齐而错位。这是"数据形状必须先对齐"的工程细节。
/**
* 功能5:热度走势对比「动图」
* ECharts timeline + 分组柱状图:自动播放年份 → 形成动图效果
* 每个年份帧展示该年在 CVPR/ICCV/ECCV 上各热词的词频对比
*/
function renderHeatTimeline(dom, trend) {
const { years, keywords, counts } = trend;
const colorMap = { CVPR: "#4f7cff", ICCV: "#22c1a4", ECCV: "#f5a524" };
// 每一帧的 series:三个会议各一条柱状系列
const confSeries = (yi) =>
["CVPR", "ICCV", "ECCV"].map((c) => ({
name: c,
type: "bar",
data: keywords.map((k) => (counts[k] && counts[k][c] ? counts[k][c][yi] : 0)),
itemStyle: { color: colorMap[c], borderRadius: [4, 4, 0, 0] },
barMaxWidth: 26
}));
// 为每个年份生成一帧 option
const options = years.map((y, yi) => ({
title: { text: y + " 年", subtext: "Top 热词在三大顶会的词频对比", left: "center" },
series: confSeries(yi)
}));
chart.setOption({
baseOption: {
timeline: {
axisType: "category",
autoPlay: true, // ← 自动播放,形成"动图"效果
loop: true,
playInterval: 1400,
data: years.map(String)
},
legend: { data: ["CVPR", "ICCV", "ECCV"] },
xAxis: { type: "category", data: keywords },
yAxis: { type: "value", name: "论文数" },
series: confSeries(0)
},
options: options // ← 每一帧
});
}
思路:作业要求"以动图的形式呈现"。我选择 ECharts timeline + baseOption/options 而不是生成 GIF,原因有三:
timeline 的 autoPlay 天然产生动画效果,且用户可暂停、可拖动时间轴,交互性优于固定 GIF;baseOption 放所有帧共享的配置(坐标系、图例、时间轴),options 放每帧独有的配置(标题、series 数据),两者合并渲染——这是 ECharts timeline 的标准用法。
/** 按需渲染对应页面(保证图表容器可见后尺寸正确) */
function renderPage(page) {
if (page === "home") renderHome();
else if (page === "papers") renderPapers();
else if (page === "trend") renderTrend();
}
function navigate(page) {
$$(".nav-item").forEach((a) => a.classList.toggle("active", a.dataset.page === page));
$$(".page").forEach((p) => p.classList.toggle("active", p.id === "page-" + page));
if (location.hash !== "#" + page) location.hash = page;
renderPage(page); // ← 必须在页面可见之后再渲染
}
思路:这是一个容易踩坑的地方。ECharts 在容器 display:none 时初始化会得到 0×0 尺寸,图表不可见。所以渲染顺序必须是"先切换页面可见 → 再初始化图表",而不是在页面加载时一次性初始化全部图表。
配合的另一个细节是 hashchange 的处理:
window.addEventListener("hashchange", function () {
const page = (location.hash || "#home").slice(1);
const el = $("#page-" + page);
// 仅在该页面尚未激活时跳转,避免 navigate() 改写 hash 后重复渲染
// (重复渲染会重建图表,导致热度走势动图重新开始播放)
if (el && !el.classList.contains("active")) navigate(page);
});
思路:navigate() 内部会改写 location.hash,这会再次触发 hashchange,造成重复渲染。表现出的 Bug 是"进入走势页后动图刚播两秒就重新开始"。加一个"已激活则跳过"的判断即可打破这个循环。
《构建之法》第 4 章讲两人合作,强调驾驶员与领航员的分工、代码复审、及时沟通。我原以为"和 AI 结对"就是"我描述需求、AI 出代码",效率高、不用吵架。
真正做下来才发现:AI 是一个执行力极强、但不会质疑自己前提的搭档。
最典型的例子就是爬取功能。AI 写的 DBLP 爬虫,代码结构清晰、注释完整、错误处理齐全,我第一遍读代码时完全找不出问题。直到我实际跑一遍,才发现它从头到尾就没拿到过数据——因为 DBLP 早已启用反爬、且不支持跨域。AI 不会告诉我"我假设了这个 API 可用,但这是我假设的",它只会自信地输出。
那一刻我才真正理解**"领航员"的价值:不是写得比驾驶员快,而是盯着驾驶员没在看的地方。在人人结对里,这个角色由同伴承担;在人机结对里,必须由我自己承担,且没有第二个人兜底**。
**"读懂"不等于"验证"**。我以前判断 AI 代码好坏的标准是"逻辑我能不能看懂"。现在我的标准是"我有没有跑过、有没有看过真实输出"。案例 C1 就是活教材——逻辑满分,实际零分。
AI 会按"理想化场景"写代码。中文分号 ; 那个 Bug 很小,但很说明问题:AI 默认用户输入是规范的英文标点。真实用户的输入永远比想象中脏,这类问题只有站在用户角度测试才会暴露。
统计类项目里,"口径"比"算法"重要。热词统计没有复杂算法,难点全在口径定义上:词频按文档频次还是总次数?同义关键词怎么合并?噪声词怎么过滤?这些决定结果有没有意义。我花在口径上的时间远超写统计代码的时间。
爬取功能降级到模拟数据时,我做了个选择:给每条数据打上 source 标签,在界面上用不同颜色明示来源(OpenAlex 绿色、模拟橙色)。
这其实增加了一点开发成本,但我觉得这是必要的。一个"热词统计平台"如果混入了假数据却不告诉用户,它输出的所有分析结论都是不可信的——这比功能少一个更严重。技术上的降级可以接受,但降级必须可见。
【截图占位:界面上的 source 来源标签截图】
charts.js 的 ECharts 配置、style.css 的整体样式,AI 一次输出就基本可用。| 局限 | 本次的具体表现 | 我的应对 |
|---|---|---|
| 不会主动验证前提 | 默认 DBLP API 可被浏览器直接调用,实际早已反爬 + 无 CORS | 所有涉及外部依赖的代码,必须亲自联网实测 |
| 知识可能过时 | 第三方接口的反爬策略、限流规则会变,AI 的知识未必是最新的 | 把"实测结果"反馈给 AI,让它基于新事实重新设计 |
| 倾向于"多给" | 需求分析时多列了 3 个不相关功能;代码里加了不必要的复杂度 | 人类负责做减法,敢于拒绝 AI 的产出 |
| 按理想场景编码 | 分隔符正则只考虑英文标点,漏掉中文全角符号 | 站在真实用户角度设计测试用例 |
| 可能自信地给出错误方案 | 若我不实测,爬取功能会带着"正确"的外表一直错到演示 | 把 AI 当执行者而非决策者,关键结论必须自己验证 |
人机结对 vs 人人结对:
| 维度 | 人人结对 | 人机结对(本次) |
|---|---|---|
| 角色分工 | 驾驶员 + 领航员,可轮换 | 我 = 驾驶员 + 领航员,AI = 高速执行器 |
| 沟通成本 | 高(需要同步理解) | 极低(描述清楚即可) |
| 代码审查 | 同伴会主动质疑 | AI 不会质疑自己的前提,审查责任全在人类 |
| 情绪因素 | 存在(分歧、疲劳) | 无 |
| 兜底 | 有第二个人 | 没有第二个人,自己是唯一防线 |
一句话总结:AI 把"写代码"这件事的速度提升了数倍,但它同时把"验证代码"的责任全部压回到了我身上。这次作业里,真正决定成品能不能用的,不是 AI 写得多快,而是我有没有老老实实去跑它、看它、质疑它。
所以我的结论是:AI 结对不是"省事",而是把人的精力从"敲代码"转移到了"做判断"——判断需求、判断口径、判断 AI 说的到底对不对。
# 克隆仓库(注意:git 克隆地址使用「仓库名」,与网页 URL 中的数字 ID 不同)
git clone https://codehub.devcloud.cn-north-4.huaweicloud.com/19c4f4b120bc43f7a93cda0c3fc8e4d2/102400423.git
# 运行(纯前端,零依赖)
# 方式一:直接双击 index.html
# 方式二:启动本地静态服务器
python -m http.server 8080 # 然后访问 http://localhost:8080
自测脚本:
node test/selftest.js # 离线自测(52 项)
node test/selftest.js --live # 含联网实测(55 项)