# -*- coding: utf-8 -*- """ 抖音门店公开信息采集器 v0.1 不装任何第三方库,直接调用本机 Chrome 无头模式,读取渲染后的页面。 只采集前台公开可见的信息(评分、评价数、团购商品与已售数),不登录、不破解、不采个人信息。 用法(在 对标试点 目录下执行): python 02_采集脚本/collect_douyin.py --dump 读取 01_对标店清单.csv,逐个打开门店链接,把渲染后的页面存到 02_采集脚本/dump/ 每页间隔约 4 秒,控制频率 python 02_采集脚本/collect_douyin.py --parse 解析 dump/ 里已保存的页面,生成 03_分析/采集快照_<日期>.csv 注意: 解析规则需要先用 1-2 个真实页面校准,第一次跑请只放 1-2 条链接 """ import csv import glob import hashlib import os import re import subprocess import sys import time from datetime import date BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DUMP_DIR = os.path.join(BASE, '02_采集脚本', 'dump') LIST_CSV = os.path.join(BASE, '01_对标店清单.csv') SNAP_DIR = os.path.join(BASE, '03_分析') CHROME_CANDIDATES = [ r'C:\Program Files\Google\Chrome\Application\chrome.exe', r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', ] CHROME = next((p for p in CHROME_CANDIDATES if os.path.exists(p)), None) def load_links(): """从清单里取出所有不重复的门店链接""" if not os.path.exists(LIST_CSV): sys.exit('找不到 01_对标店清单.csv') seen, out = set(), [] with open(LIST_CSV, encoding='utf-8-sig') as f: for r in csv.DictReader(f): for key in ('本店POI链接', '竞对POI链接'): link = (r.get(key) or '').strip() if link and link not in seen: seen.add(link) out.append((r.get('本店编码', ''), r.get('竞对门店名', ''), link)) return out def dump_pages(): if not CHROME: sys.exit('本机没有找到 Chrome / Edge,请确认安装路径') os.makedirs(DUMP_DIR, exist_ok=True) links = load_links() print(f'共 {len(links)} 个待采集链接') for i, (code, name, link) in enumerate(links, 1): fp = os.path.join(DUMP_DIR, f'{code}_{hashlib.md5(link.encode()).hexdigest()[:8]}.html') if os.path.exists(fp) and os.path.getsize(fp) > 50000: print(f'[{i}/{len(links)}] 已存在,跳过 {name or code}') continue print(f'[{i}/{len(links)}] {name or code} {link}') try: subprocess.run([CHROME, '--headless=new', '--disable-gpu', '--window-size=1200,2400', '--virtual-time-budget=8000', '--dump-dom', link], capture_output=True, timeout=90) # dump-dom 输出到 stdout,这里重跑一次拿文本 res = subprocess.run([CHROME, '--headless=new', '--disable-gpu', '--window-size=1200,2400', '--virtual-time-budget=8000', '--dump-dom', link], capture_output=True, timeout=90) html = (res.stdout or b'').decode('utf-8', 'ignore') if len(html) < 20000: print(' 页面内容过短,可能需要登录或被风控,请人工核对这个链接') with open(fp, 'w', encoding='utf-8') as f: f.write(html) except subprocess.TimeoutExpired: print(' 超时,跳过') time.sleep(4) def num_cn(s): """把 '1.2万' '1000+' 转成数字,解析不了返回 None""" s = s.replace('+', '').replace(',', '').strip() m = re.match(r'^([\d.]+)(万|w)?$', s) if not m: return None v = float(m.group(1)) if m.group(2): v *= 10000 return v def parse_page(html): """首次校准前的草稿规则。跑一次真实 dump 后按实际页面结构重写这里。""" items = [] rating = None m = re.search(r'(\d\.\d)\s*分', html) if m: rating = float(m.group(1)) reviews = None m = re.search(r'(\d+)\s*条评价', html) if m: reviews = int(m.group(1)) # 团购商品: 匹配 "商品名 ... 已售 xxx" 的松散结构,实际结构以真实 dump 为准 for blk in re.findall(r'