"""seed_long_report.py —— 压力测试:把示例报告按真实篇幅扩到约 1.6 万字,写回同一条成果。 目的:验收第 13 项「长报告可读(1 万字级不崩)」。 可重复执行:先运行 seed_report.py 得到基准报告,再运行本脚本扩容; 再运行一次 seed_report.py 即可恢复基准版本。 python3 tools/seed_long_report.py --dry-run python3 tools/seed_long_report.py """ import json import os import sys import urllib.request BASE = "https://kangshifu.fmode.cn/parse" APP_ID = "FKieRbIoGt_main" MK = os.environ.get("PARSE_MASTER_KEY", "") DRY = "--dry-run" in sys.argv H = {"X-Parse-Application-Id": APP_ID, "Content-Type": "application/json"} if MK: H["X-Parse-Master-Key"] = MK TARGET = "夜宵场景低卡高蛋白拌面 - competitor_report" # 扩写素材:按"场景 × 人群 × 行为"把每条痛声讲细,内容仍是数据,渲染仍走同一套 section 类型 EXPAND = { "para": ( "把这条原声放回它的语境里看:说话的人并不是在挑剔口味,而是在描述一种每天都会发生的将就——" "手边只有一壶开水,泡出来的面软塌成一团,酱包再香也救不回来。这类评论在几个平台上高度相似," "说明它不是个别人的偏好,而是「速食拌面」这个形态本身在场景里暴露出来的结构性缺口。" "如果我们只把它读成「消费者对口感有要求」,就得不出任何可以立项的动作;但如果把它读成" "「在没有锅的条件下,当前工艺无法交付有嚼劲的面」,那么要解决的问题就很具体:面饼复水后的结构强度。" ), "table": { "cols": ["分层", "典型人群", "出现频次", "原声样例(节选)", "对产品的含义"], "rows": [ ["加班夜宵(22:00–1:00)", "互联网 / 设计 / 医护夜班", 486, "「泡完就坨,根本没有嚼劲」", "夜宵是最高频场景,口感是此刻唯一的取悦点"], ["宿舍速食", "高校学生 / 合租青年", 372, "「只有麻酱和辣,想吃川味没有」", "口味需要地域细分,而不是继续加辣度"], ["办公室午餐", "白领 / 工位即食", 258, "「拌好以后坨掉,边工作边吃根本不行」", "需要耐放、不易坨的形态与包装"], ["露营 / 户外", "周末露营 / 徒步", 193, "「户外多一步倒汤,就不想买拌面」", "包装结构要能把「倒汤」这一步去掉"], ["家庭囤货", "三口之家 / 老人同住", 311, "「一家人吃,口味不够分」", "大包装要有组合口味,而不是同一口味堆量"], ], }, "findings": [ {"id": "f-a", "conclusion": "「软塌」的根因是工艺,不是口味", "attitude": "在 214 条相关原声里,提到口感的多于提到味道的。把预算投在调味迭代上,解决不了复购问题——这是产品适配章节要回答的第一件事。", "evidence": "c-texture"}, {"id": "f-b", "conclusion": "地域风味是空档,不是「再加一个辣」", "attitude": "川味的麻、西北油泼的香、东北酸菜的酸,在搜索端都有人找,在货架上都没有。缺的不是辣度,是地方性。", "evidence": "c-sauce"}, {"id": "f-c", "conclusion": "露营不是「没人买」,是「多一步就不买」", "attitude": "户外用户的支付意愿不低,但对步骤极其敏感:倒面汤这一步就会损失一半潜在用户。包装上的每一个动作,都要算成本。", "evidence": "c-portable"}, ], "funnel": [ {"stage": "S1", "name": "品类池", "value": 46, "unit": "个细分品类", "scope": "拌面 / 泡面 / 鲜面 / 自热 / 冻干 / 螺蛳粉", "note": "先把品类地图铺开,确定竞争发生哪条线上。"}, {"stage": "S2", "name": "目标人群", "value": 2.7, "unit": "亿人", "scope": "18–35 岁,每周至少吃一次速食面条的城市人群", "note": "用 VOC 去掉两端:只吃外卖的人、只吃现煮鲜面的人。"}, {"stage": "S3", "name": "细分市场", "value": 3000, "unit": "万人", "scope": "把速食面当一顿正餐而不是垫一口的人", "note": "「不将就」的需求集中在这一格。"}, {"stage": "S4", "name": "可切入场景", "value": 900, "unit": "万人", "scope": "加班 / 宿舍 / 露营 三类没有锅的场景", "note": "场景越多,包装与口味越难统一;先做最大的一块。"}, {"stage": "S5", "name": "首推机会", "value": 180, "unit": "万人", "scope": "「不需要锅也能吃到锅气」的即食鲜拌面", "note": "把「锅气」做成一句能记住、能复述的话。"}, ], } def call(method, path, body=None): data = json.dumps(body, ensure_ascii=False).encode() if body is not None else None req = urllib.request.Request(BASE + path, data=data, headers=H, method=method) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode() or "{}") def expand(report): """每章:原文 ×2 + 追加一节"长论证";结论块扩到 3 条;表格换成更细的分层表。 (示例内容本身是短篇骨架,这里只是按真实报告的篇幅把它撑长,用于压测。)""" for ch in report.get("chapters", []): # 每章追加 2 节长论证,让整篇达到真实报告的万字级篇幅(约 2 万字) secs = ch.setdefault("sections", []) for k in range(1, 3): parts = [] for j in range(4): parts.append(EXPAND["para"]) parts.append(f"回到第 {j + 1} 个观察点:{EXPAND['table']['cols'][(j + k) % 5]}。" "这不是某一款产品的问题,而是整个品类在没有锅的场景里共同面对的约束;" "谁先把这条约束变成产品力,谁就拿到这个场景的解释权。") secs.append({"id": f"{ch['id']}-long{k}", "no": f"{ch.get('no')}.{k + 3}", "title": f"长论证 {k} · 把问题讲透", "kind": "longpara", "text": "\n\n".join(parts)}) secs = ch.get("sections", []) extra = [] for s in secs: k = s.get("kind") if k == "para": s["text"] = s["text"] + EXPAND["para"] extra.append({"id": s["id"] + "-x", "no": (s.get("no") or "") + ".1", "title": "细读 · 正反两面", "kind": "two", "data": { "can": ["有稳定的高频场景(夜宵 / 宿舍 / 办公)", "我方产线与调味能力可覆盖", "渠道能触达即食购买点"], "cannot": ["无法在两周内改变面饼工艺", "不能宣称疗效或替代正餐营养", "价格不能与外卖正面竞争"]}}) elif k == "table": s["title"] = s["title"] + " · 分层明细" s["data"] = EXPAND["table"] elif k == "findings": s["data"] = EXPAND["findings"] elif k == "funnel": s["data"] = EXPAND["funnel"] secs.extend(extra) ch["lead"] = ch.get("lead", "") + EXPAND["para"][:120] return report def main(): if not MK and not DRY: sys.exit("PARSE_MASTER_KEY 未设置") rows = call("GET", "/classes/KfArtifact?limit=1000&order=createdAt")["results"] target = next((a for a in rows if a.get("title") == TARGET), None) if not target or not target.get("report"): sys.exit("先运行 tools/seed_report.py") rep = expand(target["report"]) body = json.dumps(rep, ensure_ascii=False) if DRY: print(json.dumps({"bytes": len(body.encode())}, ensure_ascii=False)) return call("PUT", f"/classes/KfArtifact/{target['objectId']}", {"report": rep}) print("long report written:", target["objectId"], "| bytes", len(body.encode())) if __name__ == "__main__": main()