user_timeline.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env python3
  2. # coding=utf-8
  3. """单用户完整行为时间线:六张离线日志合并为 Excel。"""
  4. import argparse
  5. from datetime import datetime
  6. from pathlib import Path
  7. import re
  8. import pandas as pd
  9. from odps_module import ODPSClient
  10. EXCLUDED_BUSINESSTYPES = {
  11. "deviceId", "openGIdSuccess", "buttonView", "systemInfo", "videoPlayCancel", "windowView"
  12. }
  13. def sql_text(value):
  14. return str(value).replace("'", "''")
  15. def build_apptype_filter(value):
  16. return "" if value is None else f" AND apptype='{sql_text(value)}'"
  17. def safe_name(value):
  18. return re.sub(r"[^A-Za-z0-9_-]", "_", str(value))
  19. SQL_ALL = """
  20. WITH v AS (
  21. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'video' AS 来源,
  22. businesstype, CAST(NULL AS STRING) AS eventid, machineinfo_system AS 系统, pagesource, CAST(NULL AS STRING) AS endRoutePath, CAST(NULL AS STRING) AS objecttype,
  23. CAST(NULL AS STRING) AS topic, CAST(NULL AS STRING) AS shareid,
  24. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, videoid AS 视频id,
  25. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  26. FROM loghubods.video_action_log_applet
  27. WHERE dt='{day}'{apptype_filter} AND mid='{mc}'
  28. AND businesstype <> 'videoPreView'
  29. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  30. ),
  31. a AS (
  32. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'ad' AS 来源,
  33. businesstype, CAST(eventid AS STRING) AS eventid, GET_JSON_OBJECT(machineinfo, '$.system') AS 系统, pagesource, CAST(NULL AS STRING) AS endRoutePath, CAST(NULL AS STRING) AS objecttype,
  34. CAST(NULL AS STRING) AS topic, CAST(NULL AS STRING) AS shareid,
  35. CAST(NULL AS STRING) AS isAdPlaying, creativecode AS creativeCode, headvideoid AS 视频id,
  36. hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  37. FROM loghubods.ad_action_log_own
  38. WHERE dt='{day}'{apptype_filter} AND machinecode='{mc}'
  39. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  40. ),
  41. p AS (
  42. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'play' AS 来源,
  43. businesstype, CAST(eventid AS STRING) AS eventid, machineinfo_system AS 系统, pagesource, CAST(NULL AS STRING) AS endRoutePath, CAST(NULL AS STRING) AS objecttype,
  44. CAST(NULL AS STRING) AS topic, CAST(NULL AS STRING) AS shareid,
  45. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, videoid AS 视频id,
  46. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  47. FROM loghubods.video_play_log
  48. WHERE dt='{day}'{apptype_filter} AND mid='{mc}'
  49. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  50. ),
  51. s AS (
  52. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'simpleevent' AS 来源,
  53. businesstype, CAST(eventid AS STRING) AS eventid, system AS 系统, pagesource, endroutepath AS endRoutePath, objecttype,
  54. CAST(NULL AS STRING) AS topic, CAST(NULL AS STRING) AS shareid,
  55. GET_JSON_OBJECT(extparams, '$.isAdPlaying') AS isAdPlaying,
  56. GET_JSON_OBJECT(extparams, '$.creativeCode') AS creativeCode, videoid AS 视频id,
  57. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  58. FROM loghubods.simpleevent_log
  59. WHERE dt='{day}'{apptype_filter} AND machinecode='{mc}'
  60. AND (businesstype IS NULL OR businesstype <> 'openGIdError')
  61. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  62. ),
  63. u AS (
  64. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'useractive' AS 来源,
  65. businesstype, CAST(eventid AS STRING) AS eventid, system AS 系统, pagesource, CAST(NULL AS STRING) AS endRoutePath, CAST(NULL AS STRING) AS objecttype,
  66. CAST(NULL AS STRING) AS topic, CAST(NULL AS STRING) AS shareid,
  67. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, CAST(NULL AS STRING) AS 视频id,
  68. CAST(NULL AS STRING) AS hotsencetype, path, subsessionid, sessionid
  69. FROM loghubods.useractive_log
  70. WHERE dt='{day}'{apptype_filter} AND machinecode='{mc}'
  71. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  72. ),
  73. r AS (
  74. SELECT CAST(clienttimestamp AS BIGINT) AS ts, apptype AS 产品apptype, 'share' AS 来源,
  75. type AS businesstype, CAST(eventid AS STRING) AS eventid, CAST(NULL AS STRING) AS 系统, pagesource, CAST(NULL AS STRING) AS endRoutePath, CAST(NULL AS STRING) AS objecttype,
  76. topic, shareid,
  77. CAST(NULL AS STRING) AS isAdPlaying, CAST(NULL AS STRING) AS creativeCode, CAST(NULL AS STRING) AS 视频id,
  78. CAST(NULL AS STRING) AS hotsencetype, CAST(NULL AS STRING) AS path, subsessionid, sessionid
  79. FROM loghubods.user_share_log
  80. WHERE dt='{day}'{apptype_filter} AND machinecode='{mc}'
  81. AND clienttimestamp IS NOT NULL AND clienttimestamp<>''
  82. ),
  83. t AS (
  84. SELECT * FROM v UNION ALL SELECT * FROM a UNION ALL SELECT * FROM p
  85. UNION ALL SELECT * FROM s UNION ALL SELECT * FROM u UNION ALL SELECT * FROM r
  86. )
  87. SELECT t.ts, t.产品apptype, t.来源, t.businesstype, t.eventid, t.系统, t.endRoutePath, t.objecttype, t.pagesource, t.topic, t.shareid,
  88. t.isAdPlaying, t.creativeCode, t.视频id, b.title AS 视频标题,
  89. t.hotsencetype, t.path, t.subsessionid, t.sessionid
  90. FROM t
  91. LEFT JOIN videoods.dim_video b ON t.视频id = b.videoid
  92. ORDER BY t.ts
  93. """
  94. def text(value):
  95. return "" if pd.isna(value) else str(value)
  96. def normalize_device_system(value):
  97. value = text(value).lower()
  98. if "ios" in value or "iphone" in value or "ipad" in value:
  99. return "iOS"
  100. if "android" in value:
  101. return "Android"
  102. return ""
  103. def add_device_info(df, group_column=None):
  104. df["机型信息"] = df["系统"].map(normalize_device_system)
  105. if group_column is None:
  106. known = df.loc[df["机型信息"] != "", "机型信息"].drop_duplicates()
  107. if len(known) == 1:
  108. df.loc[df["机型信息"] == "", "机型信息"] = known.iloc[0]
  109. else:
  110. known = df.loc[df["机型信息"] != "", [group_column, "机型信息"]].drop_duplicates()
  111. systems = known.groupby(group_column)["机型信息"].agg(lambda values: values.iloc[0] if len(values) == 1 else "")
  112. df.loc[df["机型信息"] == "", "机型信息"] = df.loc[df["机型信息"] == "", group_column].map(systems).fillna("")
  113. df = df.drop(columns="系统")
  114. df.insert(df.columns.get_loc("来源") + 1, "机型信息", df.pop("机型信息"))
  115. return df
  116. EVENTID_BEHAVIOR = {
  117. "107001": "视频封面加载完成", "107002": "视频封面加载失败", "130010": "广告组件加入页面",
  118. "22022221": "详情接口请求成功", "22022222": "详情接口请求失败", "550001": "用户截图",
  119. }
  120. def behavior_definition(row):
  121. source = text(row["来源"])
  122. businesstype = text(row["businesstype"])
  123. pagesource = text(row["pagesource"])
  124. eventid = text(row.get("eventid", ""))
  125. objecttype = text(row.get("objecttype", ""))
  126. topic = text(row.get("topic", ""))
  127. is_head_video_page = pagesource.endswith("user-videos-share")
  128. if source == "video" and is_head_video_page:
  129. return {"videoView": "头部视频曝光", "videoPlay": "头部视频播放"}.get(businesstype, "")
  130. if source == "ad":
  131. return {"adRequest": "广告请求", "adLoaded": "广告加载", "adView": "广告曝光", "adPlay": "广告播放", "adCloseBtnTap": "广告关闭"}.get(businesstype, "")
  132. if source == "play":
  133. return {"videoPlaySuccess": "播放成功", "videoPlaySlow": "播放卡顿", "videoRealPlay": "有效播放"}.get(businesstype, "")
  134. if source == "share" and topic == "click":
  135. return "点击卡片"
  136. if source == "useractive" and businesstype == "path":
  137. return "打开应用"
  138. if source == "simpleevent":
  139. if businesstype == "buttonClick":
  140. definition = {"videoBackIcon": "点击视频页返回图标", "weapp_quitbutton": "从分享视频页返回首页"}.get(objecttype, "")
  141. if definition:
  142. return definition
  143. if businesstype == "pageView" and pagesource.endswith("category_55"):
  144. return "首页分类页曝光(分类55)"
  145. if is_head_video_page:
  146. definition = {"pageView": "视频分享页曝光", "detailRequest": "视频分享页详情接口请求成功", "userCaptureScreen": "用户在视频分享页截图"}.get(businesstype, "")
  147. if definition:
  148. return definition
  149. definition = {"userPause": "视频暂停", "userActiveEnd": "小程序进入后台", "userActiveStart": "小程序进入前台", "decideSharePageJump": "准备跳转", "jumpSwiperPage": "跳转到沉浸式"}.get(businesstype, "")
  150. if definition:
  151. return definition
  152. return EVENTID_BEHAVIOR.get(eventid, "")
  153. def main():
  154. parser = argparse.ArgumentParser(description="查询单用户离线行为时间线")
  155. parser.add_argument("user_id", help="machinecode/mid")
  156. parser.add_argument("date", help="yyyyMMdd")
  157. parser.add_argument("apptype", nargs="?", default=None, help="可选;不传则查询全部产品")
  158. parser.add_argument("--output-dir", type=Path, default=Path("."))
  159. args = parser.parse_args()
  160. datetime.strptime(args.date, "%Y%m%d")
  161. df = ODPSClient().execute_sql(SQL_ALL.format(mc=sql_text(args.user_id), day=args.date, apptype_filter=build_apptype_filter(args.apptype)))
  162. df = df[~df["businesstype"].isin(EXCLUDED_BUSINESSTYPES)].sort_values("ts", kind="stable").reset_index(drop=True)
  163. df = df.rename(columns={"endroutepath": "endRoutePath", "isadplaying": "isAdPlaying", "creativecode": "creativeCode"})
  164. df.insert(0, "北京时间", pd.to_datetime(df["ts"], unit="ms", utc=True).dt.tz_convert("Asia/Shanghai").dt.tz_localize(None))
  165. df = add_device_info(df.drop(columns="ts"))
  166. df.insert(df.columns.get_loc("businesstype") + 1, "行为", df.apply(behavior_definition, axis=1))
  167. df = df.rename(columns={"businesstype": "事件类型", "eventid": "事件ID"})
  168. df.insert(0, "用户ID", args.user_id)
  169. df.insert(0, "产品apptype", df.pop("产品apptype"))
  170. args.output_dir.mkdir(parents=True, exist_ok=True)
  171. out = args.output_dir / f"timeline_{safe_name(args.user_id[-12:])}_{args.date}.xlsx"
  172. with pd.ExcelWriter(out, engine="openpyxl", datetime_format="yyyy/mm/dd hh:mm:ss") as writer:
  173. df.to_excel(writer, sheet_name="行为路径", index=False)
  174. for cell in writer.sheets["行为路径"]["C"][1:]:
  175. cell.number_format = "yyyy/mm/dd hh:mm:ss"
  176. counts = df["来源"].value_counts().to_dict()
  177. for source in ["video", "ad", "play", "simpleevent", "useractive", "share"]:
  178. print(f"[ROWS] {source}={counts.get(source, 0)}", flush=True)
  179. print(f"[XLSX] 日期={args.date} 事件数={len(df)} -> {out.resolve()}", flush=True)
  180. if __name__ == "__main__":
  181. main()