export_work_day_overview_xlsx.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 从 stdin 读取 JSON(数据总览账号列表),生成 xlsx 并输出到 stdout(二进制)。
  5. 注意:使用 openpyxl,便于之后扩展更多导出样式。
  6. 输入 JSON 格式:
  7. {
  8. "accounts": [
  9. {
  10. "account": "xxx",
  11. "platform": "douyin",
  12. "totalIncome": 0.0,
  13. "yesterdayIncome": 0.0,
  14. "totalViews": 0,
  15. "yesterdayViews": 0,
  16. "fansCount": 0,
  17. "yesterdayComments": 0,
  18. "yesterdayLikes": 0,
  19. "yesterdayFansIncrease": 0,
  20. "updateTime": "2026-01-27 14:30:00"
  21. }
  22. ]
  23. }
  24. """
  25. import json
  26. import sys
  27. from io import BytesIO
  28. from datetime import datetime
  29. # Ensure stdin is read as UTF-8 (important on Windows when Node passes UTF-8 JSON)
  30. if sys.platform == "win32":
  31. import io as _io
  32. if hasattr(sys.stdin, "buffer"):
  33. sys.stdin = _io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
  34. try:
  35. from openpyxl import Workbook
  36. from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
  37. from openpyxl.utils import get_column_letter
  38. except Exception as e: # pragma: no cover - 仅作为运行时保护
  39. sys.stderr.write(
  40. "Missing dependency: openpyxl. Please install it in your python env.\n"
  41. "Example: pip install -r server/python/requirements.txt\n"
  42. f"Detail: {e}\n"
  43. )
  44. sys.exit(3)
  45. HEADERS = [
  46. "账号",
  47. "平台",
  48. "总收益",
  49. "昨日收益",
  50. "总播放量",
  51. "昨日播放量",
  52. "粉丝数",
  53. "昨日评论",
  54. "昨日点赞",
  55. "昨日涨粉",
  56. "更新时间",
  57. ]
  58. COL_WIDTHS = [22, 12, 12, 12, 12, 12, 10, 10, 10, 10, 20]
  59. def _safe_int(v):
  60. try:
  61. if v is None or v == "":
  62. return None
  63. return int(float(v))
  64. except Exception:
  65. return None
  66. def _safe_float(v):
  67. try:
  68. if v is None or v == "":
  69. return None
  70. return float(v)
  71. except Exception:
  72. return None
  73. def _safe_str(v) -> str:
  74. """
  75. 将任意值转换为字符串,并过滤掉 Excel 不支持的非法 Unicode 代理字符。
  76. 只移除 U+D800~U+DFFF 范围的代理项,不影响正常中文等字符。
  77. """
  78. if v is None:
  79. return ""
  80. try:
  81. s = str(v)
  82. except Exception:
  83. s = repr(v)
  84. # 去掉孤立代理(surrogates),避免 "surrogates not allowed" 错误
  85. return "".join(ch for ch in s if not (0xD800 <= ord(ch) <= 0xDFFF))
  86. def _format_date_only(value: str) -> str:
  87. """
  88. 将各种时间字符串格式化为 YYYY-MM-DD,仅保留日期部分。
  89. 如果无法解析,则尽量取前 10 位作为日期返回。
  90. """
  91. if not value:
  92. return ""
  93. s = str(value).strip()
  94. # 先尝试 ISO 格式解析
  95. try:
  96. # 兼容 "...Z" 结尾
  97. if s.endswith("Z"):
  98. s_clean = s[:-1]
  99. else:
  100. s_clean = s
  101. # 如果中间是 ' ',替换成 'T'
  102. s_clean = s_clean.replace(" ", "T")
  103. dt = datetime.fromisoformat(s_clean)
  104. return dt.strftime("%Y-%m-%d")
  105. except Exception:
  106. pass
  107. # 退化处理:如果长度>=10,优先取前 10 位
  108. if len(s) >= 10:
  109. return s[:10]
  110. return s
  111. def build_xlsx(accounts):
  112. platform_name_map = {
  113. "douyin": "抖音",
  114. "baijiahao": "百家号",
  115. "weixin_video": "视频号",
  116. "xiaohongshu": "小红书",
  117. }
  118. wb = Workbook()
  119. ws = wb.active
  120. ws.title = "数据总览"
  121. # 表头
  122. ws.append(HEADERS)
  123. header_font = Font(bold=True)
  124. header_fill = PatternFill("solid", fgColor="F2F2F2")
  125. center = Alignment(horizontal="center", vertical="center", wrap_text=False)
  126. left = Alignment(horizontal="left", vertical="center", wrap_text=False)
  127. thin = Side(style="thin", color="D9D9D9")
  128. border = Border(left=thin, right=thin, top=thin, bottom=thin)
  129. for col_idx in range(1, len(HEADERS) + 1):
  130. cell = ws.cell(row=1, column=col_idx)
  131. cell.font = header_font
  132. cell.fill = header_fill
  133. cell.alignment = center
  134. cell.border = border
  135. # 列宽
  136. for i, w in enumerate(COL_WIDTHS, start=1):
  137. col_letter = get_column_letter(i)
  138. ws.column_dimensions[col_letter].width = w
  139. # 数据行
  140. for a in accounts:
  141. platform_raw = (a.get("platform") or "").strip()
  142. platform_cn = platform_name_map.get(platform_raw, platform_raw)
  143. ws.append([
  144. _safe_str(a.get("account")),
  145. _safe_str(platform_cn),
  146. _safe_float(a.get("totalIncome")),
  147. _safe_float(a.get("yesterdayIncome")),
  148. _safe_int(a.get("totalViews")),
  149. _safe_int(a.get("yesterdayViews")),
  150. _safe_int(a.get("fansCount")) or 0,
  151. _safe_int(a.get("yesterdayComments")) or 0,
  152. _safe_int(a.get("yesterdayLikes")) or 0,
  153. _safe_int(a.get("yesterdayFansIncrease")) or 0,
  154. _safe_str(_format_date_only(a.get("updateTime"))),
  155. ])
  156. money_cols = {"C", "D"}
  157. int_cols = {"E", "F", "G", "H", "I", "J"}
  158. for row in range(2, ws.max_row + 1):
  159. for col in range(1, len(HEADERS) + 1):
  160. c = ws.cell(row=row, column=col)
  161. c.border = border
  162. if col == 1:
  163. c.alignment = left
  164. else:
  165. c.alignment = center
  166. for c_letter in money_cols:
  167. c = ws[f"{c_letter}{row}"]
  168. if c.value is not None:
  169. c.number_format = "0.00"
  170. for c_letter in int_cols:
  171. c = ws[f"{c_letter}{row}"]
  172. if c.value is not None:
  173. c.number_format = "0"
  174. ws.freeze_panes = "A2"
  175. bio = BytesIO()
  176. wb.save(bio)
  177. return bio.getvalue()
  178. def main():
  179. try:
  180. raw = sys.stdin.read()
  181. payload = json.loads(raw) if raw.strip() else {}
  182. except Exception as e:
  183. sys.stderr.write(f"Invalid JSON input: {e}\n")
  184. sys.exit(2)
  185. accounts = payload.get("accounts") or []
  186. xlsx_bytes = build_xlsx(accounts)
  187. sys.stdout.buffer.write(xlsx_bytes)
  188. if __name__ == "__main__":
  189. main()