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

这篇只做一件事:

从 0 开始,在 VPS 上把来源文件(例如国家发改委公开文件)抓通。

让完全不熟的人也能照着复制、粘贴、执行。

做完后,会得到这些内容:

  • 一个 Python 虚拟环境
  • 一个爬虫脚本目录
  • 一个数据目录
  • 一个日志目录
  • 一个可以抓国家发改委数据的脚本
  • 抓下来的 html 文件
  • 抓下来的 txt 文件
  • 一个索引文件
  • 一个 URL 去重文件

最终目录会类似这样:

/srv/policybot
├── crawler
│   └── ndrc.py
├── data
│   └── ndrc
│       ├── index
│       │   ├── policy_index.csv
│       │   └── url_index.txt
│       ├── meta
│       ├── raw_html
│       └── text
├── export
└── logs

一、创建项目总目录

执行:

mkdir -p /srv/policybot
cd /srv/policybot

检查当前位置:

pwd

正常输出:

/srv/policybot

二、检查 Python 环境

执行:

which python3
python3 --version

正常输出:

/usr/bin/python3
Python 3.11.x

如果系统提示 command not found,先安装 Python:

apt update
apt install python3 python3-venv python3-pip -y

安装完成后再次执行:

which python3
python3 --version

确认能够看到 Python 路径和版本号。

三、创建 Python 虚拟环境

执行:

python3 -m venv .venv

查看目录:

ls -a

正常输出应包含:

.
..
.venv

说明虚拟环境已经创建完成。


四、进入虚拟环境

执行:

source .venv/bin/activate

成功后,命令行前面会出现:

(.venv)

例如:

(.venv) root@server:/srv/policybot#

如果没有出现 (.venv),说明没有成功进入虚拟环境。

重新执行:

source /srv/policybot/.venv/bin/activate

五、安装依赖

进入虚拟环境后执行:

pip install requests beautifulsoup4 lxml

安装完成后,测试依赖是否正常:

python -c "import requests; from bs4 import BeautifulSoup; import lxml; print('ok')"

正常输出:

ok

说明 Python 环境已经可以正常运行爬虫脚本。

六、创建项目目录结构

执行:

mkdir -p /srv/policybot/crawler
mkdir -p /srv/policybot/data/ndrc
mkdir -p /srv/policybot/data/ndrc/index
mkdir -p /srv/policybot/data/ndrc/raw_html
mkdir -p /srv/policybot/data/ndrc/text
mkdir -p /srv/policybot/data/ndrc/meta
mkdir -p /srv/policybot/export
mkdir -p /srv/policybot/logs

查看目录结构:

tree -L 3 /srv/policybot

如果系统没有 tree 命令,先安装:

apt install tree -y

正常目录结构应类似:

/srv/policybot
├── crawler
├── data
│   └── ndrc
│       ├── index
│       ├── meta
│       ├── raw_html
│       └── text
├── export
└── logs

七、创建索引文件

执行:

touch /srv/policybot/data/ndrc/index/policy_index.csv
touch /srv/policybot/data/ndrc/index/url_index.txt

查看文件:

ls /srv/policybot/data/ndrc/index

正常输出:

policy_index.csv
url_index.txt

八、创建接口测试脚本

执行:

cat > /srv/policybot/crawler/test_api.py <<'PY'
import requests
import json

API_URL = "https://fwfx.ndrc.gov.cn/api/query"

params = {
    "qt": "",
    "tab": "all",
    "page": 1,
    "pageSize": 20,
    "siteCode": "bm04000fgk",
    "sort": "dateDesc"
}

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()))
    if "data" in data and isinstance(data["data"], dict):
        print("data 层 keys:", list(data["data"].keys()))
except Exception as e:
    print("JSON 解析失败:", e)
PY

运行脚本:

cd /srv/policybot/crawler
python test_api.py

正常输出示例:

状态码: 200
前500字符:
{"code":200,"data":{...}}

最外层 keys: ['code', 'data', 'ok']
data 层 keys: ['totalHits', 'resultList']

说明接口访问成功。

九、创建列表测试脚本

执行:

cat > /srv/policybot/crawler/test_list.py <<'PY'
import requests

API_URL = "https://fwfx.ndrc.gov.cn/api/query"

params = {
    "qt": "",
    "tab": "all",
    "page": 1,
    "pageSize": 20,
    "siteCode": "bm04000fgk",
    "sort": "dateDesc"
}

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):
    title = str(item.get("title", "")).strip()
    url = str(item.get("URL") or item.get("url") or "").strip()
    doc_date = str(item.get("docDate", "")).strip()

    print("-" * 40)
    print("序号:", i)
    print("标题:", title)
    print("日期:", doc_date)
    print("链接:", url)
PY

运行脚本:

python test_list.py

正常输出示例:

拿到条目数: 20
----------------------------------------
序号: 1
标题: 关于加强投资项目在线审批监管平台和工程建设项目审批管理系统数据共享的通知
日期: 2026-02-13
链接: https://www.ndrc.gov.cn/...

说明接口数据结构正确,并且能够获取政策标题、日期和详情页链接。


十、创建正式爬虫脚本

执行:

cat > /srv/policybot/crawler/ndrc.py <<'PY'
import requests
from bs4 import BeautifulSoup
import os
import hashlib
import csv
import time

API_URL = "https://fwfx.ndrc.gov.cn/api/query"

INDEX_DIR = "/srv/policybot/data/ndrc/index"
HTML_DIR = "/srv/policybot/data/ndrc/raw_html"
TEXT_DIR = "/srv/policybot/data/ndrc/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 = {
        "qt": "",
        "tab": "all",
        "page": page,
        "pageSize": 20,
        "siteCode": "bm04000fgk",
        "sort": "dateDesc"
    }

    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]

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

十一、运行爬虫

执行:

cd /srv/policybot/crawler
python ndrc.py

运行时会看到类似输出:

page: 1
saved: 关于加强投资项目在线审批监管平台和工程建设项目审批管理系统数据共享的通知
saved: 关于推进能源领域数字化发展的通知
page: 2
saved: ...

说明爬虫正在抓取政策页面并保存数据。


十二、检查抓取结果

查看 html 文件数量:

find /srv/policybot/data/ndrc/raw_html -type f -name "*.html" | wc -l

查看 txt 文件数量:

find /srv/policybot/data/ndrc/text -type f -name "*.txt" | wc -l

查看索引记录数量:

wc -l /srv/policybot/data/ndrc/index/policy_index.csv
wc -l /srv/policybot/data/ndrc/index/url_index.txt

如果这些数字接近,说明数据保存正常。

十三、查看抓取的文件

查看最近生成的 html 文件:

ls -lt /srv/policybot/data/ndrc/raw_html | head

查看最近生成的 txt 文件:

ls -lt /srv/policybot/data/ndrc/text | head

如果看到大量类似文件名,说明页面已经成功保存。

2026-02-13_9dc84789f0.html
2026-02-13_9dc84789f0.txt

每一条政策会生成两个文件:

  • 一个 .html 原始网页
  • 一个 .txt 提取后的文本

十四、查看索引文件

查看索引文件内容:

head /srv/policybot/data/ndrc/index/policy_index.csv

示例输出:

关于加强投资项目在线审批监管平台和工程建设项目审批管理系统数据共享的通知,2026-02-13,https://www.ndrc.gov.cn/...,2026-02-13_xxxxx.html,2026-02-13_xxxxx.txt

字段顺序为:

标题,日期,原始链接,html文件名,txt文件名

十五、查看 URL 去重文件

查看已经抓过的 URL:

head /srv/policybot/data/ndrc/index/url_index.txt

示例:

https://www.ndrc.gov.cn/xxxx
https://www.ndrc.gov.cn/xxxx
https://www.ndrc.gov.cn/xxxx

这个文件用于 防止重复抓取

脚本再次运行时,如果 URL 已经存在,就会跳过。


十六、再次运行爬虫

再次执行:

cd /srv/policybot/crawler
python ndrc.py

运行时可能看到:

skip: 关于加强投资项目在线审批监管平台和工程建设项目审批管理系统数据共享的通知

这表示:

该页面之前已经抓过,因此被跳过。


十七、检查是否存在空文件

执行:

find /srv/policybot/data -type f -size 0

如果没有任何输出,说明没有空文件。

如果出现文件路径,说明某次抓取失败,需要检查脚本。


十八、统计总抓取数量

统计 html 文件数量:

find /srv/policybot/data/ndrc/raw_html -type f -name "*.html" | wc -l

统计 txt 文件数量:

find /srv/policybot/data/ndrc/text -type f -name "*.txt" | wc -l

统计索引数量:

wc -l /srv/policybot/data/ndrc/index/policy_index.csv

正常情况下,这三个数字应该接近。

例如:

556
556
556

十九、查找某条政策是否存在

例如查关键词:

grep "矿井水保护和利用" /srv/policybot/data/ndrc/index/policy_index.csv

如果出现多条记录,说明该政策相关页面已经抓取。


二十、第一章完成状态

完成第一章后,服务器上应存在:

/srv/policybot
├── crawler
│   ├── test_api.py
│   ├── test_list.py
│   └── ndrc.py
├── data
│   └── ndrc
│       ├── index
│       │   ├── policy_index.csv
│       │   └── url_index.txt
│       ├── raw_html
│       ├── text
│       └── meta
├── export
└── logs

并且:

  • raw_html 目录中存在 .html
  • text 目录中存在 .txt
  • policy_index.csv 已记录数据
  • url_index.txt 已记录 URL

说明第一个政策来源已经成功抓通。