金融决策依赖数据。无论你是追踪持仓股票、构建市场仪表板,还是为研究模型提供数据,推动市场运转的公开数字(报价、指数水平、公司财报)都是原始材料。这些数据中的大部分清晰地展示在金融网站上,但手动跨数十个标的提取既耗时又费力,而且展示这些数据的实时页面是为在浏览器中渲染而构建的,并非为了将干净的答案直接交给一个裸脚本。
本指南展示如何用 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 页面。
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 视为密码:它用于验证你的请求,请妥善保管,不要纳入版本控制。
搭建项目
创建虚拟环境以隔离项目依赖,然后安装爬虫所需的库。
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 选择器提取单个字段。json 和 csv 均随标准库附带,导出步骤无需额外安装。
第一步:获取渲染完成的报价页面
先获取完整的页面。导入 CrawlingAPI 类,用你的 JS token 初始化,并请求一个公开报价 URL。金融页面异步流式传输数字,因此传入 ajax_wait 和 page_wait 以等待动态内容在页面被捕获之前加载完成。在解析之前检查 Crawlbase 的 pc_status 可以让失败清晰可见而非悄无声息。
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 在加载后固定等待若干毫秒,以便延迟流入的数字在被捕获之前出现。五秒是合理的起点;如果数字仍然缺失,可适当提高。运行脚本,你应该看到真实的报价标记,而非普通请求返回的空壳,这在编写任何选择器之前确认了渲染正常工作。
金融报价页面需要在一次调用中获得可信 IP 后面的渲染页面,这正是上面的 ajax_wait 和 page_wait 选项所完成的工作。Crawling API 接收 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并将渲染完成的 HTML 交给你,让你无需自行运行无头浏览器集群和代理池。先在免费套餐中将其指向公开报价页面测试。
第二步:解析报价字段
拿到渲染完成的 HTML 后,将其加载到 BeautifulSoup 并提取标题数字。大多数金融页面用稳定的 data-* 属性或字段名标记其关键字段,这比生成的 class 名称更持久。每次查找都有保护,确保缺失字段返回 None 而非让运行崩溃。
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 值(regularMarketPrice、regularMarketChange、regularMarketVolume 等)是金融页面标记实时数字的典型方式。在浏览器开发者工具中打开你的目标页面,找到包含每个数字的属性或 class,然后替换真实的选择器;将查找集中在一个字典中使每个字段的修改只需改一行。
金融网站会在没有通知的情况下更改其标记,不同来源的确切 data-field 名称也各不相同。将这里的选择器视为起始模板而非契约。当某个字段返回 None 时,重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,而非出了问题的迹象。
第三步:组装完整脚本
现在将各部分组合为一个可运行的脚本:遍历标的列表,用小型重试封装器获取并解析每个报价,将记录导出为 JSON 和 CSV。
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 以匹配你所针对的公开来源。
输出结果示例
运行完整脚本后,你将得到每个标的的干净结构化记录,可直接用于分析、数据库或电子表格。以下数值为示例,并非实时数据。
[ { "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_wait和page_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 次请求免费,无需信用卡。
