每日总结脚本模板
用 Shell 脚本打造每日自动简报:天气+热搜+GitHub Trending 一键推送到钉钉
每天早上打开电脑,一条包含天气、国内外热搜、GitHub Trending 的 Markdown 简报已经躺在钉钉群里——这不是什么 SaaS 产品,而是一个不到 200 行的 Shell 脚本 + crontab 定时任务。本文介绍如何部署和使用这套每日简报系统。
效果预览
脚本执行后,钉钉群会收到如下格式的 Markdown 消息:
☀️ 鹤童大人,早上好!
📅 今天是 2026年06月23日 周一
🌇 杭州:
🌤️ 天气:⛅ 🌡️ 温度:+28°C 🌬️ 风速:↗12km/h 💧 湿度:65%
祝您一天好心情!🦞
━━━━━━━━━━━━━━━━━━━━
📰 国内热搜
🔍 百度热搜 Top 10(2026-06-23):
1. xxx - 🔥4892301
...
🎵 抖音热榜 Top 10
📰 今日头条 Top 10
━━━━━━━━━━━━━━━━━━━━
🌍 国外热搜
▶️ YouTube 热门
📈 GitHub Trending Top 10
━━━━━━━━━━━━━━━━━━━━
祝您工作顺利!🦞
架构概览
整个系统由三部分组成:
| 组件 | 作用 | 技术 |
|---|---|---|
主脚本 daily_briefing.sh |
编排数据采集 + 消息组装 + 发送 | Bash + 内嵌 Python |
YouTube 抓取脚本 fetch_youtube.py |
独立抓取 YouTube RSS(需代理) | Python 标准库 |
| 定时调度 crontab / launchd | 每天固定时间触发脚本 | macOS 原生 |
数据源覆盖 6 个平台:wttr.in(天气)、百度热搜、抖音热榜、今日头条、YouTube Trending、GitHub Trending。所有抓取逻辑零外部依赖,仅使用 Python 标准库(urllib + json + re),无需 pip install 任何包。
快速开始
Step 1: 准备文件
将以下两个文件放到同一目录下(见文末完整脚本):
~/scripts/daily-briefing/
├── daily_briefing.sh # 主脚本
└── fetch_youtube.py # YouTube 抓取(独立文件,因需代理环境)
Step 2: 配置 Webhook 和城市
编辑 daily_briefing.sh 头部两处变量:
# 钉钉机器人 Webhook 地址
WEBHOOK_URL="xxx"
# 天气查询城市(wttr.in 支持中文拼音或英文)
LOCATION="Hangzhou"
获取钉钉 Webhook:钉钉群 → 设置 → 智能群助手 → 添加机器人 → 自定义 → 复制 Webhook 地址。安全设置建议选"关键词",填入
简报即可(消息标题包含此词)。
Step 3: 赋予执行权限并测试
chmod +x ~/scripts/daily-briefing/daily_briefing.sh
chmod +x ~/scripts/daily-briefing/fetch_youtube.py
# 手动执行一次验证
~/scripts/daily-briefing/daily_briefing.sh
看到终端输出 简报已发送 且钉钉群收到消息即表示成功。如果某个平台显示"暂无数据",通常是网络问题(YouTube/GitHub 可能需要代理),不影响其他模块正常推送。
Step 4: 配置定时任务
方式 A:crontab(推荐,简单直接)
crontab -e
添加一行,每天早上 8:30 执行:
30 8 * * * /bin/bash ~/scripts/daily-briefing/daily_briefing.sh >> ~/scripts/daily-briefing/cron.log 2>&1
注意:macOS 的 crontab 默认不加载
.zshrc/.bash_profile,如果脚本依赖 PATH 中的命令(如 python3),建议使用绝对路径或在脚本头部显式声明export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"。
方式 B:launchd(macOS 原生,支持错过补执行)
创建 plist 文件:
cat > ~/Library/LaunchAgents/com.user.dailybriefing.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.dailybriefing</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/YOUR_USERNAME/scripts/daily-briefing/daily_briefing.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/scripts/daily-briefing/cron.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/scripts/daily-briefing/cron.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
</dict>
</plist>
EOF
# 加载任务
launchctl load ~/Library/LaunchAgents/com.user.dailybriefing.plist
# 验证是否加载成功
launchctl list | grep dailybriefing
launchd vs crontab:launchd 在 Mac 休眠唤醒后会补执行错过的任务,crontab 不会。如果你的 Mac 经常合盖,推荐 launchd。
关键设计解读
为什么用 Shell + 内嵌 Python 而不是纯 Python?
- Shell 负责编排:curl 取天气、环境变量传递、最终发送,这些是 Shell 的舒适区
- Python 负责解析:百度热搜的 HTML 正则提取、GitHub Trending 的 DOM 解析,用 Python 标准库比 Shell awk/sed 可读性好一个量级
- 零依赖:内嵌 Python 只用标准库,不需要虚拟环境和 pip,部署就是复制文件
YouTube 为什么单独拆成文件?
YouTube Trending 需要走代理才能访问。独立成 fetch_youtube.py 后,可以在该脚本内部单独处理代理初始化(如读取系统 PAC 配置),而不影响主脚本的其他模块。主脚本通过 $SCRIPT_DIR/fetch_youtube.py 调用,即使 YouTube 抓取失败也不会阻塞整个简报。
容错设计
每个数据源的抓取都包裹在 try/except 中,失败时返回 "暂无数据" 而非中断脚本。天气接口额外做了空值和 "Unknown location" 的双重判断。这保证了任何一个平台挂了,其余内容照常推送。
自定义扩展
修改推送时间
crontab 格式:分 时 日 月 周
# 工作日 9:00 推送
0 9 * * 1-5 /bin/bash ~/scripts/daily-briefing/daily_briefing.sh
# 每周一 8:00 推送周报版
0 8 * * 1 /bin/bash ~/scripts/daily-briefing/daily_briefing.sh
新增数据源
在主脚本中添加一个新的数据采集块,遵循相同模式:
# ============ N. 新平台 ============
NEW_DATA=$(python3 << 'EOF'
import urllib.request, json
try:
# 抓取逻辑...
print(result)
except:
print('暂无数据')
EOF
)
然后在底部 Python 发送块中:① 添加 os.environ.get('NEW_DATA', '') 读取;② 在 message 字符串中插入展示位置。
更换推送渠道
当前使用钉钉 Webhook,如需切换到飞书/企业微信/Slack,只需修改底部 Python 发送块的请求格式:
| 平台 | msgtype | 关键字段 |
|---|---|---|
| 钉钉 | markdown |
{"msgtype":"markdown","markdown":{"title":"...","text":"..."}} |
| 飞书 | interactive |
卡片 JSON 格式 |
| 企业微信 | markdown |
{"msgtype":"markdown","markdown":{"content":"..."}} |
| Slack | mrkdwn |
{"text":"...", "blocks":[...]} |
常见问题
Q: macOS 提示"无法打开,因为来自身份不明的开发者"
A: xattr -d com.apple.quarantine ~/scripts/daily-briefing/daily_briefing.sh
Q: crontab 执行时 python3 找不到
A: 在脚本头部加 export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH",或使用 python3 绝对路径 $(which python3)。
Q: 某个平台一直显示"暂无数据"
A: 手动运行对应的 Python 代码块排查。常见原因:IP 被封(换代理)、接口改版(需更新解析逻辑)、超时(调大 timeout 参数)。
Q: 想同时推送到多个群
A: 将 WEBHOOK_URL 改为数组,底部发送块循环调用即可。
完整脚本
以下是两个脚本的完整源码,复制到对应文件即可使用。
fetch_youtube.py
#!/usr/bin/env python3
"""抓取 YouTube 频道 RSS 并输出热门视频列表"""
import urllib.request
import re
import ssl
CHANNEL_URL = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCX6OQ3DkcsbYNE6H8uQQuVA'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
TIMEOUT = 1000
try:
req = urllib.request.Request(CHANNEL_URL, headers={'User-Agent': USER_AGENT})
context = ssl.create_default_context()
rss = urllib.request.urlopen(req, timeout=TIMEOUT, context=context).read().decode('utf-8', errors='ignore')
entries = re.findall(
r'<entry>.*?<title>([^<]+)</title>.*?<link rel="alternate" href="([^"]+)".*?</entry>',
rss, re.DOTALL
)
output = [f"{i}. [{title}]({link})" for i, (title, link) in enumerate(entries[:10], 1)]
print('\n'.join(output) if output else '暂无数据')
except Exception:
print('暂无数据')
daily_briefing.sh
#!/bin/bash
# 每日上午简报脚本 - 多平台热搜
WEBHOOK_URL="your_webhook_url"
# ============ 1. 今日天气 ============
DATE=$(date +%Y年%m月%d日)
WEEKDAY=$(date -u +%A | sed 's/Monday/周一/;s/Tuesday/周二/;s/Wednesday/周三/;s/Thursday/周四/;s/Friday/周五/;s/Saturday/周六/;s/Sunday/周日/')
TODAY_DATE=$(date +%Y-%m-%d)
WEATHER_RAW=$(curl -s "wttr.in/${LOCATION}?format=%c|%t|%w|%h" 2>/dev/null | head -1)
if [ -z "$WEATHER_RAW" ] || [ "$WEATHER_RAW" = "Unknown location" ]; then
WEATHER_ICON="❓"; WEATHER_TEMP="暂无"; WEATHER_WIND="暂无"; WEATHER_HUM="暂无"
else
WEATHER_ICON=$(echo "$WEATHER_RAW" | cut -d'|' -f1)
WEATHER_TEMP=$(echo "$WEATHER_RAW" | cut -d'|' -f2)
WEATHER_WIND=$(echo "$WEATHER_RAW" | cut -d'|' -f3)
WEATHER_HUM=$(echo "$WEATHER_RAW" | cut -d'|' -f4)
fi
# ============ 2. 百度热搜 ============
BAIDU_HOT=$(python3 << 'EOF'
import urllib.request, re, json
try:
req = urllib.request.Request('https://top.baidu.com/board?tab=realtime', headers={'User-Agent': 'Mozilla/5.0'})
html = urllib.request.urlopen(req, timeout=10).read().decode('utf-8', errors='ignore')
match = re.search(r'<!--s-data:(.*?)-->', html, re.DOTALL)
if match:
data = json.loads(match.group(1))
cards = data.get('data', {}).get('cards', [])
content = [item for card in cards if 'content' in card for item in card['content']]
result = [f"{i}. [{item.get('word', '')}](https://www.baidu.com/s?wd={item.get('word', '').replace(' ', '+')}) - 🔥{item.get('hotScore', 0)}" for i, item in enumerate(content[:10], 1)]
print('\n'.join(result) if result else '暂无数据')
else:
print('暂无数据')
except:
print('暂无数据')
EOF
)
# ============ 3. 抖音热榜 ============
DOUYIN_HOT=$(python3 << 'EOF'
import urllib.request, json
try:
req = urllib.request.Request('https://www.douyin.com/aweme/v1/web/hot/search/list/?device_platform=webapp&aid=6383', headers={'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.douyin.com/'})
data = json.loads(urllib.request.urlopen(req, timeout=10).read().decode('utf-8', errors='ignore'))
word_list = data.get('data', {}).get('word_list', [])
result = [f"{i}. [{item.get('word', '')}](https://www.douyin.com/hot/{item.get('event_id', '')}) - 🔥{item.get('hot_value', 0)}" for i, item in enumerate(word_list[:10], 1)]
print('\n'.join(result) if result else '暂无数据')
except:
print('暂无数据')
EOF
)
# ============ 4. 今日头条 ============
TOUTIAO_HOT=$(python3 << 'EOF'
import urllib.request, json
try:
req = urllib.request.Request('https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc', headers={'User-Agent': 'Mozilla/5.0'})
result = json.loads(urllib.request.urlopen(req, timeout=10).read().decode('utf-8', errors='ignore'))
data = result.get('data', [])
output = [f"{i}. [{item.get('Title', '')}](https://www.toutiao.com/trending/{item.get('ClusterIdStr', '')}/) - 🔥{item.get('HotValue', 0)}" for i, item in enumerate(data[:10], 1)]
print('\n'.join(output) if output else '暂无数据')
except:
print('暂无数据')
EOF
)
# ============ 5. YouTube 热门视频 ============
# 调用独立 Python 脚本抓取,确保系统 PAC 代理能正确初始化
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
YOUTUBE_TRENDING=$(python3 "$SCRIPT_DIR/fetch_youtube.py")
# ============ 6. Twitter/X 热门趋势 ============
# (已禁用)
# ============ 8. GitHub Trending ============
GITHUB_TRENDING=$(python3 << 'EOF'
import urllib.request
from html.parser import HTMLParser
try:
html = urllib.request.urlopen(urllib.request.Request('https://github.com/trending?since=daily', headers={'User-Agent': 'Mozilla/5.0'}), timeout=10).read().decode('utf-8', errors='ignore')
class Parser(HTMLParser):
def __init__(self):
super().__init__()
self.repos, self.current = [], {}
self.in_row, self.in_h2, self.in_desc = False, False, False
def handle_starttag(self, tag, attrs):
d = dict(attrs)
cls = d.get('class', '')
if tag == 'article' and 'Box-row' in cls:
self.in_row, self.current = True, {}
if self.in_row and tag == 'h2':
self.in_h2 = True
if self.in_row and tag == 'a' and self.in_h2 and 'href' in d:
h = d['href']
if h.count('/') == 2 and h.startswith('/'):
self.current['url'] = f'https://github.com{h}'
self.current['name'] = h[1:]
if self.in_row and tag == 'p' and 'col-9' in cls:
self.in_desc = True
self.current['desc'] = ''
def handle_endtag(self, tag):
if tag == 'h2':
self.in_h2 = False
if tag == 'p' and self.in_desc:
self.in_desc = False
if 'desc' in self.current:
self.current['desc'] = self.current['desc'].strip()
if tag == 'article' and self.in_row:
if self.current and len(self.repos) < 10:
self.repos.append(self.current)
self.in_row, self.current = False, {}
def handle_data(self, data):
if self.in_desc:
self.current['desc'] = self.current.get('desc', '') + data
p = Parser()
p.feed(html)
output = []
for i, r in enumerate(p.repos[:10], 1):
name = r.get('name', '')
url = r.get('url', '')
desc = r.get('desc', '').strip()
if desc:
output.append(f"{i}. [{name}]({url}) - {desc}")
else:
output.append(f"{i}. [{name}]({url})")
print('\n'.join(output) if output else '暂无数据')
except:
print('暂无数据')
EOF
)
# ============ 发送消息 ============
export DATE WEEKDAY TODAY_DATE WEATHER_ICON WEATHER_TEMP WEATHER_WIND WEATHER_HUM BAIDU_HOT DOUYIN_HOT TOUTIAO_HOT YOUTUBE_TRENDING TWITTER_TRENDING GITHUB_TRENDING WEBHOOK_URL
python3 << 'PYEOF'
import json, urllib.request, os
date, weekday, today = os.environ.get('DATE', ''), os.environ.get('WEEKDAY', ''), os.environ.get('TODAY_DATE', '')
weather = (os.environ.get('WEATHER_ICON', ''), os.environ.get('WEATHER_TEMP', ''), os.environ.get('WEATHER_WIND', ''), os.environ.get('WEATHER_HUM', ''))
baidu, douyin, toutiao = os.environ.get('BAIDU_HOT', ''), os.environ.get('DOUYIN_HOT', ''), os.environ.get('TOUTIAO_HOT', '')
youtube, twitter, github = os.environ.get('YOUTUBE_TRENDING', ''), os.environ.get('TWITTER_TRENDING', ''), os.environ.get('GITHUB_TRENDING', '')
webhook = os.environ.get('WEBHOOK_URL', '')
header = (
f"☀️ 鹤童大人,早上好! \n"
f"📅 今天是 {date} {weekday} \n"
f"🌇 杭州: \n"
f"🌤️ 天气:{weather[0]} \n"
f"🌡️ 温度:{weather[1]} \n"
f"🌬️ 风速:{weather[2]} \n"
f"💧 湿度:{weather[3]} \n"
f"祝您一天好心情!🦞"
)
message = (
f"{header}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n\n"
f"📰 国内热搜\n\n"
f"**🔍 百度热搜 Top 10({today}):**\n"
f"{baidu}\n\n"
f"**🎵 抖音热榜 Top 10({today}):**\n"
f"{douyin}\n\n"
f"**📰 今日头条 Top 10({today}):**\n"
f"{toutiao}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n\n"
f"🌍 国外热搜\n\n"
f"**▶️ YouTube 热门({today}):**\n"
f"{youtube}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n\n"
f"**📈 GitHub Trending Top 10({today}):**\n"
f"{github}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n\n"
f"祝您工作顺利!🦞"
)
req = urllib.request.Request(webhook, data=json.dumps({"msgtype": "markdown", "markdown": {"title": "每日简报", "text": message}}).encode('utf-8'), headers={'Content-Type': 'application/json'})
try:
print(urllib.request.urlopen(req).read().decode('utf-8'))
except Exception as e:
print(f"Error: {e}")
PYEOF
echo "简报已发送"

浙公网安备 33010602011771号