generator.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # coding=utf-8
  2. """
  3. 报告生成模块
  4. 提供报告数据准备和 HTML 生成功能:
  5. - prepare_report_data: 准备报告数据
  6. - generate_html_report: 生成 HTML 报告
  7. """
  8. from pathlib import Path
  9. from typing import Dict, List, Optional, Callable
  10. def prepare_report_data(
  11. stats: List[Dict],
  12. failed_ids: Optional[List] = None,
  13. new_titles: Optional[Dict] = None,
  14. id_to_name: Optional[Dict] = None,
  15. mode: str = "daily",
  16. rank_threshold: int = 3,
  17. matches_word_groups_func: Optional[Callable] = None,
  18. load_frequency_words_func: Optional[Callable] = None,
  19. ) -> Dict:
  20. """
  21. 准备报告数据
  22. Args:
  23. stats: 统计结果列表
  24. failed_ids: 失败的 ID 列表
  25. new_titles: 新增标题
  26. id_to_name: ID 到名称的映射
  27. mode: 报告模式 (daily/incremental/current)
  28. rank_threshold: 排名阈值
  29. matches_word_groups_func: 词组匹配函数
  30. load_frequency_words_func: 加载频率词函数
  31. Returns:
  32. Dict: 准备好的报告数据
  33. """
  34. processed_new_titles = []
  35. # 在增量模式下隐藏新增新闻区域
  36. hide_new_section = mode == "incremental"
  37. # 只有在非隐藏模式下才处理新增新闻部分
  38. if not hide_new_section:
  39. filtered_new_titles = {}
  40. if new_titles and id_to_name:
  41. # 如果提供了匹配函数,使用它过滤
  42. if matches_word_groups_func and load_frequency_words_func:
  43. word_groups, filter_words, global_filters = load_frequency_words_func()
  44. for source_id, titles_data in new_titles.items():
  45. filtered_titles = {}
  46. for title, title_data in titles_data.items():
  47. if matches_word_groups_func(title, word_groups, filter_words, global_filters):
  48. filtered_titles[title] = title_data
  49. if filtered_titles:
  50. filtered_new_titles[source_id] = filtered_titles
  51. else:
  52. # 没有匹配函数时,使用全部
  53. filtered_new_titles = new_titles
  54. # 打印过滤后的新增热点数(与推送显示一致)
  55. original_new_count = sum(len(titles) for titles in new_titles.values()) if new_titles else 0
  56. filtered_new_count = sum(len(titles) for titles in filtered_new_titles.values()) if filtered_new_titles else 0
  57. if original_new_count > 0:
  58. print(f"频率词过滤后:{filtered_new_count} 条新增热点匹配(原始 {original_new_count} 条)")
  59. if filtered_new_titles and id_to_name:
  60. for source_id, titles_data in filtered_new_titles.items():
  61. source_name = id_to_name.get(source_id, source_id)
  62. source_titles = []
  63. for title, title_data in titles_data.items():
  64. url = title_data.get("url", "")
  65. mobile_url = title_data.get("mobileUrl", "")
  66. ranks = title_data.get("ranks", [])
  67. processed_title = {
  68. "title": title,
  69. "source_name": source_name,
  70. "time_display": "",
  71. "count": 1,
  72. "ranks": ranks,
  73. "rank_threshold": rank_threshold,
  74. "url": url,
  75. "mobile_url": mobile_url,
  76. "is_new": True,
  77. }
  78. source_titles.append(processed_title)
  79. if source_titles:
  80. processed_new_titles.append(
  81. {
  82. "source_id": source_id,
  83. "source_name": source_name,
  84. "titles": source_titles,
  85. }
  86. )
  87. processed_stats = []
  88. for stat in stats:
  89. if stat["count"] <= 0:
  90. continue
  91. processed_titles = []
  92. for title_data in stat["titles"]:
  93. processed_title = {
  94. "title": title_data["title"],
  95. "source_name": title_data["source_name"],
  96. "time_display": title_data["time_display"],
  97. "count": title_data["count"],
  98. "ranks": title_data["ranks"],
  99. "rank_threshold": title_data["rank_threshold"],
  100. "url": title_data.get("url", ""),
  101. "mobile_url": title_data.get("mobileUrl", ""),
  102. "is_new": title_data.get("is_new", False),
  103. }
  104. processed_titles.append(processed_title)
  105. processed_stats.append(
  106. {
  107. "word": stat["word"],
  108. "count": stat["count"],
  109. "percentage": stat.get("percentage", 0),
  110. "titles": processed_titles,
  111. }
  112. )
  113. return {
  114. "stats": processed_stats,
  115. "new_titles": processed_new_titles,
  116. "failed_ids": failed_ids or [],
  117. "total_new_count": sum(
  118. len(source["titles"]) for source in processed_new_titles
  119. ),
  120. }
  121. def generate_html_report(
  122. stats: List[Dict],
  123. total_titles: int,
  124. failed_ids: Optional[List] = None,
  125. new_titles: Optional[Dict] = None,
  126. id_to_name: Optional[Dict] = None,
  127. mode: str = "daily",
  128. is_daily_summary: bool = False,
  129. update_info: Optional[Dict] = None,
  130. rank_threshold: int = 3,
  131. output_dir: str = "output",
  132. date_folder: str = "",
  133. time_filename: str = "",
  134. render_html_func: Optional[Callable] = None,
  135. matches_word_groups_func: Optional[Callable] = None,
  136. load_frequency_words_func: Optional[Callable] = None,
  137. enable_index_copy: bool = True,
  138. ) -> str:
  139. """
  140. 生成 HTML 报告
  141. Args:
  142. stats: 统计结果列表
  143. total_titles: 总标题数
  144. failed_ids: 失败的 ID 列表
  145. new_titles: 新增标题
  146. id_to_name: ID 到名称的映射
  147. mode: 报告模式 (daily/incremental/current)
  148. is_daily_summary: 是否是每日汇总
  149. update_info: 更新信息
  150. rank_threshold: 排名阈值
  151. output_dir: 输出目录
  152. date_folder: 日期文件夹名称
  153. time_filename: 时间文件名
  154. render_html_func: HTML 渲染函数
  155. matches_word_groups_func: 词组匹配函数
  156. load_frequency_words_func: 加载频率词函数
  157. enable_index_copy: 是否复制到 index.html
  158. Returns:
  159. str: 生成的 HTML 文件路径
  160. """
  161. if is_daily_summary:
  162. if mode == "current":
  163. filename = "当前榜单汇总.html"
  164. elif mode == "incremental":
  165. filename = "当日增量.html"
  166. else:
  167. filename = "当日汇总.html"
  168. else:
  169. filename = f"{time_filename}.html"
  170. # 构建输出路径
  171. output_path = Path(output_dir) / date_folder / "html"
  172. output_path.mkdir(parents=True, exist_ok=True)
  173. file_path = str(output_path / filename)
  174. # 准备报告数据
  175. report_data = prepare_report_data(
  176. stats,
  177. failed_ids,
  178. new_titles,
  179. id_to_name,
  180. mode,
  181. rank_threshold,
  182. matches_word_groups_func,
  183. load_frequency_words_func,
  184. )
  185. # 渲染 HTML 内容
  186. if render_html_func:
  187. html_content = render_html_func(
  188. report_data, total_titles, is_daily_summary, mode, update_info
  189. )
  190. else:
  191. # 默认简单 HTML
  192. html_content = f"<html><body><h1>Report</h1><pre>{report_data}</pre></body></html>"
  193. # 写入文件
  194. with open(file_path, "w", encoding="utf-8") as f:
  195. f.write(html_content)
  196. # 如果是每日汇总且启用 index 复制
  197. if is_daily_summary and enable_index_copy:
  198. # 生成到根目录(供 GitHub Pages 访问)
  199. root_index_path = Path("index.html")
  200. with open(root_index_path, "w", encoding="utf-8") as f:
  201. f.write(html_content)
  202. # 同时生成到 output 目录(供 Docker Volume 挂载访问)
  203. output_index_path = Path(output_dir) / "index.html"
  204. Path(output_dir).mkdir(parents=True, exist_ok=True)
  205. with open(output_index_path, "w", encoding="utf-8") as f:
  206. f.write(html_content)
  207. return file_path