export_work_day_overview_xlsx.py 6.0 KB

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