加密货币市场全天候运转,CoinGecko 等网站上的公开行情页面包含驱动价格追踪、投资组合仪表盘和研究所需的结构化数字:每种币的名称和符号、当前价格、24小时涨跌幅、24小时交易量以及市值。这些数据实时刷新,手动核对几十种代币既耗时又容易出错。

本指南向您展示如何用 Python 可靠地提取加密货币行情数据。您将构建一个小型可运行爬虫,通过 Crawling API 获取渲染后的行情页面,用 BeautifulSoup 解析每一行,处理分页,并导出为干净的 JSON 和 CSV。整个流程仅限于公开行情数据(任何访客无需账号即可看到的内容),文末的合法性部分并非套话。

您将构建的内容

一个 Python 脚本,获取公开的加密货币行情页面,遍历渲染表格中的每一行,并为每种代币提取结构化记录。运行示例以 CoinGecko 主行情表为准,我们提取以下字段:

  • 名称:完整的币名,如 Bitcoin 或 Ethereum。
  • 符号:代币符号,如 BTC 或 ETH。
  • 价格:您所选报价货币的当前价格。
  • 24小时涨跌幅:过去 24 小时的价格变化百分比。
  • 24小时交易量:过去 24 小时的总交易量。
  • 市值:该代币的总市值。

为什么普通请求在加密货币行情页面上会失败

用裸 HTTP 客户端请求加密货币行情 URL,您会得到状态码 200 但正文中只有一小部分数据。两个因素对您不利。首先,这些行情表格通过 JavaScript 在浏览器中加载各行:初始 HTML 是一个薄壳,只有在页面脚本运行后才会填充内容。解析第一个响应,您只能得到一个空表而非完整的代币列表。其次,高流量行情网站会迅速识别自动流量:来自数据中心 IP 且在紧密循环中请求的模式会在到达渲染内容之前被限速或挑战。

因此,一个可用的行情数据爬虫需要在单次请求中实现两件事:渲染页面的浏览器,以及平台视为真实访客的 IP。您可以自己组合无头浏览器和轮换住宅代理池,但将它们组合并保持运行正常才是大部分工作。Crawling API 将两者整合进一次调用:您发送带 JavaScript token 的 URL,它在可信 IP 后渲染页面,并为您返回可解析的完整 HTML。

为什么需要 JS token

Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 则先在真实浏览器中渲染页面。加密货币行情表格在客户端填充各行,因此您需要在此使用 JS token。普通 token 会返回与普通请求相同的薄壳,其中几乎没有可解析的有用内容。

前提条件

在编写代码之前,您需要准备几样东西,都不会花太长时间。

基础 Python 知识。您应当能够编写和运行 Python 脚本,并使用 pip 安装包。如果您不熟悉解析部分,BeautifulSoup 指南是本教程的好伴侣。

Python 3.8 或更高版本。使用 python --version 确认版本。如果尚未安装,请从 python.org 或通过 Anaconda 等发行版安装,并确保 Python 在您的 PATH 中。

Crawlbase 账户和 JS token。注册后,打开控制台,从账户文档页面复制 JavaScript(JS)token。Crawlbase 提供 1,000 次免费请求,足够完成本指南。请像对待密码一样保管 token,勿将其纳入版本控制。

搭建项目

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的库。

bash
python --version

python -m venv crypto_env
source crypto_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,请使用 crypto_env\Scripts\activate 代替 source 命令。两个依赖项各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML 以便按 CSS 选择器提取各字段。jsoncsv 均为标准库,导出步骤无需额外安装。

步骤一:获取渲染后的行情页面

首先获取完整页面。导入 CrawlingAPI 类,用您的 JS token 初始化,并请求行情 URL。表格异步加载,因此传入 ajax_waitpage_wait 以等待动态内容加载完毕再捕获页面。在解析之前检查 Crawlbase 的 pc_status,可以让失败清晰呈现而非被静默忽略。

python
from crawlbase import CrawlingAPI

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

OPTIONS = {
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0",
    "ajax_wait": "true",
    "page_wait": 5000,
}

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

if __name__ == "__main__":
    market_url = "https://www.coingecko.com/"
    html = crawl(market_url)
    print(html[:500] if html else "No HTML returned")

两个等待选项对于客户端渲染目标至关重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 则在加载后额外等待固定毫秒数,以便实时更新的行在页面被捕获前稳定下来。五秒是合理的起点;如果表格返回数据稀少,可适当延长。运行 python crypto_scraper.py,您应该看到真实的行情标记,而非普通请求返回的空壳,这表示渲染在编写任何选择器之前就已正常工作。

Crawlbase Crawling API

加密货币行情页面需要在可信 IP 后获取渲染表格,通过一次调用完成,这正是上面的 ajax_waitpage_wait 选项所设置的。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并为您返回完整 HTML,让您无需自行运行无头集群和代理池。先在免费套餐上指向公开行情页面进行测试。

步骤二:解析单行代币数据

行情页面是一个表格,每个 tr 对应一种代币。将渲染后的 HTML 加载到 BeautifulSoup 中,读取您关心的单元格。列按已知顺序排列,名称和符号位于同一个代币单元格中,因此一个将各字段映射到对应单元格的小辅助函数可以让解析逻辑保持清晰。每次查找都有守护,确保字段缺失时返回 None 而非导致程序崩溃。

python
from bs4 import BeautifulSoup

ROW_SELECTOR = 'table[data-coin-table-target="table"] > tbody > tr'

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

def parse_row(row):
    return {
        "name": text_of(row, 'td[data-view-component="true"] a span.tw-text-gray-700'),
        "symbol": text_of(row, 'td[data-view-component="true"] a span.tw-text-gray-500'),
        "price": text_of(row, 'td[data-target="price.price"]'),
        "change_24h": text_of(row, 'td.tw-text-right span[data-target="price-change-percentage-24h"]'),
        "volume_24h": text_of(row, 'td.tw-text-right span[data-coin-table-target="totalVolume"]'),
        "market_cap": text_of(row, 'td.tw-text-right span[data-coin-table-target="marketCap"]'),
    }

text_of 辅助函数在行内查询一个元素并返回其去除首尾空白的文本,或在元素缺失时返回 None,确保某种代币缺少字段时不会中断循环。代币单元格在两个嵌套的 span 中分别保存完整 namesymbol,而价格、24小时涨跌幅、交易量和市值各自位于右对齐的单元格中。以行为单位读取每条记录,使所有字段与正确的代币对齐,即使某列发生偏移也能保证准确性。

选择器会发生漂移

行情网站生成的类名和 data-target 属性会在不通知的情况下更改。请将这里的选择器视为起始模板,而非固定合约。当某个字段返回 None 时,请在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,不是出了什么问题的标志。

步骤三:遍历页面上的每一行

有了行解析器,选择表格中的所有行并将每行映射为一条记录。在获取操作周围加一个小型重试包装,可以避免单次慢请求终止整个运行。

python
import time

def fetch_html(page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        html = crawl(page_url)
        if html:
            return html
        if attempt < max_retries:
            print(f"Retrying ({attempt + 1}/{max_retries})...")
            time.sleep(1)
    print(f"Unable to fetch {page_url}")
    return None

def parse_market(html):
    soup = BeautifulSoup(html, "html.parser")
    rows = soup.select(ROW_SELECTOR)
    return [parse_row(r) for r in rows if r.select_one('td[data-target="price.price"]')]

fetch_html 在失败时最多重试两次(中间有短暂停顿),成功时返回 HTML,放弃时返回 Noneparse_market 选择每一行代币并将其映射为记录,跳过没有价格单元格的行,避免分隔行或标题行产生空条目。结果是来自单页的干净代币字典列表。

步骤四:处理行情的分页

单页只是完整行情的一个切片。CoinGecko 使用 ?page= 查询参数进行分页,因此您逐页遍历到您设置的上限,并收集所有页面的行数据。用 max_pages 参数限制爬取上限,防止大型行情失控运行,而页面之间的短暂停顿则限制了运行速率,避免对网站造成过大压力。

python
def collect_all_coins(base_url, max_pages):
    records = []
    for page in range(1, max_pages + 1):
        page_url = f"{base_url}?page={page}"
        html = fetch_html(page_url)
        if not html:
            continue
        page_coins = parse_market(html)
        if not page_coins:
            break
        records.extend(page_coins)
        print(f"Page {page}: {len(page_coins)} coins")
        time.sleep(2)
    return records

collect_all_coins 依次请求每一页,解析其各行,并在某页未返回代币时提前停止, 这发生在您超过最后一个有数据的页面时。页面之间的 time.sleep(2) 分散了请求。调整 max_pages 控制您深入行情排名的程度;仅第一页就已覆盖市值最大的代币。

步骤五:组合完整脚本

现在将各部分整合到一个可运行的脚本中:跨页收集代币,然后将记录导出到 JSON 和 CSV。

python
import csv
import json
import time
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

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

OPTIONS = {
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0",
    "ajax_wait": "true",
    "page_wait": 5000,
}

ROW_SELECTOR = 'table[data-coin-table-target="table"] > tbody > tr'

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

def fetch_html(page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        html = crawl(page_url)
        if html:
            return html
        if attempt < max_retries:
            time.sleep(1)
    return None

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

def parse_row(row):
    return {
        "name": text_of(row, 'td[data-view-component="true"] a span.tw-text-gray-700'),
        "symbol": text_of(row, 'td[data-view-component="true"] a span.tw-text-gray-500'),
        "price": text_of(row, 'td[data-target="price.price"]'),
        "change_24h": text_of(row, 'td.tw-text-right span[data-target="price-change-percentage-24h"]'),
        "volume_24h": text_of(row, 'td.tw-text-right span[data-coin-table-target="totalVolume"]'),
        "market_cap": text_of(row, 'td.tw-text-right span[data-coin-table-target="marketCap"]'),
    }

def parse_market(html):
    soup = BeautifulSoup(html, "html.parser")
    rows = soup.select(ROW_SELECTOR)
    return [parse_row(r) for r in rows if r.select_one('td[data-target="price.price"]')]

def collect_all_coins(base_url, max_pages):
    records = []
    for page in range(1, max_pages + 1):
        html = fetch_html(f"{base_url}?page={page}")
        if not html:
            continue
        page_coins = parse_market(html)
        if not page_coins:
            break
        records.extend(page_coins)
        time.sleep(2)
    return records

def save_outputs(records):
    with open("crypto_market.json", "w") as f:
        json.dump(records, f, indent=2)
    if not records:
        return
    with open("crypto_market.csv", "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)

def main():
    market_url = "https://www.coingecko.com/"
    coins = collect_all_coins(market_url, max_pages=2)
    save_outputs(coins)
    print(f"Saved {len(coins)} coins")

if __name__ == "__main__":
    main()

该脚本跨最多两个行情页面收集代币记录,并用两秒睡眠限制循环速率。save_outputs 同时写入 JSON 文件和 CSV(以第一条记录的键作为标题),使您拥有下游工具所需格式的数据。调整 max_pages 和行情 URL,以适应您需要的排名范围。

输出示例

运行 python crypto_scraper.py,您将得到每种代币的干净结构化记录,可直接用于分析、存入数据库或导入电子表格。

json
[
  {
    "name": "Bitcoin",
    "symbol": "BTC",
    "price": "$86,650.00",
    "change_24h": "2.4%",
    "volume_24h": "$28,540,118,233",
    "market_cap": "$1,712,884,991,402"
  },
  {
    "name": "Ethereum",
    "symbol": "ETH",
    "price": "$2,015.42",
    "change_24h": "1.1%",
    "volume_24h": "$14,902,551,870",
    "market_cap": "$243,118,440,905"
  }
]

对应的 CSV 包含相同的列,每种代币一行,可直接导入 pandas 或任意电子表格,按市值排序、按24小时涨跌幅筛选或绘制交易量图表。上面的数值仅作示例;实时价格和百分比不断变化,这正是按计划定期采集而非仅采集一次的原因。

大规模运行时保持不被屏蔽

即使渲染问题已解决,高流量行情网站仍会监测爬虫特征的流量。以下几个习惯可以保持较长时间运行的健康,适用于任何防护严密的商业目标。

  • 限制请求速率。在紧密循环中猛烈请求页面是最快被限速或挑战的方式。上面的两秒停顿是下限,而非上限;对于较大的任务请适当延长,避免以紧密周期重复请求同一页面。
  • 依赖 IP 轮换。住宅 IP 池将请求分散到众多真实用户地址,使任何单个地址都不会触发速率限制。Crawling API 为您处理这些;如果您自己搭建技术栈,这是最关键的部分。
  • 读取状态码。当运行开始返回非 200 的 pc_status 值时,说明当前速率或 IP 等级已不够用。将其视为退让的信号,而非可以忽略的噪声。

对于较大规模的爬取,异步 Crawler 可将请求排队并通过 webhook 交付结果,适合在不保持开放连接的情况下运行多个行情页面。有关更广泛的策略,请参阅 如何在不被屏蔽的情况下爬取网站。如果您希望将这些数据纳入监控工作流,同样的方法适用于 价格情报;另有一篇指南涵盖从 CoinMarketCap 爬取加密货币价格,供您作为第二数据来源。

爬取加密货币行情数据是否合法?

爬取加密货币行情网站是否被允许,取决于该网站的服务条款、您所在的司法管辖区以及您对数据的用途。CoinGecko 等网站公开发布其行情表格,这些数字本身(价格、24小时涨跌幅、交易量、市值)是事实性的公开行情数据而非个人数据,这使本指南涉及的字段集在隐私方面风险较低。但这并不豁免您遵守网站条款的义务:大多数行情网站在其使用条款中限制大量自动访问,因此请阅读这些条款和网站的 robots.txt,并将两者视为您采集内容和频率的边界。

以下几条值得遵守。仅采集公开行情数据(任何访客无需账号即可看到的数字),并将请求量控制在不给网站服务器造成负担的水平。不要爬取登录墙、付费墙或账号后面的任何内容,也不要采集或围绕个人数据建立用户档案, 这超出了本指南的范围,并会涉及 GDPR 和 CCPA 义务。尊重行情网站发布的任何编辑内容的版权:价格数字是事实,但书面市场分析则不是您可以转载的内容。

对于生产用途,更清晰的路径是官方 API。大多数加密货币行情网站(包括 CoinGecko)提供公开 API,以结构化 JSON 格式返回相同的价格、交易量和市值字段,并有明确的速率限制和条款。API 比 HTML 选择器更稳定,在页面布局更改时不会失效,且能让您遵守服务提供商的许可使用规则。对于快速一次性提取或 API 未公开的字段,使用爬虫爬取渲染页面;对于构建持久或商业性内容,则应使用官方 API 或授权数据源。

回顾

核心要点

  • 行情表格是客户端渲染的。普通请求返回的是空表薄壳,因此您必须在解析之前先渲染页面。
  • 您需要渲染和可信 IP 同时具备。带 JS token 的 Crawling API 在一次调用中同时实现两者;ajax_waitpage_wait 控制等待内容的时长。
  • 逐行解析。将每个 tr 作为一种代币读取,并将其单元格映射到名称、符号、价格、24小时涨跌幅、24小时交易量和市值,使每个字段与正确的代币对齐。
  • 分页并导出。沿 ?page= 查询参数遍历到上限,用短暂停顿限制运行速率,并将记录写入 JSON 和 CSV。
  • 生产用途优先选择官方 API。坚守公开行情数据,遵守网站的服务条款和 robots.txt,对于任何持久或商业性内容使用服务提供商的公开 API 或授权数据源。

常见问题

为什么普通请求返回空的加密货币表格?

因为行情页面通过 JavaScript 在客户端加载各行。初始 HTML 是一个空壳,只有在浏览器中运行脚本后才会填充,因此原始 HTTP 请求返回状态码 200 但表格为空。要获取各行,您必须先渲染页面,这正是 Crawling API 的 JS token 为您处理的事情。

这里需要普通 token 还是 JS token?

JS token。普通 token 获取静态 HTML,在加密货币行情页面上与普通请求返回的空表壳相同。JS token 在返回 HTML 之前先在真实浏览器中渲染页面,因此当 BeautifulSoup 解析时,代币行已经存在。

我可以从行情页面提取哪些字段?

公开的每种代币数字:名称和代币符号、当前价格、24小时涨跌幅百分比、24小时交易量以及市值。请坚守任何访客无需账号即可看到的数据,并将书面市场分析或编辑内容视为受版权保护的内容,而非可以转载的内容。

我的选择器返回 None,是什么发生了变化?

几乎可以肯定是网站的标记发生了变化。生成的类名和 data-target 属性(data-coin-table-target 钩子、右对齐的单元格类)会在不通知的情况下更改,因此上个月有效的选择器现在可能已失效。请在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作。

我应该爬取页面还是使用官方 API?

对于任何持久或商业性内容,请优先使用官方 API。大多数加密货币行情网站(包括 CoinGecko)提供公开 API,以结构化 JSON 格式返回价格、交易量和市值数据,并有明确的条款和速率限制,这比解析 HTML 更稳定。对于 API 未公开的字段或快速一次性提取,爬取渲染页面最合适。

我应该多久采集一次加密货币行情数据?

这取决于您的用例。对于接近实时的仪表盘,您可能每隔几分钟采集一次;对于趋势研究,每小时或每日快照通常就足够了。无论频率如何,请限制请求速率,遵守网站的速率限制,避免频繁的计划任务变成网站屏蔽的爬虫特征流量。

开始构建

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

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

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