Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import gc | |
| import traceback | |
| from contextlib import contextmanager | |
| from datetime import datetime | |
| from pathlib import Path | |
| from threading import Thread | |
| from typing import Any | |
| from uuid import uuid4 | |
| import pandas as pd | |
| from werkzeug.datastructures import FileStorage | |
| from werkzeug.utils import secure_filename | |
| ALLOWED_EXTENSIONS = {"xlsx", "xls", "xlsm", "csv"} | |
| analysis_tasks: dict[str, dict[str, Any]] = {} | |
| KEYWORD_INDEX = 3 | |
| MATCH_TYPE_INDEX = 5 | |
| SPEND_INDEX = 14 | |
| SALES_INDEX = 15 | |
| class PlacementSummaryError(Exception): | |
| pass | |
| class AnalysisTaskNotFound(Exception): | |
| pass | |
| def allowed_file(filename: str) -> bool: | |
| return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS | |
| def safe_table_reader(file_path: Path): | |
| dataframe: pd.DataFrame | None = None | |
| suffix = file_path.suffix.lower() | |
| try: | |
| if suffix == ".csv": | |
| dataframe = pd.read_csv(file_path) | |
| else: | |
| dataframe = pd.read_excel(file_path, sheet_name=0) | |
| yield dataframe | |
| finally: | |
| if dataframe is not None: | |
| del dataframe | |
| gc.collect() | |
| def start_analysis_task(uploaded_file: FileStorage, upload_dir: Path, output_dir: Path) -> str: | |
| filename = uploaded_file.filename or "" | |
| if not allowed_file(filename): | |
| raise PlacementSummaryError("仅支持上传 .xlsx、.xls、.xlsm 或 .csv 文件。") | |
| timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") | |
| safe_name = secure_filename(filename) or f"placement_{timestamp}.xlsx" | |
| upload_path = upload_dir / f"{timestamp}_{safe_name}" | |
| uploaded_file.save(upload_path) | |
| task_id = uuid4().hex | |
| analysis_tasks[task_id] = { | |
| "status": "processing", | |
| "result": None, | |
| "filename": upload_path.name, | |
| "start_time": datetime.now().isoformat(), | |
| } | |
| Thread( | |
| target=_process_analysis, | |
| kwargs={ | |
| "task_id": task_id, | |
| "file_path": upload_path, | |
| "output_dir": output_dir, | |
| }, | |
| daemon=True, | |
| ).start() | |
| return task_id | |
| def _process_analysis(task_id: str, file_path: Path, output_dir: Path) -> None: | |
| try: | |
| result = analyze_placement_data(file_path=file_path, output_dir=output_dir) | |
| analysis_tasks[task_id]["status"] = "completed" | |
| analysis_tasks[task_id]["result"] = result | |
| analysis_tasks[task_id]["end_time"] = datetime.now().isoformat() | |
| except Exception as exc: | |
| traceback.print_exc() | |
| analysis_tasks[task_id]["status"] = "error" | |
| analysis_tasks[task_id]["result"] = { | |
| "success": False, | |
| "error": f"分析过程中出错: {exc}", | |
| } | |
| analysis_tasks[task_id]["end_time"] = datetime.now().isoformat() | |
| finally: | |
| file_path.unlink(missing_ok=True) | |
| def analyze_placement_data(file_path: Path, output_dir: Path) -> dict[str, Any]: | |
| with safe_table_reader(file_path) as dataframe: | |
| if dataframe.shape[1] <= SALES_INDEX: | |
| return { | |
| "success": False, | |
| "error": "源文件列数不足,至少需要包含到 P 列。", | |
| } | |
| working = dataframe.copy() | |
| column_names = [str(col).strip() for col in working.columns] | |
| working.columns = column_names | |
| extracted = pd.DataFrame( | |
| { | |
| "关键词": working.iloc[:, KEYWORD_INDEX], | |
| "匹配方式": working.iloc[:, MATCH_TYPE_INDEX], | |
| "花费": working.iloc[:, SPEND_INDEX], | |
| "广告销售额": working.iloc[:, SALES_INDEX], | |
| } | |
| ) | |
| extracted["关键词"] = extracted["关键词"].fillna("").astype(str).str.strip() | |
| extracted["匹配方式"] = extracted["匹配方式"].fillna("").astype(str).str.strip() | |
| extracted["花费"] = pd.to_numeric(extracted["花费"], errors="coerce").fillna(0) | |
| extracted["广告销售额"] = pd.to_numeric(extracted["广告销售额"], errors="coerce").fillna(0) | |
| extracted = extracted[extracted["关键词"] != ""].copy() | |
| if extracted.empty: | |
| return { | |
| "success": False, | |
| "error": "未识别到有效的关键词数据,请检查 D 列是否有内容。", | |
| } | |
| summary = ( | |
| extracted.groupby(["关键词", "匹配方式"], as_index=False, dropna=False) | |
| .agg({"花费": "sum", "广告销售额": "sum"}) | |
| .sort_values(["花费", "广告销售额"], ascending=[False, False]) | |
| .reset_index(drop=True) | |
| ) | |
| summary.insert(0, "排名", range(1, len(summary) + 1)) | |
| summary["ACOS"] = summary.apply( | |
| lambda row: "有点击不出单" | |
| if row["广告销售额"] == 0 | |
| else f"{(row['花费'] / row['广告销售额']):.2%}", | |
| axis=1, | |
| ) | |
| enriched = extracted.merge( | |
| summary[["关键词", "匹配方式", "花费", "广告销售额", "ACOS"]], | |
| on=["关键词", "匹配方式"], | |
| how="left", | |
| suffixes=("_原始", "_汇总"), | |
| ) | |
| total_spend = float(summary["花费"].sum()) | |
| total_sales = float(summary["广告销售额"].sum()) | |
| valid_acos = summary[summary["广告销售额"] > 0].copy() | |
| average_acos = ( | |
| (valid_acos["花费"].sum() / valid_acos["广告销售额"].sum()) | |
| if not valid_acos.empty and valid_acos["广告销售额"].sum() > 0 | |
| else None | |
| ) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| output_filename = f"投放词汇总_{timestamp}.xlsx" | |
| output_path = output_dir / output_filename | |
| summary_stats = pd.DataFrame( | |
| { | |
| "指标": [ | |
| "有效原始行数", | |
| "关键词+匹配方式组合数", | |
| "总花费", | |
| "总广告销售额", | |
| "平均ACOS", | |
| ], | |
| "数值": [ | |
| f"{len(extracted):,}", | |
| f"{len(summary):,}", | |
| f"¥{total_spend:,.2f}", | |
| f"¥{total_sales:,.2f}", | |
| f"{average_acos:.2%}" if average_acos is not None else "有点击不出单", | |
| ], | |
| } | |
| ) | |
| with pd.ExcelWriter(output_path, engine="openpyxl") as writer: | |
| dataframe.to_excel(writer, sheet_name="原始数据", index=False) | |
| summary.to_excel(writer, sheet_name="投放词汇总", index=False) | |
| enriched.to_excel(writer, sheet_name="原始数据_带汇总", index=False) | |
| summary_stats.to_excel(writer, sheet_name="数据摘要", index=False) | |
| return { | |
| "success": True, | |
| "output_file": str(output_path), | |
| "output_filename": output_filename, | |
| "stats": { | |
| "total_rows": int(len(extracted)), | |
| "total_groups": int(len(summary)), | |
| "total_spend": total_spend, | |
| "total_sales": total_sales, | |
| "avg_acos": f"{average_acos:.2%}" if average_acos is not None else "有点击不出单", | |
| }, | |
| "top_terms": summary.head(10).to_dict("records"), | |
| } | |
| def get_task_status(task_id: str) -> dict[str, Any]: | |
| if task_id not in analysis_tasks: | |
| raise AnalysisTaskNotFound("任务不存在。") | |
| task = analysis_tasks[task_id] | |
| return { | |
| "status": task["status"], | |
| "result": task["result"], | |
| } | |