Merge pull request 'feature/hk-holiday-support' (#2) from feature/hk-holiday-support into master
Reviewed-on: #2
This commit is contained in:
127
README.md
127
README.md
@@ -6,14 +6,17 @@
|
|||||||
[](https://www.jsdelivr.com/package/gh/NateScarlet/holiday-cn)
|
[](https://www.jsdelivr.com/package/gh/NateScarlet/holiday-cn)
|
||||||

|

|
||||||
|
|
||||||
中国法定节假日数据 自动每日抓取国务院公告
|
中国大陆及香港节假日数据
|
||||||
|
|
||||||
|
- 中国大陆:自动每日抓取国务院公告
|
||||||
|
- 香港:来源于香港特别行政区政府 [1823.gov.hk](https://www.1823.gov.hk/common/ical/tc.json),数据覆盖 2024 年起
|
||||||
|
|
||||||
- [x] 提供 JSON 格式节假日数据
|
- [x] 提供 JSON 格式节假日数据
|
||||||
- [x] CI 自动更新
|
- [x] CI 自动更新
|
||||||
- [x] 数据变化时自动发布新版本 ( `Watch` - `Release only` 以获取邮件提醒! )
|
- [x] 数据变化时自动发布新版本 ( `Watch` - `Release only` 以获取邮件提醒! )
|
||||||
- [x] [发布页面]提供 JSON 打包下载
|
- [x] [发布页面]提供 JSON 打包下载
|
||||||
|
|
||||||
数据格式:
|
数据格式(中国大陆、香港通用):
|
||||||
|
|
||||||
[JSON Schema](./schema.json)
|
[JSON Schema](./schema.json)
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@
|
|||||||
interface Holidays {
|
interface Holidays {
|
||||||
/** 完整年份, 整数。*/
|
/** 完整年份, 整数。*/
|
||||||
year: number;
|
year: number;
|
||||||
/** 所用国务院文件网址列表 */
|
/** 所用数据来源网址列表 */
|
||||||
papers: string[];
|
papers: string[];
|
||||||
days: {
|
days: {
|
||||||
/** 节日名称 */
|
/** 节日名称 */
|
||||||
@@ -36,15 +39,23 @@ interface Holidays {
|
|||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
|
**中国大陆**
|
||||||
|
|
||||||
- 年份是按照国务院文件标题年份而不是日期年份,12 月份的日期可能会被下一年的文件影响,因此应检查两个文件。
|
- 年份是按照国务院文件标题年份而不是日期年份,12 月份的日期可能会被下一年的文件影响,因此应检查两个文件。
|
||||||
|
|
||||||
- `与周末连休` 的周末不是法定节假日,数据里不会包含,见[《全国年节及纪念日放假办法》](https://www.gov.cn/zhengce/content/202411/content_6986380.htm) [#213](https://github.com/NateScarlet/holiday-cn/issues/213#issuecomment-1869546011) [#221](https://github.com/NateScarlet/holiday-cn/issues/221)
|
- `与周末连休` 的周末不是法定节假日,数据里不会包含,见[《全国年节及纪念日放假办法》](https://www.gov.cn/zhengce/content/202411/content_6986380.htm) [#213](https://github.com/NateScarlet/holiday-cn/issues/213#issuecomment-1869546011) [#221](https://github.com/NateScarlet/holiday-cn/issues/221)
|
||||||
|
|
||||||
|
**香港**
|
||||||
|
|
||||||
|
- 香港不设调休补班制度,数据中不会出现 `isOffDay: false` 的条目。
|
||||||
|
- 假期名称为繁体中文。
|
||||||
|
- 数据覆盖范围取决于港府发布进度,通常提前公布至次年。
|
||||||
|
|
||||||
## 通过互联网使用
|
## 通过互联网使用
|
||||||
|
|
||||||
提示:任何第三方服务都可能故障或停止服务,如果稳定性要求高请自己搭建静态文件服务。
|
提示:任何第三方服务都可能故障或停止服务,如果稳定性要求高请自己搭建静态文件服务。
|
||||||
|
|
||||||
数据地址格式:
|
### 中国大陆
|
||||||
|
|
||||||
`https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/{年份}.json`
|
`https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/{年份}.json`
|
||||||
|
|
||||||
@@ -62,18 +73,124 @@ interface Holidays {
|
|||||||
|
|
||||||
~~`https://natescarlet.coding.net/p/github/d/holiday-cn/git/raw/master/{年份}.json`~~
|
~~`https://natescarlet.coding.net/p/github/d/holiday-cn/git/raw/master/{年份}.json`~~
|
||||||
|
|
||||||
|
### 香港
|
||||||
|
|
||||||
|
`https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/hk/{年份}.json`
|
||||||
|
|
||||||
## ICalendar 订阅
|
## ICalendar 订阅
|
||||||
|
|
||||||
网址格式参见上一节
|
### 中国大陆
|
||||||
|
|
||||||
`{年份}.ics` 为对应年份的节假日
|
`{年份}.ics` 为对应年份的节假日
|
||||||
|
|
||||||
`holiday-cn.ics` 为 3 年前至次年的节假日
|
`holiday-cn.ics` 为 3 年前至次年的节假日
|
||||||
|
|
||||||
|
### 香港
|
||||||
|
|
||||||
|
`hk/{年份}.ics` 为对应年份的节假日
|
||||||
|
|
||||||
|
`holiday-hk.ics` 为 3 年前至次年的节假日
|
||||||
|
|
||||||
感谢 @retanoj 的 ics 格式转换实现
|
感谢 @retanoj 的 ics 格式转换实现
|
||||||
|
|
||||||
## 作为 git 子模块使用
|
## 作为 git 子模块使用
|
||||||
|
|
||||||
参见 [Git 工具 - 子模块](https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97)
|
参见 [Git 工具 - 子模块](https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97)
|
||||||
|
|
||||||
|
## 数据生成
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r dev-requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
更新当年及次年数据(所有地区):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/update.py
|
||||||
|
```
|
||||||
|
|
||||||
|
只更新指定地区:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/update.py --region cn # 仅中国大陆
|
||||||
|
python scripts/update.py --region hk # 仅香港
|
||||||
|
```
|
||||||
|
|
||||||
|
更新所有历史年份(仅中国大陆,香港数据源仅覆盖 2024 年起):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python scripts/update.py --all --region cn
|
||||||
|
```
|
||||||
|
|
||||||
|
CI 每日自动执行 `python scripts/update.py --release`,数据有变化时自动提交并发布新版本。
|
||||||
|
|
||||||
|
## 导入到业务系统(teramesh-backend)
|
||||||
|
|
||||||
|
数据通过 Django management command 导入工作日历:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 进入应用容器
|
||||||
|
kubectl exec -it pod/teramesh-app-0 -- /bin/sh
|
||||||
|
|
||||||
|
# 导入所有地区(中国大陆 + 香港)
|
||||||
|
python manage.py populate_working_calendar
|
||||||
|
|
||||||
|
# 只导入指定地区
|
||||||
|
python manage.py populate_working_calendar --region CN
|
||||||
|
python manage.py populate_working_calendar --region HK
|
||||||
|
```
|
||||||
|
|
||||||
|
命令会导入当年及次年的假期数据。数据按 `country` 级别写入,部署在香港(`country=HK`)的站点自动使用香港假期,内地站点使用内地假期,无需额外配置。
|
||||||
|
|
||||||
|
## 扩展支持新地区
|
||||||
|
|
||||||
|
如需接入其他地区(如美国、日本等),按以下步骤操作:
|
||||||
|
|
||||||
|
**1. 新增数据抓取模块**
|
||||||
|
|
||||||
|
在 `scripts/` 下新建 `fetch_{地区代码}.py`,实现 `fetch_{地区代码}_holiday(year: int) -> dict`,返回格式与现有地区一致:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"year": 2025,
|
||||||
|
"papers": ["数据来源URL"],
|
||||||
|
"days": [
|
||||||
|
{"name": "假期名称", "date": "2025-01-01", "isOffDay": True}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:若该地区无调休补班制度,`isOffDay` 始终为 `True`。
|
||||||
|
|
||||||
|
**2. 在 `scripts/update.py` 的 `REGIONS` 中注册**
|
||||||
|
|
||||||
|
```python
|
||||||
|
REGIONS = {
|
||||||
|
"cn": { ... },
|
||||||
|
"hk": { ... },
|
||||||
|
"us": { # 新增
|
||||||
|
"fetch": fetch_us_holiday,
|
||||||
|
"start_year": 2024,
|
||||||
|
"subdir": "us",
|
||||||
|
"main_ics_name": "holiday-us.ics",
|
||||||
|
"cal_name": "美国联邦假期",
|
||||||
|
"cal_desc": "美国联邦公共假期数据。",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 在 `teramesh-backend` 的 `HOLIDAY_SOURCES` 中注册**
|
||||||
|
|
||||||
|
```python
|
||||||
|
HOLIDAY_SOURCES = {
|
||||||
|
"CN": f"{GITEA_BASE}/{{year}}.json",
|
||||||
|
"HK": f"{GITEA_BASE}/hk/{{year}}.json",
|
||||||
|
"US": f"{GITEA_BASE}/us/{{year}}.json", # 新增
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
之后运行 `python manage.py populate_working_calendar --region US` 即可导入,其余地区不受影响。
|
||||||
|
|
||||||
[发布页面]: https://github.com/NateScarlet/holiday-cn/releases
|
[发布页面]: https://github.com/NateScarlet/holiday-cn/releases
|
||||||
|
|||||||
100
hk/2026.ics
Normal file
100
hk/2026.ics
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
CLASS:PUBLIC
|
||||||
|
X-WR-CALDESC:香港公众假期数据,来源:香港特别行政区政
|
||||||
|
府 1823.gov.hk。
|
||||||
|
X-WR-CALNAME:香港公众假期
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Asia/Shanghai
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:19700101T000000
|
||||||
|
TZOFFSETFROM:+0800
|
||||||
|
TZOFFSETTO:+0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:一月一日假期
|
||||||
|
DTSTART;VALUE=DATE:20260101
|
||||||
|
DTEND;VALUE=DATE:20260102
|
||||||
|
DTSTAMP;VALUE=DATE:20260101
|
||||||
|
UID:2026-01-01/2026-01-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:農曆年初一假期
|
||||||
|
DTSTART;VALUE=DATE:20260217
|
||||||
|
DTEND;VALUE=DATE:20260220
|
||||||
|
DTSTAMP;VALUE=DATE:20260217
|
||||||
|
UID:2026-02-17/2026-02-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:耶穌受難節假期
|
||||||
|
DTSTART;VALUE=DATE:20260403
|
||||||
|
DTEND;VALUE=DATE:20260405
|
||||||
|
DTSTAMP;VALUE=DATE:20260403
|
||||||
|
UID:2026-04-03/2026-04-05/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:清明節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260406
|
||||||
|
DTEND;VALUE=DATE:20260408
|
||||||
|
DTSTAMP;VALUE=DATE:20260406
|
||||||
|
UID:2026-04-06/2026-04-08/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:勞動節假期
|
||||||
|
DTSTART;VALUE=DATE:20260501
|
||||||
|
DTEND;VALUE=DATE:20260502
|
||||||
|
DTSTAMP;VALUE=DATE:20260501
|
||||||
|
UID:2026-05-01/2026-05-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:佛誕翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260525
|
||||||
|
DTEND;VALUE=DATE:20260526
|
||||||
|
DTSTAMP;VALUE=DATE:20260525
|
||||||
|
UID:2026-05-25/2026-05-26/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:端午節假期
|
||||||
|
DTSTART;VALUE=DATE:20260619
|
||||||
|
DTEND;VALUE=DATE:20260620
|
||||||
|
DTSTAMP;VALUE=DATE:20260619
|
||||||
|
UID:2026-06-19/2026-06-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:香港特別行政區成立紀念日假期
|
||||||
|
DTSTART;VALUE=DATE:20260701
|
||||||
|
DTEND;VALUE=DATE:20260702
|
||||||
|
DTSTAMP;VALUE=DATE:20260701
|
||||||
|
UID:2026-07-01/2026-07-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:中秋節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260926
|
||||||
|
DTEND;VALUE=DATE:20260927
|
||||||
|
DTSTAMP;VALUE=DATE:20260926
|
||||||
|
UID:2026-09-26/2026-09-27/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:國慶日假期
|
||||||
|
DTSTART;VALUE=DATE:20261001
|
||||||
|
DTEND;VALUE=DATE:20261002
|
||||||
|
DTSTAMP;VALUE=DATE:20261001
|
||||||
|
UID:2026-10-01/2026-10-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:重陽節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20261019
|
||||||
|
DTEND;VALUE=DATE:20261020
|
||||||
|
DTSTAMP;VALUE=DATE:20261019
|
||||||
|
UID:2026-10-19/2026-10-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:聖誕節假期
|
||||||
|
DTSTART;VALUE=DATE:20261225
|
||||||
|
DTEND;VALUE=DATE:20261227
|
||||||
|
DTSTAMP;VALUE=DATE:20261225
|
||||||
|
UID:2026-12-25/2026-12-27/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
95
hk/2026.json
Normal file
95
hk/2026.json
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json",
|
||||||
|
"$id": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/hk/2026.json",
|
||||||
|
"year": 2026,
|
||||||
|
"papers": [
|
||||||
|
"https://www.1823.gov.hk/common/ical/tc.json"
|
||||||
|
],
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"name": "一月一日",
|
||||||
|
"date": "2026-01-01",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "農曆年初一",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "農曆年初二",
|
||||||
|
"date": "2026-02-18",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "農曆年初三",
|
||||||
|
"date": "2026-02-19",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "耶穌受難節",
|
||||||
|
"date": "2026-04-03",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "耶穌受難節翌日",
|
||||||
|
"date": "2026-04-04",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "清明節翌日",
|
||||||
|
"date": "2026-04-06",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "復活節星期一翌日",
|
||||||
|
"date": "2026-04-07",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "勞動節",
|
||||||
|
"date": "2026-05-01",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "佛誕翌日",
|
||||||
|
"date": "2026-05-25",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "端午節",
|
||||||
|
"date": "2026-06-19",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "香港特別行政區成立紀念日",
|
||||||
|
"date": "2026-07-01",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "中秋節翌日",
|
||||||
|
"date": "2026-09-26",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "國慶日",
|
||||||
|
"date": "2026-10-01",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "重陽節翌日",
|
||||||
|
"date": "2026-10-19",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "聖誕節",
|
||||||
|
"date": "2026-12-25",
|
||||||
|
"isOffDay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "聖誕節後第一個周日",
|
||||||
|
"date": "2026-12-26",
|
||||||
|
"isOffDay": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
hk/2027.ics
Normal file
16
hk/2027.ics
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
CLASS:PUBLIC
|
||||||
|
X-WR-CALDESC:香港公众假期数据,来源:香港特别行政区政
|
||||||
|
府 1823.gov.hk。
|
||||||
|
X-WR-CALNAME:香港公众假期
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Asia/Shanghai
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:19700101T000000
|
||||||
|
TZOFFSETFROM:+0800
|
||||||
|
TZOFFSETTO:+0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
END:VCALENDAR
|
||||||
9
hk/2027.json
Normal file
9
hk/2027.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json",
|
||||||
|
"$id": "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/hk/2027.json",
|
||||||
|
"year": 2027,
|
||||||
|
"papers": [
|
||||||
|
"https://www.1823.gov.hk/common/ical/tc.json"
|
||||||
|
],
|
||||||
|
"days": []
|
||||||
|
}
|
||||||
100
holiday-hk.ics
Normal file
100
holiday-hk.ics
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
CLASS:PUBLIC
|
||||||
|
X-WR-CALDESC:香港公众假期数据,来源:香港特别行政区政
|
||||||
|
府 1823.gov.hk。
|
||||||
|
X-WR-CALNAME:香港公众假期
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Asia/Shanghai
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:19700101T000000
|
||||||
|
TZOFFSETFROM:+0800
|
||||||
|
TZOFFSETTO:+0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:一月一日假期
|
||||||
|
DTSTART;VALUE=DATE:20260101
|
||||||
|
DTEND;VALUE=DATE:20260102
|
||||||
|
DTSTAMP;VALUE=DATE:20260101
|
||||||
|
UID:2026-01-01/2026-01-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:農曆年初一假期
|
||||||
|
DTSTART;VALUE=DATE:20260217
|
||||||
|
DTEND;VALUE=DATE:20260220
|
||||||
|
DTSTAMP;VALUE=DATE:20260217
|
||||||
|
UID:2026-02-17/2026-02-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:耶穌受難節假期
|
||||||
|
DTSTART;VALUE=DATE:20260403
|
||||||
|
DTEND;VALUE=DATE:20260405
|
||||||
|
DTSTAMP;VALUE=DATE:20260403
|
||||||
|
UID:2026-04-03/2026-04-05/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:清明節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260406
|
||||||
|
DTEND;VALUE=DATE:20260408
|
||||||
|
DTSTAMP;VALUE=DATE:20260406
|
||||||
|
UID:2026-04-06/2026-04-08/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:勞動節假期
|
||||||
|
DTSTART;VALUE=DATE:20260501
|
||||||
|
DTEND;VALUE=DATE:20260502
|
||||||
|
DTSTAMP;VALUE=DATE:20260501
|
||||||
|
UID:2026-05-01/2026-05-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:佛誕翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260525
|
||||||
|
DTEND;VALUE=DATE:20260526
|
||||||
|
DTSTAMP;VALUE=DATE:20260525
|
||||||
|
UID:2026-05-25/2026-05-26/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:端午節假期
|
||||||
|
DTSTART;VALUE=DATE:20260619
|
||||||
|
DTEND;VALUE=DATE:20260620
|
||||||
|
DTSTAMP;VALUE=DATE:20260619
|
||||||
|
UID:2026-06-19/2026-06-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:香港特別行政區成立紀念日假期
|
||||||
|
DTSTART;VALUE=DATE:20260701
|
||||||
|
DTEND;VALUE=DATE:20260702
|
||||||
|
DTSTAMP;VALUE=DATE:20260701
|
||||||
|
UID:2026-07-01/2026-07-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:中秋節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20260926
|
||||||
|
DTEND;VALUE=DATE:20260927
|
||||||
|
DTSTAMP;VALUE=DATE:20260926
|
||||||
|
UID:2026-09-26/2026-09-27/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:國慶日假期
|
||||||
|
DTSTART;VALUE=DATE:20261001
|
||||||
|
DTEND;VALUE=DATE:20261002
|
||||||
|
DTSTAMP;VALUE=DATE:20261001
|
||||||
|
UID:2026-10-01/2026-10-02/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:重陽節翌日假期
|
||||||
|
DTSTART;VALUE=DATE:20261019
|
||||||
|
DTEND;VALUE=DATE:20261020
|
||||||
|
DTSTAMP;VALUE=DATE:20261019
|
||||||
|
UID:2026-10-19/2026-10-20/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
SUMMARY:聖誕節假期
|
||||||
|
DTSTART;VALUE=DATE:20261225
|
||||||
|
DTEND;VALUE=DATE:20261227
|
||||||
|
DTSTAMP;VALUE=DATE:20261225
|
||||||
|
UID:2026-12-25/2026-12-27/NateScarlet/holiday-cn
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
45
scripts/fetch_hk.py
Normal file
45
scripts/fetch_hk.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""Fetch Hong Kong public holiday data from 1823.gov.hk."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Traditional Chinese names, consistent with CN data format
|
||||||
|
HK_ICAL_URL = "https://www.1823.gov.hk/common/ical/tc.json"
|
||||||
|
|
||||||
|
# Earliest year available from the 1823.gov.hk API
|
||||||
|
HK_START_YEAR = 2024
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_hk_holiday(year: int) -> dict:
|
||||||
|
"""Fetch HK public holidays for a given year.
|
||||||
|
|
||||||
|
HK has no makeup work day (调休) concept, so all entries are isOffDay=True.
|
||||||
|
Data coverage starts from HK_START_YEAR.
|
||||||
|
"""
|
||||||
|
response = requests.get(HK_ICAL_URL)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
events = data["vcalendar"][0]["vevent"]
|
||||||
|
days = []
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
dtstart = event["dtstart"][0] # "YYYYMMDD"
|
||||||
|
date = datetime.date(int(dtstart[:4]), int(dtstart[4:6]), int(dtstart[6:8]))
|
||||||
|
if date.year != year:
|
||||||
|
continue
|
||||||
|
days.append(
|
||||||
|
{
|
||||||
|
"name": event["summary"],
|
||||||
|
"date": date.isoformat(),
|
||||||
|
"isOffDay": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
days.sort(key=lambda d: d["date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"year": year,
|
||||||
|
"papers": [HK_ICAL_URL],
|
||||||
|
"days": days,
|
||||||
|
}
|
||||||
@@ -60,11 +60,16 @@ def _iter_date_ranges(days: Sequence[dict]) -> Iterator[Tuple[dict, dict]]:
|
|||||||
yield fr, to
|
yield fr, to
|
||||||
|
|
||||||
|
|
||||||
def generate_ics(days: Sequence[dict], filename: Text) -> None:
|
def generate_ics(
|
||||||
|
days: Sequence[dict],
|
||||||
|
filename: Text,
|
||||||
|
cal_name: str = "中国法定节假日",
|
||||||
|
cal_desc: str = "中国法定节假日数据,自动每日抓取国务院公告。",
|
||||||
|
) -> None:
|
||||||
"""Generate ics from days."""
|
"""Generate ics from days."""
|
||||||
cal = Calendar()
|
cal = Calendar()
|
||||||
cal.add("X-WR-CALNAME", "中国法定节假日")
|
cal.add("X-WR-CALNAME", cal_name)
|
||||||
cal.add("X-WR-CALDESC", "中国法定节假日数据,自动每日抓取国务院公告。")
|
cal.add("X-WR-CALDESC", cal_desc)
|
||||||
cal.add("VERSION", "2.0")
|
cal.add("VERSION", "2.0")
|
||||||
cal.add("METHOD", "PUBLISH")
|
cal.add("METHOD", "PUBLISH")
|
||||||
cal.add("CLASS", "PUBLIC")
|
cal.add("CLASS", "PUBLIC")
|
||||||
|
|||||||
@@ -14,9 +14,32 @@ from zipfile import ZipFile
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from fetch import CustomJSONEncoder, fetch_holiday
|
from fetch import CustomJSONEncoder, fetch_holiday
|
||||||
|
from fetch_hk import HK_START_YEAR, fetch_hk_holiday
|
||||||
from generate_ics import generate_ics
|
from generate_ics import generate_ics
|
||||||
from filetools import workspace_path
|
from filetools import workspace_path
|
||||||
|
|
||||||
|
SCHEMA_URL = "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json"
|
||||||
|
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master"
|
||||||
|
|
||||||
|
REGIONS = {
|
||||||
|
"cn": {
|
||||||
|
"fetch": fetch_holiday,
|
||||||
|
"start_year": 2007,
|
||||||
|
"subdir": None,
|
||||||
|
"main_ics_name": "holiday-cn.ics",
|
||||||
|
"cal_name": "中国法定节假日",
|
||||||
|
"cal_desc": "中国法定节假日数据,自动每日抓取国务院公告。",
|
||||||
|
},
|
||||||
|
"hk": {
|
||||||
|
"fetch": fetch_hk_holiday,
|
||||||
|
"start_year": HK_START_YEAR,
|
||||||
|
"subdir": "hk",
|
||||||
|
"main_ics_name": "holiday-hk.ics",
|
||||||
|
"cal_name": "香港公众假期",
|
||||||
|
"cal_desc": "香港公众假期数据,来源:香港特别行政区政府 1823.gov.hk。",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ChinaTimezone(tzinfo):
|
class ChinaTimezone(tzinfo):
|
||||||
"""Timezone of china."""
|
"""Timezone of china."""
|
||||||
@@ -31,25 +54,34 @@ class ChinaTimezone(tzinfo):
|
|||||||
return timedelta()
|
return timedelta()
|
||||||
|
|
||||||
|
|
||||||
def update_data(year: int) -> Iterator[str]:
|
def _region_paths(region: str, year: int):
|
||||||
"""Update and store data for a year."""
|
"""Return (json_path, ics_path, id_url) for a region and year."""
|
||||||
|
subdir = REGIONS[region]["subdir"]
|
||||||
|
if subdir:
|
||||||
|
os.makedirs(workspace_path(subdir), exist_ok=True)
|
||||||
|
json_path = workspace_path(subdir, f"{year}.json")
|
||||||
|
ics_path = workspace_path(subdir, f"{year}.ics")
|
||||||
|
id_url = f"{GITHUB_RAW_BASE}/{subdir}/{year}.json"
|
||||||
|
else:
|
||||||
|
json_path = workspace_path(f"{year}.json")
|
||||||
|
ics_path = workspace_path(f"{year}.ics")
|
||||||
|
id_url = f"{GITHUB_RAW_BASE}/{year}.json"
|
||||||
|
return json_path, ics_path, id_url
|
||||||
|
|
||||||
json_filename = workspace_path(f"{year}.json")
|
|
||||||
ics_filename = workspace_path(f"{year}.ics")
|
|
||||||
with open(json_filename, "w", encoding="utf-8", newline="\n") as f:
|
|
||||||
data = fetch_holiday(year)
|
|
||||||
|
|
||||||
|
def update_data(year: int, region: str = "cn") -> Iterator[str]:
|
||||||
|
"""Update and store data for a year and region."""
|
||||||
|
cfg = REGIONS[region]
|
||||||
|
json_path, ics_path, id_url = _region_paths(region, year)
|
||||||
|
|
||||||
|
data = cfg["fetch"](year)
|
||||||
|
|
||||||
|
with open(json_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
json.dump(
|
json.dump(
|
||||||
dict(
|
dict(
|
||||||
(
|
(
|
||||||
(
|
("$schema", SCHEMA_URL),
|
||||||
"$schema",
|
("$id", id_url),
|
||||||
"https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/schema.json",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"$id",
|
|
||||||
f"https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/{year}.json",
|
|
||||||
),
|
|
||||||
*data.items(),
|
*data.items(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -59,25 +91,38 @@ def update_data(year: int) -> Iterator[str]:
|
|||||||
cls=CustomJSONEncoder,
|
cls=CustomJSONEncoder,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield json_filename
|
yield json_path
|
||||||
generate_ics(data["days"], ics_filename)
|
generate_ics(
|
||||||
yield ics_filename
|
data["days"],
|
||||||
|
ics_path,
|
||||||
|
cal_name=cfg["cal_name"],
|
||||||
|
cal_desc=cfg["cal_desc"],
|
||||||
|
)
|
||||||
|
yield ics_path
|
||||||
|
|
||||||
|
|
||||||
def update_main_ics(fr_year, to_year):
|
def update_main_ics(fr_year: int, to_year: int, region: str = "cn"):
|
||||||
|
cfg = REGIONS[region]
|
||||||
|
subdir = cfg["subdir"]
|
||||||
all_days = []
|
all_days = []
|
||||||
|
|
||||||
for year in range(fr_year, to_year + 1):
|
for year in range(fr_year, to_year + 1):
|
||||||
filename = workspace_path(f"{year}.json")
|
if subdir:
|
||||||
|
filename = workspace_path(subdir, f"{year}.json")
|
||||||
|
else:
|
||||||
|
filename = workspace_path(f"{year}.json")
|
||||||
if not os.path.isfile(filename):
|
if not os.path.isfile(filename):
|
||||||
continue
|
continue
|
||||||
with open(filename, "r", encoding="utf8") as inf:
|
with open(filename, "r", encoding="utf8") as inf:
|
||||||
data = json.loads(inf.read())
|
data = json.loads(inf.read())
|
||||||
all_days.extend(data.get("days"))
|
all_days.extend(data.get("days", []))
|
||||||
|
|
||||||
filename = workspace_path("holiday-cn.ics")
|
filename = workspace_path(cfg["main_ics_name"])
|
||||||
generate_ics(
|
generate_ics(
|
||||||
all_days,
|
all_days,
|
||||||
filename,
|
filename,
|
||||||
|
cal_name=cfg["cal_name"],
|
||||||
|
cal_desc=cfg["cal_desc"],
|
||||||
)
|
)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@@ -87,30 +132,41 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--all",
|
"--all",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Update all years since 2007, default is this year and next year",
|
help="Update all years since each region's start year, default is this year and next year",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--release",
|
"--release",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="create new release if repository data is not up to date",
|
help="create new release if repository data is not up to date",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--region",
|
||||||
|
choices=list(REGIONS.keys()),
|
||||||
|
default=None,
|
||||||
|
help="Region to update (default: all regions)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
now = datetime.now(ChinaTimezone())
|
now = datetime.now(ChinaTimezone())
|
||||||
is_release = args.release
|
is_release = args.release
|
||||||
|
regions_to_update = list(REGIONS.keys()) if args.region is None else [args.region]
|
||||||
|
|
||||||
filenames = []
|
filenames = []
|
||||||
progress = tqdm(range(2007 if args.all else now.year, now.year + 2))
|
for region in regions_to_update:
|
||||||
for i in progress:
|
cfg = REGIONS[region]
|
||||||
progress.set_description(f"Updating {i} data")
|
year_start = cfg["start_year"] if args.all else max(cfg["start_year"], now.year)
|
||||||
filenames += list(update_data(i))
|
progress = tqdm(range(year_start, now.year + 2))
|
||||||
progress.set_description("Updating holiday-cn.ics")
|
for year in progress:
|
||||||
filenames.append(update_main_ics(now.year - 4, now.year + 1))
|
progress.set_description(f"Updating {region} {year}")
|
||||||
|
filenames += list(update_data(year, region))
|
||||||
|
progress.set_description(f"Updating {cfg['main_ics_name']}")
|
||||||
|
filenames.append(update_main_ics(now.year - 4, now.year + 1, region))
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
subprocess.run(["git", "add", *filenames], check=True)
|
subprocess.run(["git", "add", *filenames], check=True)
|
||||||
diff = subprocess.run(
|
diff = subprocess.run(
|
||||||
["git", "diff", "--stat", "--cached", "*.json", "*.ics"],
|
["git", "diff", "--stat", "--cached"],
|
||||||
check=True,
|
check=True,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -160,13 +216,23 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
def pack_data(file):
|
def pack_data(file):
|
||||||
"""Pack data json in zip file."""
|
"""Pack all region JSON data into a zip file."""
|
||||||
|
|
||||||
zip_file = ZipFile(file, "w")
|
zip_file = ZipFile(file, "w")
|
||||||
for i in os.listdir(workspace_path()):
|
# Root-level {year}.json files (CN)
|
||||||
if not re.match(r"\d+\.json", i):
|
for name in os.listdir(workspace_path()):
|
||||||
|
if re.match(r"\d+\.json", name):
|
||||||
|
zip_file.write(workspace_path(name), name)
|
||||||
|
# Subdirectory region files (e.g. hk/{year}.json)
|
||||||
|
for region, cfg in REGIONS.items():
|
||||||
|
subdir = cfg["subdir"]
|
||||||
|
if not subdir:
|
||||||
continue
|
continue
|
||||||
zip_file.write(workspace_path(i), i)
|
subdir_path = workspace_path(subdir)
|
||||||
|
if not os.path.isdir(subdir_path):
|
||||||
|
continue
|
||||||
|
for name in os.listdir(subdir_path):
|
||||||
|
if re.match(r"\d+\.json", name):
|
||||||
|
zip_file.write(workspace_path(subdir, name), f"{subdir}/{name}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user