金融决策依赖数据。无论你是追踪持仓股票、构建市场仪表板,还是为研究模型提供数据,推动市场运转的公开数字(报价、指数水平、公司财报)都是原始材料。这些数据中的大部分清晰地展示在金融网站上,但手动跨数十个标的提取既耗时又费力,而且展示这些数据的实时页面是为在浏览器中渲染而构建的,并非为了将干净的答案直接交给一个裸脚本。

本指南展示如何用 Python 以可靠的方式爬取金融数据。它涵盖哪些公开来源值得关注,然后演示一个小型可运行的爬虫:通过 Crawling API 获取渲染完成的金融报价页面,解析标题字段(价格、涨跌、成交量及相邻字段),并导出干净的 JSON 和 CSV。本文所有内容均限于公开市场数据:任何人无需登录即可看到的事实性数字,不涉及个人或付费内容。文末的合法性部分并非套话,请在指向真实规模运行之前仔细阅读。

哪些公开金融数据值得采集

在编写代码之前,了解哪些数据既有用又公平合理很有帮助。公开金融数据大致分为几个类别,每类驱动不同的分析。

  • 市场报价。 股票、ETF 或基金的实时价格、当日涨跌、涨跌幅、当日区间和成交量。这是最常追踪的数据,也是下文实例的重点。
  • 指数。 宽基市场或行业指数等基准的总体水平,用于衡量持仓相对于更广泛市场的表现。
  • 上市公司财报。 上市公司须披露的数据:营业收入、盈利、市值以及公司简介页面上显示的汇总比率。
  • 公开市场新闻与标题。 用于情感分析的编辑标题和时间戳。采集事实性标题和链接,而非受版权保护的完整文章正文。

Forbes、Reuters、Bloomberg、MarketWatch 和《金融时报》等综合商业来源值得阅读,但对于程序化数据源,你需要的是能够公开结构化报价字段的页面,这正是我们下面的目标。如需了解更广泛的市场情况,我们整理的 全球最佳金融数据提供商 对比了主要来源。

你将构建什么

一个 Python 脚本,接收标的的公开金融报价 URL,通过 Crawling API 获取渲染完成的页面,并提取结构化记录。下列字段是典型的公开市场字段;确切的 CSS 选择器取决于你所针对的来源页面,因此请将其视为需要适配的模板。

  • Symbol 报价对应的股票代码。
  • Price 当前报价。
  • Change 当日绝对价格变动。
  • Change percent 当日涨跌幅(百分比)。
  • Volume 成交股数。
  • Day range 当日盘中低点和高点。
  • Market cap 页面显示的公司市值。

为什么普通请求在金融页面上会失败

用裸 HTTP 客户端请求现代金融报价 URL,通常会得到状态 200 但响应体中几乎没有数字。有两件事对你不利。首先,大多数金融网站在客户端渲染报价:价格、涨跌和成交量在初始 HTML 加载后由 JavaScript 流式写入,因此第一个响应是一个空壳,解析它只会得到页面框架而非你需要的数字。其次,金融网站密切监视自动化流量,因为实时报价数据很有价值。数据中心 IP 和非浏览器请求模式会在到达渲染内容之前被速率限制、IP 封锁或要求过挑战。

因此,一个有效的金融数据爬虫需要在一次请求中同时具备两样东西:一个真正渲染页面的浏览器,以及一个网站认为是真实访客的 IP。你可以用无头浏览器加轮换住宅代理自行搭建,但保持这套设施健康运行才是大部分工作。Crawling API 将两者合并为一次调用:你发送带有 JavaScript token 的 URL,它在可信 IP 后面渲染页面,并返回渲染完成的 HTML 供你解析。关于为何客户端渲染页面需要这样处理,可参阅 用 Python 爬取 JavaScript 页面

为什么需要 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 finance_env
source finance_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,使用 finance_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_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__":
    quote_url = "https://www.example-finance.com/quote/AAPL"
    html = crawl(quote_url)
    print(html[:500] if html else "No HTML returned")

quote_url 替换为你打算追踪的公开报价页面。两个等待选项对客户端渲染的金融页面很重要:ajax_wait 等待异步内容加载完成,page_wait 在加载后固定等待若干毫秒,以便延迟流入的数字在被捕获之前出现。五秒是合理的起点;如果数字仍然缺失,可适当提高。运行脚本,你应该看到真实的报价标记,而非普通请求返回的空壳,这在编写任何选择器之前确认了渲染正常工作。

Crawlbase Crawling API

金融报价页面需要在一次调用中获得可信 IP 后面的渲染页面,这正是上面的 ajax_waitpage_wait 选项所完成的工作。Crawling API 接收 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并将渲染完成的 HTML 交给你,让你无需自行运行无头浏览器集群和代理池。先在免费套餐中将其指向公开报价页面测试。

第二步:解析报价字段

拿到渲染完成的 HTML 后,将其加载到 BeautifulSoup 并提取标题数字。大多数金融页面用稳定的 data-* 属性或字段名标记其关键字段,这比生成的 class 名称更持久。每次查找都有保护,确保缺失字段返回 None 而非让运行崩溃。

python
from bs4 import BeautifulSoup

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

def parse_quote(html, symbol, url):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "symbol": symbol,
        "price": text_of(soup, '[data-field="regularMarketPrice"]'),
        "change": text_of(soup, '[data-field="regularMarketChange"]'),
        "change_percent": text_of(soup, '[data-field="regularMarketChangePercent"]'),
        "volume": text_of(soup, '[data-field="regularMarketVolume"]'),
        "day_range": text_of(soup, '[data-field="regularMarketDayRange"]'),
        "market_cap": text_of(soup, '[data-field="marketCap"]'),
        "link": url,
    }

text_of 辅助函数查询一个元素并返回其去除空白后的文本,当元素不存在时返回 None,因此缺少某个字段的报价页面不会中断循环。这里的 data-field 值(regularMarketPriceregularMarketChangeregularMarketVolume 等)是金融页面标记实时数字的典型方式。在浏览器开发者工具中打开你的目标页面,找到包含每个数字的属性或 class,然后替换真实的选择器;将查找集中在一个字典中使每个字段的修改只需改一行。

选择器会发生变化

金融网站会在没有通知的情况下更改其标记,不同来源的确切 data-field 名称也各不相同。将这里的选择器视为起始模板而非契约。当某个字段返回 None 时,重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,而非出了问题的迹象。

第三步:组装完整脚本

现在将各部分组合为一个可运行的脚本:遍历标的列表,用小型重试封装器获取并解析每个报价,将记录导出为 JSON 和 CSV。

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

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_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,
}

# Map each ticker to its public quote page URL.
QUOTE_BASE = "https://www.example-finance.com/quote/"
SYMBOLS = ["AAPL", "MSFT", "GOOGL"]

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(soup, selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_quote(html, symbol, url):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "symbol": symbol,
        "price": text_of(soup, '[data-field="regularMarketPrice"]'),
        "change": text_of(soup, '[data-field="regularMarketChange"]'),
        "change_percent": text_of(soup, '[data-field="regularMarketChangePercent"]'),
        "volume": text_of(soup, '[data-field="regularMarketVolume"]'),
        "day_range": text_of(soup, '[data-field="regularMarketDayRange"]'),
        "market_cap": text_of(soup, '[data-field="marketCap"]'),
        "link": url,
    }

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

def main():
    records = []
    for symbol in SYMBOLS:
        url = QUOTE_BASE + symbol
        html = fetch_html(url)
        if html:
            records.append(parse_quote(html, symbol, url))
        time.sleep(2)

    save_outputs(records)
    print(f"Saved {len(records)} quotes")

if __name__ == "__main__":
    main()

脚本遍历 SYMBOLS,构建每个报价 URL,用重试封装器获取,将其解析为记录,并通过两秒等待控制循环节奏。save_outputs 使用第一条记录的键作为列头,将 JSON 和 CSV 都写出,你可以根据下游工具的需求选择格式。向 SYMBOLS 添加标的并调整 QUOTE_BASE 以匹配你所针对的公开来源。

输出结果示例

运行完整脚本后,你将得到每个标的的干净结构化记录,可直接用于分析、数据库或电子表格。以下数值为示例,并非实时数据。

json
[
  {
    "symbol": "AAPL",
    "price": "225.40",
    "change": "+1.85",
    "change_percent": "+0.83%",
    "volume": "48,210,300",
    "day_range": "223.10 - 226.05",
    "market_cap": "3.42T",
    "link": "https://www.example-finance.com/quote/AAPL"
  },
  {
    "symbol": "MSFT",
    "price": "418.20",
    "change": "-2.40",
    "change_percent": "-0.57%",
    "volume": "19,840,100",
    "day_range": "416.00 - 421.30",
    "market_cap": "3.11T",
    "link": "https://www.example-finance.com/quote/MSFT"
  }
]

对应的 CSV 包含相同的列,每个标的一行,可直接加载到 pandas 或任何电子表格中,按涨跌排序、按成交量筛选,或对多次运行的价格绘制走势图。将脚本配置为 cron 任务定期运行,你就拥有了所追踪标的的简单公开报价时间序列。

扩展规模与保持不被封锁

追踪数百个标的,或在紧密的计划上轮询同一组标的,是金融爬取变得更难的地方,因为这正是这些网站所监视的流量特征。以下几个习惯可以让更长时间的运行保持健康。

  • 控制请求频率。 在紧密循环中轮询观察列表是触发限流或挑战的最快方式。上面的两秒等待是下限,而非上限;对于更大的任务适当增大,错开运行而非同时触发每个标的。
  • 善用轮换。 住宅 IP 池将请求分散到多个真实用户地址,使没有单一地址触发速率限制。Crawling API 在服务端处理这些;如果你自建方案,这是最需要做对的部分。
  • 关注状态码。 开始返回非 200 pc_status 值的运行正在告诉你当前的速率或 IP 层级已经不够用了。将其视为需要降速的信号,而非可以忽略的噪声。

对于更大规模的任务,异步 Crawler 将请求排队并将结果投递到 webhook,适合在不保持长连接的情况下轮询多个标的。更广泛的参考请见 大规模金融数据爬取 以及我们关于 不被封锁地爬取 的通用指南。如果你的最终目标是竞争或定价分析,我们的 利用网络爬取实现价格情报 展示了团队如何将此类数据转化为决策。

爬取金融数据合法吗

爬取金融数据是否被允许,取决于来源的服务条款、你的司法管辖区以及你对数据的用途。市场报价、指数水平以及上市公司须披露的财报,都是事实性的公开信息,采集公开事实用于分析总体上是可辩护的。但为其提供服务的页面属于某人,大多数金融网站在其服务条款中限制自动访问和批量采集。本文的代码不会改变这一点,它只是让技术部分能够运行。请阅读目标网站的服务条款和 robots.txt,尊重任何声明的速率限制,并将两者视为你采集工作的边界。

以下几条值得坚守。只采集公开的非付费数据:任何人无需账户即可看到的报价字段、指数水平和已披露的财报。不爬取任何登录或付费墙之后的内容,也不绕过身份验证或计量机制以获取高级数据,这超出了公开采集的范畴,进入了未经授权访问的领域。保持请求量低到不会给来源服务器造成压力。请记住,即使是事实性数据在你转发时也会带有许可条款:许多网站根据限制转发的协议从交易所和数据供应商处获取报价,因此为个人分析采集数据与拥有出售或发布的权利是两回事。编辑内容(如完整文章正文)受版权保护;情感分析采集标题和链接,而非正文。

本指南特意将范围限定在公开市场数据上,因为这是让工作具有可辩护性的界限。对于个人研究之外的任何用途,尤其是生产或商业产品,应使用官方市场数据 API 或授权数据源,而非更复杂的爬虫。大多数主要金融平台提供自己的数据 API 或与成熟的市场数据供应商合作,交易所也直接授权实时和历史数据。这些途径给你稳定的协议、有文档的字段和转发权利,这些都是爬取公开页面永远无法给你的。用爬取来做原型和填补空白;当数据驱动真正的业务时,请获取数据授权。

回顾

核心要点

  • 了解什么是公平合理的采集范围。 公开市场数据(报价、指数、已披露的公司财报、新闻标题)是事实性的,属于合理范围;个人和付费内容则不在其列。
  • 金融页面是客户端渲染的。 普通请求返回的是缺少数字的空壳,因此在解析之前必须先渲染页面。
  • 你同时需要渲染和可信 IP。 带有 JS token 的 Crawling API 在一次调用中完成这两项;ajax_waitpage_wait 控制等待内容加载的时长。
  • 解析、循环、导出。 将每个字段映射到带保护的选择器,用重试封装器遍历你的标的,用短暂等待控制运行节奏,写出 JSON 和 CSV。
  • 生产环境使用授权数据。 遵守每个来源的服务条款和 robots.txt,注意报价数据带有转发条款,对于任何商业用途应转向官方市场数据 API 或授权数据源。

常见问题

为什么普通请求返回的金融页面没有数字?

因为大多数金融网站通过 JavaScript 在客户端加载其报价字段。初始 HTML 是一个空壳,只有在页面脚本运行后才会填充,因此裸 HTTP 请求返回状态 200,但价格、涨跌和成交量都缺失。要获取这些数字,必须先渲染页面,这正是 Crawling API 的 JS token 为你处理的工作。

金融页面需要普通 token 还是 JS token?

需要 JS token。普通 token 获取静态 HTML,对于客户端渲染的金融页面,这与普通请求返回的空壳一样。JS token 在返回 HTML 之前先在真实浏览器中渲染页面,确保报价字段在 BeautifulSoup 解析时已经存在。

哪些金融数据可以安全爬取?

公开的非付费市场数据:实时报价、指数水平、上市公司须披露的财报,以及附有链接的事实性新闻标题。坚守任何访客无需账户即可看到的数据,不采集登录或付费墙之后的任何内容,避免转发受版权保护的文章正文或授权数据源。

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

几乎可以肯定是来源的标记发生了变化。金融网站会在没有通知的情况下更改其生成的 class 名称和 data-field 属性,而且这些属性名称在不同来源之间也各不相同,因此上个月有效的选择器可能已经失效。在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作。

生产环境中我应该爬取还是使用官方市场数据 API?

对于任何商业或生产级用途,应使用官方 API 或授权数据源。爬取公开页面非常适合原型开发和填补空白,但官方 API 给你有文档的字段、稳定的协议以及公开页面永远无法给你的转发权利。大多数主要金融平台提供数据 API 或与成熟的市场数据供应商合作。

如何在不被封锁的情况下爬取多个标的?

用等待间隔控制请求频率,错开运行而非同时轮询每个标的,并依靠轮换住宅 IP 使没有单一地址触发速率限制。Crawling API 在服务端轮换 IP,异步 Crawler 将大批量任务排队并将结果投递到 webhook。关注 pc_status 码,在不再返回 200 时降低速率。

开始构建

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

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

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