88
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2601_FZU_SE |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业 |
| 这个作业的目标 | 开发并部署一个 CV 顶会论文趋势分析平台,完成爬取、管理、热门分析、关键词图谱、热度动图五大功能,并与 AI 结对协作完成全部文档与原型交付。 |
| 其他参考文献 | 邹欣.《构建之法》(第 3 版)第 3、8 章;jieba 分词:github.com/fxsjy/jieba |
opencode(AI 代码助手 / 结对编程终端)DeepSeek V4 Pro规则:预估列在动手前填写;实际列在全部工作完成后填写;
偏差 = 实际 - 预估;偏差原因逐条分析,不虚构。
| 子任务 | 预估(h) | 实际(h) | 偏差(h) |
|---|---|---|---|
| 阅读《构建之法》第 3、8 章 | 2 | 2 | 0 |
| 与 AI 头脑风暴(角色/竞品/优先级) | 2 | 3 | 1 |
| NABCD 人工验证与定稿 | 4 | 4 | 0 |
| 学习原型工具(墨刀) | 3 | 3 | 0 |
| 与 AI 讨论页面信息架构/配色/文案 | 3 | 2 | -1 |
| 五页高保真原型设计 | 5 | 8 | 3 |
| 原型发布分享链接 + 阶段一博客 | 2 | 2 | 0 |
| 阶段一小计 | 21 | 24 | 3 |
| 子任务 | 预估(h) | 实际(h) | 偏差(h) |
|---|---|---|---|
| 仓库初始化与文档基线(README/.gitignore/codestyle/AGENTS.md) | 1 | 1 | 0 |
| 数据管道:DBLP bibtex 下载 + arXiv 补摘要 | 5 | 12 | 7 |
| 关键词抽取(jieba + 停用词 + 词形还原 + 噪音词表) | 4 | 4 | 0 |
| 热度统计与 Top10(TF-IDF + 热度公式) | 4 | 4 | 0 |
| 功能1、2 后端接口(爬取/导入/增删改查/未命中兜底) | 6 | 5 | -1 |
| Vue3 前端初始化(对照原型重写,路由 + 全局布局) | 4 | 2 | -2 |
| 热门总览页(Top10 + 图谱 + 下钻) | 5 | 4 | -1 |
| 热度走势页(连续插值动图 + 三会分面 + 单词走势) | 5 | 3 | -2 |
| 论文管理页 + 数据导入页联调 | 3 | 3 | 0 |
| 详情页 + 关于页 | 3 | 2 | -1 |
| 测试(fixtures + pytest + 口径验证) | 3 | 4 | 1 |
| Bug 修复(表结构 abstract_flag、动图溢出、隔年会议) | 2 | 3 | 1 |
| CodeArts 部署(ECS + gunicorn + 公网访问) | 2 | 2 | 0 |
| Git 操作(dev 分支、15+ commit、merge main、tag v1.0.0、Release) | 2 | 2 | 0 |
| 博客收尾 | 3 | 4 | 1 |
| 阶段二小计 | 52 | 55 | 3 |
| 阶段 | 预估(h) | 实际(h) | 偏差(h) |
|---|---|---|---|
| 合计 | 72 | 78 | 6 |
偏差最大任务
| 任务 | 原因分析 |
|---|---|
| 数据管道:DBLP bibtex 下载 + arXiv 补摘要 | 本地 dblp.org bibtex 下载不稳定 ,改用ECS,选择「先跑完再补失败卷宗」的策略。 |
说明:本文档为阶段一需求分析成果,与 AI 协作完成初稿,经人工逐条验证后定稿。
CV 入门者(小刚)面前有两道墙:
| 用户 | 核心需求 | 刚性 |
|---|---|---|
| 入门爱好者(小刚) | 零门槛建立方向认知,从热词一路下钻到论文 | 定义产品主线 |
| 本科生 | 批量导入、自动补全元数据、综述查漏 | 高频一次性 |
| 研究生 | 选题热度依据、热词可回查、列表长期管理 | 最高频 |
| 教师/研究员 | 立项依据图表、统计口径可引用 | 低频高价值 |
| 算法工程师 | 判断技术窗口期、图表进汇报 | 中频 |
| 自媒体 | 现成年度榜单与动图素材 | 低频高传播 |
现有工具全部假设用户已经知道要搜什么;没有产品服务"从兴趣出发、零检索能力"的入门用户,也没有"会议 × 年份 × 热词"的动态对比视角。
不做"CV 版 Google Scholar",做"从热词进入的 CV 顶会趋势分析平台"。
数据靠聚合不靠对抗爬虫;差异化押在入口形态与呈现方式上。
热度 = 词频 × (1 + 年增长率),同时保留纯词频口径;两种口径使教师/工程师引用时有据可依;| 原型页面 | 承载功能 | 角色服务重点 |
|---|---|---|
| 首页 / 热门方向总览 | 功能3(Top 10)+ 功能4(图谱入口)+ 小刚引导路径 | 小刚、自媒体 |
| 热度走势对比页 | 功能5(动图,三会 × 多年) | 研究生、教师、工程师 |
| 论文列表管理页 | 功能2(增删改查、精确/模糊查询、未命中自动爬取) | 本科生、研究生 |
| 论文爬取 / 导入页 | 功能1(单篇输入 + 批量导入 txt/csv) | 本科生 |
| 详情 / 关于页 | 论文摘要/关键词/原文链接 + "了解更多"(三会背景与统计口径) | 全部角色 |
| 对象 | 可感知收益 |
|---|---|
| 小刚 | 无需检索能力,3 分钟从"电影灵感"走到"看懂领域结构" |
| 本科生 | 批量导入自动补全元数据;图谱发现综述遗漏方向 |
| 研究生 | 热词可回查支撑论文,趋势数据敢写进论文 |
| 教师/工程师 | 动图截帧进 PPT,口径页保证数据经得起追问 |
| 自媒体 | 现成年度榜单 + 动图 + 口径说明,直接出内容 |
技术红利:数据管道一次建成后,每年新会议发布只需重跑清洗与统计即可刷新全部榜单/图谱/动图,维护成本趋近于零。
竞品全部免费、背后是巨头或头部媒体,正面对抗没有胜算;但所有竞品共享同一个盲区假设——**"用户已经知道要搜什么"。我们的策略是错位竞争 + 借力**:不抢它们的战场,做它们入口之前的"那一层"。
| 优势 | 对应竞品缺口 |
|---|---|
| 唯一"零输入"入口(热词榜 + 图谱 + 动图,不需检索式) | arXiv/PwC/CP 全部要求先输入 |
| 唯一"会议 × 年份 × 热词"切片视角 | arXiv 无会议字段、PwC 无会议维度、盘点文无自定义 |
| 唯一动图竞速呈现 | 全部竞品均为静态图表 |
| 唯一可回查的统计口径 | 盘点文人工选词无口径,学者不敢引用 |
| 唯一中文 + 入门科普路径 | 前三家全英文,盘点文不可交互 |
| 阶段 | 必交物 | 载体 |
|---|---|---|
| 阶段一:需求理解与原型设计 | ① NABCD 完整模型(含与 AI 协作的关键对话截图)② 五页高保真原型(首页/热门总览、动图页、列表管理页、爬取/导入页、详情/关于页)③ 原型分享链接 ④ PSP 表(预估列) | 博客 |
| 阶段二:编程实现与部署 | ① 功能 1–5 全部实现 ② CodeArts 云服务器部署链接③ 数据入库结果与统计口径说明 ④ 动图效果展示(GIF)⑤ PSP 表(实际列 + 偏差分析)⑥ 扩展功能说明(若有) | 博客 + 云服务器 |
热度 = 词频 × (1 + 年增长率),逐条解释);本项目原型设计采用了 墨刀 (Modao) 作为专用原型设计工具。
所有页面均是在墨刀中通过拖拽组件、调整图层并设置交互完成的,符合课程“必须使用专用原型设计工具(如Axure、墨刀、Figma等)”的要求。

重构后:


核心内容:平台核心数据的综合仪表盘。




核心内容:平台背景与规则说明。
该页面包含五个主要板块:

具体项目结果此处以视频方式展示:
[video(video-4Nc1AorT-1790264275245)(type-csdn)(url-https://live.csdn.net/v/embed/545927)(image-https://i-blog.csdnimg.cn/direct/c97e1a95f09047188216d44f3bffd13f.png)(title-9%E6%9C%8824%E6%97%A5)]
结对伙伴:OpenCode(主要)+ deepseek v4pro(需求分析辅助)
协作原则:AI 负责发散与初稿,人负责验证、裁决与兜底;AI 输出未经人工验证不进入定稿。
| 阶段 | 关键提示词(摘要) | AI 输出 | 采纳/拒绝及理由 |
|---|---|---|---|
| 需求分析 | "从不同角色视角列出潜在用户、竞品分析、功能优先级" | 六类用户、四竞品表、P0–P3 优先级 | 采纳 80%;**拒绝"做登录注册"(需求未要求且违背零门槛定位);修正"Top10 按年切片"**为按需求原文"分析已爬取论文"的全量聚合 |
| 竞品取舍 | "保留 4 个竞品且强项详细" | arXiv/CP/PwC/盘点文四表 | 采纳;DBLP、Semantic Scholar 降级为数据供应商不列竞品 |
| 原型设计 | "生成初始版的HTML 原型" | 单文件六页原型 | 采纳;运行后人工发现 3 个缺陷(年份不可选、动图跳帧、柱子溢出),逐条回传修正 |
| 数据源 | "用 DBLP bibtex 批量建库" | fetcher.pipeline | 拒绝并降级:本地网络拉不动,AI 声称"云上可通"未被采信,先小规模验证;最终改老师数据集(附录14)为主源 |
| 表结构 | "创建 papers 表" | 骨架版无 abstract_flag | 人工检查发现缺字段,补 ALTER 修正(AI 初版缺陷被纠正) |
| 动图 | "实现连续生长竞速图" | 写死像素宽度 | 发现溢出 Bug,人工定位原因后改为容器宽度动态计算 |
| 部署 | "密钥登录步骤" | 给出控制台流程 | 采纳;实际界面无对应入口,人工换用密码/VNC 路径 |
提示词:让 AI 按 NABCD 框架输出潜在用户、场景、竞品、优先级。

输出:六类用户画像、六竞品错位分析、三个设计反转(入口/结构/形态)。
人工修改:剔除课程老师角色;修正功能3 统计口径;竞品从 6 个收敛为 4 个并把 DBLP 改称数据供应商;页面补"论文详情页"使动线完整。
理由:AI 发散广但不懂作业验收细节,口径类结论必须回对需求原文。
提示词:

输出:
AI 提问/探查:按你给的 dblp.org/db/conf/cvpr/cvpr2022.bib 去拉数据时,AI 主动抓包探测,发现三件事:① DBLP 全站启用了 Anubis 反爬(proof-of-work 挑战);② 你给的 .bib URL 已失效(404);③ ECCV 在 DBLP 里是分卷的(eccv2022-1 ~ eccv2022-39),不是单个卷宗。
AI 输出:逆向出 Anubis 的 fast 算法(SHA-256 工作量证明,找 nonce 使 hash 前 2 字节为 0),并给出三个候选拉取方案(bibtex 端点分页 / 检索 API / 会议 HTML 解析)。
人工修改:确认采用「bibtex 端点分页」;AI 据此实现 Anubis 绕过、discover_volumes 分卷发现、parse_bibtex 解析。数据链路从「照搬旧 URL」修正为「真实可用的检索端点」,这是整个数据管道能跑通的前提。
现象:本机 IP 被 DBLP 深度限流(大量 429/连接重置)
我的决策:建议改用ECS;提供云服务器信息,选择「先跑完再补失败卷宗」的策略。


main 存文档基线,dev 开发;.gitignore 忽略 *.db/node_modules/dist。codestyle.md 标注来源 PEP 8 + Google Style。AGENTS.md 固化数据链路、业务口径、协作红线(AI 每新会话自动读取,防跑偏)。papers(abstract 可空)、conferences、paper_conference、keywords、paper_keyword(weight 字段供 TF-IDF)。abstract_flag——导入数据前人工检查发现,补 ALTER TABLE ADD COLUMN 修正。no_abstract(统计仍纳入,仅标题抽词)。test.db 验证,避开与全量导入的生产库冲突。jieba + 停用词 + 词形还原 → 标题×3/摘要×1 加权 → TF-IDF → 热度公式。method/propose 等)回填词表——案例 B 素材。overflow:hidden + resize 重绘(案例 C 主素材)。nohup 挂后台写日志(合盖断网不影响)。npm run build 后由 Flask 托管 → gunicorn 起服务 → 公网访问冒烟测试。dev 开发(21 次 commit)→ merge 到 main → tag v1.0.0 → 创建 Release。| 决策点 | 初方案 | 最终定稿 | 原因 |
|---|---|---|---|
| 数据源 | DBLP bibtex 批量 | 老师数据集(附录14)+ DBLP API 兜底 | 本地网络拉不动,AI 初方案验证失败 |
| 摘要 | 无 | arXiv 补 + no_abstract 标记 | DBLP 无摘要字段,禁止伪造 |
| 前端 | 复用原型 HTML | Vue 3 重写 | 作业推荐框架 + 规避"盲抄 AI 代码"红线 |
| Top10 口径 | 按年切片 | 全量聚合 + 年份下拉可选 | 需求原文"分析已爬取的论文" |
| 动图 | 逐年跳帧 | 年粒度 + 线性插值连续生长 | 演示要求连续增长;口径如实标注 |
| 登录注册 | 考虑 | 不做 | 需求未要求;登录墙违背小刚零门槛定位 |

_PROC_KEYWORDS = (
"conference on computer vision and pattern recognition",
"international conference on computer vision",
"european conference on computer vision",
"proceedings",
)
def is_proceedings(title):
text = (title or "").lower()
return any(k in text for k in _PROC_KEYWORDS)
def fetch_volume(session, conf, volume_slug):
query = f"toc:db/conf/{conf.lower()}/{volume_slug}.bht:"
papers, offset = [], 0
while True:
resp = session.get_dblp(_SEARCH_API, params={
"q": query, "h": "1000", "format": "json", "f": str(offset)}, timeout=120)
hits = resp.json()["result"]["hits"]
total = int(hits.get("@total", 0))
hit_list = hits.get("hit") or []
if isinstance(hit_list, dict):
hit_list = [hit_list]
for hit in hit_list:
info = hit.get("info", {})
title = info.get("title")
if is_proceedings(title):
continue
papers.append({"title": title, "year": info.get("year"),
"venue": info.get("venue"),
"ee": info.get("ee") or info.get("url")})
offset += len(hit_list)
if not hit_list or offset >= total:
break
return papers
def lemmatize(word):
if word.endswith("ies") and len(word) > 4: return word[:-3] + "y"
if word.endswith("ing") and len(word) > 5: return word[:-3]
if word.endswith("ed") and len(word) > 4: return word[:-2]
if word.endswith("s") and not word.endswith("ss") and len(word) > 3: return word[:-1]
return word
def extract_keywords(title, abstract=None, abstract_flag="ok"):
weights = {}
for word in tokenize(title):
w = lemmatize(word)
if w in STOP_WORDS or len(w) < 2: continue
weights[w] = weights.get(w, 0) + 3
if abstract_flag != "no_abstract" and abstract:
for word in tokenize(abstract):
w = lemmatize(word)
if w in STOP_WORDS or len(w) < 2: continue
weights[w] = weights.get(w, 0) + 1
return weights
def compute_hotness(freq, prev_freq):
growth = (freq - prev_freq) / prev_freq if prev_freq > 0 else 0.0
return freq * (1.0 + growth)
def compute_tfidf(weighted_by_paper):
n = len(weighted_by_paper)
tf, df = {}, {}
for paper in weighted_by_paper:
for word, weight in paper.items():
tf[word] = tf.get(word, 0) + weight
df[word] = df.get(word, 0) + 1
return {w: tf[w] * (math.log(n / df[w]) + 1.0) for w in tf}
def top_keywords(weighted_by_paper, paper_ids, k=10, scorer=None):
freq, doc_ids = {}, {}
for pid, paper in zip(paper_ids, weighted_by_paper):
for word, weight in paper.items():
freq[word] = freq.get(word, 0) + weight
doc_ids.setdefault(word, []).append(pid)
def score_of(w):
return scorer(w, freq[w], doc_ids[w]) if scorer else freq[w]
words = sorted(freq, key=lambda w: (-score_of(w), w))
return [{"word": w, "score": score_of(w), "freq": freq[w],
"paper_ids": sorted(doc_ids[w])} for w in words[:k]]
def search(self, conference=None, year=None, keyword=None, query=None,
exact=False, page=1, page_size=20):
where, params = [], []
if conference:
where.append("c.name = ?"); params.append(normalize_conf(conference))
if year is not None:
where.append("p.year = ?"); params.append(year)
if keyword:
where.append("p.keywords_text LIKE ?"); params.append(f"%{keyword}%")
if query:
if exact:
where.append("p.title = ? COLLATE NOCASE"); params.append(query.strip())
else:
where.append("(p.title LIKE ? OR p.keywords_text LIKE ? OR CAST(p.id AS TEXT) = ?)")
params.extend([f"%{query}%", f"%{query}%", query.strip()])
base = ("FROM papers p LEFT JOIN paper_conference pc ON p.id = pc.paper_id "
"LEFT JOIN conferences c ON pc.conference_id = c.id " +
("WHERE " + " AND ".join(where) if where else ""))
total = conn.execute(f"SELECT COUNT(*) {base}", params).fetchone()[0]
rows = conn.execute(f"SELECT p.*, c.name AS conference {base} "
"ORDER BY p.id LIMIT ? OFFSET ?",
params + [page_size, (page - 1) * page_size]).fetchall()
return total, [dict(r) for r in rows]
@bp.get("/hot")
def hot():
year = _parse_year(request.args.get("year"))
conf = request.args.get("conf", "all")
method = request.args.get("method", "freq")
if year is None or method not in ("tfidf", "freq"):
return fail(CODE_BAD_REQUEST, "参数非法")
return ok(compute_top(_db(), year=year, conf=conf, method=method))
@bp.get("/graph")
def graph():
return ok(compute_graph(_db(), year=_parse_year(request.args.get("year")),
conf=request.args.get("conf", "all")))
@bp.get("/trend")
def trend():
keyword = (request.args.get("keyword") or "").strip()
if not keyword:
return fail(CODE_BAD_REQUEST, "缺少 keyword")
return ok(compute_trend(_db(), keyword))
const USE_MOCK = import.meta.env.VITE_USE_MOCK !== 'false'
export function fetchHot({ year = 'all', conf = 'all', method = 'freq' } = {}) {
if (!USE_MOCK) return httpGet('/hot', { year, conf, method })
return ok({ method, total_papers: 14826, no_abstract_count: 2310, top: mockData.HOT_TOP })
}
function renderRaceFrame() {
const y1 = YEARS[raceSeg], y2 = YEARS[raceSeg + 1], p = Math.min(1, raceP)
const vals = raceValues()
const frame = allRaceWords()
.map(w => ({ name: w, value: vals[w][raceSeg] + (vals[w][raceSeg + 1] - vals[w][raceSeg]) * p }))
.filter(it => it.value >= 1).sort((a, b) => b.value - a.value)
const names = frame.map(it => it.name)
const data = frame.map(it => ({ value: Math.round(it.value),
itemStyle: { color: CONF_COLORS[confOf(it.name, p)] } }))
raceChart.setOption({
yAxis: { type: 'category', data: names, inverse: true },
animationDurationUpdate: 80, animationEasingUpdate: 'linear',
series: [{ type: 'bar', realtimeSort: true, data }],
})
}
raceTimer = setInterval(() => { raceP += 0.05; /* 80ms×20=1.6s/年 */ }, 80)
《构建之法》第4章讲两人合作:结对编程有"驾驶员"与"领航员",代码复审保证质量。这次"人机结对"让我体会到:AI 是不知疲倦的驾驶员,而我必须永远做领航员与复审者。
心路上,最深的教训是不能轻信 AI。它曾把数据源 URL 说得言之凿凿,执行后才发现本地根本拉不通;表结构缺字段、动图溢出,都是它初版代码的坑。三次返工让我明白:AI 的"自信"不等于正确,它的答案必须先验证再用——这正对应第4章"代码复审"的价值。
收获有三:
评价伙伴:优点明显——发散快、出稿快、代码规范、能讲设计思路;局限同样明显——会幻觉、无责任感、不会主动质疑错误需求。它像一位能力很强但需要严管的实习生:给清规格就高效,撒手不管就翻车。