#!/usr/bin/env python3 # 从本机 Excel 补齐「数据库没有表」的渠道数据(Adsterra / TrafficStars / Eagllwin 等)。 # 数据库只有 mintegral/bigo/mediago,其余渠道由运营手工维护到 Excel,本脚本读取后 # 输出成与 db_fetch.py 相同结构的 JSON(stdout),供 daily_sync 合并进 mtg_data.json。 # # ── Excel 规范 ────────────────────────────────────────────── # 路径:<运营>/Voypace/Voypace补充数据.xlsx (可用环境变量 VOYPACE_XLSX 覆盖) # 第一个 sheet,首行为表头,需包含以下列(列名大小写/顺序不限): # date 日期,yyyy-MM-dd(或 Excel 日期) # campid 对应 campaigns.json 里的 campid # impression 展示 # clicks 点击 # conversions 转化/安装 # spend 花费(USD) # 可选列:location(国家,缺省用 campaigns.json 里的 geo) # 渠道/产品/包名 一律由 campid 反查 campaigns.json,Excel 无需再填。 # 只读取 campid 命中 campaigns.json 的行;其它行忽略。 # ──────────────────────────────────────────────────────────── import json, sys, os, datetime HERE = os.path.dirname(os.path.abspath(__file__)) _camps = json.load(open(os.path.join(HERE, "campaigns.json"))) CAMPS = {str(r["campid"]): r for r in _camps} DEFAULT_XLSX = os.environ.get( "VOYPACE_XLSX", "/Users/wangyuan/Documents/SHAREit/运营/Voypace/Voypace补充数据.xlsx", ) def norm_date(v): if v is None: return "" if isinstance(v, (datetime.datetime, datetime.date)): return v.strftime("%Y-%m-%d") s = str(v).strip() return s[:10] if s else "" def to_int(v): try: return int(float(str(v).replace(",", "").strip() or 0)) except: return 0 def to_float(v): try: return float(str(v).replace(",", "").replace("$", "").strip() or 0) except: return 0.0 def main(path): if not os.path.exists(path): # 没有补充表就输出空数组,daily_sync 照常跑 print("[]"); return try: import openpyxl except ImportError: sys.stderr.write("openpyxl 未安装,跳过 Excel 补数\n") print("[]"); return wb = openpyxl.load_workbook(path, data_only=True) ws = wb.worksheets[0] rows = list(ws.iter_rows(values_only=True)) if not rows: print("[]"); return header = [str(c).strip().lower() if c is not None else "" for c in rows[0]] def col(*names): for n in names: if n in header: return header.index(n) return None ci = { "date": col("date", "时间", "日期"), "campid": col("campid", "adping id", "adping_id"), "loc": col("location", "地区", "国家"), "imp": col("impression", "impressions", "展示"), "clk": col("clicks", "click", "点击"), "cv": col("conversions", "install", "installs", "转化", "安装"), "spend": col("spend", "花费", "cost"), } out = [] for r in rows[1:]: if ci["campid"] is None: break cid = str(r[ci["campid"]]).strip() if r[ci["campid"]] is not None else "" cid = cid.split(".")[0] # 去掉可能的 .0 if cid not in CAMPS: continue info = CAMPS[cid] loc = "" if ci["loc"] is not None and r[ci["loc"]] is not None: loc = str(r[ci["loc"]]).strip().upper() if not loc: loc = str(info.get("geo", "")).upper() imp = to_int(r[ci["imp"]]) if ci["imp"] is not None else 0 clk = to_int(r[ci["clk"]]) if ci["clk"] is not None else 0 cv = to_int(r[ci["cv"]]) if ci["cv"] is not None else 0 sp = to_float(r[ci["spend"]]) if ci["spend"] is not None else 0.0 dt = norm_date(r[ci["date"]]) if ci["date"] is not None else "" if not dt: continue out.append({ "date": dt, "channel": info["channel"], "campid": cid, "offer_name": f'{cid}_{info["product"]}', "product": info["product"], "pkg": info["pkg"], "location": loc, "bid_type": "", "impression": imp, "clicks": clk, "conversions": cv, "ctr": round(clk/imp, 5) if imp else 0.0, "cvr": round(cv/clk, 5) if clk else 0.0, "spend": round(sp, 2), }) print(json.dumps(out, ensure_ascii=False)) if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_XLSX)