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