This commit is contained in:
sansan 2025-08-06 19:30:05 +08:00
parent 5d30063f6c
commit ac27c47fcf
3 changed files with 543 additions and 104 deletions

628
main.py
View File

@ -15,7 +15,7 @@ import requests
import yaml
VERSION = "2.0.2"
VERSION = "2.0.3"
# === 配置管理 ===
@ -1410,142 +1410,576 @@ def render_html_content(
<html>
<head>
<meta charset="UTF-8">
<title>频率词统计报告</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>热点新闻分析</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
tr:nth-child(even) { background-color: #f9f9f9; }
.word { font-weight: bold; }
.count { text-align: center; }
.percentage { text-align: center; }
.titles { max-width: 500px; }
.source { color: #666; font-style: italic; }
.error { color: #d9534f; }
.news-link {
color: #007bff;
text-decoration: none;
border-bottom: 1px dotted #007bff;
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
margin: 0;
padding: 16px;
background: #fafafa;
color: #333;
line-height: 1.5;
}
.news-link:hover {
color: #0056b3;
text-decoration: underline;
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 16px rgba(0,0,0,0.06);
}
.news-link:visited {
color: #6f42c1;
.header {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 32px 24px;
text-align: center;
}
.no-link {
color: #333;
.header-title {
font-size: 22px;
font-weight: 700;
margin: 0 0 20px 0;
}
.new-title {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 3px;
padding: 2px 6px;
margin: 2px 0;
.header-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
font-size: 14px;
opacity: 0.95;
}
.new-section {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 5px;
padding: 10px;
margin-top: 10px;
.info-item {
text-align: center;
}
.new-section h3 {
color: #0c5460;
.info-label {
display: block;
font-size: 12px;
opacity: 0.8;
margin-bottom: 4px;
}
.info-value {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.word-group {
margin-bottom: 40px;
}
.word-group:first-child {
margin-top: 0;
}
.word-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.word-info {
display: flex;
align-items: center;
gap: 12px;
}
.word-name {
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
}
.word-count {
color: #666;
font-size: 13px;
font-weight: 500;
}
.word-count.hot { color: #dc2626; font-weight: 600; }
.word-count.warm { color: #ea580c; font-weight: 600; }
.word-index {
color: #999;
font-size: 12px;
}
.news-item {
margin-bottom: 20px;
padding: 16px 0;
border-bottom: 1px solid #f5f5f5;
position: relative;
display: flex;
gap: 12px;
align-items: center;
}
.news-item:last-child {
border-bottom: none;
}
.news-item.new::after {
content: "NEW";
position: absolute;
top: 12px;
right: 0;
background: #fbbf24;
color: #92400e;
font-size: 9px;
font-weight: 700;
padding: 3px 6px;
border-radius: 4px;
letter-spacing: 0.5px;
}
.news-number {
color: #999;
font-size: 13px;
font-weight: 600;
min-width: 20px;
text-align: center;
flex-shrink: 0;
background: #f8f9fa;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
align-self: flex-start;
margin-top: 8px;
}
.news-content {
flex: 1;
min-width: 0;
padding-right: 40px;
}
.news-item.new .news-content {
padding-right: 50px;
}
.news-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.source-name {
color: #666;
font-size: 12px;
font-weight: 500;
}
.rank-num {
color: #fff;
background: #6b7280;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
}
.rank-num.top { background: #dc2626; }
.rank-num.high { background: #ea580c; }
.time-info {
color: #999;
font-size: 11px;
}
.count-info {
color: #059669;
font-size: 11px;
font-weight: 500;
}
.news-title {
font-size: 15px;
line-height: 1.4;
color: #1a1a1a;
margin: 0;
}
.news-link {
color: #2563eb;
text-decoration: none;
}
.news-link:hover {
text-decoration: underline;
}
.news-link:visited {
color: #7c3aed;
}
.new-section {
margin-top: 40px;
padding-top: 24px;
border-top: 2px solid #f0f0f0;
}
.new-section-title {
color: #1a1a1a;
font-size: 16px;
font-weight: 600;
margin: 0 0 20px 0;
}
.new-source-group {
margin-bottom: 24px;
}
.new-source-title {
color: #666;
font-size: 13px;
font-weight: 500;
margin: 0 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px solid #f5f5f5;
}
.new-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f9f9f9;
}
.new-item:last-child {
border-bottom: none;
}
.new-item-number {
color: #999;
font-size: 12px;
font-weight: 600;
min-width: 18px;
text-align: center;
flex-shrink: 0;
background: #f8f9fa;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.new-item-rank {
color: #fff;
background: #6b7280;
font-size: 10px;
font-weight: 700;
padding: 3px 6px;
border-radius: 8px;
min-width: 20px;
text-align: center;
flex-shrink: 0;
}
.new-item-rank.top { background: #dc2626; }
.new-item-rank.high { background: #ea580c; }
.new-item-content {
flex: 1;
min-width: 0;
}
.new-item-title {
font-size: 14px;
line-height: 1.4;
color: #1a1a1a;
margin: 0;
}
.error-section {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
}
.error-title {
color: #dc2626;
font-size: 14px;
font-weight: 600;
margin: 0 0 8px 0;
}
.error-list {
list-style: none;
padding: 0;
margin: 0;
}
.error-item {
color: #991b1b;
font-size: 13px;
padding: 2px 0;
font-family: 'SF Mono', Consolas, monospace;
}
@media (max-width: 480px) {
body { padding: 12px; }
.header { padding: 24px 20px; }
.content { padding: 20px; }
.header-info { grid-template-columns: 1fr; gap: 12px; }
.news-header { gap: 6px; }
.news-content { padding-right: 45px; }
.news-item { gap: 8px; }
.new-item { gap: 8px; }
.news-number { width: 20px; height: 20px; font-size: 12px; }
}
</style>
</head>
<body>
<h1>频率词统计报告</h1>
"""
<div class="container">
<div class="header">
<div class="header-title">热点新闻分析</div>
<div class="header-info">
<div class="info-item">
<span class="info-label">报告类型</span>
<span class="info-value">"""
# 处理报告类型显示
if is_daily_summary:
if mode == "current":
html += "<p>报告类型: 当前榜单模式</p>"
html += "当前榜单"
elif mode == "incremental":
html += "<p>报告类型: 增量模式</p>"
html += "增量模式"
else:
html += "<p>报告类型: 当日汇总</p>"
html += "当日汇总"
else:
html += "<p>报告类型: 实时分析</p>"
html += "实时分析"
html += """</span>
</div>
<div class="info-item">
<span class="info-label">新闻总数</span>
<span class="info-value">"""
html += f"{total_titles}"
# 计算筛选后的热点新闻数量
hot_news_count = sum(len(stat["titles"]) for stat in report_data["stats"])
html += """</span>
</div>
<div class="info-item">
<span class="info-label">热点新闻</span>
<span class="info-value">"""
html += f"{hot_news_count}"
html += """</span>
</div>
<div class="info-item">
<span class="info-label">生成时间</span>
<span class="info-value">"""
now = get_beijing_time()
html += f"<p>总标题数: {total_titles}</p>"
html += f"<p>生成时间: {now.strftime('%Y-%m-%d %H:%M:%S')}</p>"
html += now.strftime('%m-%d %H:%M')
html += """</span>
</div>
</div>
</div>
<div class="content">"""
# 处理失败ID错误信息
if report_data["failed_ids"]:
html += """
<div class="error">
<h2>请求失败的平台</h2>
<ul>
"""
<div class="error-section">
<div class="error-title"> 请求失败的平台</div>
<ul class="error-list">"""
for id_value in report_data["failed_ids"]:
html += f"<li>{html_escape(id_value)}</li>"
html += f'<li class="error-item">{html_escape(id_value)}</li>'
html += """
</ul>
</div>
"""
</ul>
</div>"""
html += """
<table>
<tr>
<th>排名</th>
<th>频率词</th>
<th>出现次数</th>
<th>占比</th>
<th>相关标题</th>
</tr>
"""
# 处理主要统计数据
if report_data["stats"]:
total_count = len(report_data["stats"])
for i, stat in enumerate(report_data["stats"], 1):
count = stat["count"]
# 确定热度等级
if count >= 10:
count_class = "hot"
elif count >= 5:
count_class = "warm"
else:
count_class = ""
for i, stat in enumerate(report_data["stats"], 1):
formatted_titles = []
escaped_word = html_escape(stat["word"])
html += f"""
<div class="word-group">
<div class="word-header">
<div class="word-info">
<div class="word-name">{escaped_word}</div>
<div class="word-count {count_class}">{count} </div>
</div>
<div class="word-index">{i}/{total_count}</div>
</div>"""
for title_data in stat["titles"]:
formatted_title = format_title_for_platform("html", title_data)
formatted_titles.append(formatted_title)
# 处理每个词组下的新闻标题,给每条新闻标上序号
for j, title_data in enumerate(stat["titles"], 1):
is_new = title_data.get("is_new", False)
new_class = "new" if is_new else ""
html += f"""
<div class="news-item {new_class}">
<div class="news-number">{j}</div>
<div class="news-content">
<div class="news-header">
<span class="source-name">{html_escape(title_data["source_name"])}</span>"""
# 处理排名显示
ranks = title_data.get("ranks", [])
if ranks:
min_rank = min(ranks)
max_rank = max(ranks)
rank_threshold = title_data.get("rank_threshold", 10)
# 确定排名等级
if min_rank <= 3:
rank_class = "top"
elif min_rank <= rank_threshold:
rank_class = "high"
else:
rank_class = ""
if min_rank == max_rank:
rank_text = str(min_rank)
else:
rank_text = f"{min_rank}-{max_rank}"
html += f'<span class="rank-num {rank_class}">{rank_text}</span>'
# 处理时间显示
time_display = title_data.get("time_display", "")
if time_display:
# 简化时间显示格式,将波浪线替换为~
simplified_time = time_display.replace(" ~ ", "~").replace("[", "").replace("]", "")
html += f'<span class="time-info">{html_escape(simplified_time)}</span>'
# 处理出现次数
count_info = title_data.get("count", 1)
if count_info > 1:
html += f'<span class="count-info">{count_info}次</span>'
html += """
</div>
<div class="news-title">"""
# 处理标题和链接
escaped_title = html_escape(title_data["title"])
link_url = title_data.get("mobile_url") or title_data.get("url", "")
if link_url:
escaped_url = html_escape(link_url)
html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
else:
html += escaped_title
html += """
</div>
</div>
</div>"""
escaped_word = html_escape(stat["word"])
html += f"""
<tr>
<td>{i}</td>
<td class="word">{escaped_word}</td>
<td class="count">{stat['count']}</td>
<td class="percentage">{stat.get('percentage', 0)}%</td>
<td class="titles">{"<br>".join(formatted_titles)}</td>
</tr>
"""
html += """
</table>
"""
html += """
</div>"""
# 处理新增新闻区域
if report_data["new_titles"]:
html += f"""
<div class="new-section">
<h3>🆕 本次新增热点新闻 ( {report_data['total_new_count']} )</h3>
"""
<div class="new-section">
<div class="new-section-title">本次新增热点 ( {report_data['total_new_count']} )</div>"""
for source_data in report_data["new_titles"]:
escaped_source = html_escape(source_data["source_name"])
html += f"<h4>{escaped_source} ({len(source_data['titles'])} 条)</h4><ul>"
titles_count = len(source_data["titles"])
html += f"""
<div class="new-source-group">
<div class="new-source-title">{escaped_source} · {titles_count}</div>"""
for title_data in source_data["titles"]:
title_data_copy = title_data.copy()
title_data_copy["is_new"] = False
formatted_title = format_title_for_platform("html", title_data_copy)
if "] " in formatted_title:
formatted_title = formatted_title.split("] ", 1)[1]
html += f"<li>{formatted_title}</li>"
# 为新增新闻也添加序号
for idx, title_data in enumerate(source_data["titles"], 1):
ranks = title_data.get("ranks", [])
# 处理新增新闻的排名显示
rank_class = ""
if ranks:
min_rank = min(ranks)
if min_rank <= 3:
rank_class = "top"
elif min_rank <= title_data.get("rank_threshold", 10):
rank_class = "high"
if len(ranks) == 1:
rank_text = str(ranks[0])
else:
rank_text = f"{min(ranks)}-{max(ranks)}"
else:
rank_text = "?"
html += "</ul>"
html += f"""
<div class="new-item">
<div class="new-item-number">{idx}</div>
<div class="new-item-rank {rank_class}">{rank_text}</div>
<div class="new-item-content">
<div class="new-item-title">"""
# 处理新增新闻的链接
escaped_title = html_escape(title_data["title"])
link_url = title_data.get("mobile_url") or title_data.get("url", "")
if link_url:
escaped_url = html_escape(link_url)
html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
else:
html += escaped_title
html += """
</div>
</div>
</div>"""
html += "</div>"
html += """
</div>"""
html += """
</div>"""
html += """
</div>
</div>
</body>
</html>
"""

View File

@ -7,7 +7,7 @@
[![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
[![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
[![Version](https://img.shields.io/badge/version-v2.0.2-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
[![Version](https://img.shields.io/badge/version-v2.0.3-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
[![企业微信通知](https://img.shields.io/badge/企业微信-通知支持-00D4AA?style=flat-square)](https://work.weixin.qq.com/)
[![Telegram通知](https://img.shields.io/badge/Telegram-通知支持-00D4AA?style=flat-square)](https://telegram.org/)
@ -23,6 +23,8 @@
> 本项目以轻量,易部署为目标,主要处理 issues
>
> 遇到问题提 issues或【硅基茶水间】公众号留言
>
> **Fork** 的同时,如果觉得有帮助,请顺手点个 ⭐️ **Star**,感谢支持
## ✨ 核心功能
@ -72,10 +74,10 @@ GitHub 一键 Fork 即可使用,无需编程基础。
**典型场景:** 股市投资监控、品牌舆情追踪、行业动态关注、生活资讯获取
**💡 提示:** 想要网页版?进入你的仓库 Settings → Pages启用 GitHub Pages。比如我这里[TrendRadar](https://sansan0.github.io/TrendRadar/)。
**💡 提示:** 想要**实时更新**的网页版?进入你的仓库 Settings → Pages启用 GitHub Pages。比如我这里[TrendRadar](https://sansan0.github.io/TrendRadar/)。
<p align="center">
<img src="_image/2025-06-14.jpg" alt="最新推送效果" width="400"/>
<img src="_image/2025-06-14.jpg" alt="飞书推送效果" width="400"/>
</p>
<details>
@ -118,14 +120,17 @@ GitHub 一键 Fork 即可使用,无需编程基础。
- **小版本更新**:直接在 GitHub 网页编辑器中,用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件
- **大版本升级**:从 v1.x 升级到 v2.0 建议删除现有 fork 后重新 fork这样更省力且避免配置冲突
### 2025/07/28 - v2.0.2
### 2025/08/06 - v2.0.3
- 重构代码
- 解决版本号容易被遗漏修改的问题
- 优化 github page 的网页版效果,方便移动端使用
<details>
<summary><strong>👉 历史更新</strong></summary>
### 2025/07/28 - v2.0.2
- 重构代码
- 解决版本号容易被遗漏修改的问题
### 2025/07/27 - v2.0.1

View File

@ -1 +1 @@
2.0.2
2.0.3