一、要解决什么问题
前面已经完成了国家发改委的数据抓取与初步清洗。
这一次要解决的是:
以后如果要抓别的网站,应该怎么照着同样的方法做。
目标不是一下子把所有网站都写完。
目标是把流程固定下来,让以后新增网站时只需要改少数几个地方。
会做这些事:
- 新建第二个来源的目录
- 新建第二个来源的索引文件
- 复制一份已有脚本作为模板
- 明确哪些地方必须改
- 明确哪些地方不要改
- 给出一个通用模板脚本
- 以后新增第三个、第四个网站时都按同样方法操作
先不指定某个具体部门的网站结构。
先把“如何新增一个来源”这件事彻底固定下来。
二、新增一个网站时,目录必须怎么建
假设第二个来源叫:
miit
这里只是举例,表示“工信部”这类第二个来源。
后面你也可以把这个名字换成:
statsgovmofpboc
新增一个来源时,先创建目录:
mkdir -p /srv/policybot/data/miit/index
mkdir -p /srv/policybot/data/miit/raw_html
mkdir -p /srv/policybot/data/miit/text
mkdir -p /srv/policybot/data/miit/meta
mkdir -p /srv/policybot/export/miit
然后创建索引文件:
touch /srv/policybot/data/miit/index/policy_index.csv
touch /srv/policybot/data/miit/index/url_index.txt
检查目录:
tree -L 3 /srv/policybot/data/miit
正常应类似:
/srv/policybot/data/miit
├── index
│ ├── policy_index.csv
│ └── url_index.txt
├── meta
├── raw_html
└── text
这一步的规则以后永远不变:
新增一个来源,就新建一套独立的数据目录。
不要把第二个网站的数据塞到:
/srv/policybot/data/ndrc
里面。
三、新增一个来源时,脚本必须怎么建
最简单的做法不是从头重写,而是:
先复制第一份已经跑通的脚本。
例如复制国家发改委脚本:
cp /srv/policybot/crawler/ndrc.py /srv/policybot/crawler/miit.py
这样第二个来源的脚本就有了。
检查:
ls /srv/policybot/crawler
正常应看到:
filter_ndrc.py
ndrc.py
miit.py
test_api.py
test_list.py
后面要改的是:
miit.py
四、新增一个来源时,哪些地方必须改
打开新脚本:
nano /srv/policybot/crawler/miit.py
新增一个来源时,一般只需要重点改这几类内容:
1. 接口地址
例如原来:
API_URL = "https://fwfx.ndrc.gov.cn/api/query"
如果第二个网站接口不同,就改成它自己的接口。
2. 数据保存目录
原来国家发改委脚本里的目录是:
INDEX_DIR = "/srv/policybot/data/ndrc/index"
HTML_DIR = "/srv/policybot/data/ndrc/raw_html"
TEXT_DIR = "/srv/policybot/data/ndrc/text"
第二个网站必须改成:
INDEX_DIR = "/srv/policybot/data/miit/index"
HTML_DIR = "/srv/policybot/data/miit/raw_html"
TEXT_DIR = "/srv/policybot/data/miit/text"
3. 请求参数
例如国家发改委原来写的是:
params = {
"qt": "",
"tab": "all",
"page": page,
"pageSize": 20,
"siteCode": "bm04000fgk",
"sort": "dateDesc"
}
不同网站的参数名一般不一样。
这一部分必须根据第二个网站真实接口来改。
4. 返回字段路径
国家发改委原来是:
return data.get("data", {}).get("resultList", [])
别的网站可能是:
data["rows"]data["list"]data["items"]data["result"]
所以这一行必须按第二个网站的真实返回结构改。
5. 字段名
原来取字段的代码是:
title = str(item.get("title", "")).strip()
url = str(item.get("URL") or item.get("url") or "").strip()
date = str(item.get("docDate", "")).strip()
别的网站不一定叫这些名字。
可能会是:
namepublishTimereleaseDatelinkdetailUrl
所以这里也要按实际返回结构改。
五、新增一个来源时,哪些地方不要改
这一步很关键。
不是所有东西都要改。
以下这些逻辑一般可以直接复用:
1. URL 去重逻辑
def load_index():
...
def save_url(url):
...
这套逻辑对所有来源都适用。
2. CSV 索引逻辑
def save_csv(title, date, url, html_file, txt_file):
...
这套逻辑也可以通用。
3. html 保存逻辑
with open(html_path, "w", encoding="utf-8") as f:
f.write(r.text)
只要详情页仍然是网页,这部分一般不用改。
4. txt 提取逻辑
soup = BeautifulSoup(r.text, "lxml")
text = soup.get_text("\n", strip=True)
这部分也通常可以先保持不变。
5. 年月归档逻辑
year = date[:4]
month = date[5:7]
...
如果第二个网站的日期格式还是:
YYYY-MM-DD
这一部分也可以直接复用。
六、给第二个网站准备一个通用模板脚本
下面给出一个“第二个来源通用模板”。
以后新增来源时,就复制这一份,再改变量。
cat > /srv/policybot/crawler/template_site.py <<'PY'
import requests
from bs4 import BeautifulSoup
import os
import hashlib
import csv
import time
API_URL = "替换成第二个网站的接口地址"
INDEX_DIR = "/srv/policybot/data/站点名/index"
HTML_DIR = "/srv/policybot/data/站点名/raw_html"
TEXT_DIR = "/srv/policybot/data/站点名/text"
URL_INDEX_FILE = os.path.join(INDEX_DIR, "url_index.txt")
CSV_INDEX_FILE = os.path.join(INDEX_DIR, "policy_index.csv")
os.makedirs(INDEX_DIR, exist_ok=True)
os.makedirs(HTML_DIR, exist_ok=True)
os.makedirs(TEXT_DIR, exist_ok=True)
def load_index():
if not os.path.exists(URL_INDEX_FILE):
return set()
with open(URL_INDEX_FILE, "r", encoding="utf-8") as f:
return set(line.strip() for line in f if line.strip())
def save_url(url):
with open(URL_INDEX_FILE, "a", encoding="utf-8") as f:
f.write(url + "\n")
def save_csv(title, date, url, html_file, txt_file):
with open(CSV_INDEX_FILE, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([title, date, url, html_file, txt_file])
def get_page(page):
params = {
"page": page
}
r = requests.get(API_URL, params=params, timeout=30)
r.raise_for_status()
data = r.json()
return data.get("data", {}).get("resultList", [])
def fetch_page(title, url, date):
r = requests.get(url, timeout=30)
r.raise_for_status()
r.encoding = "utf-8"
file_hash = hashlib.md5(url.encode()).hexdigest()[:10]
year = date[:4]
month = date[5:7]
html_dir = os.path.join(HTML_DIR, year, month)
txt_dir = os.path.join(TEXT_DIR, year, month)
os.makedirs(html_dir, exist_ok=True)
os.makedirs(txt_dir, exist_ok=True)
html_file = f"{date}_{file_hash}.html"
txt_file = f"{date}_{file_hash}.txt"
html_path = os.path.join(html_dir, html_file)
txt_path = os.path.join(txt_dir, txt_file)
with open(html_path, "w", encoding="utf-8") as f:
f.write(r.text)
soup = BeautifulSoup(r.text, "lxml")
text = soup.get_text("\n", strip=True)
with open(txt_path, "w", encoding="utf-8") as f:
f.write(text)
save_url(url)
save_csv(title, date, url, html_file, txt_file)
print("saved:", title)
def main():
visited = load_index()
for page in range(1, 50):
print("page:", page)
items = get_page(page)
if not items:
print("页面为空,停止")
break
for item in items:
title = str(item.get("title", "")).strip()
url = str(item.get("URL") or item.get("url") or "").strip()
date = str(item.get("docDate", "")).strip()
if not url:
continue
if url in visited:
print("skip:", title)
continue
try:
fetch_page(title, url, date)
except Exception as e:
print("error:", title, url, e)
time.sleep(2)
if __name__ == "__main__":
main()
PY
这个模板不要直接运行。
它的作用是:
- 提醒自己哪些地方要替换
- 下次新增来源时不用从零写
七、如何先测试第二个网站,不要一上来就写正式脚本
无论新增哪个网站,都先做两步:
第一步:测试接口
新建一个测试脚本:
cat > /srv/policybot/crawler/test_site_api.py <<'PY'
import requests
import json
API_URL = "替换成第二个网站接口"
params = {
"page": 1
}
r = requests.get(API_URL, params=params, timeout=30)
print("状态码:", r.status_code)
print("前500字符:")
print(r.text[:500])
try:
data = r.json()
print("\n最外层 keys:", list(data.keys()))
except Exception as e:
print("JSON 解析失败:", e)
PY
运行:
cd /srv/policybot/crawler
python test_site_api.py
第二步:测试前几条数据
新建第二个测试脚本:
cat > /srv/policybot/crawler/test_site_list.py <<'PY'
import requests
API_URL = "替换成第二个网站接口"
params = {
"page": 1
}
r = requests.get(API_URL, params=params, timeout=30)
data = r.json()
items = data.get("data", {}).get("resultList", [])
print("拿到条目数:", len(items))
for i, item in enumerate(items[:5], start=1):
print("-" * 40)
print(item)
PY
运行:
python test_site_list.py
先确认返回结构,再改正式脚本。
不要先上正式版。
八、给第二个网站准备过滤脚本时怎么做
如果第二个网站抓下来的内容里也有:
- 解读
- 记者问
- 专家文章
那就同样复制一份过滤脚本。
例如:
cp /srv/policybot/crawler/filter_ndrc.py /srv/policybot/crawler/filter_miit.py
然后把里面的路径从:
INPUT_FILE = "/srv/policybot/data/ndrc/index/policy_index.csv"
OUTPUT_ALL = "/srv/policybot/export/ndrc/policy_with_flags.csv"
改成:
INPUT_FILE = "/srv/policybot/data/miit/index/policy_index.csv"
OUTPUT_ALL = "/srv/policybot/export/miit/policy_with_flags.csv"
其它过滤逻辑先不变。
九、以后每新增一个来源时的固定流程
把流程固定下来:
第 1 步:新建目录
mkdir -p /srv/policybot/data/来源名/index
mkdir -p /srv/policybot/data/来源名/raw_html
mkdir -p /srv/policybot/data/来源名/text
mkdir -p /srv/policybot/data/来源名/meta
mkdir -p /srv/policybot/export/来源名
touch /srv/policybot/data/来源名/index/policy_index.csv
touch /srv/policybot/data/来源名/index/url_index.txt
第 2 步:复制脚本模板
cp /srv/policybot/crawler/ndrc.py /srv/policybot/crawler/来源名.py
第 3 步:改 5 个地方
- 接口地址
- 数据保存目录
- 请求参数
- 返回字段路径
- 字段名
第 4 步:先测接口
python test_site_api.py
python test_site_list.py
第 5 步:再运行正式脚本
python 来源名.py
第 6 步:检查文件数量
find /srv/policybot/data/来源名/raw_html -type f | wc -l
find /srv/policybot/data/来源名/text -type f | wc -l
wc -l /srv/policybot/data/来源名/index/policy_index.csv
第 7 步:再做清洗脚本
如果需要区分正式文件和解读,再复制一份过滤脚本。
十、完成后项目会变成什么样
完成后,目录会逐渐变成:
/srv/policybot
├── crawler
│ ├── ndrc.py
│ ├── filter_ndrc.py
│ ├── miit.py
│ ├── filter_miit.py
│ └── template_site.py
├── data
│ ├── ndrc
│ │ ├── index
│ │ ├── meta
│ │ ├── raw_html
│ │ └── text
│ └── miit
│ ├── index
│ ├── meta
│ ├── raw_html
│ └── text
├── export
│ ├── ndrc
│ └── miit
└── logs
此时每个来源都有:
- 独立脚本
- 独立索引
- 独立原始数据目录
- 独立导出目录
这才是后面能长期维护的结构。
十一、结束后的状态
完成后,你已经具备:
- 增加新来源的固定方法
- 目录结构固定方法
- 复制脚本模板的方法
- 接口测试的方法
- 正式抓取的方法
- 清洗脚本复用的方法
从现在开始,后面新增任何部门、研究院、政策网站,逻辑都完全一致。
你只需要改:
- 接口
- 参数
- 返回字段
- 数据目录名
其余部分都复用。