fd-cn-gov 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fd_cn_gov-0.1.1/LICENSE +21 -0
- fd_cn_gov-0.1.1/PKG-INFO +152 -0
- fd_cn_gov-0.1.1/README.md +124 -0
- fd_cn_gov-0.1.1/fd_cn_gov/__init__.py +6 -0
- fd_cn_gov-0.1.1/fd_cn_gov/build_registry.py +185 -0
- fd_cn_gov-0.1.1/fd_cn_gov/cli.py +119 -0
- fd_cn_gov-0.1.1/fd_cn_gov/registry/__init__.py +82 -0
- fd_cn_gov-0.1.1/fd_cn_gov/registry/registry.db +0 -0
- fd_cn_gov-0.1.1/fd_cn_gov/registry/registry.json +2195 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scraw_contract.py +147 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/__init__.py +0 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mee_gsgg_archive.py +158 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mem_tzgg_archive.py +142 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mnr_tzgg_archive.py +156 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/moa_govpublic_archive.py +170 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mof_gkml_archive.py +191 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mofcom_xwfb_archive.py +128 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mohurd_xinwen_archive.py +189 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/mot_shuju_archive.py +182 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/ndrc_tzgg_archive.py +157 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/pbc_xinwen_archive.py +169 -0
- fd_cn_gov-0.1.1/fd_cn_gov/scripts/safe_whxw_archive.py +148 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/PKG-INFO +152 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/SOURCES.txt +28 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/dependency_links.txt +1 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/entry_points.txt +2 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/requires.txt +2 -0
- fd_cn_gov-0.1.1/fd_cn_gov.egg-info/top_level.txt +1 -0
- fd_cn_gov-0.1.1/pyproject.toml +48 -0
- fd_cn_gov-0.1.1/setup.cfg +4 -0
fd_cn_gov-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cli-anything
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fd_cn_gov-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fd-cn-gov
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Scrapers + datasource registry for Chinese central-government ministry open-information archives.
|
|
5
|
+
Author: cli-anything
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/FindDataOfficial/cn-goverment-datasource
|
|
8
|
+
Project-URL: Repository, https://github.com/FindDataOfficial/cn-goverment-datasource
|
|
9
|
+
Project-URL: Changelog, https://github.com/FindDataOfficial/cn-goverment-datasource/releases
|
|
10
|
+
Project-URL: Issues, https://github.com/FindDataOfficial/cn-goverment-datasource/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: scrapling[fetchers]
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# fd-cn-gov
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/fd-cn-gov/)
|
|
32
|
+
[](https://pypi.org/project/fd-cn-gov/)
|
|
33
|
+
[](https://github.com/FindDataOfficial/cn-goverment-datasource/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
Scrapers + a self-contained datasource registry for **Chinese central-government ministry open-information archives**. Catalog-crawls the public notice / news / data archives of 11 ministries (MOF, PBC, NDRC, MOFCOM, MOHURD, MOT, MOA, SAFE, MNR, MEE, MEM), emitting one JSON record per listed document, and ships a SQLite + JSON registry describing every datasource and its column schema.
|
|
36
|
+
|
|
37
|
+
Standalone: no monorepo, no MCP server, no `daas.db` dependency. The `MANIFEST` at the top of each scraper is the single source of truth; the bundled `registry.db` / `registry.json` are derived artifacts.
|
|
38
|
+
|
|
39
|
+
## Ministries
|
|
40
|
+
|
|
41
|
+
| Name | Label | Seed URL |
|
|
42
|
+
|------|-------|----------|
|
|
43
|
+
| `mee_gsgg_archive` | MEE Notice Archive (生态环境部公示公告) | https://www.mee.gov.cn/ywdt/gsgg/ |
|
|
44
|
+
| `mem_tzgg_archive` | MEM Notice Archive (应急管理部通知公告) | https://www.mem.gov.cn/gk/tzgg/ |
|
|
45
|
+
| `mnr_tzgg_archive` | MNR Notice Archive (自然资源部通知公告) | https://www.mnr.gov.cn/gk/tzgg/ |
|
|
46
|
+
| `moa_govpublic_archive` | MOA GovPublic Archive (农业农村部 机构分类) | https://www.moa.gov.cn/govpublic/ |
|
|
47
|
+
| `mof_gkml_archive` | MOF gkml Archive (财政部信息公开) | https://www.mof.gov.cn/gkml/ |
|
|
48
|
+
| `mofcom_xwfb_archive` | MOFCOM News Preview (商务部新闻发布) | https://www.mofcom.gov.cn/xwfb/index.html |
|
|
49
|
+
| `mohurd_xinwen_archive` | MOHURD Xinwen Archive (住建部新闻动态) | https://www.mohurd.gov.cn/xinwen/ |
|
|
50
|
+
| `mot_shuju_archive` | MOT Data Hub Archive (交通运输部数据) | https://www.mot.gov.cn/shuju/index.html |
|
|
51
|
+
| `ndrc_tzgg_archive` | NDRC Notice Archive (发改委通知公告) | https://www.ndrc.gov.cn/xwdt/tzgg/ |
|
|
52
|
+
| `pbc_xinwen_archive` | PBC News Archive (人民银行新闻发布) | https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html |
|
|
53
|
+
| `safe_whxw_archive` | SAFE News Archive (外汇局外汇新闻) | https://www.safe.gov.cn/safe/whxw/index.html |
|
|
54
|
+
|
|
55
|
+
Each scraper emits a JSON array of records to stdout with at least `title`, `date` (`YYYY-MM-DD`, from the URL `t<YYYYMMDD>_` token with a `<span>` fallback), and `url` (absolute, the primary key). Some add `section` / `subsection` / `doc_type` / `department`. See `fd-cn-gov describe <name>` for the exact columns of any source.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install fd-cn-gov
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires Python ≥3.10. Dependencies: `scrapling` (HTTP + adaptive parsing), `sqlalchemy`.
|
|
64
|
+
|
|
65
|
+
### From source
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install git+https://github.com/FindDataOfficial/cn-goverment-datasource.git
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## CLI
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# List the 11 registered sources
|
|
75
|
+
fd-cn-gov list
|
|
76
|
+
|
|
77
|
+
# Show one source's identity + full column schema
|
|
78
|
+
fd-cn-gov describe mof_gkml_archive
|
|
79
|
+
|
|
80
|
+
# Crawl one archive (default: 50 pages per sub-archive; prints JSON to stdout)
|
|
81
|
+
fd-cn-gov crawl mof_gkml_archive --max-pages 2 > records.json
|
|
82
|
+
|
|
83
|
+
# Full crawl, no page cap (use sparingly — see Polite crawling below)
|
|
84
|
+
fd-cn-gov crawl mof_gkml_archive --all > records.json
|
|
85
|
+
|
|
86
|
+
# Regenerate the bundled registry from the scripts' MANIFESTs
|
|
87
|
+
fd-cn-gov build-registry
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Python API
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import fd_cn_gov
|
|
94
|
+
|
|
95
|
+
# Discover datasources
|
|
96
|
+
for s in fd_cn_gov.list_sources():
|
|
97
|
+
print(s.name, s.label, s.url)
|
|
98
|
+
|
|
99
|
+
# One source + its column schema
|
|
100
|
+
src = fd_cn_gov.get_source("mof_gkml_archive")
|
|
101
|
+
cols = fd_cn_gov.get_columns("mof_gkml_archive") # -> list[Column]
|
|
102
|
+
# Column(name='url', type='string', primary_key=True, nullable=False,
|
|
103
|
+
# description='absolute document URL (.htm/.html/.pdf)',
|
|
104
|
+
# source_field='a@href', semantic_type='url', ...)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Registry schema
|
|
108
|
+
|
|
109
|
+
The bundled `fd_cn_gov/registry/registry.db` is a 3-table SQLite, mirroring the column shapes of the originating `daas.db` for these sources (no foreign keys, no stale-FK footgun):
|
|
110
|
+
|
|
111
|
+
- **`sources`** — one row per ministry: `id, name, label, url, description, category, category_label, config_json`
|
|
112
|
+
- **`datasource_columns`** — one row per output column: `datasource_id, table_name, column_name, column_type, is_primary_key, is_nullable, description, source_field, unit, semantic_type`
|
|
113
|
+
- **`scraw_configs`** — one row per scraper: `name, url, columns_json`
|
|
114
|
+
|
|
115
|
+
`registry.json` is a deterministic full dump of the same three tables (`sort_keys`), so the registry is diff-friendly in code review.
|
|
116
|
+
|
|
117
|
+
To regenerate after editing a `MANIFEST`:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
fd-cn-gov build-registry
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The build is **logically idempotent**: re-running produces an identical `.dump` and a byte-identical `registry.json`. (Only SQLite's file-change-counter header byte differs on each write — unavoidable on any SQLite write; compare via `.dump` or `registry.json` for equality.)
|
|
124
|
+
|
|
125
|
+
## Polite crawling
|
|
126
|
+
|
|
127
|
+
These are `.gov.cn` hosts. Each scraper paces requests (`SLEEP ≈ 0.3s` between pages) and defaults to a **50-page cap per sub-archive**. Use `--max-pages N` to bound a crawl further; reserve `--all` for when you genuinely need full history and can afford the time. Do not run multiple `--all` crawls in parallel against the same ministry.
|
|
128
|
+
|
|
129
|
+
## Project layout
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
fd-cn-gov/
|
|
133
|
+
├── pyproject.toml
|
|
134
|
+
├── README.md
|
|
135
|
+
├── LICENSE
|
|
136
|
+
└── fd_cn_gov/
|
|
137
|
+
├── __init__.py # public read API: list_sources / get_source / get_columns
|
|
138
|
+
├── cli.py # fd-cn-gov CLI
|
|
139
|
+
├── scraw_contract.py # ScrawManifest / ScrawColumn dataclasses (vendored, trimmed)
|
|
140
|
+
├── build_registry.py # regenerates registry.db + registry.json from MANIFESTs
|
|
141
|
+
├── scripts/ # 11 ministry scrapers, each with a module-level MANIFEST
|
|
142
|
+
│ ├── mof_gkml_archive.py
|
|
143
|
+
│ └── ...
|
|
144
|
+
└── registry/
|
|
145
|
+
├── __init__.py # read API over the bundled DB
|
|
146
|
+
├── registry.db # generated — checked in
|
|
147
|
+
└── registry.json # generated — checked in
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# fd-cn-gov
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fd-cn-gov/)
|
|
4
|
+
[](https://pypi.org/project/fd-cn-gov/)
|
|
5
|
+
[](https://github.com/FindDataOfficial/cn-goverment-datasource/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Scrapers + a self-contained datasource registry for **Chinese central-government ministry open-information archives**. Catalog-crawls the public notice / news / data archives of 11 ministries (MOF, PBC, NDRC, MOFCOM, MOHURD, MOT, MOA, SAFE, MNR, MEE, MEM), emitting one JSON record per listed document, and ships a SQLite + JSON registry describing every datasource and its column schema.
|
|
8
|
+
|
|
9
|
+
Standalone: no monorepo, no MCP server, no `daas.db` dependency. The `MANIFEST` at the top of each scraper is the single source of truth; the bundled `registry.db` / `registry.json` are derived artifacts.
|
|
10
|
+
|
|
11
|
+
## Ministries
|
|
12
|
+
|
|
13
|
+
| Name | Label | Seed URL |
|
|
14
|
+
|------|-------|----------|
|
|
15
|
+
| `mee_gsgg_archive` | MEE Notice Archive (生态环境部公示公告) | https://www.mee.gov.cn/ywdt/gsgg/ |
|
|
16
|
+
| `mem_tzgg_archive` | MEM Notice Archive (应急管理部通知公告) | https://www.mem.gov.cn/gk/tzgg/ |
|
|
17
|
+
| `mnr_tzgg_archive` | MNR Notice Archive (自然资源部通知公告) | https://www.mnr.gov.cn/gk/tzgg/ |
|
|
18
|
+
| `moa_govpublic_archive` | MOA GovPublic Archive (农业农村部 机构分类) | https://www.moa.gov.cn/govpublic/ |
|
|
19
|
+
| `mof_gkml_archive` | MOF gkml Archive (财政部信息公开) | https://www.mof.gov.cn/gkml/ |
|
|
20
|
+
| `mofcom_xwfb_archive` | MOFCOM News Preview (商务部新闻发布) | https://www.mofcom.gov.cn/xwfb/index.html |
|
|
21
|
+
| `mohurd_xinwen_archive` | MOHURD Xinwen Archive (住建部新闻动态) | https://www.mohurd.gov.cn/xinwen/ |
|
|
22
|
+
| `mot_shuju_archive` | MOT Data Hub Archive (交通运输部数据) | https://www.mot.gov.cn/shuju/index.html |
|
|
23
|
+
| `ndrc_tzgg_archive` | NDRC Notice Archive (发改委通知公告) | https://www.ndrc.gov.cn/xwdt/tzgg/ |
|
|
24
|
+
| `pbc_xinwen_archive` | PBC News Archive (人民银行新闻发布) | https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html |
|
|
25
|
+
| `safe_whxw_archive` | SAFE News Archive (外汇局外汇新闻) | https://www.safe.gov.cn/safe/whxw/index.html |
|
|
26
|
+
|
|
27
|
+
Each scraper emits a JSON array of records to stdout with at least `title`, `date` (`YYYY-MM-DD`, from the URL `t<YYYYMMDD>_` token with a `<span>` fallback), and `url` (absolute, the primary key). Some add `section` / `subsection` / `doc_type` / `department`. See `fd-cn-gov describe <name>` for the exact columns of any source.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install fd-cn-gov
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python ≥3.10. Dependencies: `scrapling` (HTTP + adaptive parsing), `sqlalchemy`.
|
|
36
|
+
|
|
37
|
+
### From source
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install git+https://github.com/FindDataOfficial/cn-goverment-datasource.git
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# List the 11 registered sources
|
|
47
|
+
fd-cn-gov list
|
|
48
|
+
|
|
49
|
+
# Show one source's identity + full column schema
|
|
50
|
+
fd-cn-gov describe mof_gkml_archive
|
|
51
|
+
|
|
52
|
+
# Crawl one archive (default: 50 pages per sub-archive; prints JSON to stdout)
|
|
53
|
+
fd-cn-gov crawl mof_gkml_archive --max-pages 2 > records.json
|
|
54
|
+
|
|
55
|
+
# Full crawl, no page cap (use sparingly — see Polite crawling below)
|
|
56
|
+
fd-cn-gov crawl mof_gkml_archive --all > records.json
|
|
57
|
+
|
|
58
|
+
# Regenerate the bundled registry from the scripts' MANIFESTs
|
|
59
|
+
fd-cn-gov build-registry
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Python API
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import fd_cn_gov
|
|
66
|
+
|
|
67
|
+
# Discover datasources
|
|
68
|
+
for s in fd_cn_gov.list_sources():
|
|
69
|
+
print(s.name, s.label, s.url)
|
|
70
|
+
|
|
71
|
+
# One source + its column schema
|
|
72
|
+
src = fd_cn_gov.get_source("mof_gkml_archive")
|
|
73
|
+
cols = fd_cn_gov.get_columns("mof_gkml_archive") # -> list[Column]
|
|
74
|
+
# Column(name='url', type='string', primary_key=True, nullable=False,
|
|
75
|
+
# description='absolute document URL (.htm/.html/.pdf)',
|
|
76
|
+
# source_field='a@href', semantic_type='url', ...)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Registry schema
|
|
80
|
+
|
|
81
|
+
The bundled `fd_cn_gov/registry/registry.db` is a 3-table SQLite, mirroring the column shapes of the originating `daas.db` for these sources (no foreign keys, no stale-FK footgun):
|
|
82
|
+
|
|
83
|
+
- **`sources`** — one row per ministry: `id, name, label, url, description, category, category_label, config_json`
|
|
84
|
+
- **`datasource_columns`** — one row per output column: `datasource_id, table_name, column_name, column_type, is_primary_key, is_nullable, description, source_field, unit, semantic_type`
|
|
85
|
+
- **`scraw_configs`** — one row per scraper: `name, url, columns_json`
|
|
86
|
+
|
|
87
|
+
`registry.json` is a deterministic full dump of the same three tables (`sort_keys`), so the registry is diff-friendly in code review.
|
|
88
|
+
|
|
89
|
+
To regenerate after editing a `MANIFEST`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
fd-cn-gov build-registry
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The build is **logically idempotent**: re-running produces an identical `.dump` and a byte-identical `registry.json`. (Only SQLite's file-change-counter header byte differs on each write — unavoidable on any SQLite write; compare via `.dump` or `registry.json` for equality.)
|
|
96
|
+
|
|
97
|
+
## Polite crawling
|
|
98
|
+
|
|
99
|
+
These are `.gov.cn` hosts. Each scraper paces requests (`SLEEP ≈ 0.3s` between pages) and defaults to a **50-page cap per sub-archive**. Use `--max-pages N` to bound a crawl further; reserve `--all` for when you genuinely need full history and can afford the time. Do not run multiple `--all` crawls in parallel against the same ministry.
|
|
100
|
+
|
|
101
|
+
## Project layout
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
fd-cn-gov/
|
|
105
|
+
├── pyproject.toml
|
|
106
|
+
├── README.md
|
|
107
|
+
├── LICENSE
|
|
108
|
+
└── fd_cn_gov/
|
|
109
|
+
├── __init__.py # public read API: list_sources / get_source / get_columns
|
|
110
|
+
├── cli.py # fd-cn-gov CLI
|
|
111
|
+
├── scraw_contract.py # ScrawManifest / ScrawColumn dataclasses (vendored, trimmed)
|
|
112
|
+
├── build_registry.py # regenerates registry.db + registry.json from MANIFESTs
|
|
113
|
+
├── scripts/ # 11 ministry scrapers, each with a module-level MANIFEST
|
|
114
|
+
│ ├── mof_gkml_archive.py
|
|
115
|
+
│ └── ...
|
|
116
|
+
└── registry/
|
|
117
|
+
├── __init__.py # read API over the bundled DB
|
|
118
|
+
├── registry.db # generated — checked in
|
|
119
|
+
└── registry.json # generated — checked in
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regenerate the bundled registry.db + registry.json from the scripts' MANIFESTs.
|
|
3
|
+
|
|
4
|
+
The MANIFEST is the source of truth; the DB is derived. Idempotent: re-running
|
|
5
|
+
produces a byte-identical DB (delete-then-insert per source, deterministic
|
|
6
|
+
JSON sort_keys).
|
|
7
|
+
|
|
8
|
+
Usage: `python -m fd_cn_gov.build_registry` or `fd-cn-gov build-registry`.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
import json
|
|
14
|
+
import sqlite3
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# The 11 ministry scrapers, alphabetical by module name (deterministic order).
|
|
18
|
+
SCRIPT_NAMES = [
|
|
19
|
+
"mee_gsgg_archive",
|
|
20
|
+
"mem_tzgg_archive",
|
|
21
|
+
"mnr_tzgg_archive",
|
|
22
|
+
"moa_govpublic_archive",
|
|
23
|
+
"mof_gkml_archive",
|
|
24
|
+
"mofcom_xwfb_archive",
|
|
25
|
+
"mohurd_xinwen_archive",
|
|
26
|
+
"mot_shuju_archive",
|
|
27
|
+
"ndrc_tzgg_archive",
|
|
28
|
+
"pbc_xinwen_archive",
|
|
29
|
+
"safe_whxw_archive",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
REGISTRY_DIR = Path(__file__).resolve().parent / "registry"
|
|
33
|
+
DB_PATH = REGISTRY_DIR / "registry.db"
|
|
34
|
+
JSON_PATH = REGISTRY_DIR / "registry.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_manifests() -> list:
|
|
38
|
+
manifests = []
|
|
39
|
+
for name in SCRIPT_NAMES:
|
|
40
|
+
mod = importlib.import_module(f"fd_cn_gov.scripts.{name}")
|
|
41
|
+
m = getattr(mod, "MANIFEST", None)
|
|
42
|
+
if m is None or m.name != name:
|
|
43
|
+
raise RuntimeError(f"{name}: MANIFEST missing or name mismatch")
|
|
44
|
+
manifests.append(m)
|
|
45
|
+
return manifests
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _create_schema(conn: sqlite3.Connection) -> None:
|
|
49
|
+
conn.executescript(
|
|
50
|
+
"""
|
|
51
|
+
DROP TABLE IF EXISTS datasource_columns;
|
|
52
|
+
DROP TABLE IF EXISTS scraw_configs;
|
|
53
|
+
DROP TABLE IF EXISTS sources;
|
|
54
|
+
|
|
55
|
+
CREATE TABLE sources (
|
|
56
|
+
id INTEGER PRIMARY KEY,
|
|
57
|
+
name TEXT NOT NULL UNIQUE,
|
|
58
|
+
label TEXT NOT NULL,
|
|
59
|
+
url TEXT NOT NULL,
|
|
60
|
+
description TEXT NOT NULL DEFAULT '',
|
|
61
|
+
category TEXT NOT NULL DEFAULT '',
|
|
62
|
+
category_label TEXT NOT NULL DEFAULT '',
|
|
63
|
+
config_json TEXT NOT NULL DEFAULT '{}'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE datasource_columns (
|
|
67
|
+
id INTEGER PRIMARY KEY,
|
|
68
|
+
datasource_id INTEGER NOT NULL,
|
|
69
|
+
table_name TEXT NOT NULL,
|
|
70
|
+
column_name TEXT NOT NULL,
|
|
71
|
+
column_type TEXT NOT NULL,
|
|
72
|
+
is_primary_key INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
is_nullable INTEGER NOT NULL DEFAULT 1,
|
|
74
|
+
description TEXT NOT NULL DEFAULT '',
|
|
75
|
+
source_field TEXT NOT NULL DEFAULT '',
|
|
76
|
+
unit TEXT NOT NULL DEFAULT '',
|
|
77
|
+
semantic_type TEXT NOT NULL DEFAULT ''
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE scraw_configs (
|
|
81
|
+
id INTEGER PRIMARY KEY,
|
|
82
|
+
name TEXT NOT NULL UNIQUE,
|
|
83
|
+
url TEXT NOT NULL,
|
|
84
|
+
columns_json TEXT NOT NULL DEFAULT '[]'
|
|
85
|
+
);
|
|
86
|
+
"""
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _write_source(conn: sqlite3.Connection, source_id: int, m) -> None:
|
|
91
|
+
conn.execute(
|
|
92
|
+
"""INSERT INTO sources
|
|
93
|
+
(id, name, label, url, description, category, category_label, config_json)
|
|
94
|
+
VALUES (?,?,?,?,?,?,?,?)""",
|
|
95
|
+
(
|
|
96
|
+
source_id, m.name, m.label, m.url, m.description,
|
|
97
|
+
m.category, m.category_label, m.to_config_json(),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
cols = m.to_columns_json()
|
|
101
|
+
for c in cols:
|
|
102
|
+
conn.execute(
|
|
103
|
+
"""INSERT INTO datasource_columns
|
|
104
|
+
(datasource_id, table_name, column_name, column_type,
|
|
105
|
+
is_primary_key, is_nullable, description, source_field,
|
|
106
|
+
unit, semantic_type)
|
|
107
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
108
|
+
(
|
|
109
|
+
source_id, c["table_name"], c["column_name"], c["column_type"],
|
|
110
|
+
c["is_primary_key"], c["is_nullable"], c["description"],
|
|
111
|
+
c["source_field"], c["unit"], c["semantic_type"],
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
conn.execute(
|
|
115
|
+
"INSERT INTO scraw_configs (name, url, columns_json) VALUES (?,?,?)",
|
|
116
|
+
(m.name, m.url, json.dumps(m.to_scraw_columns(), ensure_ascii=False)),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _dump_json(conn: sqlite3.Connection, manifests: list) -> dict:
|
|
121
|
+
"""Deterministic full dump for registry.json (sort_keys, ensure_ascii=False)."""
|
|
122
|
+
sources = [
|
|
123
|
+
{
|
|
124
|
+
"id": i + 1,
|
|
125
|
+
"name": m.name,
|
|
126
|
+
"label": m.label,
|
|
127
|
+
"url": m.url,
|
|
128
|
+
"description": m.description,
|
|
129
|
+
"category": m.category,
|
|
130
|
+
"category_label": m.category_label,
|
|
131
|
+
"config": m.to_config(),
|
|
132
|
+
}
|
|
133
|
+
for i, m in enumerate(manifests)
|
|
134
|
+
]
|
|
135
|
+
cols = []
|
|
136
|
+
with conn:
|
|
137
|
+
for row in conn.execute(
|
|
138
|
+
"""SELECT datasource_id, table_name, column_name, column_type,
|
|
139
|
+
is_primary_key, is_nullable, description, source_field,
|
|
140
|
+
unit, semantic_type
|
|
141
|
+
FROM datasource_columns ORDER BY datasource_id, rowid"""
|
|
142
|
+
):
|
|
143
|
+
cols.append(dict(row))
|
|
144
|
+
scraw = []
|
|
145
|
+
with conn:
|
|
146
|
+
for row in conn.execute(
|
|
147
|
+
"SELECT name, url, columns_json FROM scraw_configs ORDER BY name"
|
|
148
|
+
):
|
|
149
|
+
d = dict(row)
|
|
150
|
+
d["columns_json"] = json.loads(d["columns_json"])
|
|
151
|
+
scraw.append(d)
|
|
152
|
+
return {"sources": sources, "datasource_columns": cols, "scraw_configs": scraw}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build() -> str:
|
|
156
|
+
manifests = _load_manifests()
|
|
157
|
+
REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
conn = sqlite3.connect(DB_PATH)
|
|
159
|
+
conn.row_factory = sqlite3.Row
|
|
160
|
+
try:
|
|
161
|
+
with conn:
|
|
162
|
+
_create_schema(conn)
|
|
163
|
+
for i, m in enumerate(manifests, start=1):
|
|
164
|
+
_write_source(conn, i, m)
|
|
165
|
+
dump = _dump_json(conn, manifests)
|
|
166
|
+
finally:
|
|
167
|
+
conn.close()
|
|
168
|
+
JSON_PATH.write_text(
|
|
169
|
+
json.dumps(dump, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
170
|
+
encoding="utf-8",
|
|
171
|
+
)
|
|
172
|
+
n_cols = sum(len(m.to_columns_json()) for m in manifests)
|
|
173
|
+
return (
|
|
174
|
+
f"built {DB_PATH} + {JSON_PATH}: "
|
|
175
|
+
f"{len(manifests)} sources, {n_cols} columns"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> int:
|
|
180
|
+
print(build())
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""fd-cn-gov CLI: crawl, list, describe, build-registry.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
fd-cn-gov crawl <name> [--max-pages N | --all] # run a scraper
|
|
6
|
+
fd-cn-gov list # list registered sources
|
|
7
|
+
fd-cn-gov describe <name> # show a source + its columns
|
|
8
|
+
fd-cn-gov build-registry # regenerate registry.db/json
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import importlib
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from fd_cn_gov.registry import list_sources, get_source, get_columns
|
|
18
|
+
|
|
19
|
+
SCRIPT_NAMES = [
|
|
20
|
+
"mee_gsgg_archive", "mem_tzgg_archive", "mnr_tzgg_archive",
|
|
21
|
+
"moa_govpublic_archive", "mof_gkml_archive", "mofcom_xwfb_archive",
|
|
22
|
+
"mohurd_xinwen_archive", "mot_shuju_archive", "ndrc_tzgg_archive",
|
|
23
|
+
"pbc_xinwen_archive", "safe_whxw_archive",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cmd_crawl(args) -> int:
|
|
28
|
+
name = args.name
|
|
29
|
+
if name not in SCRIPT_NAMES:
|
|
30
|
+
print(
|
|
31
|
+
f"error: unknown scraper {name!r}\navailable: {' '.join(SCRIPT_NAMES)}",
|
|
32
|
+
file=sys.stderr,
|
|
33
|
+
)
|
|
34
|
+
return 2
|
|
35
|
+
# Forward the page-cap flags to the script's argparse via synthesized argv.
|
|
36
|
+
script_argv = [name]
|
|
37
|
+
if args.all:
|
|
38
|
+
script_argv.append("--all")
|
|
39
|
+
elif args.max_pages is not None:
|
|
40
|
+
script_argv += ["--max-pages", str(args.max_pages)]
|
|
41
|
+
mod = importlib.import_module(f"fd_cn_gov.scripts.{name}")
|
|
42
|
+
old = sys.argv
|
|
43
|
+
sys.argv = script_argv
|
|
44
|
+
try:
|
|
45
|
+
mod.main()
|
|
46
|
+
finally:
|
|
47
|
+
sys.argv = old
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _cmd_list(args) -> int:
|
|
52
|
+
for s in list_sources():
|
|
53
|
+
print(f"{s.name}\t{s.label}\t{s.url}")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _cmd_describe(args) -> int:
|
|
58
|
+
try:
|
|
59
|
+
s = get_source(args.name)
|
|
60
|
+
except KeyError as e:
|
|
61
|
+
print(f"error: {e}", file=sys.stderr)
|
|
62
|
+
return 2
|
|
63
|
+
print(f"name: {s.name}")
|
|
64
|
+
print(f"label: {s.label}")
|
|
65
|
+
print(f"url: {s.url}")
|
|
66
|
+
print(f"category: {s.category}")
|
|
67
|
+
print(f"description: {s.description}")
|
|
68
|
+
print("columns:")
|
|
69
|
+
for c in get_columns(args.name):
|
|
70
|
+
pk = " [PK]" if c.primary_key else ""
|
|
71
|
+
nn = "" if c.nullable else " NOT NULL"
|
|
72
|
+
print(f" - {c.name}: {c.type}{pk}{nn} ({c.semantic_type or '-'})")
|
|
73
|
+
if c.description:
|
|
74
|
+
print(f" {c.description}")
|
|
75
|
+
if c.source_field:
|
|
76
|
+
print(f" source: {c.source_field}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cmd_build_registry(args) -> int:
|
|
81
|
+
from fd_cn_gov.build_registry import build
|
|
82
|
+
print(build())
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
ap = argparse.ArgumentParser(
|
|
88
|
+
prog="fd-cn-gov",
|
|
89
|
+
description="Chinese ministry open-information scrapers + registry.",
|
|
90
|
+
)
|
|
91
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
92
|
+
|
|
93
|
+
p_crawl = sub.add_parser("crawl", help="run a scraper; prints JSON records to stdout")
|
|
94
|
+
p_crawl.add_argument("name", choices=None, help="scraper name (e.g. mof_gkml_archive)")
|
|
95
|
+
pg = p_crawl.add_mutually_exclusive_group()
|
|
96
|
+
pg.add_argument("--max-pages", type=int, help="pages per archive (default: script default, usually 50)")
|
|
97
|
+
pg.add_argument("--all", action="store_true", help="full crawl, no page cap")
|
|
98
|
+
p_crawl.set_defaults(func=_cmd_crawl)
|
|
99
|
+
|
|
100
|
+
p_list = sub.add_parser("list", help="list registered sources")
|
|
101
|
+
p_list.set_defaults(func=_cmd_list)
|
|
102
|
+
|
|
103
|
+
p_desc = sub.add_parser("describe", help="show a source + its column schema")
|
|
104
|
+
p_desc.add_argument("name", help="scraper name")
|
|
105
|
+
p_desc.set_defaults(func=_cmd_describe)
|
|
106
|
+
|
|
107
|
+
p_build = sub.add_parser("build-registry", help="regenerate registry.db + registry.json from MANIFESTs")
|
|
108
|
+
p_build.set_defaults(func=_cmd_build_registry)
|
|
109
|
+
return ap
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
113
|
+
ap = build_parser()
|
|
114
|
+
args = ap.parse_args(argv)
|
|
115
|
+
return args.func(args)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
raise SystemExit(main())
|