remove luogu && add uoj
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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/页面结构变化,可能会暂时影响抓取到的比赛数量。
|
||||
|
||||
@@ -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
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user