edit api
This commit is contained in:
@@ -1,4 +1,25 @@
|
||||
[
|
||||
{
|
||||
"platform": "AtCoder",
|
||||
"title": "AtCoder Beginner Contest 468",
|
||||
"start_time": 1784980800,
|
||||
"end_time": 1784986800,
|
||||
"url": "https://atcoder.jp/contests/abc468"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "[ICPC2017 Xi' an R] ICPC 2017 区域赛西安站重现赛",
|
||||
"start_time": 1785042000,
|
||||
"end_time": 1785060000,
|
||||
"url": "https://www.luogu.com.cn/contest/339238"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "【LGR-294-Div.4】洛谷入门赛 #50",
|
||||
"start_time": 1785063600,
|
||||
"end_time": 1785070800,
|
||||
"url": "https://www.luogu.com.cn/contest/338569"
|
||||
},
|
||||
{
|
||||
"platform": "Codeforces",
|
||||
"title": "Codeforces Round (Div. 1)",
|
||||
@@ -13,11 +34,60 @@
|
||||
"end_time": 1785085500,
|
||||
"url": "https://codeforces.com/contests/2250"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "【LGR-295-Div.2】洛谷 7 月月赛 III & FDOI Round 2 - 泠",
|
||||
"start_time": 1785477600,
|
||||
"end_time": 1785492000,
|
||||
"url": "https://www.luogu.com.cn/contest/330173"
|
||||
},
|
||||
{
|
||||
"platform": "AtCoder",
|
||||
"title": "RECRUIT Nihonbashi Half Marathon 2026 Summer (AtCoder Heuristic Contest 069)",
|
||||
"start_time": 1785492000,
|
||||
"end_time": 1786356000,
|
||||
"url": "https://atcoder.jp/contests/ahc069"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "【LGR-292-Div.3】洛谷基础赛 #37 & MSOI Round 1",
|
||||
"start_time": 1785564000,
|
||||
"end_time": 1785576600,
|
||||
"url": "https://www.luogu.com.cn/contest/325521"
|
||||
},
|
||||
{
|
||||
"platform": "Codeforces",
|
||||
"title": "Codeforces Round (Div. 2)",
|
||||
"start_time": 1785584100,
|
||||
"end_time": 1785591300,
|
||||
"url": "https://codeforces.com/contests/2248"
|
||||
},
|
||||
{
|
||||
"platform": "AtCoder",
|
||||
"title": "AtCoder Beginner Contest 469",
|
||||
"start_time": 1785585600,
|
||||
"end_time": 1785591600,
|
||||
"url": "https://atcoder.jp/contests/abc469"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "[ICPC2017 Hong Kong R] ICPC 2017 区域赛香港站重现赛",
|
||||
"start_time": 1785733200,
|
||||
"end_time": 1785751200,
|
||||
"url": "https://www.luogu.com.cn/contest/340426"
|
||||
},
|
||||
{
|
||||
"platform": "洛谷",
|
||||
"title": "Math×Girl²",
|
||||
"start_time": 1786190400,
|
||||
"end_time": 1786276800,
|
||||
"url": "https://www.luogu.com.cn/contest/315863"
|
||||
},
|
||||
{
|
||||
"platform": "AtCoder",
|
||||
"title": "UNIQUE VISION Programming Contest 2026 Summer (AtCoder Regular Contest 226)",
|
||||
"start_time": 1786276800,
|
||||
"end_time": 1786284000,
|
||||
"url": "https://atcoder.jp/contests/arc226"
|
||||
}
|
||||
]
|
||||
+95
-38
@@ -2,7 +2,7 @@ import requests
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# ---------- 工具函数 ----------
|
||||
@@ -12,11 +12,42 @@ def now_ts():
|
||||
def parse_duration_to_end(start_ts, seconds):
|
||||
return start_ts + seconds
|
||||
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
||||
}
|
||||
|
||||
MONTH_MAP = {"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
|
||||
"jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12}
|
||||
|
||||
def parse_atcoder_time(text):
|
||||
return int(datetime.strptime(text.strip(), "%Y-%m-%d %H:%M:%S%z").timestamp())
|
||||
|
||||
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
|
||||
|
||||
def parse_usaco_day_range(date_text, season):
|
||||
match = re.match(r"(\w{3})\s+(\d{1,2})(?:-(?:(\w{3})\s+)?(\d{1,2}))?", date_text)
|
||||
if not match:
|
||||
return None
|
||||
start_mon, start_day, end_mon, end_day = match.groups()
|
||||
start_month = MONTH_MAP.get(start_mon.lower())
|
||||
end_month = MONTH_MAP.get((end_mon or start_mon).lower())
|
||||
if not start_month or not end_month:
|
||||
return None
|
||||
|
||||
start_dt = datetime(infer_usaco_year(season, start_month), start_month, int(start_day),
|
||||
0, 0, 0, tzinfo=timezone.utc)
|
||||
end_dt = datetime(infer_usaco_year(season, end_month), end_month, int(end_day or start_day),
|
||||
23, 59, 59, tzinfo=timezone.utc)
|
||||
return int(start_dt.timestamp()), int(end_dt.timestamp())
|
||||
|
||||
# ---------- Codeforces ----------
|
||||
def fetch_codeforces():
|
||||
url = "https://codeforces.com/api/contest.list?gym=false"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp = requests.get(url, headers=HEADERS, timeout=15)
|
||||
data = resp.json()
|
||||
if data["status"] != "OK":
|
||||
return []
|
||||
@@ -39,7 +70,8 @@ def fetch_codeforces():
|
||||
def fetch_atcoder():
|
||||
url = "https://kenkoooo.com/atcoder/resources/contests.json"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp = requests.get(url, headers=HEADERS, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
contests = []
|
||||
now = now_ts()
|
||||
@@ -53,6 +85,31 @@ def fetch_atcoder():
|
||||
"end_time": start + c["duration_second"],
|
||||
"url": f"https://atcoder.jp/contests/{c['id']}"
|
||||
})
|
||||
if contests:
|
||||
return contests
|
||||
|
||||
page = requests.get("https://atcoder.jp/contests/?lang=en", headers=HEADERS, timeout=15)
|
||||
page.raise_for_status()
|
||||
soup = BeautifulSoup(page.text, "html.parser")
|
||||
upcoming = soup.select_one("#contest-table-upcoming table tbody")
|
||||
if not upcoming:
|
||||
return []
|
||||
for row in upcoming.select("tr"):
|
||||
cols = row.find_all("td")
|
||||
if len(cols) < 3:
|
||||
continue
|
||||
start_time = parse_atcoder_time(cols[0].get_text(strip=True))
|
||||
duration_parts = [int(part) for part in cols[2].get_text(strip=True).split(":")]
|
||||
duration = duration_parts[0] * 3600 + duration_parts[1] * 60
|
||||
link = cols[1].find("a", href=True)
|
||||
if start_time > now and link:
|
||||
contests.append({
|
||||
"platform": "AtCoder",
|
||||
"title": link.get_text(strip=True),
|
||||
"start_time": start_time,
|
||||
"end_time": start_time + duration,
|
||||
"url": f"https://atcoder.jp{link['href']}"
|
||||
})
|
||||
return contests
|
||||
except Exception as e:
|
||||
print(f"AtCoder error: {e}")
|
||||
@@ -60,48 +117,37 @@ def fetch_atcoder():
|
||||
|
||||
# ---------- USACO (解析官网) ----------
|
||||
def fetch_usaco():
|
||||
url = "http://www.usaco.org/index.php?page=contests"
|
||||
url = "https://usaco.org/index.php?page=contests"
|
||||
try:
|
||||
resp = requests.get(url, timeout=15)
|
||||
resp = requests.get(url, headers=HEADERS, timeout=15)
|
||||
resp.raise_for_status()
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
# USACO 赛程通常在 <div class="panel"> 里的 <table> 中
|
||||
# 具体解析需观察当前页面结构(以下基于典型结构)
|
||||
panel = soup.find("div", class_="panel")
|
||||
panel = None
|
||||
season = None
|
||||
for candidate in soup.find_all("div", class_="panel"):
|
||||
heading = candidate.find("h2")
|
||||
if not heading:
|
||||
continue
|
||||
match = re.search(r"(\d{4}-\d{4})\s+Schedule", heading.get_text(" ", strip=True))
|
||||
if match:
|
||||
panel = candidate
|
||||
season = match.group(1)
|
||||
break
|
||||
if not panel:
|
||||
return []
|
||||
# 查找所有含有月份的行,例如 "Dec 16-19: USACO December Contest"
|
||||
text = panel.get_text(separator="\n")
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
contests = []
|
||||
# 获取年份:页面标题或当前年附近推断
|
||||
# 简单策略:当前年,若当前月 >= 8 则 12月属于今年,否则属于去年/明年
|
||||
now = datetime.now(timezone.utc)
|
||||
year = now.year
|
||||
month = now.month
|
||||
for line in lines:
|
||||
# 匹配类似 "Dec 16-19: USACO December Contest"
|
||||
match = re.match(r"(\w{3})\s+(\d{1,2})-(\d{1,2}):(.+)", line)
|
||||
match = re.match(r"((?:\w{3}\s+)?\d{1,2}(?:-(?:\w{3}\s+)?\d{1,2})?):\s*(.+)", line)
|
||||
if match:
|
||||
mon_str, day_start, day_end, title = match.groups()
|
||||
# 推断年份
|
||||
month_map = {"jan":1,"feb":2,"mar":3,"apr":4,"may":5,"jun":6,
|
||||
"jul":7,"aug":8,"sep":9,"oct":10,"nov":11,"dec":12}
|
||||
m = month_map.get(mon_str.lower())
|
||||
if not m:
|
||||
date_text, title = match.groups()
|
||||
parsed_range = parse_usaco_day_range(date_text, season)
|
||||
if not parsed_range:
|
||||
continue
|
||||
# 如果当前月 >= 8 并且赛事月是 12-7,需要判断
|
||||
if month >= 8:
|
||||
# 从 8 月到 12 月都算今年;来年的 1-7 月算是下一年
|
||||
event_year = year if m >= 8 else year + 1
|
||||
else:
|
||||
# 当前月 1-7,去年的 8-12 已经过去,今年的 1-7 为今年
|
||||
event_year = year if m <= 7 else year - 1
|
||||
# 构造开始时间 (使用 UTC,忽略具体时区,作为近似)
|
||||
start_dt = datetime(event_year, m, int(day_start), 0, 0, 0, tzinfo=timezone.utc)
|
||||
end_dt = datetime(event_year, m, int(day_end), 23, 59, 59, tzinfo=timezone.utc)
|
||||
start_ts = int(start_dt.timestamp())
|
||||
end_ts = int(end_dt.timestamp())
|
||||
if start_ts > now.timestamp():
|
||||
start_ts, end_ts = parsed_range
|
||||
if end_ts > now.timestamp():
|
||||
contests.append({
|
||||
"platform": "USACO",
|
||||
"title": title.strip(),
|
||||
@@ -117,15 +163,26 @@ def fetch_usaco():
|
||||
# ---------- 洛谷 ----------
|
||||
def fetch_luogu():
|
||||
url = "https://www.luogu.com.cn/contest/list?_contentOnly=1"
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
headers = {
|
||||
**HEADERS,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.luogu.com.cn/contest/list",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"x-lentille-request": "content-only"
|
||||
}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=15)
|
||||
session = requests.Session()
|
||||
session.headers.update(headers)
|
||||
session.get("https://www.luogu.com.cn/contest/list", timeout=15)
|
||||
resp = session.get(url, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data.get("code") != 200:
|
||||
if data.get("status") != 200 and data.get("code") != 200:
|
||||
return []
|
||||
contests = []
|
||||
now = now_ts()
|
||||
for c in data["currentData"]["contests"]["result"]:
|
||||
contest_data = data.get("data") or data.get("currentData") or {}
|
||||
for c in contest_data.get("contests", {}).get("result", []):
|
||||
# endTime 是秒级时间戳,只保留尚未结束的比赛
|
||||
if c["endTime"] > now:
|
||||
contests.append({
|
||||
|
||||
Reference in New Issue
Block a user