擒揭 发表于 6 天前

自动拉取各大OJ的比赛日程并导入日历软件

我的博客:自动拉取各大OJ的比赛日程并导入日历软件
参考文章:使用日历app轻松订阅各大OJ平台上的比赛(ics格式)
tips:代码为Grok3生成,能跑。
过去看比赛日期的方式都弱爆了!需要自己手动打开各OJ的网页,有时忘看了还会错过比赛,而现在我们再也不需要担心这个问题了。
正文

快速开始

如果你不想自己部署,可以使用我的订阅链接,但维护有成本(SCF云函数,域名,对象存储等),我或许不会维护很久。
Win电脑与Android手机

在Outlook注册一个Outlook账号,如果已经有就登录,手机用户需要下载Outlook软件。
登入后,在左边点击日历按钮,点击左侧的“添加日历”,点击“Web订阅”,填入我的订阅链接:
https://oss.misaka2298.icu/oss/calendar.ics
点击导入,电脑端的操作到这里就结束了,注意上方的可视范围从默认的"工作周"切换为"周",不然看不到周末的比赛。
手机端需要多一步操作:在Outlook的设置里找到“日历”设置,勾选“同步日历”,并同意Outlook申请的日历访问权限,然后手机日历就会自动同步了。
iPhone

我手头没有iPhone机器,这里引用我参考文章里的步骤:

[*]打开ios日历,点击添加日历 - 添加订阅日历
[*]粘贴链接
[*]进行自定义设置,完成
链接同https://oss.misaka2298.icu/oss/calendar.ics
自己部署

可能产生的费用

腾讯云SCF函数:个人标准版函数套餐12.8元/月。
域名:冷门顶级域名(如我的.icu)约100/年,首年优惠。当然也可以选用网上公益的二级域名服务,请自行搜索。
对象存储:

[*]CloudFlareR2(本文使用):基本免费,但需要一张银行卡(支持银联)
[*]其他对象存储:按量收费,如果访问量大可能产生较高的费用。
如果可以接受的话,下面是教程。
环境安装

首先,需要Python3.9的环境(截止到本文发布),安装时记得勾选"Add to PATH",安装后重启。
找个空文件夹,打开cmd,执行下面的命令:
mkdir layer
cd layer
python -m pip install requests==2.28.2 beautifulsoup4==4.12.3 ics==0.7.1 boto3==1.34.149 urllib3==1.26.18 tatsu==5.7.4 -t .把layer文件夹压缩成zip,备用。
对象存储

打开CloudFlare控制台,没有账号就注册一个,在左侧选项卡找到R2对象存储,按提示初始化,注意需要银行卡。
当然如果你要用其他对象存储服务商的话可以自行研究。
点击{}API,点击管理API令牌,然后创建UserAPI令牌,权限为管理员读写,名称自己取,然后记住你的访问密钥 ID和机密访问密钥,注意这两个东西只会出现一次。
返回R2控制台,创建新的存储桶,名称自己起,位置选亚太,除非你在外国。
进入存储桶,在设置中添加自定义域,这里不再赘述,网上也有很多公益的二级域名供使用,请自行搜索教程。
SCF自动拉取

打开腾讯云SCF控制台,没注册的话注册一个。
点击左侧“函数服务”,点击新建。
点击"从头开始",函数类型选事件函数,名称自己起,地域无所谓,运行环境python3.9,时区UTC。
在下方粘贴我的代码:
import json
import requests
import re
import datetime
import urllib
from bs4 import BeautifulSoup
import ics
import boto3
from botocore.client import Config
import os
import time

R2_ACCESS_KEY = os.environ.get('R2_ACCESS_KEY', '')
R2_SECRET_KEY = os.environ.get('R2_SECRET_KEY', '')
R2_ENDPOINT_URL = os.environ.get('R2_ENDPOINT_URL', '')
R2_BUCKET_NAME = os.environ.get('R2_BUCKET_NAME', '')
R2_OBJECT_NAME = os.environ.get('R2_OBJECT_NAME', 'calendar.ics')

def get_luogu_contests():
    headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'
    }
    try:
      start_time = time.time()
      res = requests.get('https://www.luogu.com.cn/contest/list', headers=headers, timeout=5)
      print(f"洛谷请求耗时 {time.time() - start_time:.2f} 秒")
      mat = re.findall(r'JSON\.parse\(decodeURIComponent\("(\S+)"\)', res.text)
      mat = json.loads(urllib.parse.unquote(mat))
      contests = []
      for t in mat['currentData']['contests']['result']:
            title = t['name']
            start_time = datetime.datetime.fromtimestamp(t['startTime'])
            end_time = datetime.datetime.fromtimestamp(t['endTime'])
            start_time = start_time + datetime.timedelta(hours=-8)
            end_time = end_time + datetime.timedelta(hours=-8)
            contests.append({"title": title, "start": start_time, "end": end_time})
      return contests
    except Exception as e:
      print(f"洛谷抓取失败: {e}")
      return []

def get_atcoder_contests():
    try:
      start_time = time.time()
      url = "https://atcoder.jp/contests/"
      res = requests.get(url, timeout=5)
      print(f"AtCoder 请求耗时 {time.time() - start_time:.2f} 秒")
      soup = BeautifulSoup(res.text, 'html.parser')
      contests = []
      for row in soup.select('#contest-table-upcoming tbody tr'):
            cols = row.find_all('td')
            start_time = datetime.datetime.strptime(cols.text, '%Y-%m-%d %H:%M:%S%z')
            title = cols.text.strip()
            title = re.sub(r'[^\w\s\-\(\)]', '', title).strip()
            duration = cols.text.strip()
            hours, minutes = map(int, duration.split(':'))
            end_time = start_time + datetime.timedelta(hours=hours, minutes=minutes)
            contests.append({"title": title, "start": start_time, "end": end_time})
      return contests
    except Exception as e:
      print(f"AtCoder 抓取失败: {e}")
      return []

def get_codeforces_contests():
    try:
      start_time = time.time()
      url = "https://codeforces.com/api/contest.list?gym=false"
      res = requests.get(url, timeout=5)
      print(f"Codeforces 请求耗时 {time.time() - start_time:.2f} 秒")
      data = res.json()
      contests = []
      for contest in data['result']:
            if contest['phase'] == 'BEFORE':
                title = contest['name']
                start_time = datetime.datetime.fromtimestamp(contest['startTimeSeconds'], datetime.timezone.utc)
                duration = contest['durationSeconds']
                end_time = start_time + datetime.timedelta(seconds=duration)
                contests.append({"title": title, "start": start_time, "end": end_time})
      return contests
    except Exception as e:
      print(f"Codeforces 抓取失败: {e}")
      return []

def main_handler(event, context):
    start_time = time.time()
    print("函数开始执行")

    registered = [
      get_luogu_contests(),
      get_atcoder_contests(),
      get_codeforces_contests()
    ]
    print(f"数据抓取总耗时 {time.time() - start_time:.2f} 秒")

    calendar = ics.Calendar()
    calendar_start = time.time()
    for dat in registered:
      if dat:
            for res in dat:
                print(res['title'], '|', res['start'], '|', res['end'])
                e = ics.Event()
                e.name = res['title']
                e.begin = res['start']
                e.end = res['end']
                calendar.events.add(e)
    print(f"日历创建耗时 {time.time() - calendar_start:.2f} 秒")

    ics_content = calendar.serialize()

    try:
      upload_start = time.time()
      s3 = boto3.client('s3',
                        endpoint_url=R2_ENDPOINT_URL,
                        aws_access_key_id=R2_ACCESS_KEY,
                        aws_secret_access_key=R2_SECRET_KEY,
                        config=Config(signature_version='s3v4'))
      s3.put_object(Bucket=R2_BUCKET_NAME, Key=R2_OBJECT_NAME, Body=ics_content, ContentType='text/calendar', ACL='public-read')
      print(f"ICS 文件上传到 Cloudflare R2 成功,耗时 {time.time() - upload_start:.2f} 秒")
    except Exception as e:
      print(f"上传到 R2 失败: {e}")
      return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
      }

    print(f"总执行时间: {time.time() - start_time:.2f} 秒")
    return {
      'statusCode': 200,
      'body': json.dumps({'message': 'ICS 文件生成并上传成功'})
    }

if __name__ == '__main__':
    main_handler({}, {})在12~17行填写你CloudFlare存储桶的信息:

[*]R2_ACCESS_KEY:APIKey的访问密钥 ID
[*]R2_SECRET_KEY:APIKey的机密访问密钥
[*]R2_ENDPOINT_URL:你的存储桶 - 设置 - S3API的那一串链接
[*]R2_BUCKET_NAME:存储桶名
[*]R2_OBJECT_NAME:保存的文件名,扩展名为.ics
拉到最底下,在触发器配置中勾选自定义创建,触发别名/版本中选择版本:$LATEST,触发周期选择每一天。
然后,在高级配置中把执行超时时间改为10秒。
同意协议,点击完成。
返回SCF控制台,在左侧进入层的配置页面。
点击新建,层名称随便写,层代码为你前面打包的layer.zip,运行环境添加Python3.9,点击确定。
在左侧进入函数服务的配置界面,进入你刚创建的函数,在上方进入层管理,点击绑定,绑定你刚创建的层。
点击上方的函数代码,点击下方的测试,观察执行摘要中的返回结果,如果一切顺利,这里应该是
{"statusCode": 200, "body": "{\"message\": \"ICS \文\件\生\成\并\上\传\成\功\"}"}看看部署结果

返回CloudFlareR2控制台,进入存储桶,寻找你生成的calender.ics。
如果存在的话,复制它的自定义域。
复制后在浏览器打开你复制的链接,如果可以下载就是成功了,导入教程同上。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 自动拉取各大OJ的比赛日程并导入日历软件