review-five-topic-supplement.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. """Record explicit topic passages after reviewing the collected source bodies."""
  2. import importlib.util
  3. import json
  4. from pathlib import Path
  5. import re
  6. from lxml import html
  7. ROOT = Path(__file__).resolve().parents[1]
  8. WORK = ROOT / 'work/five-topic-collection-20261110/supplement-01'
  9. def module(name, file):
  10. spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / file)
  11. value = importlib.util.module_from_spec(spec)
  12. spec.loader.exec_module(value)
  13. return value
  14. def main():
  15. community = module('community', 'collect-five-topic-supplement.py')
  16. base = community.base
  17. prior = json.loads((WORK.parent / 'reviewed-public-sources.json').read_bytes())
  18. recovered = community.archived_sources({r['url'] for r in prior['selected']})
  19. base.save(WORK / 'archive-recovered.json', {'sources': list(recovered.values())})
  20. rows = {url: {**row, 'collectionDirectory': 'public-sources', 'extractor': 'community'}
  21. for url, row in recovered.items()}
  22. for filename, directory, extractor in [
  23. ('industry-collection-result.json', 'industry-sources', 'industry'),
  24. ('external-collection-result.json', 'external-sources', 'external')]:
  25. for row in json.loads((WORK / filename).read_bytes())['sources']:
  26. rows[row['url']] = {**row, 'collectionDirectory': directory, 'extractor': extractor}
  27. # Yicai's article-main wrapper includes share controls and an AI template.
  28. # Extract only its actual article paragraphs, retaining the original bytes.
  29. yicai_url = 'https://www.yicai.com/news/102758260.html'
  30. yicai = rows[yicai_url]
  31. raw = (WORK / 'external-sources' / yicai['httpReceipt']['responseFile']).read_bytes()
  32. assert base.digest(raw) == yicai['httpReceipt']['sha256']
  33. root = html.fromstring(raw.decode('utf8'))
  34. actual = root.xpath('//*[@id="multi-text"]')
  35. assert len(actual) == 1
  36. yicai['body'] = base.paragraphs(actual[0])
  37. yicai['bodySha256'] = base.digest(yicai['body'].encode())
  38. yicai['bodyExtractionSelector'] = '//*[@id="multi-text"]'
  39. dates = [node.text_content().strip() for node in root.xpath('//*[contains(concat(" ",normalize-space(@class)," ")," f-fl ")]')]
  40. dates = [d for d in dates if re.fullmatch(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', d)]
  41. assert len(set(dates)) == 1
  42. yicai['publishedAtRaw'] = dates[0]
  43. # Each tuple below is an actual reviewed literal passage, not a title-only
  44. # keyword assignment. Related technical references are labelled separately.
  45. selected = []
  46. def approve(url, evidence, note):
  47. row = rows[url]
  48. topic_rows = []
  49. for topic, relation, quote in evidence:
  50. assert quote in row['body'], 'REVIEW_PASSAGE_NOT_IN_BODY:' + url + ':' + quote
  51. topic_rows.append({'topic': topic, 'relation': relation, 'passages': [quote]})
  52. selected.append({**row, 'topics': topic_rows, 'reviewNote': note})
  53. def smzdm(post, evidence, note): approve('https://post.smzdm.com/talk/p/' + post + '/', evidence, note)
  54. smzdm('a6p37nn0', [('wellness', 'ingredient_taste_reference', '面条有劲道,入口爽滑')], '菌菇产品的口感参考,不证明养生需求或功效')
  55. smzdm('al8eq0og', [('wellness', 'ingredient_taste_reference', '大面饼配上三个原汁调料包')], '菌菇汤面包装与调味体验,仅为作者说法')
  56. smzdm('a7n4v8el', [('wellness', 'ingredient_taste_reference', '小鸡炖蘑菇的方便面包装是深绿色的')], '蘑菇风味和包装参考,不把风味当作营养功效')
  57. smzdm('awzz6m04', [('wellness', 'ingredient_taste_reference', '可以煮着吃,也可以泡着吃')], '鸡汤蘑菇面的操作、口感和份量自述')
  58. smzdm('a7n82dzd', [('premium', 'consumer_experience', '汤底熬的实在太浓郁啦')], '拉面说复购、汤底与操作体验,无独立购买核验')
  59. smzdm('aenwg444', [('premium', 'consumer_experience', '单纯烧滚的水泡不行')], '满汉大餐肉块与冲泡限制的自述')
  60. smzdm('a8xx3k50', [('premium', 'consumer_experience', '确实有大块的牛肉和萝卜')], '速达面馆配料和宽面口感自述')
  61. smzdm('ad9ooqqx', [('student', 'consumer_scenario', '每天在宿舍,一到晚上就肚子饿')], '宿舍储备与口味选择,不归为家庭囤货')
  62. smzdm('akk3r058', [('student', 'consumer_scenario', '强烈建议有条件的同学煮着吃')], '宿舍设施与冲泡/煮制差异')
  63. smzdm('an9pvlxp', [('student', 'consumer_scenario', '除了少了一点,没有任何毛病')], '带去学校的份量不足体验')
  64. smzdm('a30dq7kd', [('student', 'consumer_scenario', '像我们宿舍一般就是泡个十多分钟左右'),
  65. ('novelty', 'flavour_reference', '藤椒味的话跟麻辣味的话是非常好吃的')], '阿宽干拌宽面,保留作者原说法,不确认素食资格')
  66. smzdm('axzl5x44', [('student', 'consumer_scenario', '他的量也很大足够,我们吃一顿')], '宿舍饱腹与配料自述,不采信营养均衡结论')
  67. smzdm('akko2xnr', [('student', 'consumer_scenario', '份量真的超级多')], '宿舍泡面份量和多品牌口味简评')
  68. smzdm('aevd7zmz', [('student', 'consumer_scenario', '虽然只有两包调料包但是料很足'),
  69. ('novelty', 'flavour_reference', '咸蛋黄泡面')], '咸蛋黄风味与挂汁体验,不视为已验证购买')
  70. def foodaily(number, evidence, note): approve('https://www.foodaily.com/articles/' + str(number), evidence, note)
  71. foodaily(42112, [('wellness', 'nutrition_process_reference', '推出红烧牛肉面减卡高纤面')], '减卡数字、航天工艺及销量均为报道声明,未独立复算')
  72. foodaily(42186, [('premium', 'industry_report', '统一在山姆会员店推出17.48元/袋的“上汤瑶柱银丝面”')], '产品结构和溢价对比,原日期的报道价格,不新增现价观察')
  73. foodaily(41869, [('wellness', 'chinese_ingredient_report', '中国品牌在方便面健康化上的路径更加本土化,主要围绕低GI、杂粮化和药食同源多线并行')], '含中式食材与海外营养产品,报道数字及功效均待独立核验')
  74. foodaily(41834, [('novelty', 'industry_report', '“美式可乐炸鸡味拌面”“德式黑啤小龙虾味拌面”')], '全球风味新品案例,不以热榜声明认定市场份额')
  75. foodaily(42295, [('stockpile', 'packaging_purchase_report', '大卖场的到店顾客以家庭计划性囤货采购为主'),
  76. ('novelty', 'trial_packaging_tradeoff', '当消费者想尝试新口味却必须一次买五包时,尝试意愿会大幅降低')], '家庭组合装和尝鲜门槛,统计为报道引用,未新增一手调查')
  77. foodaily(40765, [('novelty', 'industry_report', '日清曾与热门动漫《死神》《进击的巨人》、手游《阴阳师》等IP跨界合作')], '联名产品案例与产能风险,保留报道年代')
  78. foodaily(40304, [('premium', 'industry_opinion', '将产品定价抬至20-30元')], '高端定位与替代选择讨论,非消费者人数或全市场事实')
  79. foodaily(40076, [('novelty', 'industry_opinion', '用蛋白质+碳水的营养组合,覆盖夜宵、加班等碎片化场景')], '康师傅与伊利跨界场景设想,不认定营养作用成立')
  80. foodaily(39041, [('premium', 'industry_report', '统一先后推出满汉大餐、汤达人的“极味馆”系列')], '高端品牌图谱,销量与增速为原文引用')
  81. foodaily(38656, [('wellness', 'chinese_ingredient_report', '南洋叻沙风味、当归鸡风味和韩式泡菜风味'),
  82. ('novelty', 'industry_report', '推出新品旺旺Q米面')], '当归鸡仅为风味/商家配方定位,不确认有效添加剂量')
  83. foodaily(37891, [('novelty', 'industry_opinion', '这么大一个口味的市场被让了出来')], '酸味需求与产品创新观点,不确认文章中的燃脂等功效')
  84. foodaily(37136, [('novelty', 'industry_report', '白象把香菜口味方便面打造成爆品和宣传口')], '口味差异化案例,保留观点与事实的边界')
  85. foodaily(37590, [('wellness', 'nutrition_process_reference', '使用膳食纤维和蛋白粉替代面条中心的面粉')], '海外营养强化与风味技术参考,不列为中式草本原件')
  86. foodaily(37043, [('premium', 'industry_report', '面饼+调料共106g的常规装桶面卖5.8元')], '原文渠道价格和质价比讨论,不认定当前成交价')
  87. foodaily(35809, [('premium', 'industry_report', '将含肉量增加到80g'),
  88. ('novelty', 'industry_report', '重庆小面、贵州酸汤面、台式牛肉面')], '冷冻即食面产品报道;计划推出与已经上市不混用')
  89. foodaily(35248, [('premium', 'nutrition_process_reference', '消费者只需拆袋加热至沸腾即可食用,全程不用加水')], '冷冻整餐工艺与复原体验;国家专利数未独立核验')
  90. foodaily(33973, [('stockpile', 'historical_market_context', '外食餐饮的迅速恢复对依赖居家场景的速食行业产生冲击')], '2023年居家消费场景变化,非2026年现况')
  91. foodaily(30909, [('stockpile', 'historical_market_context', '消费者囤积生活物资的行为会逐渐消失')], '2022年行业人士预测,保留为历史对照,不当作已验证结论')
  92. foodaily(29458, [('stockpile', 'storage_tradeoff_report', '拉面说保质期较短,难以储存'),
  93. ('premium', 'price_convenience_tradeoff', '价格和性价比也成为她考虑的因素')], '媒体转述的储存、煮制和份量体验,人物不转算购买人数')
  94. foodaily(20075, [('premium', 'industry_report', '高端非油炸方便面——老范家速食面馆面'),
  95. ('wellness', 'nutrition_process_reference', '用非油炸的蒸煮技术开辟方便面新品类')], '2019年产品技术与海外场景报道,不当作当前增长证据')
  96. foodaily(16000, [('wellness', 'nutrition_process_reference', '减盐方便面')], '2017年日本控盐/控糖技术方向资料,非中式养生人群证据')
  97. foodaily(42345, [('novelty', 'industry_report', '辛拉面泡菜辣炒面与辛拉面Rosé罗杰奶油番茄拌面')], '新口味与炒制香气产品案例')
  98. foodaily(40264, [('premium', 'industry_report', '把餐厅的招牌味道搬进家庭厨房')], '包装冷冻面与家庭复原,不采用堂食店铺数据作样本')
  99. approve(yicai_url, [('wellness', 'chinese_ingredient_report', '荞麦、山药、青稞、猴头菇、藜麦')], '记者走访报道;明确添加量和真实作用待证,不复制功效结论')
  100. approve('https://www.163.com/dy/article/K5N9D6110511MKLQ.html',
  101. [('wellness', 'chinese_ingredient_opinion', '拉面说的这两款和999联名的虫草花鸡汤面和草本猪肚鸡面')],
  102. '保留原文转载署名及评论性质;药材功效和配方说法未核实,不做健康建议')
  103. ids, fingerprints = set(), set()
  104. for row in selected:
  105. assert row['sourceId'] not in ids and row['bodySha256'] not in fingerprints
  106. ids.add(row['sourceId']); fingerprints.add(row['bodySha256'])
  107. raw = (WORK / row['collectionDirectory'] / row['httpReceipt']['responseFile']).read_bytes()
  108. assert base.digest(raw) == row['httpReceipt']['sha256']
  109. assert row['publishedAtRaw'], 'ORIGINAL_PUBLICATION_DATE_REQUIRED'
  110. selected_urls = {r['url'] for r in selected}
  111. excluded = [{'url': row['url'], 'title': row['title'],
  112. 'reason': 'Not selected: duplicate/rephrased reporting, weak topic connection, generic promotion, mixed unrelated article content, or no specific new research evidence.'}
  113. for row in rows.values() if row['url'] not in selected_urls]
  114. counts = {t['id']: sum(any(e['topic'] == t['id'] for e in row['topics']) for row in selected) for t in base.PLAN['topics']}
  115. result = {'schema': 'ksf.five-topic.reviewed-supplement.v1', 'reviewedAt': base.now(), 'selected': selected,
  116. 'excluded': excluded, 'countsByTopic': counts, 'acceptedUniqueArticles': len(selected),
  117. 'limits': ['主题关联基于列出的实际段落,全文保留各原作者/媒体声明,不将其全部认定为事实',
  118. '养生含草本/食材报道、菌菇风味体验与营养工艺参考,不能合并推断养生功效',
  119. '独立URL不等于独立证据或独立消费者;同一产品报道可共享来源',
  120. '历史居家场景与预测仅作对照,不等同当前囤货需求统计']}
  121. base.save(WORK / 'reviewed-selection.json', result)
  122. print(json.dumps({'ok': True, 'uniqueArticles': len(selected), 'countsByTopic': counts,
  123. 'sourceTypes': {p: sum(r['platform'] == p for r in selected) for p in sorted({r['platform'] for r in selected})}}, ensure_ascii=False))
  124. if __name__ == '__main__': main()