抓取页面只是工作的一半。你提取的数据必须存放在某个持久的地方,供团队成员和分析工具访问,并且不会因笔记本电脑崩溃而丢失。本地 CSV 文件用于一次性任务尚可,但一旦抓取变成定期流程,单块硬盘就成了隐患:容量会耗尽,机器间传输繁琐,一次磁盘故障就可能让你损失无法挽回的数据。

本指南将向你展示如何用 Python 端到端地将抓取数据存储到云端。你将构建一个小型的可运行流程:通过 Crawling API 抓取页面,将结果整理为干净的记录,然后写入持久的云端目标, 使用 S3 风格存储桶的对象存储,以及托管关系型数据库。所有示例均使用中性示例 URL 和环境变量占位符作为凭据,方便你在不改变代码结构的情况下适配自己的目标和云服务商。

你将构建什么

一个 Python 脚本,通过 Crawling API 从公开示例列表页抓取少量数据集,将每行数据规范化为带类型的记录,然后以两种方式将这些记录发送到云端。你可以只保留其中一条路径,也可以两条都保留。各组成部分如下:

  • 抓取:通过 Crawling API 获取已渲染的页面,返回完整 HTML。
  • 转换:将原始 HTML 转换为字段名和类型一致的结构化记录列表。
  • 对象存储:将 JSON Lines 文件上传到 S3 风格的存储桶,用于廉价持久的归档。
  • 托管数据库:将相同的记录插入 Postgres 表,以便查询和关联。
  • Crawlbase Cloud Storage:一个可选的单参数路径,在服务端保存原始抓取响应。

为何将抓取数据存储在云端

本地存储在未出问题之前很方便。当抓取规模从数百行增长为定期向仪表盘供数据的常规任务时,三个问题会同时出现。容量成为持续成本:你需要购买磁盘来保证备份安全,还要花时间管理它们。访问变得尴尬:锁在一台机器上的数据很难与团队共享,或馈送给在其他地方运行的工具。持久性也很脆弱:电源问题、固件损坏和简单的人为失误都可能让一块磁盘瘫痪,连同所有未经备份的数据一起消失。

云存储解决了这三个问题。对象存储和托管数据库天生具备冗余能力,数据跨多个地点复制,而不是停在一块硬盘上。它们无需你自己配置硬件即可扩展,只要有凭据就可从任何地方访问,还将备份和持久性职责移交给了云服务商。对于抓取流程而言,这意味着你可以从数据落地的那一刻起就将其视为持久资产,而不是需要在本地磁盘上费心照料的东西。

两种目标,以及各自的适用场景

本教程写入两种云存储,因为抓取数据通常都需要这两种。对象存储(S3 兼容存储桶)是原始和归档数据的理想归宿:按 GB 收费低廉,对文件格式无要求,非常适合保存未经处理的抓取输出,以便日后重新处理。托管关系型数据库(此处为 Postgres)是结构化可查询副本的理想归宿,一致的列和类型让你可以用 SQL 进行过滤、聚合和关联。常见模式是同时写入两者,下面的代码正是这样做的。更深入的对比请参阅 云存储与本地存储 以及 云存储的优势

前提条件

在写代码之前,需要先准备好几样东西。都不复杂。

Python 3.8 或更高版本。python --version 确认你的版本。如果没有,请从 python.org 或 Anaconda 等发行版安装,并确保 Python 在 PATH 中。

Crawlbase 账户和 token。注册后打开仪表盘,复制你的 token。Crawlbase 最多提供 20,000 次免费请求作为起点,足够完成本指南的学习。请像对待密码一样保护 token,不要将其纳入版本控制。如果目标页面的内容在客户端渲染,请使用 JavaScript token;对于静态页面,普通 token 即可。

云凭据。对象存储路径需要 S3 风格的存储桶和一对访问密钥。数据库路径需要托管 Postgres 实例的连接字符串。两者均通过以下代码中的环境变量提供,绝不硬编码。

熟悉 Python 和基本抓取知识。如果你对解析部分还不熟悉,BeautifulSoup 指南用 Python 抓取网站 的教程是很好的参考。

搭建项目

创建虚拟环境以隔离依赖,然后安装流程所需的库。

bash
python --version

python -m venv cloud_env
source cloud_env/bin/activate

pip install crawlbase beautifulsoup4 boto3 psycopg2-binary

在 Windows 上,用 cloud_env\Scripts\activate 替换 source 那行来激活环境。四个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 用于解析返回的 HTML,boto3 负责与 S3 风格对象存储通信,psycopg2-binary 连接 Postgres。json 模块随标准库附带,因此归档格式不需要额外安装。

第 1 步:通过 Crawling API 抓取页面

首先获取已渲染的页面。导入 CrawlingAPI 类,用你的 token 初始化它,然后请求目标 URL。在解析前检查 Crawlbase 的 cb_status(legacy pc_status) 可以让失败变得显而易见,而不是悄然无声。我们在这里使用中性示例列表页;在适配流程时请替换为你自己的 URL。

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def crawl(page_url):
    response = api.get(page_url)
    if response["headers"]["cb_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

if __name__ == "__main__":
    page_url = "https://example.com/products"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

python cloud_pipeline.py 运行此脚本,你应该会看到打印出真实的页面标记,在编写任何选择器之前先确认抓取正常工作。如果目标在客户端填充内容,请用你的 JavaScript token 初始化客户端,并向 api.get 传入 {"ajax_wait": "true", "page_wait": 5000},让 API 先渲染页面。对于 JS 重度目标,用 Python 抓取 JavaScript 页面指南涵盖了相关细节。

Crawlbase Crawling API

上面那个 api.get 调用所做的事远不止一个普通请求。Crawling API 在你传入 JavaScript token 时会渲染页面,在服务端轮换住宅 IP,并处理 CAPTCHA,让你无需自己运行无头浏览器集群或代理池就能获得完整的 HTML。先在免费套餐对公开页面测试,然后用同样的代码扩展规模。

第 2 步:将 HTML 转换为结构化记录

原始 HTML 不适合直接存入数据库。转换步骤将其变为字段名和类型一致的字典列表,使每条记录具有相同的结构。将 HTML 加载到 BeautifulSoup,遍历页面上的每个条目,提取你需要的字段。这里的选择器仅供示意;请替换为与你目标匹配的选择器。

python
from bs4 import BeautifulSoup

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def to_price(raw):
    if not raw:
        return None
    digits = raw.replace("$", "").replace(",", "").strip()
    return float(digits) if digits.replace(".", "").isdigit() else None

def transform(html):
    soup = BeautifulSoup(html, "html.parser")
    records = []
    for card in soup.select("div.product"):
        records.append({
            "name": text_of(card, "h2.title"),
            "price": to_price(text_of(card, "span.price")),
            "sku": text_of(card, "span.sku"),
            "in_stock": text_of(card, "span.stock") == "In stock",
        })
    return [r for r in records if r["name"]]

三个小辅助函数让记录保持整洁。text_of 返回元素的去空白文本,若元素缺失则返回 None,这样某张卡片的空缺不会导致循环崩溃。to_price 去掉货币符号和千位分隔符并转换为浮点数,使数据库列可以是数字类型而非文本。最后的过滤器丢弃没有名称的行,这些行通常是布局残留而非真实条目。结果是一个带类型的记录列表,可以直接存储。关于如何更好地组织抓取数据,请参阅 结构化和清洗网络抓取数据

第 3 步:上传到 S3 风格对象存储

第一个云端目标是对象存储。对象存储是原始或归档数据的天然归宿:廉价、持久,且对存入内容的格式无要求。我们将记录写成 JSON Lines 格式(每行一个 JSON 对象),易于追加,也便于后续流式读取。凭据来自环境变量,以确保源代码中不含任何敏感信息。

python
import os
import json
import boto3

def upload_to_s3(records, key):
    s3 = boto3.client(
        "s3",
        endpoint_url=os.environ.get("S3_ENDPOINT_URL"),
        aws_access_key_id=os.environ["S3_ACCESS_KEY"],
        aws_secret_access_key=os.environ["S3_SECRET_KEY"],
    )
    body = "\n".join(json.dumps(r) for r in records)
    s3.put_object(
        Bucket=os.environ["S3_BUCKET"],
        Key=key,
        Body=body.encode("utf-8"),
        ContentType="application/x-ndjson",
    )
    print(f"Uploaded {len(records)} records to s3://{os.environ['S3_BUCKET']}/{key}")

endpoint_url 参数使本方案不局限于 AWS:将其留空以使用 AWS S3,或将其指向任何 S3 兼容的服务商(例如自托管的 MinIO 实例或其他云的对象存储)。运行前设置好四个环境变量,例如 export S3_BUCKET=my-scrape-archive 以及对应的密钥。Key 是存储桶中的对象路径;像 scrapes/2026-06-11/products.jsonl 这样带日期戳的键可以让每次运行的归档相互独立,便于查找。

不要把凭据写进代码

永远不要在提交的脚本中硬编码访问密钥或连接字符串。如本文代码所示,从环境变量或密钥管理器中读取它们。一旦密钥进入版本控制,你就必须轮换它。

第 4 步:插入托管数据库

第二个目标是托管 Postgres 数据库,这里存放用于查询的结构化副本。下面的函数从单个环境变量打开连接,确保目标表存在,然后插入记录。使用参数化查询(%s 占位符)可以正确转义值,而不是将其拼接进 SQL。

python
import os
import psycopg2
from psycopg2.extras import execute_values

CREATE = """
CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price NUMERIC,
    sku TEXT,
    in_stock BOOLEAN,
    scraped_at TIMESTAMPTZ DEFAULT now()
)
"""

def save_to_db(records):
    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    with conn, conn.cursor() as cur:
        cur.execute(CREATE)
        rows = [(r["name"], r["price"], r["sku"], r["in_stock"]) for r in records]
        execute_values(
            cur,
            "INSERT INTO products (name, price, sku, in_stock) VALUES %s",
            rows,
        )
    conn.close()
    print(f"Inserted {len(records)} rows into products")

DATABASE_URL 设置为托管 Postgres 的连接字符串,例如 postgresql://user:pass@host:5432/dbname,并将其保存在环境中而非文件里。CREATE TABLE IF NOT EXISTS 使函数可以安全地重复运行,scraped_at 列为每次加载打上时间戳以便追踪变更,而 execute_values 将插入操作批量合并为一次往返,而不是每行一次查询。数据入库后,你可以用纯 SQL 进行过滤和聚合,再将其拉入 pandas 进行分析

第 5 步:组装完整流程

现在将各步骤串联成一个可运行的脚本:抓取、转换,然后将记录发送到两个目标。保留适合你工作流的存储调用;此处两者均已展示。

python
import os
import json
from datetime import date
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

# crawl, transform, upload_to_s3 and save_to_db are defined above

def main():
    page_url = "https://example.com/products"
    html = crawl(page_url)
    if not html:
        print("Nothing scraped, stopping.")
        return

    records = transform(html)
    print(f"Parsed {len(records)} records")
    if not records:
        return

    key = f"scrapes/{date.today().isoformat()}/products.jsonl"
    upload_to_s3(records, key)
    save_to_db(records)

if __name__ == "__main__":
    main()

流程线性清晰,易于推理:获取页面,若抓取失败则提前退出,将 HTML 转换为记录,若无内容可存则再次退出,然后将记录归档到存储桶并加载到数据库。日期戳对象键让每次运行的归档相互独立,而数据库则通过时间戳累积每次加载。设置好环境变量后用 python cloud_pipeline.py 运行。

输出看起来是什么样的

对象存储路径写入一个 JSON Lines 文件,每条记录占一行,这就是落入存储桶的内容:

json
{"name": "Aluminium Tripod", "price": 129.99, "sku": "TRP-014", "in_stock": true}
{"name": "USB-C Hub", "price": 39.5, "sku": "HUB-203", "in_stock": false}
{"name": "Wireless Mouse", "price": 24.0, "sku": "MSE-088", "in_stock": true}

数据库路径将相同的记录存储为带类型的列,因此一个简单的查询就能确认加载结果并展示可供分析的结构:

sql
SELECT name, price, in_stock FROM products WHERE in_stock = true ORDER BY price;

--      name       | price  | in_stock
-- ----------------+--------+----------
--  Wireless Mouse |  24.00 | t
--  Aluminium Tripod| 129.99 | t

两份副本就位后,你拥有了廉价持久的原始记录归档和一个可查询的结构化表,由同一次运行写入。

一个单参数快捷方式:Crawlbase Cloud Storage

如果你的目标只是在服务端保留每次抓取响应的副本,而不必先搭建自己的存储桶或数据库,Crawlbase Cloud Storage 提供了一个单参数路径。在 Crawling API 请求中添加 &store=true,响应副本就会自动保存到云端,你可以通过 API 或仪表盘对其进行搜索、检索或删除。

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

response = api.get("https://example.com/products", {"store": "true"})
# the response also includes a storage RID you can use to fetch it later
print(response["headers"].get("storage_url"))

每个保存的请求都有一个唯一标识符(RID),可用于查看或删除它。这是保留原始响应最快捷的方式,与 异步 Crawler 搭配使用效果尤佳,尤其是在发出大量请求并希望服务端处理存储的场景下。对于较大的结构化数据集,你仍然需要自己的数据库,但就原始响应归档而言,store 参数在简洁性上难以超越。

扩展流程

上面的流程只抓取一个页面。将其变成定期任务,主要关乎节奏和健壮性。几个习惯可以让更大规模的运行保持健康:

  • 批量写入。积累记录后批量上传或插入,而不是逐行操作。execute_values 已经批量处理数据库插入;对象上传也采用同样的方式,每次运行写一个文件而非每条记录一个文件。
  • 为键打日期戳。使用像 scrapes/2026-06-11/products.jsonl 这样带日期的对象键,使每次运行相互隔离,不会覆盖历史记录。数据库的 scraped_at 列在查询端起到同样的作用。
  • 按计划运行。将脚本封装在 cron 任务或计划任务中,保持云端副本持续更新。由于表使用了 IF NOT EXISTS,且存储桶键带有日期,重复运行是安全的。
  • 规模扩大时卸载抓取任务。对于大量页面,异步 Crawler 可将请求排队并通过 webhook 传递结果,适合高吞吐量场景,无需保持长连接。

负责任地抓取

请负责任地收集数据。只抓取公开数据,遵守每个网站的服务条款及其 robots.txt,保持合理的请求频率,不要给你所依赖的服务器造成压力。当你收集的数据涉及可识别个人身份的信息时,GDPR 和 CCPA 等隐私法律适用,因此除非你有合法依据和明确目的,否则应避免收集个人数据。将数据存储在云端不会改变上述要求:收集时的谨慎态度同样延伸到你保留数据的方式,而将个人数据保留超过必要时限只会增加风险。

回顾

核心要点

  • 存储是流程的一部分。从数据落地的那一刻起就将其视为持久资产;本地 CSV 用于一次性任务尚可,但不适合定期任务。
  • 存储前先转换。将原始 HTML 规范化为带类型、字段名一致的记录,这样数据库列可以是数字类型,归档也保持一致。
  • 为任务选择合适的存储。对象存储(S3 风格存储桶)对原始或归档数据廉价且持久;托管数据库用于结构化可查询的副本;同时写入两者是常见模式。
  • 不要把凭据写进代码。从环境变量或密钥管理器读取访问密钥和连接字符串,永远不要硬编码在提交的脚本中。
  • store 参数是快捷方式。添加 &store=true 可将原始抓取响应保存到 Crawlbase Cloud Storage,这是无需先搭建自己基础设施就保留响应的最快方式。

常见问题

抓取数据应该存储在对象存储还是数据库中?

取决于你如何使用它。对象存储(S3 兼容存储桶)廉价持久,适合任何形状的原始或归档数据,是保存未处理抓取输出的理想归宿。托管关系型数据库适合你需要用 SQL 查询、过滤和关联的结构化副本。许多流程同时写入两者:将原始记录归档到存储桶,将清洗后的记录加载到数据库。

如何让云凭据不出现在代码中?

从环境变量或密钥管理器中读取,而不是硬编码。本指南中的代码从 os.environ 读取 S3 密钥和 Postgres 连接字符串,因此提交的文件中不含任何敏感信息。进入版本控制的密钥就是你必须轮换的密钥,所以请将其保存在环境中。

Crawlbase Cloud Storage 与上传到自己的存储桶有何区别?

Crawlbase Cloud Storage 是单参数路径:在 Crawling API 请求中添加 &store=true,原始响应就会在服务端保存,通过 RID 可检索,无需自己搭建任何基础设施。上传到自己的存储桶或数据库则让你完全掌控格式、模式、保留策略和存储位置,这对结构化数据集而言是必要的。两者互补:store 参数用于快速原始响应归档,自有存储用于经过处理的数据。

S3 代码可以与 AWS 以外的服务商一起使用吗?

可以。boto3 客户端有一个 endpoint_url 参数;将其留空以使用 AWS S3,或将其指向任何 S3 兼容的服务商,例如自托管的 MinIO 实例或其他云的对象存储。其余代码保持不变,这也是示例从环境变量读取 endpoint 的原因。

如何定期运行这个脚本以保持云端副本的最新状态?

将脚本封装在 cron 任务或计划任务中,以与数据变化频率匹配的节奏运行。流程可以安全重复:数据库表使用 CREATE TABLE IF NOT EXISTS,对象键带有日期戳所以运行之间不会相互覆盖,每条数据库行都带有 scraped_at 时间戳以便追踪变更。对于大量页面,将抓取任务交给异步 Crawler,这样任务就不会因单个连接而形成瓶颈。

将抓取的个人数据存储在云端安全吗?

首先将其视为法律和隐私问题。除非你有合法依据和明确目的,否则应避免收集与可识别个人相关的数据,因为 GDPR 和 CCPA 等隐私法律无论数据存储在哪里都适用。如果你确实持有个人数据,只存储必要的内容,保留时间不超过必要期限,并对访问进行安全控制。将个人数据保留超过必要时限只会增加风险而不会增加价值。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量