| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- """Curated selection with replayable text boundaries and exact topic quotations."""
- import importlib.util
- import json
- from pathlib import Path
- import re
- import urllib.parse
- from lxml import html
- ROOT=Path(__file__).resolve().parents[1]
- WORK=ROOT/'work/five-topic-research-20261111'
- spec=importlib.util.spec_from_file_location('base_review',ROOT/'scripts/collect-five-topic-public.py')
- b=importlib.util.module_from_spec(spec);spec.loader.exec_module(b)
- def lines_select(row, start=None, stop=None, indexes=None):
- lines=row['body'].splitlines()
- if indexes is None:
- first=next((i for i,line in enumerate(lines) if start in line),None) if start else 0
- assert first is not None,('START_NOT_FOUND',start)
- last=next((i for i,line in enumerate(lines[first+1:],first+1) if stop in line),len(lines)) if stop else len(lines)
- indexes=list(range(first,last))
- row['paragraphIndexes']=indexes
- row['body']='\n'.join(lines[i] for i in indexes)
- row['bodySha256']=b.digest(row['body'].encode())
- return row
- def topic(row, key, phrase, relation):
- paragraph=next((line for line in row['body'].splitlines() if phrase in line),None)
- assert paragraph,('QUOTE_NOT_FOUND',row['url'],phrase)
- row['topics'].append({'topic':key,'relation':relation,'passages':[paragraph]})
- def main():
- rows=json.loads((WORK/'reextracted-sources.json').read_bytes())
- rows+=json.loads((WORK/'focused-collection-result.json').read_bytes())['sources']
- rows+=json.loads((WORK/'experience-collection-result.json').read_bytes())['sources']
- chosen=[]
- def choose(part, kind, note):
- original=next(x for x in rows if part in x['url']);r=json.loads(json.dumps(original))
- raw=(WORK/'sources'/r['httpReceipt']['responseFile']).read_bytes();assert b.digest(raw)==r['httpReceipt']['sha256']
- tree=html.fromstring(raw.decode(r['sourceEncoding']))
- if r['platform']=='smzdm':
- r['bodyExtractionSelector']='//article';assert len(tree.xpath('//article'))==1
- r['body']=b.paragraphs(tree.xpath('//article')[0])
- r['postId']=re.search(r'/(?:talk/)?p/([a-z0-9]+)/',r['url'])[1]
- if 'sohu.com' in r['url']:
- r['platform']='sohu';r['postId']=re.search(r'/a/([0-9_]+)',r['url'])[1]
- r['publishedAtRaw']=tree.xpath('//meta[@property="og:release_date"]/@content')[0]
- if 'sina.cn' in r['url']:
- r['platform']='sina';r['postId']=re.search(r'detail-([a-z0-9]+)\.d',r['url'])[1]
- r['url']=r['url'].split('?')[0]
- r.update(entity='social',id=r['platform']+':'+r['postId'],sourceType=kind,reviewNote=note,topics=[],contentScope='packaged_noodles')
- r['bodySha256']=b.digest(r['body'].encode());chosen.append(r);return r
- r=choose('/a30dq7kd/','community_experience','宿舍冲泡耗时和藤椒、麻辣口味体验;身份和配方未独立核实。')
- topic(r,'student','泡个十多分钟','dormitory_preparation_time')
- topic(r,'novelty','藤椒味','pepper_flavor_trial')
- r=choose('/aov0k0on/','community_experience','5连包、面饼保护和分步加料体验;非油炸和重量仅为作者表述,不转为商品规格。')
- topic(r,'student','先泡面饼','hot_water_preparation')
- topic(r,'novelty','兰州拉面','regional_flavor_case')
- topic(r,'stockpile','5连包','multipack_protection')
- r=choose('/akko2xnr/','community_experience','保留短帖真实分量和口味偏好,不扩写为调查或购买评价。')
- topic(r,'student','一袋半','portion_preference')
- r=choose('/aevdzmom/','community_experience','宿舍饱腹与泡椒酸辣偏好;不据此推断学生总体偏好。')
- topic(r,'student','在宿舍','dormitory_satiety')
- topic(r,'novelty','泡椒','sour_spicy_flavor_case')
- r=choose('/a20o44kn/','community_experience','晚自习后进食和宿舍整箱储备;单帖不推算人数或市场份额。')
- topic(r,'student','晚自习','after_class_late_night')
- topic(r,'stockpile','各屯了一箱','dormitory_case_stocking')
- r=choose('/arqpdepg/','community_experience','重点保留汤味偏好和喝汤后口渴体验;历史与营养泛论不是研究结论。')
- lines_select(r,start='方便面已经成为')
- r['contentScope']='packaged_noodles_excerpt';r['title']+='(食用体验摘录)'
- topic(r,'student','夜宵','late_night_soup_experience')
- topic(r,'wellness','觉得渴','adjacent_health_taste_sodium_hypothesis')
- r=choose('/736876/','community_experience','2018年冻干牛肉面体验;工艺、健康宣传和历史报价均未独立核实,非当前售价或功效证据。')
- topic(r,'student','等待5分钟','late_night_hot_water_preparation')
- topic(r,'premium','大片蔬菜','freeze_dried_texture_experience')
- r=choose('/85734155_119756','editorial_comparative_review','2016年30款评测的评分框架与具体体验,作历史设计参考;非当前榜单,未核实每一款商品规格。')
- topic(r,'student','深夜','late_night_historical_comparison')
- topic(r,'novelty','辣白菜','regional_flavor_historical_comparison')
- topic(r,'premium','工具方便度','comparative_review_dimensions')
- r=choose('detail-imzzcerw1177484','industry_product_process_reporting','2023年上观新闻转载;熟面急冻、高汤和一体化是报道声明,专利与参数未逐一独立验证;需要冷链和加热,不是宿舍常温开水即食方案。')
- topic(r,'premium','4-6个小时','frozen_soup_process_reporting')
- topic(r,'novelty','全程不用加水','frozen_integrated_format_case')
- r=choose('/articles/41519','industry_reporting_or_opinion','保留DT商业观察署名;新品、财务、份额和消费者引语均是报道口径,不新增为电商商品或独立评论。')
- # Exclude publisher event promotions, retaining the reported article body.
- promo=next((line for line in r['body'].splitlines() if 'Foodaily创新博览会' in line),None)
- if promo:lines_select(r,stop=promo)
- topic(r,'premium','隔热包装','packaging_and_real_ingredient_design')
- topic(r,'novelty','地域风味上','regional_flavor_portfolio')
- topic(r,'student','年轻人的夜宵场景','late_night_scene_reporting')
- topic(r,'stockpile','家庭消费场景','family_channel_positioning')
- r=choose('/articles/25010','industry_reporting_or_opinion','2021年深燃历史报道,用于高端溢价与价值感取舍;所列售价、销量和市场规模均非本轮实时验证。')
- promo=next((line for line in r['body'].splitlines() if 'Foodaily创新博览会' in line),None)
- if promo:lines_select(r,stop=promo)
- topic(r,'premium','原材料上下功夫','historical_premium_value_tradeoff')
- r=choose('/articles/42324','industry_product_news_excerpt','只摘录盒马冻干方便面小节;其他饮料与招商文不入库。配方、温度、冻干和热风干燥均为报道声明,并非实测工艺参数。')
- lines=r['body'].splitlines();indexes=[i for i,line in enumerate(lines) if '据官方介绍,新品冻干方便面' in line]
- assert len(indexes)==1;lines_select(r,indexes=indexes)
- r['title']='盒马上新冻干方便面系列(Foodaily新品周报原文摘录)';r['contentScope']='packaged_noodles_excerpt'
- topic(r,'premium','汤菜一体','integrated_freeze_dried_format_reporting')
- topic(r,'novelty','番茄炒蛋','new_flavor_lineup_reporting')
- r=choose('/articles/41662','industry_product_news_excerpt','只摘录方便面新品两段;原文减卡、减脂和高纤为品牌经媒体转述的营养声称,不证明减重功效,未核对实物标签。')
- lines=r['body'].splitlines();indexes=[i for i,line in enumerate(lines) if line.startswith('4月27日,康师傅推出') or line.startswith('历经几十年发展,方便面行业')]
- assert len(indexes)==2;lines_select(r,indexes=indexes)
- r['title']='康师傅上新红烧牛肉面减脂高纤面(Foodaily热闻原文摘录)';r['contentScope']='packaged_noodles_excerpt'
- topic(r,'wellness','0油炸','adjacent_health_non_fried_fiber_claim_case')
- r=choose('/articles/42270','industry_interview_excerpt','只选居家煮面需求段,排除餐厅打卡段;报道人物均为化名,作者中产判断不是经验证的收入分层或总体需求。')
- lines=r['body'].splitlines();start=lines.index('“过日子”的最优解')
- end=next(i for i,line in enumerate(lines) if line.startswith('参考资料:'))
- lines_select(r,indexes=[0,1]+list(range(start,end)))
- r['title']='中产“过日子人”,爱上高阶泡面(居家需求段落摘录)';r['contentScope']='packaged_noodles_excerpt'
- topic(r,'premium','性价比最高','home_meal_convenience_value_tradeoff')
- # Government publications are documents, not social posts or mandatory standards.
- for host,selector,docid,status,title in [
- ('ganxian.gov.cn',"//*[@id='zoomcon']",'gx-51706ef906154551b70187447738432f','official_explanation_republication','解读《关于党参等9种新增按照传统既是食品又是中药材的物质公告》(2023年第9号)'),
- ('baiyin.gov.cn',"//div[contains(concat(' ',normalize-space(@class),' '),' content ')]",'baiyin-0eece4641557434f8f67a48c85112ebc','consumer_guidance','解码食品科技——正确认识预包装食品保质期'),
- ('scjgj.km.gov.cn',"//*[contains(concat(' ',normalize-space(@class),' '),' articleContMain ')]",'kunming-4719685','consumer_guidance_republication','减盐食品不宜敞开吃')]:
- receipts=[(f,json.loads(f.read_bytes())) for f in (WORK/'sources').glob('*.http.json')]
- f,receipt=next((f,r) for f,r in receipts if host in r['requestedUrl'] and r['httpStatus']==200)
- raw=(f.parent/receipt['responseFile']).read_bytes();assert b.digest(raw)==receipt['sha256']
- tree=html.fromstring(raw.decode('utf8'));nodes=tree.xpath(selector);assert len(nodes)==1
- body=b.paragraphs(nodes[0]);date=tree.xpath('//meta[@name="PubDate"]/@content')
- assert date
- notes={'pageMetaPubDate':date[0]}
- if host=='baiyin.gov.cn':
- visible=re.search(r'发布时间:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})',tree.text_content())
- assert visible;date=[visible[1]];notes['visiblePublishedAt']=date[0];notes['dateDiscrepancyRetained']=True
- row={'entity':'documents','id':docid,'platform':'document','postId':docid,'title':title,'body':body,'bodySha256':b.digest(body.encode()),
- 'url':receipt['finalUrl'],'publishedAtRaw':date[0],'sourceEncoding':'utf8','bodyExtractionSelector':selector,
- 'collectionDirectory':'sources','httpReceipt':receipt,'declaredStatus':status,'sourceType':'official_explanation' if host=='ganxian.gov.cn' else 'official_consumer_guidance',
- 'reviewNote':'政府网站正文;属于解读或消费科普,不是新增国家标准或产品认证。转载发布时间与原公告年份分开保留。',
- 'dateEvidence':notes,'topics':[],'clauses':[],'limits':['非国家标准全文,不据科普单篇确认法规现行性;引用原标准时须结合研究库既有标准及生效日期复核。']}
- if host=='ganxian.gov.cn':
- topic(row,'wellness','上述物质作为食药物质','traditional_ingredient_application_boundary')
- row['clauses']=[{'name':'党参传统食用方式及同用限制','phrase':'党参。在','application':'中式汤底原料候选;需要核对植物基原、食用方式和产品标签,不视为疗效依据。'},
- {'name':'食药物质适量食用与特殊人群边界','phrase':'上述物质作为食药物质','application':'研发需明确适用人群;不得把食药目录等同普通食品功效许可。'}]
- elif host=='baiyin.gov.cn':
- topic(row,'stockpile','按需采购,尽快食用','storage_packaging_and_opening')
- row['clauses']=[{'name':'标签贮存条件与合理储存','phrase':'1.查看标签','application':'囤货规格与家庭储存方案需配套温湿度、避光等标签条件。'},
- {'name':'开封后的密封体系变化','phrase':'3.按需采购','application':'比较单份独立包装与大包装开封后的使用便利;保质期必须实测验证。'}]
- else:
- topic(row,'wellness','减盐食品也不一定是低钠食品','adjacent_health_sodium_claim_boundary')
- row['clauses']=[{'name':'减盐与低钠的差别','phrase':'阮光锋分析说','application':'是2023年媒体科普转载;相对减少与绝对低钠的阈值应回到适用标准验证,不能把减盐说成低钠。'},
- {'name':'钠含量的每100克与每份口径','phrase':'所以,科学减盐需要','application':'汤料研发比较须统一每份重量和营养表单位;不据主观咸淡判断钠含量。'}]
- for c in row['clauses']:
- c['excerpt']=next(line for line in body.splitlines() if c['phrase'] in line);assert c['excerpt'] in body
- chosen.append(row)
- assert len({r['id'] for r in chosen})==len(chosen)
- for r in chosen:
- assert r['topics'] and all(p in r['body'] for t in r['topics'] for p in t['passages'])
- result={'schema':'ksf.five-topic.reviewed-multisource.v1','selected':chosen,
- 'countsByTopic':{t:sum(any(v['topic']==t for v in r['topics']) for r in chosen) for t in ['wellness','student','premium','stockpile','novelty']},
- 'limits':['跨课题关联不重复计算总资料数;政策条款是同一文件的摘录,不额外算独立文件。',
- '非油炸、高纤、控钠是中式养生的相邻健康化研究,不等同中医功效或食药原料合法性。',
- '本轮未取得新增电商商品和购买评价;公开体验不冒称电商评价。',
- '所有历史资料保留日期,只作历史比较,不直接作为2026年市场趋势。']}
- b.save(WORK/'reviewed-selection.json',result)
- print(json.dumps({'selected':len(chosen),'social':sum(r['entity']=='social' for r in chosen),'documents':sum(r['entity']=='documents' for r in chosen),'clauses':sum(len(r.get('clauses',[])) for r in chosen),'topics':result['countsByTopic']},ensure_ascii=False))
- if __name__=='__main__':main()
|