名称:github-trending 描述:获取并显示GitHub趋势仓库和开发者数据。用于构建展示趋势仓库的仪表板、发现热门项目或跟踪GitHub趋势。触发词包括GitHub趋势、趋势仓库、热门仓库、GitHub发现。
GitHub趋势数据
访问GitHub趋势仓库和开发者数据。
重要说明
GitHub不提供官方的趋势API。 必须直接抓取github.com/trending页面或使用GitHub搜索API作为替代方案。
方法一:直接网页抓取(推荐)
使用Cheerio抓取github.com/trending:
import * as cheerio from 'cheerio';
interface TrendingRepo {
owner: string;
name: string;
fullName: string;
url: string;
description: string;
language: string;
languageColor: string;
stars: number;
forks: number;
starsToday: number;
}
async function scrapeTrending(options: {
language?: string;
since?: 'daily' | 'weekly' | 'monthly';
} = {}): Promise<TrendingRepo[]> {
// 构建URL:github.com/trending 或 github.com/trending/typescript?since=weekly
let url = 'https://github.com/trending';
if (options.language) {
url += `/${encodeURIComponent(options.language)}`;
}
if (options.since) {
url += `?since=${options.since}`;
}
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; TrendingBot/1.0)',
},
});
if (!response.ok) {
throw new Error(`获取趋势数据失败: ${response.status}`);
}
const html = await response.text();
const $ = cheerio.load(html);
const repos: TrendingRepo[] = [];
// 每个趋势仓库位于article.Box-row元素中
$('article.Box-row').each((_, element) => {
const $el = $(element);
// 获取仓库链接(例如,/owner/repo)
const repoLink = $el.find('h2 a').attr('href')?.trim() || '';
const [, owner, name] = repoLink.split('/');
// 获取描述
const description = $el.find('p.col-9').text().trim();
// 获取语言
const language = $el.find('[itemprop="programmingLanguage"]').text().trim();
// 从彩色点获取语言颜色
const langColorStyle = $el.find('.repo-language-color').attr('style') || '';
const langColorMatch = langColorStyle.match(/background-color:\s*([^;]+)/);
const languageColor = langColorMatch ? langColorMatch[1].trim() : '';
// 获取星星数(总计)
const starsText = $el.find('a[href$="/stargazers"]').text().trim();
const stars = parseNumber(starsText);
// 获取分支数
const forksText = $el.find('a[href$="/forks"]').text().trim();
const forks = parseNumber(forksText);
// 获取今日/本周/本月新增星星数
const starsTodayText = $el.find('.float-sm-right, .d-inline-block.float-sm-right').text().trim();
const starsToday = parseNumber(starsTodayText);
if (owner && name) {
repos.push({
owner,
name,
fullName: `${owner}/${name}`,
url: `https://github.com${repoLink}`,
description,
language,
languageColor,
stars,
forks,
starsToday,
});
}
});
return repos;
}
function parseNumber(text: string): number {
const clean = text.replace(/,/g, '').trim();
if (clean.includes('k')) {
return Math.round(parseFloat(clean) * 1000);
}
return parseInt(clean) || 0;
}
方法二:GitHub搜索API(官方替代方案)
使用GitHub的搜索API查找近期创建且星标数高的仓库:
interface GitHubSearchResult {
total_count: number;
items: GitHubRepo[];
}
interface GitHubRepo {
full_name: string;
html_url: string;
description: string;
language: string;
stargazers_count: number;
forks_count: number;
created_at: string;
}
async function getTrendingViaSearch(options: {
language?: string;
days?: number;
minStars?: number;
} = {}): Promise<GitHubRepo[]> {
const days = options.days || 7;
const minStars = options.minStars || 100;
// 计算N天前的日期
const date = new Date();
date.setDate(date.getDate() - days);
const since = date.toISOString().split('T')[0];
// 构建搜索查询
const queryParts = [
`created:>${since}`,
`stars:>=${minStars}`,
];
if (options.language) {
queryParts.push(`language:${options.language}`);
}
const query = queryParts.join(' ');
const response = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=25`,
{
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, // 可选但推荐
'User-Agent': 'TrendingApp/1.0',
},
}
);
if (!response.ok) {
throw new Error(`GitHub API错误: ${response.status}`);
}
const data: GitHubSearchResult = await response.json();
return data.items;
}
注意: 搜索API有速率限制(未认证时每分钟10次请求,带令牌时每分钟30次)。
Next.js API路由(服务器端抓取)
// app/api/trending/route.ts
import { NextRequest } from 'next/server';
import * as cheerio from 'cheerio';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const language = searchParams.get('language') || '';
const since = searchParams.get('since') || 'daily';
try {
let url = 'https://github.com/trending';
if (language) url += `/${encodeURIComponent(language)}`;
url += `?since=${since}`;
const response = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (compatible)' },
next: { revalidate: 3600 }, // 缓存1小时
});
const html = await response.text();
const repos = parseGitHubTrending(html);
return Response.json(repos);
} catch (error) {
console.error('趋势抓取失败:', error);
return Response.json(
{ error: '获取趋势仓库失败' },
{ status: 500 }
);
}
}
function parseGitHubTrending(html: string) {
const $ = cheerio.load(html);
const repos: any[] = [];
$('article.Box-row').each((_, el) => {
const $el = $(el);
const repoLink = $el.find('h2 a').attr('href') || '';
const [, owner, name] = repoLink.split('/');
repos.push({
owner,
name,
fullName: `${owner}/${name}`,
url: `https://github.com${repoLink}`,
description: $el.find('p.col-9').text().trim(),
language: $el.find('[itemprop="programmingLanguage"]').text().trim(),
stars: parseNumber($el.find('a[href$="/stargazers"]').text()),
forks: parseNumber($el.find('a[href$="/forks"]').text()),
starsToday: parseNumber($el.find('.float-sm-right').text()),
});
});
return repos;
}
function parseNumber(text: string): number {
const clean = text.replace(/,/g, '').trim();
if (clean.includes('k')) return Math.round(parseFloat(clean) * 1000);
return parseInt(clean) || 0;
}
React钩子
import { useState, useEffect } from 'react';
function useTrending(options: { language?: string; since?: string } = {}) {
const [repos, setRepos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchTrending() {
setIsLoading(true);
try {
const params = new URLSearchParams();
if (options.language) params.set('language', options.language);
if (options.since) params.set('since', options.since);
const response = await fetch(`/api/trending?${params}`);
if (!response.ok) throw new Error('获取失败');
setRepos(await response.json());
} catch (err) {
setError(err instanceof Error ? err.message : '未知错误');
} finally {
setIsLoading(false);
}
}
fetchTrending();
}, [options.language, options.since]);
return { repos, isLoading, error };
}
重要考虑因素
- 无官方API:GitHub趋势页面没有官方API - 抓取是唯一选项
- 速率限制:尊重GitHub服务器 - 积极缓存
- HTML结构变化:GitHub可能更改HTML - 监控断点
- User-Agent:始终包含User-Agent头部
- 仅服务器端:在服务器端进行抓取以避免CORS问题
资源
- GitHub趋势页面:https://github.com/trending
- GitHub搜索API:https://docs.github.com/en/rest/search