每日总结脚本模板

用 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 "简报已发送"
posted @ 2026-06-23 17:40  cwp0  阅读(52)  评论(0)    收藏  举报