#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 步骤 1:运行 main_voc分析.py 的 step 1–6(跳过 step 7 报告生成),产出 SQLite 数据库。 用法:: # 全流程(config.yaml 留空则 LLM 自动识别产品名和行业) ../310py/bin/python run_pipeline.py --input-dir "../Bikini trimmer voc" # 手动指定产品/行业(跳过 LLM 识别) ../310py/bin/python run_pipeline.py --input-dir "../Bikini trimmer voc" --product "Bikini Trimmer" --industry "个人护理" # 断点续跑(从 step 4 向量化开始;前提是 step 1–3 的 sqlite 产物已存在) ../310py/bin/python run_pipeline.py --from-step 4 # 仅重跑词频(step 6) ../310py/bin/python run_pipeline.py --from-step 6 # 仅重跑聚类(step 5) ../310py/bin/python run_pipeline.py --from-step 5 内部调用等价于(以全流程为例):: ../310py/bin/python ../main_voc分析.py --only-step 1 --input-dir "..." --product "..." --industry "..." ../310py/bin/python ../main_voc分析.py --only-step 2 --product "..." --industry "..." ...(依此类推到 step 6) """ from __future__ import annotations import argparse import logging import os import subprocess import sys from pathlib import Path import yaml logger = logging.getLogger("voc.run_pipeline") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_ROOT = SCRIPT_DIR.parent CONFIG_FILE = SCRIPT_DIR / "config.yaml" def load_config() -> dict: with CONFIG_FILE.open(encoding="utf-8") as f: return yaml.safe_load(f) or {} def run_step(python_bin: str, main_script: str, step: int, **kwargs) -> bool: """运行单个 --only-step。""" cmd = [ python_bin, main_script, "--only-step", str(step), "--product", kwargs.get("product", "亚马逊商品"), "--industry", kwargs.get("industry", "亚马逊电商"), ] if step == 1 and "input_dir" in kwargs: cmd.extend(["--input-dir", kwargs["input_dir"]]) logger.info("执行 step %s: %s", step, " ".join(cmd)) # 不 capture 输出,子进程日志实时可见,避免长时间步骤看起来像卡死 result = subprocess.run(cmd, cwd=str(PROJECT_ROOT)) if result.returncode != 0: logger.error("Step %s 失败 (exit %s)", step, result.returncode) return False logger.info("Step %s 完成", step) return True def run_build_report( python_bin: str, build_script: str, *, product: str, industry: str, ) -> bool: """运行 build_report.py,子进程日志实时输出到当前终端。""" cmd = [ python_bin, "-u", build_script, "--product", product, "--industry", industry, ] logger.info("执行报告: %s", " ".join(cmd)) env = os.environ.copy() env["PYTHONUNBUFFERED"] = "1" # 显式继承 stdout/stderr,避免长时间 LLM 步骤看起来像卡死 result = subprocess.run( cmd, cwd=str(SCRIPT_DIR), env=env, stdout=sys.stdout, stderr=sys.stderr, ) if result.returncode != 0: logger.error("build_report.py 失败 (exit %s)", result.returncode) return False return True def main(): parser = argparse.ArgumentParser(description="运行 VOC 分析流水线 step 1-6") parser.add_argument("--input-dir", help="原始 CSV 目录") parser.add_argument("--product", help="产品名") parser.add_argument("--industry", help="行业名") parser.add_argument("--from-step", type=int, default=1, choices=range(1, 7)) args = parser.parse_args() cfg = load_config() product = args.product or cfg.get("product_name", "").strip() industry = args.industry or cfg.get("industry", "").strip() # 如果产品名或行业为空,用 LLM 从原始评论中自动识别 need_detect = (not product or product == "亚马逊商品" or not industry or industry == "亚马逊电商") if need_detect: input_dir_raw = args.input_dir or cfg.get("input_dir", "") input_dir_path_raw = (SCRIPT_DIR / input_dir_raw).resolve() if input_dir_raw else None if input_dir_path_raw and input_dir_path_raw.is_dir(): from data_loader import DataLoader samples, dir_name = DataLoader.load_raw_review_samples(input_dir_path_raw, max_samples=50) if samples: import sys as _sys _sys.path.insert(0, str(SCRIPT_DIR)) from llm_analyzer import detect_product_and_industry detected_product, detected_industry = detect_product_and_industry(samples, dir_name) if not product or product == "亚马逊商品": product = detected_product logger.info("LLM 自动识别产品名: %s", product) if not industry or industry == "亚马逊电商": industry = detected_industry logger.info("LLM 自动识别行业: %s", industry) else: if not product: product = "亚马逊商品" if not industry: industry = "亚马逊电商" else: if not product: product = "亚马逊商品" if not industry: industry = "亚马逊电商" input_dir = args.input_dir or cfg.get("input_dir", "") main_script = cfg.get("main_script", "../main_voc分析.py") python_bin = cfg.get("python_bin", "../310py/bin/python") # 解析相对路径 main_script_path = (SCRIPT_DIR / main_script).resolve() python_bin_path = (SCRIPT_DIR / python_bin) # 不 resolve:保留符号链接以确保 uv venv 正确激活 input_dir_path = str((SCRIPT_DIR / input_dir).resolve()) if input_dir else "" if not python_bin_path.exists(): logger.error("Python 解释器不存在: %s", python_bin_path) sys.exit(1) if not main_script_path.is_file(): logger.error("main_voc分析.py 不存在: %s", main_script_path) sys.exit(1) logger.info("产品: %s | 行业: %s | 输入目录: %s", product, industry, input_dir_path) logger.info("从 step %s 开始", args.from_step) steps = list(range(args.from_step, 7)) # step 1-6 only for step in steps: kwargs = {"product": product, "industry": industry} if step == 1: kwargs["input_dir"] = input_dir_path if not run_step(str(python_bin_path), str(main_script_path), step, **kwargs): logger.error("流水线中止于 step %s", step) sys.exit(1) logger.info("流水线 step 1-6 全部完成。数据库已就绪。") # 自动运行 build_report.py(日志实时打印到终端) logger.info("自动运行 build_report.py 生成报告...") build_script = str(SCRIPT_DIR / "build_report.py") if not run_build_report( str(python_bin_path), build_script, product=product, industry=industry, ): sys.exit(1) logger.info("报告生成完成。") if __name__ == "__main__": main()