yyh1112's picture
Upload 30 files
82d4e48 verified
Raw
History Blame Contribute Delete
7.59 kB
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
@contextmanager
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"],
}