| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- """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()
|