├── requirements.txt ├── run.py ├── .github └── workflows │ └── fetch&push.yml ├── README.md ├── .gitignore ├── README.en.md └── risklevel.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.27.1 2 | GitPython==3.1.27 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from git import Repo 4 | from datetime import datetime, timezone, timedelta 5 | from risklevel import main as risklevel_main 6 | 7 | ABS_PATH = os.path.dirname(os.path.abspath(__file__)) 8 | API_PATH = os.path.join(ABS_PATH, 'Archive') 9 | 10 | 11 | if len(sys.argv) == 2 and sys.argv[1] == 'init': 12 | clone_path = input('Input the remote repository path: ') 13 | print('Cloning...') 14 | repo = Repo.clone_from(clone_path, API_PATH) 15 | repo.git.checkout('api') 16 | repo.git.pull() 17 | print('API folder initialized') 18 | print('Run `python3 run.py` to start fetching data') 19 | sys.exit(0) 20 | 21 | if not os.path.exists(API_PATH): 22 | print('API folder not found.') 23 | print('Run `python3 run.py init` to initialize the API folder.') 24 | sys.exit(1) 25 | 26 | repo = Repo(API_PATH) 27 | repo.git.pull() 28 | assert os.path.exists(os.path.join(API_PATH, 'latest.json')) 29 | 30 | fetch_result = risklevel_main() 31 | # get commit message as "Update at Sat Aug 13 22:29:33 CST 2022" 32 | current_time = datetime.now(tz=timezone(timedelta(hours=8))).strftime("%a %b %d %H:%M:%S %z %Y") 33 | current_time = current_time.replace('+0800', 'CST') 34 | if fetch_result: 35 | repo.git.add(all=True) 36 | commit_msg = f'Update at {current_time}' 37 | print(commit_msg) 38 | repo.git.commit('-m', commit_msg) 39 | repo.git.push() 40 | print('Update successful') 41 | else: 42 | print(f'No update needed at {current_time}') 43 | -------------------------------------------------------------------------------- /.github/workflows/fetch&push.yml: -------------------------------------------------------------------------------- 1 | name: Fetch&Push Latest Risk Data 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "23 */8 * * *" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-20.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Sync Risk Data 22 | uses: actions/checkout@v3 23 | with: 24 | ref: api 25 | path: Archive 26 | 27 | - name: Steup Python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: 3.8 31 | 32 | - name: Install Dependencies 33 | run: | 34 | rm -rf Archive/.git 35 | python -m pip install --upgrade pip 36 | pip install -r requirements.txt 37 | 38 | - name: Fetch and Save Latest Risk Data 39 | env: 40 | FORCE_UPDATE: ${{ secrets.FORCE_UPDATE }} 41 | run: | 42 | if python -u risklevel.py; then 43 | echo "push_required=1" >> $GITHUB_ENV; 44 | echo "msg=Update at `TZ=Asia/Shanghai date`" >> $GITHUB_ENV; 45 | echo "Updated risk data successfully."; 46 | else 47 | echo "No update required."; 48 | fi 49 | 50 | - name: Push to api Branch 51 | if: env.push_required == 1 52 | uses: s0/git-publish-subdir-action@develop 53 | env: 54 | REPO: self 55 | BRANCH: api 56 | FOLDER: Archive 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | MESSAGE: ${{ env.msg }} 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RiskLevelAPI 2 | 3 | 中文 | [English](README.en.md) 4 | 5 | 自动获取最新新冠中高风险地区数据,可作为API调用 6 | 7 | 此API已有对应的前端项目,[在线查看](https://covid.risk-region.ml/) 。可访问 [panghaibin/COVID-Risk-Region](https://github.com/panghaibin/COVID-Risk-Region) 了解更多信息。 8 | 9 | ## 功能 10 | ### risklevel.py 11 | 12 | - 从 [卫健委](http://bmfw.www.gov.cn/yqfxdjcx/risk.html) 网站自动下载最新的疫情风险等级数据,并保存为 *.json* 文件,文件名为`[更新时间]-[hash值].json`(如 `2022041511-b38a8084.json`)。 13 | 14 | - 当获取到新数据时,除了保存本次数据外,还会更新 `latest.json` 和 `info.json` 文件。`latest.json` 始终保存最新的数据,`info.json` 保存了`Archive` 文件夹下所有原始 json 文件的文件名及对应的更新时间时间戳。 15 | 16 | ### GitHub Actions 17 | 18 | - 本项目已启用 GitHub Actions 用于数据的自动更新,并将其 Push 到仓库的 `api` 分支中。访问 将始终获取到最新的数据,可作为 API 调用;访问 可得到本项目存储的历史数据相应信息 19 | 20 | - 也可使用 作为 API 调用,此 API 部署在 Cloudflare Pages 上 21 | 22 | ### run.py 23 | 24 | - 脚本可同步 Git 仓库的 API 数据,运行时会自动获取最新的数据,并将其 Push 到仓库的 `api` 分支中。该脚本可部署至服务器上,并在服务器上运行,可实现数据的自动更新。用于同步的 Git 仓库可自定义。 25 | 26 | ## 使用 27 | 28 | - 下载项目到本地,运行 *risklevel.py*,结果会保存在 `Archive` 文件夹中。 29 | 30 | - 或者 Fork 本项目并启用 GitHub Actions ,自动获取新冠疫情风险等级数据,并将其 Push 到仓库的 `api` 分支中。 31 | 32 | - 也可在服务器上设置定时任务,运行 run.py 脚本,实现数据的自动更新,并 Push 至指定仓库。 33 | 34 | ## 关于代码中的 *token* 和 *key* 35 | 36 | 在运行程序向网站提交请求时,你会发现代码里有一些看起来像是 `token` 或者 `key` 的秘钥。这些其实都是原网站 JavaScript 代码里的明文。直接使用即可。 37 | 38 | ## 搭载 GitHub Actions 的 API 版本 39 | 40 | 此 API 版本基于 [@KaikePing](https://github.com/KaikePing/RiskLevel) 的原版修改而来,添加了 GitHub Actions 的自动更新功能,可作为 API 供第三方调用。 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 项目文件 2 | history*.json 3 | temp*.py 4 | *.local 5 | Archive 6 | # Pycharm 7 | .idea/ 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # RiskLevelAPI 2 | 3 | [中文](README.md) | English 4 | 5 | An API of the latest risk level regions on COVID-19 in China. 6 | 7 | There is a frontend [here](https://covid.risk-region.ml/), and visit [panghaibin/COVID-Risk-Region](https://github.com/panghaibin/COVID-Risk-Region) for more information. 8 | 9 | ## Feature 10 | ### risklevel.py 11 | 12 | - Fetch the latest outbreak risk level data automatically from the [National Health Commission](http://bmfw.www.gov.cn/yqfxdjcx/risk.html) website and save it as a *.json* file with the file name `[update_time]-[hash_value].json` (e.g. `2022041511-b38a8084.json`). 13 | 14 | - When new data is fetched, the `latest.json` and `info.json` files are updated in addition to saving the current data. `latest.json` always holds the latest data, and `info.json` holds the filenames of all the original json files under the `Archive` folder and the corresponding timestamp of the update time. 15 | 16 | ### GitHub Actions 17 | 18 | - This repository is equipped with GitHub Actions for automatic data updates, which by default fetches data and pushes it to the `api` branch of the repository. Visiting will keep you up to date with the latest data, which can be called as an API; visiting to get the corresponding information on the historical data stored in this repository 19 | 20 | - You can also use and as an API, which are deployed on Cloudflare Pages 21 | 22 | ### run.py 23 | 24 | - The script will automatically fetch the latest data and push it to the `api` branch of the git repository. The script can be deployed to a server and run on the server, which can be used to automatically update the data. The git repository can be customized. 25 | 26 | ## Usage 27 | 28 | - Clone the repo to local and run *risklevel.py*, then the outcome as *.json* files will be stored in the `Archive` folder. 29 | 30 | - Or fork this repository and enable GitHub Actions to automatically update the risk level data and push to the `api` branch of the repository. 31 | 32 | - You can also set up a cron job on the server to run the *run.py* script to update the risk level data, and push to the specified repository automatically. 33 | 34 | ## About the *token* and *key* in the code 35 | 36 | When making the request to acquire the data, you may notice there are some appeared private auth-keys like `token` or `key` in the code. Those are just clear texts in the JavaScript code of that website. Feel free to use. 37 | 38 | ## The API version equipped with GitHub Actions 39 | 40 | This API version is based on [@KaikePing's](https://github.com/KaikePing/RiskLevel) original version, and adds the auto-update function based on GitHub Actions. You can use it as an API. 41 | -------------------------------------------------------------------------------- /risklevel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Aug 16 11:30:41 2021 4 | @author: roselily 5 | 6 | Modified on Fri Apr 15 14:31:50 2022 7 | @author: panghaibin 8 | """ 9 | 10 | import os 11 | import json 12 | import time 13 | import hashlib 14 | import requests 15 | from datetime import datetime 16 | 17 | # Location of Json files 18 | PATHS = 'Archive' 19 | 20 | ABS_PATH = os.path.dirname(os.path.abspath(__file__)) 21 | PATHS = os.path.join(ABS_PATH, PATHS) 22 | 23 | 24 | def fetch_new(): 25 | """ 26 | Fetch the latest risk data from the API 27 | :return: json data 28 | """ 29 | # Params from Ajax.js 30 | key = '3C502C97ABDA40D0A60FBEE50FAAD1DA' 31 | timestamp = str(int(time.time())) 32 | token = '23y0ufFl5YxIyGrI8hWRUZmKkvtSjLQA' 33 | nonce = '123456789abcdefg' 34 | pass_header = 'zdww' 35 | _ = timestamp + token + nonce + timestamp 36 | _ = _.encode('utf-8') 37 | signatureHeader = hashlib.sha256(_).hexdigest().upper() 38 | _ = timestamp + 'fTN2pfuisxTavbTuYVSsNJHetwq5bJvC' + 'QkjjtiLM2dCratiA' + timestamp 39 | _ = _.encode('utf-8') 40 | wif_signature = hashlib.sha256(_).hexdigest().upper() 41 | 42 | # Send post requests 43 | url = 'http://bmfw.www.gov.cn/bjww/interface/interfaceJson' 44 | data = { 45 | "appId": "NcApplication", 46 | "paasHeader": pass_header, 47 | "timestampHeader": timestamp, 48 | "nonceHeader": nonce, 49 | "signatureHeader": signatureHeader, 50 | "key": key 51 | } 52 | header = { 53 | "x-wif-nonce": "QkjjtiLM2dCratiA", 54 | "x-wif-paasid": "smt-application", 55 | "x-wif-signature": wif_signature, 56 | "x-wif-timestamp": timestamp, 57 | "Origin": "http://bmfw.www.gov.cn", 58 | "Referer": "http://bmfw.www.gov.cn/yqfxdjcx/risk.html", 59 | "Content-Type": "application/json; charset=UTF-8" 60 | } 61 | r = requests.post(url, data=json.dumps(data), headers=header) 62 | risk_json = r.json() 63 | return risk_json 64 | 65 | 66 | def save_json(file_path, json_data): 67 | # Save Json file 68 | with open(file_path, 'w', encoding="utf-8") as outfile: 69 | json.dump(json_data, outfile, ensure_ascii=False) 70 | 71 | 72 | def get_json(file_path): 73 | if not os.path.exists(file_path): 74 | return None 75 | with open(file_path, 'r', encoding="utf-8") as outfile: 76 | json_data = json.load(outfile) 77 | return json_data 78 | 79 | 80 | def get_info_by_list(): 81 | file_list = os.listdir(PATHS) 82 | file_list.sort() 83 | info = {'file_count': 0, 'file_list': []} 84 | for file in file_list: 85 | if '-' in file: 86 | update_time = file.split('-')[0] 87 | update_time += '+0800' 88 | update_time = datetime.strptime(update_time, "%Y%m%d%H%z") 89 | update_time = int(update_time.timestamp()) 90 | info_dict = { 91 | 'file_name': file, 92 | 'update_time': update_time, 93 | } 94 | info['file_list'].append(info_dict) 95 | info['file_count'] += 1 96 | return info 97 | 98 | 99 | def main(): 100 | force_update = os.environ.get('FORCE_UPDATE', '') 101 | force_update = force_update.lower() == 'true' 102 | 103 | if not os.path.exists(PATHS): 104 | os.makedirs(PATHS) 105 | 106 | data = fetch_new() 107 | update_time = data['data']['end_update_time'] 108 | update_time += '+0800' 109 | update_time = datetime.strptime(update_time, "%Y-%m-%d %H时%z") 110 | update_timestamp = int(update_time.timestamp()) 111 | update_time = datetime.strftime(update_time, "%Y%m%d%H") 112 | data_hash = hashlib.md5(json.dumps(data, sort_keys=True).encode('utf-8')).hexdigest() 113 | data_hash = data_hash[:8] 114 | time_file_name = f'{update_time}-{data_hash}.json' 115 | time_file_path = os.path.join(PATHS, time_file_name) 116 | if os.path.exists(time_file_path) and not force_update: 117 | print('File %s already exists' % time_file_name) 118 | return False 119 | save_json(time_file_path, data) 120 | print('File %s saved' % time_file_name) 121 | save_json(os.path.join(PATHS, 'latest.json'), data) 122 | print('File latest.json updated') 123 | 124 | info_path = os.path.join(PATHS, 'info.json') 125 | info = get_json(info_path) 126 | if info is None or force_update: 127 | info = get_info_by_list() 128 | else: 129 | info['file_count'] += 1 130 | info['file_list'].append({ 131 | 'file_name': time_file_name, 132 | 'update_time': update_timestamp, 133 | 'save_time': int(time.time()), 134 | }) 135 | save_json(info_path, info) 136 | print('File info.json updated') 137 | return True 138 | 139 | 140 | if __name__ == '__main__': 141 | if main(): 142 | exit(0) 143 | else: 144 | exit(1) 145 | --------------------------------------------------------------------------------