remove luogu && add uoj

This commit is contained in:
hanyixuanten
2026-07-20 19:05:09 +08:00
parent b213ccf50a
commit f1516b6ef4
6 changed files with 89 additions and 52 deletions
+2
View File
@@ -37,6 +37,8 @@ When removing a provider, remove or adjust the same surfaces so stale platform l
## Validation
Do not proactively create a Python virtual environment unless the user asks for one. Run Python commands directly with `python`.
After changing fetching, filtering, output shape, provider support, or workflow-generated files, run:
```bash
+2 -2
View File
@@ -9,7 +9,7 @@ Fetch programming contest schedules from several online judge platforms and expo
- Codeforces
- AtCoder
- USACO
- Luogu
- UOJ
## Output Files
@@ -93,6 +93,6 @@ AI coding agents and maintainers should follow [AGENTS.md](AGENTS.md) when chang
## Notes
- AtCoder uses the AtCoder Problems API and falls back to the official AtCoder contest page for upcoming contests when needed.
- Luogu requires a session request and content-only request headers, so the script uses `requests.Session()`.
- UOJ is parsed from its public contests page, including the timeanddate duration parameters used by contest links.
- USACO schedule data depends on the current layout of the official USACO contests page. If USACO has no future schedule published, it may produce no upcoming USACO contests.
- Network or upstream API changes can temporarily reduce the number of fetched contests.
+2 -2
View File
@@ -9,7 +9,7 @@
- Codeforces
- AtCoder
- USACO
- 洛谷
- UOJ
## 输出文件
@@ -93,6 +93,6 @@ AI coding agents 和维护者在修改比赛提供商、输出契约、工作流
## 注意事项
- AtCoder 使用 AtCoder Problems API;在需要时会回退解析 AtCoder 官方比赛页面以获取即将开始的比赛。
- 洛谷需要先建立会话,并带上 content-only 请求头,因此脚本使用了 `requests.Session()`
- UOJ 从公开比赛列表页面解析,并读取比赛链接中的 timeanddate 持续时间参数
- USACO 赛程依赖 USACO 官网当前页面结构。如果 USACO 暂未发布未来赛程,可能不会产生即将开始的 USACO 比赛。
- 网络问题或上游 API/页面结构变化,可能会暂时影响抓取到的比赛数量。
+32
View File
@@ -559,6 +559,14 @@
"status": "finished",
"url": "https://atcoder.jp/contests/awc0107"
},
{
"platform": "UOJ",
"title": "UOJ Long Round #4",
"start_time": 1783216800,
"end_time": 1783432800,
"status": "finished",
"url": "https://uoj.ac/contest/108"
},
{
"platform": "AtCoder",
"title": "AtCoder Daily Training ALL 2026/07/08 16:00start",
@@ -695,6 +703,14 @@
"status": "finished",
"url": "https://atcoder.jp/contests/adt_easy_20260710_2"
},
{
"platform": "UOJ",
"title": "UOJ NOI Round #10 笔试",
"start_time": 1783681200,
"end_time": 1783683000,
"status": "finished",
"url": "https://uoj.ac/contest/109"
},
{
"platform": "AtCoder",
"title": "AtCoder Weekday Contest 0110 Beta",
@@ -703,6 +719,14 @@
"status": "finished",
"url": "https://atcoder.jp/contests/awc0110"
},
{
"platform": "UOJ",
"title": "UOJ NOI Round #10 Day1",
"start_time": 1783729800,
"end_time": 1783748400,
"status": "finished",
"url": "https://uoj.ac/contest/110"
},
{
"platform": "AtCoder",
"title": "AtCoder Beginner Contest 466",
@@ -711,6 +735,14 @@
"status": "finished",
"url": "https://atcoder.jp/contests/abc466"
},
{
"platform": "UOJ",
"title": "UOJ NOI Round #10 Day2",
"start_time": 1783816200,
"end_time": 1783834200,
"status": "finished",
"url": "https://uoj.ac/contest/111"
},
{
"platform": "AtCoder",
"title": "AtCoder Regular Contest-- 224",
+49 -46
View File
@@ -3,6 +3,7 @@ import json
import time
import re
from datetime import datetime, timezone
from urllib.parse import urljoin, urlparse, parse_qs
from bs4 import BeautifulSoup
# ---------- 工具函数 ----------
@@ -63,6 +64,24 @@ MONTH_MAP = {"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
def parse_atcoder_time(text):
return int(datetime.strptime(text.strip(), "%Y-%m-%d %H:%M:%S%z").timestamp())
def parse_uoj_time(text):
return int(datetime.strptime(text.strip(), "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc).timestamp()) - 8 * 3600
def parse_uoj_duration(time_url, duration_text):
params = parse_qs(urlparse(time_url).query)
seconds = 0
if params.get("ah"):
seconds += int(float(params["ah"][0]) * 3600)
if params.get("am"):
seconds += int(float(params["am"][0]) * 60)
if seconds:
return seconds
match = re.search(r"([\d.]+)\s*小时", duration_text)
if match:
return int(float(match.group(1)) * 3600)
return 0
def infer_usaco_year(season, month_num):
start_year, end_year = map(int, season.split("-"))
return start_year if month_num >= 8 else end_year
@@ -201,55 +220,39 @@ def fetch_usaco(include_all=False):
print(f"USACO error: {e}")
return []
# ---------- 洛谷 ----------
def fetch_luogu(include_all=False, min_end_time=None):
url = "https://www.luogu.com.cn/contest/list"
headers = {
**HEADERS,
"Accept": "application/json, text/plain, */*",
"Referer": "https://www.luogu.com.cn/contest/list",
"X-Requested-With": "XMLHttpRequest",
"x-lentille-request": "content-only"
}
# ---------- UOJ ----------
def fetch_uoj(include_all=False, min_end_time=None):
url = "https://uoj.ac/contests"
try:
session = requests.Session()
session.headers.update(headers)
session.get("https://www.luogu.com.cn/contest/list", timeout=15)
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
contests = []
now = now_ts()
page = 1
total = None
while True:
resp = session.get(url, params={"page": page, "_contentOnly": 1}, timeout=15)
resp.raise_for_status()
data = resp.json()
if data.get("status") != 200 and data.get("code") != 200:
return contests
contest_data = data.get("data") or data.get("currentData") or {}
page_data = contest_data.get("contests", {})
result = page_data.get("result", [])
if total is None:
total = page_data.get("count", len(result))
if not result:
break
for c in result:
# endTime 是秒级时间戳;默认只保留尚未结束的比赛,全量模式保留全部
if (include_all and (min_end_time is None or c["endTime"] >= min_end_time)) or c["endTime"] > now:
contests.append(make_contest(
"洛谷",
c["name"],
c["startTime"],
c["endTime"],
f"https://www.luogu.com.cn/contest/{c['id']}"
))
if include_all and min_end_time is not None and max(c["endTime"] for c in result) < min_end_time:
break
if not include_all or page * page_data.get("perPage", len(result)) >= total:
break
page += 1
for row in soup.select("table tr"):
cols = row.find_all("td")
if len(cols) < 3:
continue
contest_link = cols[0].find("a", href=re.compile(r"^/contest/\d+$"))
time_link = cols[1].find("a", href=True)
if not contest_link or not time_link:
continue
start = parse_uoj_time(time_link.get_text(strip=True))
duration = parse_uoj_duration(time_link["href"], cols[2].get_text(" ", strip=True))
if duration <= 0:
continue
end = start + duration
if (include_all and (min_end_time is None or end >= min_end_time)) or end > now:
contests.append(make_contest(
"UOJ",
contest_link.get_text(strip=True),
start,
end,
urljoin("https://uoj.ac", contest_link["href"])
))
return contests
except Exception as e:
print(f"Luogu error: {e}")
print(f"UOJ error: {e}")
return []
# ---------- 主逻辑 ----------
@@ -260,7 +263,7 @@ def main():
all_contests.extend(fetch_codeforces())
all_contests.extend(fetch_atcoder())
all_contests.extend(fetch_usaco())
all_contests.extend(fetch_luogu())
all_contests.extend(fetch_uoj())
all_contests.sort(key=lambda x: x["start_time"])
@@ -273,7 +276,7 @@ def main():
recent_finished_contests.extend(fetch_codeforces(include_all=True))
recent_finished_contests.extend(fetch_atcoder(include_all=True))
recent_finished_contests.extend(fetch_usaco(include_all=True))
recent_finished_contests.extend(fetch_luogu(include_all=True, min_end_time=recent_finished_min_end_time))
recent_finished_contests.extend(fetch_uoj(include_all=True, min_end_time=recent_finished_min_end_time))
recent_finished_contests = filter_recent_finished(deduplicate_contests(recent_finished_contests))
recent_finished_contests.sort(key=lambda x: x["end_time"])
+2 -2
View File
@@ -67,7 +67,7 @@ $platform_class = [
'Codeforces' => 'cf',
'AtCoder' => 'atc',
'USACO' => 'usaco',
'洛谷' => 'luogu'
'UOJ' => 'uoj'
];
// 辅助函数:Unix 时间戳转中文日期
@@ -97,7 +97,7 @@ function format_time($ts) {
.platform.cf { background: #1f8acb; }
.platform.atc { background: #5b8c5a; }
.platform.usaco { background: #e67e22; }
.platform.luogu { background: #e74c3c; }
.platform.uoj { background: #c0392b; }
.info { flex: 1; margin-left: 15px; }
.title { font-size: 18px; font-weight: 600; color: #2c3e50; }
.time { font-size: 14px; color: #7f8c8d; margin-top: 5px; }