项目初始化

This commit is contained in:
2026-07-02 11:31:16 +08:00
commit eef4c76e3f
64 changed files with 13369 additions and 0 deletions

120
daily/skills_board.py Normal file
View File

@@ -0,0 +1,120 @@
"""从 skills.sh 官网抓取 Trending / Hot 完整榜单(突破 feed.json 50 条限制)。"""
from __future__ import annotations
import logging
import re
from typing import Any, Literal
import certifi
import httpx
from daily.config import env
logger = logging.getLogger(__name__)
Board = Literal["trending", "hot"]
SKILLS_SITE = "https://www.skills.sh"
USER_AGENT = "Mozilla/5.0 (compatible; skills-hot-daily/1.0; +https://skills.sh)"
_SKILL_RE = re.compile(
r'\{"source":"(?P<source>[^"]+)","skillId":"(?P<skill_id>[^"]+)",'
r'"name":"(?P<name>[^"]+)","installs":(?P<installs>\d+)'
)
_RSC_CHUNK_RE = re.compile(r"self\.__next_f\.push\(\[1,\"(.*?)\"\]\)", re.DOTALL)
def board_source() -> str:
return (env("SKILLS_BOARD_SOURCE") or "website").strip().lower()
def _fetch_html(path: str) -> str:
url = f"{SKILLS_SITE}{path}"
headers = {"User-Agent": USER_AGENT, "Accept": "text/html"}
with httpx.Client(timeout=30.0, verify=certifi.where(), follow_redirects=True) as client:
resp = client.get(url, headers=headers)
resp.raise_for_status()
return resp.text
def _rsc_blob(html: str) -> str:
chunks = _RSC_CHUNK_RE.findall(html)
blob = "\n".join(chunks)
return blob.encode("utf-8").decode("unicode_escape", errors="ignore")
def _parse_initial_skills(blob: str, *, limit: int) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
seen: set[str] = set()
for match in _SKILL_RE.finditer(blob):
source = match.group("source")
skill_id = match.group("skill_id")
uid = f"{source}/{skill_id}"
if uid in seen:
continue
seen.add(uid)
items.append(
{
"id": uid,
"title": skill_id,
"source": source,
"installs": int(match.group("installs")),
"link": f"{SKILLS_SITE}/{source}/{skill_id}",
"description": "",
}
)
if len(items) >= limit:
break
return items
def fetch_board(board: Board, *, limit: int) -> list[dict[str, Any]]:
path = "/trending" if board == "trending" else "/hot"
try:
html = _fetch_html(path)
items = _parse_initial_skills(_rsc_blob(html), limit=limit)
if items:
logger.info("skills.sh %s: %d items (limit=%d)", board, len(items), limit)
return items
except Exception as exc:
logger.warning("skills.sh %s fetch failed, fallback to feed.json: %s", board, exc)
return []
def enrich_from_feed(items: list[dict[str, Any]], feed: dict[str, Any]) -> None:
desc_by_id: dict[str, str] = {}
for key in ("topTrending", "topHot", "topAllTime"):
for row in feed.get(key, []):
uid = str(row.get("id") or f"{row.get('source')}/{row.get('title')}")
desc = (row.get("description") or "").strip()
if desc:
desc_by_id[uid] = desc
for item in items:
if not item.get("description"):
item["description"] = desc_by_id.get(item["id"], "")
def load_boards(
feed: dict[str, Any],
*,
trending_limit: int,
hot_limit: int,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""优先 skills.sh 官网;失败时回退 feed.json。"""
if board_source() == "feed":
return (
list(feed.get("topTrending", [])[:trending_limit]),
list(feed.get("topHot", [])[:hot_limit]),
)
trending = fetch_board("trending", limit=trending_limit)
hot = fetch_board("hot", limit=hot_limit)
if not trending:
trending = list(feed.get("topTrending", [])[:trending_limit])
else:
enrich_from_feed(trending, feed)
if not hot:
hot = list(feed.get("topHot", [])[:hot_limit])
else:
enrich_from_feed(hot, feed)
return trending, hot