VPS 搭建文件抓取系统(4)

一、要解决什么问题

前面已经完成了国家发改委的数据抓取与初步清洗。

这一次要解决的是:

以后如果要抓别的网站,应该怎么照着同样的方法做。

目标不是一下子把所有网站都写完。
目标是把流程固定下来,让以后新增网站时只需要改少数几个地方。

会做这些事:

  1. 新建第二个来源的目录
  2. 新建第二个来源的索引文件
  3. 复制一份已有脚本作为模板
  4. 明确哪些地方必须改
  5. 明确哪些地方不要改
  6. 给出一个通用模板脚本
  7. 以后新增第三个、第四个网站时都按同样方法操作

先不指定某个具体部门的网站结构。
先把“如何新增一个来源”这件事彻底固定下来。


二、新增一个网站时,目录必须怎么建

假设第二个来源叫:

miit

这里只是举例,表示“工信部”这类第二个来源。
后面你也可以把这个名字换成:

  • stats
  • gov
  • mof
  • pboc

新增一个来源时,先创建目录:

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()

别的网站不一定叫这些名字。
可能会是:

  • name
  • publishTime
  • releaseDate
  • link
  • detailUrl

所以这里也要按实际返回结构改。


五、新增一个来源时,哪些地方不要改

这一步很关键。
不是所有东西都要改。

以下这些逻辑一般可以直接复用:

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 个地方

  1. 接口地址
  2. 数据保存目录
  3. 请求参数
  4. 返回字段路径
  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

此时每个来源都有:

  • 独立脚本
  • 独立索引
  • 独立原始数据目录
  • 独立导出目录

这才是后面能长期维护的结构。


十一、结束后的状态

完成后,你已经具备:

  1. 增加新来源的固定方法
  2. 目录结构固定方法
  3. 复制脚本模板的方法
  4. 接口测试的方法
  5. 正式抓取的方法
  6. 清洗脚本复用的方法

从现在开始,后面新增任何部门、研究院、政策网站,逻辑都完全一致。

你只需要改:

  • 接口
  • 参数
  • 返回字段
  • 数据目录名

其余部分都复用。