collect-five-topic-focused-20261111.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. """Target gaps with readable original community pages; never publish search hits."""
  2. from concurrent.futures import ThreadPoolExecutor, as_completed
  3. import importlib.util
  4. import json
  5. from pathlib import Path
  6. import re
  7. import urllib.parse
  8. from lxml import html
  9. ROOT = Path(__file__).resolve().parents[1]
  10. spec = importlib.util.spec_from_file_location('multi', ROOT/'scripts/collect-five-topic-multisource.py')
  11. m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
  12. WORK = m.WORK
  13. QUERIES = [
  14. ('student', 'site:post.smzdm.com 宿舍 泡面 测评'),
  15. ('student', 'site:post.smzdm.com 夜宵 拌面 实测'),
  16. ('student', 'site:sspai.com 方便面 测评'),
  17. ('student', 'site:post.smzdm.com 学生 方便面 开水'),
  18. ('novelty', 'site:post.smzdm.com 香菜 方便面 测评'),
  19. ('novelty', 'site:post.smzdm.com 冬阴功 泡面 试吃'),
  20. ('novelty', 'site:post.smzdm.com 藤椒 拌面 测评'),
  21. ('stockpile', 'site:post.smzdm.com 囤货 方便面 保质期'),
  22. ('wellness', 'site:post.smzdm.com 菌菇 非油炸 方便面'),
  23. ]
  24. def search(job):
  25. i,(topic,q) = job
  26. url='https://www.so.com/s?'+urllib.parse.urlencode({'q':q})
  27. raw,r=m.base.fetch(url,'focused-'+str(i),m.SEARCH)
  28. tree=html.fromstring(raw.decode('utf8',errors='replace'))
  29. if r['httpStatus']!=200 or re.search('验证码|访问验证',tree.xpath('string(//title)')):
  30. raise ValueError('SEARCH_UNAVAILABLE')
  31. rows={}
  32. for a in tree.xpath('//a[@data-mdurl]'):
  33. u=a.get('data-mdurl'); title=m.text(a)
  34. if not u or not re.search('方便面|泡面|拌面|拉面|火鸡面|合味道|汤达人',title):continue
  35. if not re.match(r'https://(?:post\.smzdm\.com/(?:talk/)?p/|sspai\.com/post/)',u):continue
  36. rows[u]={'url':u,'title':title,'topic':topic,'query':q,'searchReceipt':'focused-'+str(i)+'.http.json'}
  37. m.base.save(m.SEARCH/('focused-'+str(i)+'.results.json'),{'query':q,'httpReceipt':r,'results':list(rows.values())})
  38. return list(rows.values())
  39. def main():
  40. prior=json.loads((ROOT/'work/library-cleanup-20261111/round-02/inventory-after-result.json').read_bytes())['body']['result']
  41. known={m.canonical(r.get('url') or '') for r in prior['social']}
  42. state={'status':'running','searches':[],'sources':[],'rejected':[]};candidates={}
  43. with ThreadPoolExecutor(max_workers=3) as pool:
  44. tasks={pool.submit(search,j):j for j in enumerate(QUERIES,1)}
  45. for f in as_completed(tasks):
  46. try:
  47. rows=f.result();state['searches'].append({'query':tasks[f][1][1],'found':len(rows)})
  48. for r in rows:
  49. if m.canonical(r['url']) not in known:candidates.setdefault(r['url'],r)
  50. except Exception as e:state['rejected'].append({'job':tasks[f],'reason':str(e)})
  51. print(json.dumps({'candidates':len(candidates)}),flush=True)
  52. with ThreadPoolExecutor(max_workers=2) as pool:
  53. tasks={pool.submit(m.collect,r):r for r in candidates.values()}
  54. for f in as_completed(tasks):
  55. try:state['sources'].append(f.result())
  56. except Exception as e:state['rejected'].append({'url':tasks[f]['url'],'reason':str(e)})
  57. m.base.save(WORK/'focused-collection-result.json',state)
  58. print(json.dumps({'readable':len(state['sources']),'rejected':len(state['rejected'])}),flush=True)
  59. state.update(status='awaiting-source-review',candidateCount=len(candidates));m.base.save(WORK/'focused-collection-result.json',state)
  60. if __name__=='__main__':main()