
Content Source Aggregator
- 22 installs
- 82 repo stars
- Updated August 2, 2026
- aaaaqwq/claude-code-skills
content-source-aggregator is a Claude Code skill that collects real-time trends from 16+ platforms into a standardized hot-topic pool.
About
content-source-aggregator is a Claude Code skill that collects real-time trends and hot topics from 16+ platforms. A team uses it as the data-collection stage of a content pipeline, feeding trending items to research and content agents. It normalizes results into a dated JSON hot-topic pool with engagement metrics.
- Collects real-time trends from 16+ platforms (Reddit, GitHub, HackerNews, ArXiv, YouTube, etc.)
- Outputs a standardized JSON hot-topic pool with engagement metrics
- Runs full or single-source collection via a fetch_all.py script
Content Source Aggregator by the numbers
- 22 all-time installs (skills.sh)
- Ranked #1,278 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
content-source-aggregator capabilities & compatibility
- Capabilities
- trend aggregation · content sourcing · web scraping · data collection
- Works with
- github · linkedin
- Use cases
- research · web scraping · web search
What content-source-aggregator says it does
从 Reddit/GitHub/HackerNews/ArXiv/YouTube/B站/微博/知乎等 16+ 平台免费获取实时热点、技术趋势、AI前沿论文,输出标准化热点池。
本 skill 是内容工厂流水线的 **Phase 1(数据采集层)**
npx skills add https://github.com/aaaaqwq/claude-code-skills --skill content-source-aggregatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 22 |
|---|---|
| repo stars | ★ 82 |
| Last updated | August 2, 2026 |
| Repository | aaaaqwq/claude-code-skills ↗ |
What it does
Aggregate real-time trends and hot topics across many platforms into a standardized pool for content workflows.
Who is it for?
Content pipelines needing a real-time trending-topic feed across platforms
Skip if: Sources requiring logged-in state (Xiaohongshu, WeChat public accounts are marked pending)
When should I use this skill?
When you need real-time hot topics, tech trends, or AI papers aggregated
What you get
A deduplicated, scored, standardized hot-topic pool feeding downstream content agents.
- standardized JSON hot-topic pool
By the numbers
- 16+ platforms covered
- 15 AI/Tech subreddits
- 12 YouTube channels
Files
📡 实时数据采集中心(小data 专属)
- Author: Daniel Li
- Copyright © Daniel Li. All rights reserved.
角色定位:你是团队的数据调控大师。所有团队成员需要的实时数据、热点趋势、市场信号,都由你通过此 skill 统一采集和管理。
核心能力
覆盖 16+ 平台(按优先级排序)
| 优先级 | 平台 | 数据类型 | 方式 | 状态 |
|---|---|---|---|---|
| 🥇 | AI/Tech社区讨论 | old.reddit.com JSON API | ✅ 15个子板块 | |
| 🥇 | GitHub | 开源项目趋势 | Trending + Search API | ✅ |
| 🥇 | HackerNews | 技术创业热点 | Firebase API | ✅ |
| 🥇 | ArXiv | AI/ML前沿论文 | Atom API | ✅ cs.AI/LG/CL/CV |
| 🥈 | Twitter/X | AI KOL动态 | syndication API | ⚠️ 需代理 |
| 🥈 | YouTube | AI/Tech视频 | RSS Feed | ✅ 12频道 |
| 🥈 | ProductHunt | 新产品发布 | RSS Feed | ✅ |
| 🥉 | B站 | 国内视频热点 | 公开API | ✅ |
| 🥉 | 知乎 | 中文问答热榜 | 60s API | ✅ |
| 🥉 | 微博 | 社会热搜 | 60s API | ✅ |
| 🥉 | 头条 | 新闻热榜 | 60s API | ✅ |
| 🥉 | 抖音 | 短视频热搜 | 60s API | ✅ |
| 🥉 | LinuxDo | 技术社区 | Discourse API | ✅ |
| ⏳ | 小红书 | 生活消费 | 需登录态 | 待接入 |
| ⏳ | 微信公众号 | 深度文章 | 需登录态 | 待接入 |
使用方法
全量采集(按优先级顺序)
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py单平台采集
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source reddit
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source github
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source hackernews
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source arxiv
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source youtube
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source producthunt
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --source bilibili预览模式(不保存文件)
python3 ~/clawd/skills/content-source-aggregator/scripts/fetch_all.py --dry-run输出格式
标准化 JSON 写入 ~/clawd/workspace/content-pipeline/hotpool/YYYY-MM-DD.json
{
"date": "2026-03-08",
"fetched_at": "2026-03-08T10:00:00+08:00",
"total": 150,
"sources": {"reddit": 54, "github": 11, "hackernews": 15, ...},
"items": [
{
"source": "reddit",
"title": "...",
"url": "...",
"summary": "...",
"category": "r/artificial",
"engagement": {"upvotes": 1200, "comments": 340},
"author": "...",
"fetched_at": "..."
}
]
}配置
编辑 scripts/config.json 自定义:
source_priority— 采集优先级顺序reddit.subreddits— 关注的子板块(当前15个AI/Tech核心板块)twitter.accounts— 关注的X账号(当前28个AI KOL)youtube.channels— 关注的频道(当前12个)github.min_stars— 最低星数阈值arxiv.categories— ArXiv分类(cs.AI/LG/CL/CV)hackernews.limit— HackerNews获取数量
数据管理职责(小data 必读)
日常职责
1. 每日 3 次采集:08:00 / 14:00 / 20:00 运行全量采集 2. 数据质量监控:检查各源是否正常返回,异常立即报告 3. 热点池维护:去重、评分、分类,输出高质量热点池 4. 按需采集:团队成员(小content/小research/小market)请求特定数据时即时响应
数据服务接口
其他 agent 可以通过以下方式获取数据:
- 读文件:直接读
~/clawd/workspace/content-pipeline/hotpool/YYYY-MM-DD.json - 发指令:通过 sessions_send 请求小data采集特定平台/话题
- Cron 产出:每日定时采集结果自动写入热点池
数据质量标准
- 每条数据必须有
source、title、url - engagement 数据尽量完整(upvotes/comments/views)
- 去除广告、spam、低质量内容
- 中文和英文内容均保留
API 详情
- 端点:
https://old.reddit.com/r/{sub}/hot.json?limit=5 - 直连可用,无需代理,无需认证
- 子板块: artificial, MachineLearning, LocalLLaMA, singularity, ChatGPT, ClaudeAI, OpenAI, StableDiffusion, technology, programming, deeplearning, datascience, ArtificialIntelligence, compsci, startups
GitHub
- Trending: 页面解析
- Search API:
https://api.github.com/search/repositories?q=stars:>{min_stars}+pushed:>{date}&sort=stars - 无认证 60次/小时
HackerNews
- Top Stories:
https://hacker-news.firebaseio.com/v0/topstories.json - Item:
https://hacker-news.firebaseio.com/v0/item/{id}.json - 完全免费,无限制
ArXiv
- Atom API:
http://export.arxiv.org/api/query?search_query=cat:cs.AI+OR+cat:cs.LG&sortBy=submittedDate&sortOrder=descending&max_results=10 - 完全免费
Twitter/X ⚠️
- syndication API:
https://syndication.twitter.com/srv/timeline-profile/screen-name/{username} - 国内需代理(被墙)
- 代理配置: 环境变量
CONTENT_PROXY(默认http://127.0.0.1:7890)
YouTube
- RSS:
https://www.youtube.com/feeds/videos.xml?channel_id={id} - 免费无限制
B站
- 热门排行:
https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all - 需 Referer: https://www.bilibili.com
中文热搜(60s API)
- 微博:
https://60s.viki.moe/v2/weibo - 知乎:
https://60s.viki.moe/v2/zhihu - 头条:
https://60s.viki.moe/v2/toutiao - 抖音:
https://60s.viki.moe/v2/douyin
与团队协作
本 skill 是内容工厂流水线的 Phase 1(数据采集层):
小data(采集) → 小research(选题评分) → 小content(内容创作) → 小market(分发推广)数据流向:
- 热点池 → 小research 做话题筛选和深度调研
- GitHub Trending → 小content 写技术解读文章
- ArXiv 论文 → 小research 做论文摘要和解读
- Reddit 讨论 → 小content 写社区观点汇总
#!/usr/bin/env python3
"""
统一信息源热点采集器
从 微博/知乎/头条/抖音/B站/GitHub/YouTube/Twitter/Reddit/LinuxDo 等平台采集热门内容
HTTP请求统一用 subprocess+curl 避免 SSL 问题
"""
import json
import os
import re
import subprocess
import sys
import html as htmlmod
import urllib.parse
from datetime import datetime, timezone, timedelta
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
CONFIG_FILE = SCRIPT_DIR / "config.json"
OUTPUT_DIR = Path.home() / "clawd/workspace/content-pipeline/hotpool"
TZ_CST = timezone(timedelta(hours=8))
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
def load_config():
with open(CONFIG_FILE, "r") as f:
return json.load(f)
# 海外源列表(需要代理)
OVERSEAS_SOURCES = {"twitter", "reddit", "youtube", "github"}
# 代理地址(从环境变量或默认 Clash)
PROXY_URL = os.environ.get("CONTENT_PROXY", "http://127.0.0.1:7890")
def curl_get(url, headers=None, timeout=15, use_proxy=False):
"""用 subprocess+curl 获取 URL 内容,避免 SSL 问题。海外源自动走代理。"""
cmd = ["curl", "-s", "--max-time", str(timeout), "-L",
"-H", f"User-Agent: {UA}"]
if use_proxy and PROXY_URL:
cmd += ["--proxy", PROXY_URL]
if headers:
for k, v in headers.items():
cmd += ["-H", f"{k}: {v}"]
cmd.append(url)
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5)
if r.returncode != 0:
raise RuntimeError(f"curl failed ({r.returncode}): {r.stderr[:200]}")
return r.stdout
def curl_get_json(url, headers=None, timeout=15, use_proxy=False):
return json.loads(curl_get(url, headers, timeout, use_proxy))
# Legacy compat
http_get = curl_get
http_get_json = curl_get_json
# ── 60s API 通用解析 ──────────────────────────────
def _parse_60s_v2(source_name, api_path, category="热搜"):
"""通用 60s API v2 解析器 (字段: code/data, 每条: title/link/hot_value)"""
items = []
try:
data = curl_get_json(f"https://60s.viki.moe/v2/{api_path}")
if data.get("code") != 200:
print(f" ⚠️ {source_name} 60s API: code={data.get('code')}", file=sys.stderr)
return items
for entry in data.get("data", []):
title = entry.get("title", entry.get("name", entry.get("word", "")))
url = entry.get("link", entry.get("url", entry.get("mobileUrl", "")))
if not title:
continue
item = {
"source": source_name,
"title": title,
"url": url,
"summary": entry.get("desc", entry.get("excerpt", ""))[:200] if entry.get("desc") or entry.get("excerpt") else "",
"category": category,
"engagement": {},
}
hot = entry.get("hot_value", entry.get("hotValue", entry.get("hot", 0)))
if hot:
item["engagement"]["hot_value"] = hot
items.append(item)
except Exception as e:
print(f" ⚠️ {source_name}: {e}", file=sys.stderr)
return items
# ── 微博热搜 (60s API) ────────────────────────────
def fetch_weibo(config):
"""通过 60s API 获取微博热搜"""
return _parse_60s_v2("weibo", "weibo", "微博热搜")
# ── 知乎热榜 (60s API) ────────────────────────────
def fetch_zhihu(config):
"""通过 60s API 获取知乎热榜"""
return _parse_60s_v2("zhihu", "zhihu", "知乎热榜")
# ── 头条热榜 (60s API) ────────────────────────────
def fetch_toutiao(config):
"""通过 60s API 获取今日头条热榜"""
return _parse_60s_v2("toutiao", "toutiao", "头条热榜")
# ── 抖音热搜 (60s API, 替代原生) ──────────────────
def fetch_douyin(config):
"""通过 60s API 获取抖音热搜"""
return _parse_60s_v2("douyin", "douyin", "抖音热搜")
# ── Twitter/X ──────────────────────────────────────
def fetch_twitter(config):
items = []
accounts = config.get("accounts", [])
for account in accounts:
try:
url = f"https://syndication.twitter.com/srv/timeline-profile/screen-name/{account}"
html = curl_get(url, use_proxy=True)
texts = re.findall(r'"text":"([^"]{20,500})"', html)
for text in texts[:3]:
clean = htmlmod.unescape(text).replace("\\n", " ").strip()
if clean.startswith("RT @"):
continue
items.append({
"source": "twitter",
"title": clean[:120],
"url": f"https://x.com/{account}",
"summary": clean[:300],
"category": "Social/Tech",
"engagement": {},
"author": account,
})
except Exception as e:
print(f" ⚠️ Twitter @{account}: {e}", file=sys.stderr)
return items
# ── YouTube ────────────────────────────────────────
def fetch_youtube(config):
items = []
channels = config.get("channels", {})
for name, channel_id in channels.items():
try:
url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
xml = curl_get(url, use_proxy=True)
entries = re.findall(
r"<entry>.*?<title>(.+?)</title>.*?<yt:videoId>(.+?)</yt:videoId>.*?<published>(.+?)</published>.*?</entry>",
xml, re.DOTALL
)
for title, vid, published in entries[:3]:
title = htmlmod.unescape(title)
items.append({
"source": "youtube",
"title": title,
"url": f"https://www.youtube.com/watch?v={vid}",
"summary": f"[{name}] {title}",
"category": "Video/Tech",
"engagement": {},
"author": name,
"published": published,
})
except Exception as e:
print(f" ⚠️ YouTube {name}: {e}", file=sys.stderr)
return items
# ── B站 ───────────────────────────────────────────
def fetch_bilibili(config):
items = []
try:
data = curl_get_json(
"https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all",
headers={"Referer": "https://www.bilibili.com"}
)
if data.get("code") == 0:
for v in data["data"]["list"][:15]:
items.append({
"source": "bilibili",
"title": v["title"],
"url": f"https://www.bilibili.com/video/{v['bvid']}",
"summary": v.get("desc", "")[:200],
"category": v.get("tname", "综合"),
"engagement": {
"views": v.get("stat", {}).get("view", 0),
"likes": v.get("stat", {}).get("like", 0),
"comments": v.get("stat", {}).get("reply", 0),
},
"author": v.get("owner", {}).get("name", ""),
})
except Exception as e:
print(f" ⚠️ Bilibili 原生API: {e}, 回退60s", file=sys.stderr)
items.extend(_parse_60s_v2("bilibili", "bili", "B站热门"))
try:
data = curl_get_json(
"https://api.bilibili.com/x/web-interface/wbi/search/square?limit=10",
headers={"Referer": "https://www.bilibili.com"}
)
if data.get("code") == 0:
trending = data.get("data", {}).get("trending", {})
for t in trending.get("list", [])[:10]:
items.append({
"source": "bilibili",
"title": f"[热搜] {t.get('keyword', t.get('show_name', ''))}",
"url": f"https://search.bilibili.com/all?keyword={urllib.parse.quote(t.get('keyword', ''))}",
"summary": t.get("show_name", ""),
"category": "热搜",
"engagement": {},
})
except Exception as e:
print(f" ⚠️ Bilibili 热搜: {e}", file=sys.stderr)
return items
# ── GitHub Trending ────────────────────────────────
def fetch_github(config):
"""GitHub Trending daily(替代原来的全历史star排名)"""
items = []
try:
html = curl_get("https://github.com/trending?since=daily")
repos = re.findall(r'<h2[^>]*>\s*<a[^>]*href="/([^"]+)"[^>]*>', html)
for repo_path in repos[:15]:
repo_path = repo_path.strip()
if not repo_path or repo_path.count('/') != 1:
continue
desc_match = re.search(
rf'href="/{re.escape(repo_path)}".*?<p[^>]*>(.*?)</p>',
html, re.DOTALL
)
desc = re.sub(r'<[^>]+>', '', desc_match.group(1)).strip()[:200] if desc_match else ""
stars_match = re.search(
rf'{re.escape(repo_path)}.*?(\d[\d,]*)\s*stars today', html, re.DOTALL
)
stars_today = int(stars_match.group(1).replace(',', '')) if stars_match else 0
items.append({
"source": "github",
"title": f"{repo_path} 🔥+{stars_today}⭐/today" if stars_today else repo_path,
"url": f"https://github.com/{repo_path}",
"summary": desc,
"category": "GitHub Trending",
"engagement": {"stars_today": stars_today},
"author": repo_path.split('/')[0],
})
except Exception as e:
print(f" ⚠️ GitHub Trending: {e}, 回退Search API", file=sys.stderr)
lookback = config.get("lookback_days", 1)
min_stars = config.get("min_stars", 100)
date_str = (datetime.now(TZ_CST) - timedelta(days=lookback)).strftime("%Y-%m-%d")
try:
url = f"https://api.github.com/search/repositories?q=stars:>{min_stars}+pushed:>{date_str}&sort=stars&per_page=15"
data = curl_get_json(url)
for r in data.get("items", [])[:15]:
items.append({
"source": "github", "title": f"{r['full_name']} ⭐{r['stargazers_count']}",
"url": r["html_url"], "summary": (r.get("description") or "")[:200],
"category": r.get("language", "Unknown"),
"engagement": {"stars": r["stargazers_count"], "forks": r.get("forks_count", 0)},
"author": r["owner"]["login"],
})
except Exception as e2:
print(f" ⚠️ GitHub Search: {e2}", file=sys.stderr)
return items
# ── Reddit ─────────────────────────────────────────
def fetch_reddit(config):
items = []
subreddits = config.get("subreddits", ["technology"])
for sub in subreddits:
try:
# Reddit 官方 JSON API(用 old.reddit.com 避免重定向)
url = f"https://old.reddit.com/r/{sub}/hot.json?limit=5"
headers = {"Accept": "application/json"}
data = curl_get_json(url, headers=headers)
for p in data.get("data", {}).get("children", [])[:5]:
d = p.get("data", {})
if d.get("stickied"):
continue
items.append({
"source": "reddit", "title": d.get("title", ""),
"url": f"https://reddit.com{d.get('permalink', '')}",
"summary": (d.get("selftext") or "")[:200],
"category": f"r/{sub}",
"engagement": {"upvotes": d.get("score", 0), "comments": d.get("num_comments", 0)},
"author": d.get("author", ""),
})
except Exception as e:
print(f" ⚠️ Reddit r/{sub}: {e}", file=sys.stderr)
return items
# ── LinuxDo ────────────────────────────────────────
def fetch_linuxdo(config):
items = []
cookie_file = Path.home() / ".playwright-data/linuxdo/cookies.txt"
headers = {"Accept": "application/json", "Referer": "https://linux.do/"}
if cookie_file.exists():
headers["Cookie"] = cookie_file.read_text().strip()
try:
data = curl_get_json("https://linux.do/latest.json?order=default", headers=headers)
topics = data.get("topic_list", {}).get("topics", [])
for t in topics[:15]:
items.append({
"source": "linuxdo", "title": t.get("title", ""),
"url": f"https://linux.do/t/{t.get('slug', '')}/{t.get('id', '')}",
"summary": "", "category": str(t.get("category_id", "")),
"engagement": {"views": t.get("views", 0), "likes": t.get("like_count", 0), "comments": t.get("posts_count", 0)},
})
except Exception as e:
print(f" ⚠️ LinuxDo: {e}", file=sys.stderr)
return items
# ── 小红书 ────────────────────────────────────────
def fetch_xiaohongshu(config):
items = []
cookie_file = Path.home() / ".playwright-data/xiaohongshu/cookies.txt"
if not cookie_file.exists():
print(" ⚠️ 小红书: 需要登录态 cookie", file=sys.stderr)
return items
headers = {"Referer": "https://www.xiaohongshu.com/", "Cookie": cookie_file.read_text().strip()}
try:
html = curl_get("https://www.xiaohongshu.com/explore", headers=headers)
match = re.search(r'window\.__INITIAL_STATE__\s*=\s*({.*?})\s*</script>', html, re.DOTALL)
if match:
raw = match.group(1).replace("undefined", "null")
state = json.loads(raw)
for note in state.get("explore", {}).get("feeds", [])[:15]:
nd = note.get("noteCard", note)
items.append({
"source": "xiaohongshu", "title": nd.get("title", nd.get("displayTitle", "")),
"url": f"https://www.xiaohongshu.com/explore/{note.get('id', '')}",
"summary": nd.get("desc", "")[:200], "category": "小红书",
"engagement": {"likes": nd.get("interactInfo", {}).get("likedCount", 0)},
"author": nd.get("user", {}).get("nickname", ""),
})
except Exception as e:
print(f" ⚠️ 小红书: {e}", file=sys.stderr)
return items
# ── 微信公众号 ────────────────────────────────────
def fetch_wechat_mp(config):
items = []
cookie_file = Path.home() / ".playwright-data/sogou-weixin/cookies.txt"
headers = {"Referer": "https://weixin.sogou.com/"}
if cookie_file.exists():
headers["Cookie"] = cookie_file.read_text().strip()
keywords = config.get("keywords", ["AI", "科技", "互联网"])
for kw in keywords[:3]:
try:
url = f"https://weixin.sogou.com/weixin?type=2&query={urllib.parse.quote(kw)}&ie=utf8"
html = curl_get(url, headers=headers)
articles = re.findall(r'<a[^>]*href="([^"]*)"[^>]*target="_blank"[^>]*>(.*?)</a>', html, re.DOTALL)
for href, title_html in articles[:5]:
title = re.sub(r'<[^>]+>', '', title_html).strip()
if len(title) > 5 and "sogou" not in title.lower():
items.append({
"source": "wechat_mp", "title": title,
"url": href if href.startswith("http") else f"https://weixin.sogou.com{href}",
"summary": "", "category": f"公众号/{kw}", "engagement": {},
})
except Exception as e:
print(f" ⚠️ 微信公众号 [{kw}]: {e}", file=sys.stderr)
return items
def fetch_wechat_video(config):
print(" ⚠️ 微信视频号: 无公开 API", file=sys.stderr)
return []
# ── HackerNews ────────────────────────────────────
def fetch_hackernews(config):
"""HackerNews Top Stories(官方API,无需认证,可直连)"""
items = []
try:
limit = config.get("limit", 15)
ids = curl_get_json("https://hacker-news.firebaseio.com/v0/topstories.json")[:limit]
for story_id in ids:
try:
s = curl_get_json(f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json")
if s and s.get("type") == "story":
items.append({
"source": "hackernews",
"title": s.get("title", ""),
"url": s.get("url", f"https://news.ycombinator.com/item?id={story_id}"),
"summary": "",
"category": "Tech/Startup",
"engagement": {"upvotes": s.get("score", 0), "comments": s.get("descendants", 0)},
"author": s.get("by", ""),
"hn_url": f"https://news.ycombinator.com/item?id={story_id}",
})
except Exception:
continue
except Exception as e:
print(f" ⚠️ HackerNews: {e}", file=sys.stderr)
return items
# ── ProductHunt ───────────────────────────────────
def fetch_producthunt(config):
"""ProductHunt Today's Posts(web端RSS,可直连)"""
items = []
try:
xml = curl_get("https://www.producthunt.com/feed", use_proxy=True)
entries = re.findall(
r"<item>.*?<title><!\[CDATA\[(.+?)\]\]></title>.*?<link>(.+?)</link>.*?<description><!\[CDATA\[(.+?)\]\]></description>.*?</item>",
xml, re.DOTALL
)
for title, link, desc in entries[:10]:
items.append({
"source": "producthunt",
"title": htmlmod.unescape(title.strip()),
"url": link.strip(),
"summary": htmlmod.unescape(re.sub(r"<[^>]+>", "", desc))[:200],
"category": "Product/Startup",
"engagement": {},
})
except Exception as e:
print(f" ⚠️ ProductHunt: {e}", file=sys.stderr)
return items
# ── ArXiv AI ──────────────────────────────────────
def fetch_arxiv(config):
"""ArXiv AI/ML 最新论文(Atom API,可直连)"""
items = []
try:
categories = config.get("categories", ["cs.AI", "cs.LG", "cs.CL"])
cat_query = "+OR+".join([f"cat:{c}" for c in categories])
url = f"http://export.arxiv.org/api/query?search_query={cat_query}&sortBy=submittedDate&sortOrder=descending&max_results=10"
xml = curl_get(url)
entries = re.findall(
r"<entry>.*?<title>(.*?)</title>.*?<id>(.*?)</id>.*?<summary>(.*?)</summary>.*?</entry>",
xml, re.DOTALL
)
for title, link, summary in entries:
title = re.sub(r"\s+", " ", title.strip())
items.append({
"source": "arxiv",
"title": title,
"url": link.strip(),
"summary": re.sub(r"\s+", " ", summary.strip())[:300],
"category": "Research/AI",
"engagement": {},
})
except Exception as e:
print(f" ⚠️ ArXiv: {e}", file=sys.stderr)
return items
# ── 主流程 ─────────────────────────────────────────
FETCHERS = {
"reddit": fetch_reddit,
"github": fetch_github,
"hackernews": fetch_hackernews,
"arxiv": fetch_arxiv,
"twitter": fetch_twitter,
"youtube": fetch_youtube,
"producthunt": fetch_producthunt,
"bilibili": fetch_bilibili,
"weibo": fetch_weibo,
"zhihu": fetch_zhihu,
"toutiao": fetch_toutiao,
"douyin": fetch_douyin,
"linuxdo": fetch_linuxdo,
"xiaohongshu": fetch_xiaohongshu,
"wechat_mp": fetch_wechat_mp,
"wechat_video": fetch_wechat_video,
}
def main():
import argparse
parser = argparse.ArgumentParser(description="信息源热点采集")
parser.add_argument("--source", choices=list(FETCHERS.keys()), help="只采集指定平台")
parser.add_argument("--dry-run", action="store_true", help="只打印不保存")
args = parser.parse_args()
config = load_config()
now = datetime.now(TZ_CST)
all_items = []
# 按 source_priority 排序(优质海外源优先)
priority = config.get("source_priority", list(FETCHERS.keys()))
if args.source:
sources = [args.source]
else:
# 优先级列表中的先采集,未列出的按原顺序追加
sources = [s for s in priority if s in FETCHERS]
for s in FETCHERS:
if s not in sources:
sources.append(s)
for src in sources:
src_config = config.get(src, {})
if not src_config.get("enabled", True):
print(f"⏭️ {src}: disabled")
continue
print(f"🔍 采集 {src}...")
try:
items = FETCHERS[src](src_config)
for item in items:
item["fetched_at"] = now.isoformat()
all_items.extend(items)
print(f" ✅ {len(items)} 条")
except Exception as e:
print(f" ❌ {src}: {e}")
output = {
"date": now.strftime("%Y-%m-%d"),
"fetched_at": now.isoformat(),
"total": len(all_items),
"sources": {src: len([i for i in all_items if i["source"] == src]) for src in sources},
"items": all_items,
}
if args.dry_run:
print(json.dumps(output, indent=2, ensure_ascii=False)[:3000])
print(f"\n... 共 {len(all_items)} 条")
else:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
outfile = OUTPUT_DIR / f"{now.strftime('%Y-%m-%d')}.json"
with open(outfile, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
print(f"\n📁 已保存: {outfile}")
print(f"📊 共 {len(all_items)} 条 ({', '.join(f'{k}:{v}' for k,v in output['sources'].items())})")
if __name__ == "__main__":
main()