找回密码
 立即注册
首页 业界区 安全 自动拉取各大OJ的比赛日程并导入日历软件 ...

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

擒揭 3 天前
我的博客:自动拉取各大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,执行下面的命令:
  1. mkdir layer
  2. cd layer
  3. 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。
在下方粘贴我的代码:
  1. import json
  2. import requests
  3. import re
  4. import datetime
  5. import urllib
  6. from bs4 import BeautifulSoup
  7. import ics
  8. import boto3
  9. from botocore.client import Config
  10. import os
  11. import time
  12. R2_ACCESS_KEY = os.environ.get('R2_ACCESS_KEY', '')
  13. R2_SECRET_KEY = os.environ.get('R2_SECRET_KEY', '')
  14. R2_ENDPOINT_URL = os.environ.get('R2_ENDPOINT_URL', '')
  15. R2_BUCKET_NAME = os.environ.get('R2_BUCKET_NAME', '')
  16. R2_OBJECT_NAME = os.environ.get('R2_OBJECT_NAME', 'calendar.ics')
  17. def get_luogu_contests():
  18.     headers = {
  19.         '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'
  20.     }
  21.     try:
  22.         start_time = time.time()
  23.         res = requests.get('https://www.luogu.com.cn/contest/list', headers=headers, timeout=5)
  24.         print(f"洛谷请求耗时 {time.time() - start_time:.2f} 秒")
  25.         mat = re.findall(r'JSON\.parse\(decodeURIComponent\("(\S+)"\)', res.text)[0]
  26.         mat = json.loads(urllib.parse.unquote(mat))
  27.         contests = []
  28.         for t in mat['currentData']['contests']['result']:
  29.             title = t['name']
  30.             start_time = datetime.datetime.fromtimestamp(t['startTime'])
  31.             end_time = datetime.datetime.fromtimestamp(t['endTime'])
  32.             start_time = start_time + datetime.timedelta(hours=-8)
  33.             end_time = end_time + datetime.timedelta(hours=-8)
  34.             contests.append({"title": title, "start": start_time, "end": end_time})
  35.         return contests
  36.     except Exception as e:
  37.         print(f"洛谷抓取失败: {e}")
  38.         return []
  39. def get_atcoder_contests():
  40.     try:
  41.         start_time = time.time()
  42.         url = "https://atcoder.jp/contests/"
  43.         res = requests.get(url, timeout=5)
  44.         print(f"AtCoder 请求耗时 {time.time() - start_time:.2f} 秒")
  45.         soup = BeautifulSoup(res.text, 'html.parser')
  46.         contests = []
  47.         for row in soup.select('#contest-table-upcoming tbody tr'):
  48.             cols = row.find_all('td')
  49.             start_time = datetime.datetime.strptime(cols[0].text, '%Y-%m-%d %H:%M:%S%z')
  50.             title = cols[1].text.strip()
  51.             title = re.sub(r'[^\w\s\-\(\)]', '', title).strip()
  52.             duration = cols[2].text.strip()
  53.             hours, minutes = map(int, duration.split(':'))
  54.             end_time = start_time + datetime.timedelta(hours=hours, minutes=minutes)
  55.             contests.append({"title": title, "start": start_time, "end": end_time})
  56.         return contests
  57.     except Exception as e:
  58.         print(f"AtCoder 抓取失败: {e}")
  59.         return []
  60. def get_codeforces_contests():
  61.     try:
  62.         start_time = time.time()
  63.         url = "https://codeforces.com/api/contest.list?gym=false"
  64.         res = requests.get(url, timeout=5)
  65.         print(f"Codeforces 请求耗时 {time.time() - start_time:.2f} 秒")
  66.         data = res.json()
  67.         contests = []
  68.         for contest in data['result']:
  69.             if contest['phase'] == 'BEFORE':
  70.                 title = contest['name']
  71.                 start_time = datetime.datetime.fromtimestamp(contest['startTimeSeconds'], datetime.timezone.utc)
  72.                 duration = contest['durationSeconds']
  73.                 end_time = start_time + datetime.timedelta(seconds=duration)
  74.                 contests.append({"title": title, "start": start_time, "end": end_time})
  75.         return contests
  76.     except Exception as e:
  77.         print(f"Codeforces 抓取失败: {e}")
  78.         return []
  79. def main_handler(event, context):
  80.     start_time = time.time()
  81.     print("函数开始执行")
  82.     registered = [
  83.         get_luogu_contests(),
  84.         get_atcoder_contests(),
  85.         get_codeforces_contests()
  86.     ]
  87.     print(f"数据抓取总耗时 {time.time() - start_time:.2f} 秒")
  88.     calendar = ics.Calendar()
  89.     calendar_start = time.time()
  90.     for dat in registered:
  91.         if dat:
  92.             for res in dat:
  93.                 print(res['title'], '|', res['start'], '|', res['end'])
  94.                 e = ics.Event()
  95.                 e.name = res['title']
  96.                 e.begin = res['start']
  97.                 e.end = res['end']
  98.                 calendar.events.add(e)
  99.     print(f"日历创建耗时 {time.time() - calendar_start:.2f} 秒")
  100.     ics_content = calendar.serialize()
  101.     try:
  102.         upload_start = time.time()
  103.         s3 = boto3.client('s3',
  104.                           endpoint_url=R2_ENDPOINT_URL,
  105.                           aws_access_key_id=R2_ACCESS_KEY,
  106.                           aws_secret_access_key=R2_SECRET_KEY,
  107.                           config=Config(signature_version='s3v4'))
  108.         s3.put_object(Bucket=R2_BUCKET_NAME, Key=R2_OBJECT_NAME, Body=ics_content, ContentType='text/calendar', ACL='public-read')
  109.         print(f"ICS 文件上传到 Cloudflare R2 成功,耗时 {time.time() - upload_start:.2f} 秒")
  110.     except Exception as e:
  111.         print(f"上传到 R2 失败: {e}")
  112.         return {
  113.             'statusCode': 500,
  114.             'body': json.dumps({'error': str(e)})
  115.         }
  116.     print(f"总执行时间: {time.time() - start_time:.2f} 秒")
  117.     return {
  118.         'statusCode': 200,
  119.         'body': json.dumps({'message': 'ICS 文件生成并上传成功'})
  120.     }
  121. if __name__ == '__main__':
  122.     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,点击确定。
在左侧进入函数服务的配置界面,进入你刚创建的函数,在上方进入层管理,点击绑定,绑定你刚创建的层。
点击上方的函数代码,点击下方的测试,观察执行摘要中的返回结果,如果一切顺利,这里应该是
  1. {"statusCode": 200, "body": "{"message": "ICS \文\件\生\成\并\上\传\成\功"}"}
复制代码
看看部署结果

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册