☰
finviz-api筛选器高级用法:从URL到DataFrame的量化选股实战
2026/10/24 4:50:09 网站建设 项目流程

1. 为什么需要认真对待 finviz-api 的筛选器

做量化选股或者美股数据研究的人,大概率都绕不开 finviz 这个站点。它的筛选器(Screener)能把基本面、技术面、行业属性几十个维度组合起来做条件过滤,页面响应快、字段全,手动点几下就能筛出一批标的。但手动操作有两个绕不过去的坎:一是每次都要重复点选,二是没法把筛选结果直接喂给下游的分析脚本。

finviz-api 这个库就是来解决这个问题的。它把 finviz 的筛选参数封装成 Python 对象,让你用代码描述筛选条件,然后一次性把结果拉回来,直接变成结构化数据。标题里说的"从URL到数据",指的正是这个链路:你构造的筛选条件最终会拼成一个带查询参数的 URL,库去请求这个 URL,解析返回的 HTML 表格,最后交给你一个可以继续处理的列表。

这篇文章面向的是已经会用 pandas 做基础数据处理、想把手动筛选流程自动化的朋友。我会把筛选器的参数体系拆开讲清楚,重点放在那些文档里一笔带过、但实际用起来很容易踩坑的高级用法上,比如多条件组合的逻辑、字段值的精确写法、分页与限流、以及怎么把结果稳定地落成 DataFrame。全文基于我自己的实操经验,代码可以直接抄去改。

2. 筛选器的参数体系与核心思路拆解

2.1 筛选器本质上是一组键值对

finviz 的筛选器 URL 长这样:https://finviz.com/screener.ashx?v=111&f=cap_large,fa_div_o,sh_avgvol_o1000&ft=4&o=-marketcap。拆开看,v=111是视图编号,f=后面跟的是筛选条件,ft=是信号过滤,o=是排序字段。finviz-api 做的事情,就是让你不用手拼这个字符串,而是用finviz.Filters里的常量去组合。

理解这一点很关键:你写的每一个筛选条件,最终都会变成 URL 里的一个 token。所以当结果不符合预期时,第一件事不是怀疑库有 bug,而是把库实际请求的 URL 打印出来,看看 token 拼得对不对。我习惯在调试阶段把screener.url打出来核对,这个习惯帮我省了大量排查时间。

2.2 为什么用常量而不是裸字符串

finviz-api 把每个筛选条件都定义成了Filters类下的属性,比如Filters.MarketCap.LARGE、Filters.Dividend.YIELD_OVER_3。这些常量的值其实就是 finviz 内部的 token 字符串。用常量的好处有三个:一是 IDE 能补全,不用去背 token;二是拼错了会直接报 AttributeError,而不是悄悄拼出一个无效 URL 返回空结果;三是版本升级时 token 变了,库会跟着更新,你的代码不用动。

我见过有人图省事直接写字符串"cap_large",短期能跑,但一旦 finviz 调整了某个字段的命名,这种硬编码就会静默失效——返回的是空列表而不是报错,排查起来非常痛苦。所以我的建议是:能用常量就用常量,实在找不到对应常量再去查 token。

2.3 视图编号决定了返回哪些列

v=这个参数很多人会忽略,但它直接决定了你拿到的数据有哪些列。常用的几个视图:

视图编号含义典型列
111概览(Overview)代码、公司名、行业、市值、PE、价格、涨跌幅、成交量
121估值(Valuation)加上 P/B、P/S、PEG、EPS 等
131财务(Financial)加上股息、ROA、ROE、利润率等
141所有权(Ownership)机构持股、内部持股比例
151表现(Performance)各周期涨跌幅
161技术(Technical)RSI、均线、Beta、ATR

如果你需要股息率,就必须用 131 或更靠后的视图,用 111 是拿不到这个字段的。这一点在文档里没有强调,但实际做筛选时经常需要根据目标字段反推视图编号。我的做法是:先确定要哪些列,再选最小的能满足需求的视图编号,避免拉回一堆用不上的数据。

3. 高级筛选条件的写法与实操要点

3.1 多条件组合的默认逻辑是 AND

当你传入多个筛选条件时,finviz 默认用 AND 连接,也就是所有条件都要满足。比如同时筛"大盘股"和"股息率大于3%",返回的是两个条件都成立的标的。这个逻辑符合直觉,但要注意:同一个维度内的多个值也是 AND,这往往不是你想要的。

举个例子,如果你想筛"市值是大盘或者中盘",直觉上会写[Filters.MarketCap.LARGE, Filters.MarketCap.MID],但这样拼出来的 URL 是cap_large,cap_mid,finviz 会理解为"市值既是大盘又是中盘",结果必然是空。正确的做法是用f=参数之外的方式,或者干脆分两次请求再合并。这是新手最容易踩的坑之一。

3.2 数值型条件的区间写法

很多筛选条件支持区间,比如市值、价格、成交量。finviz-api 里对应的常量通常有_UNDER、_OVER、_BETWEEN这类后缀。以成交量为例如,Filters.AverageVolume.OVER_1000K表示日均成交量超过 100 万股。注意这里的单位是 K(千股),不是金额。

如果你需要自定义区间,比如"价格在 10 到 50 之间",标准常量可能覆盖不到,这时候有两个办法:一是用最接近的常量组合,二是直接构造 token。我一般优先用常量,因为区间边界 finviz 是固定的几档,自己拼 token 容易拼出无效值。实测下来,finviz 对价格区间的档位划分比较粗,如果你的策略需要精细区间,建议拉回数据后用 pandas 二次过滤,而不是在筛选器层面死磕。

3.3 信号过滤(ft 参数)的独立用法

ft=是信号过滤,和f=是两套体系。f=筛的是基本面和技术面的静态属性,ft=筛的是"当前是否出现某个信号",比如"今日跳空高开""突破 50 日均线""RSI 超卖"等。这两个参数可以同时用,效果是叠加。

finviz-api 里信号过滤通过finviz.Filters.Signal或者直接传ft参数实现。我常用的一个组合是:f=筛出基本面达标的池子,ft=再筛出其中技术面出现买点的标的。这样两步走比把所有条件堆在一起更清晰,也方便你分别调试——先确认基本面池子对不对,再确认信号过滤有没有生效。

注意:信号过滤的结果和当天行情强相关,同一个脚本今天跑和明天跑结果可能完全不同。做回测时不要依赖ft=,它只反映"此刻"的状态,没有历史快照。

3.4 排序与分页的正确处理

o=参数控制排序,比如o=-marketcap表示按市值降序,o=price表示按价格升序。负号代表降序,这个规则要记牢。排序字段必须是当前视图里存在的列,否则会报错或者被忽略。

分页是另一个容易被忽略的点。finviz 每页默认返回 20 条,超过的部分需要翻页。finviz-api 的Screener对象在迭代时会自动处理分页,但如果你用screener.data直接取,可能只拿到第一页。我的做法是:明确用循环迭代screener对象,让库自己去翻页,这样不会漏数据。同时要注意,翻页过程中如果筛选结果在变化(比如盘中),可能出现重复或遗漏,做严肃分析时建议在收盘后跑。

4. 从筛选到 DataFrame 的完整实操流程

4.1 环境准备与基础调用

先装库,然后跑一个最小可用的例子。这里我用一个"大盘股 + 股息率超 3% + 日均成交量超 100 万"的组合来演示。

from finviz.screener import Screener filters = [ 'cap_large', 'fa_div_o3', 'sh_avgvol_o1000', ] stock_list = Screener(filters=filters, table='Valuation', order='-marketcap') for stock in stock_list: print(stock['Ticker'], stock['Price'], stock['Dividend'])

这里table='Valuation'对应视图 121,order='-marketcap'是降序。跑通之后你会看到一批标的和它们的估值字段。如果返回空,先检查 filters 里的 token 是不是有效——把stock_list.url打出来,手动在浏览器里打开看看 finviz 怎么响应。

4.2 把结果转成 DataFrame

Screener对象本身可以迭代,每个元素是一个字典。转 DataFrame 最直接的方式是:

import pandas as pd df = pd.DataFrame(stock_list.data) print(df.shape) print(df.columns.tolist())

但这里有个细节:stock_list.data只包含当前已加载的页。如果你要全部数据,得先确保迭代完成,或者用stock_list.data之前手动触发加载。我的习惯是写一个辅助函数,把迭代结果收集成列表再转 DataFrame,这样最稳:

rows = [dict(stock) for stock in stock_list] df = pd.DataFrame(rows)

转完之后,数值列还是字符串,需要做类型转换。比如Price、Market Cap、Volume这些列,finviz 返回的是带单位或百分号的字符串,得自己清洗。这一步没有捷径,只能针对每个列写转换逻辑。

4.3 数值列的清洗与单位换算

finviz 返回的数值有几个常见的坑:市值带B(十亿)或M(百万)后缀,成交量带K或M,百分比带%,涨跌幅带正负号。清洗逻辑我一般写成这样:

def parse_number(val): if pd.isna(val) or val == '-': return None val = str(val).replace('%', '').replace(',', '') multiplier = 1 if val.endswith('B'): multiplier = 1e9 val = val[:-1] elif val.endswith('M'): multiplier = 1e6 val = val[:-1] elif val.endswith('K'): multiplier = 1e3 val = val[:-1] try: return float(val) * multiplier except ValueError: return None

这个函数能覆盖大部分情况,但要注意:有些列本身就是纯数字字符串,不带单位,直接 float 就行。建议先df.head()看一眼原始值,再决定清洗策略,不要盲目套用。

4.4 筛选条件的动态构造

实际项目里,筛选条件往往不是写死的,而是根据策略参数动态生成。比如用户输入一个市值下限,你要把它映射到 finviz 的档位。这时候就需要一个映射表:

用户输入(亿美元)对应 token
< 3cap_micro
3 - 20cap_small
20 - 100cap_mid
100 - 1000cap_large
> 1000cap_mega

这种映射的好处是把连续参数离散化到 finviz 支持的档位,避免拼出无效 token。代价是精度损失,但筛选器本来就是粗筛,精筛交给 pandas 做,这个分工是合理的。

5. 常见问题与排查技巧实录

5.1 返回空结果的排查顺序

空结果是最常见的问题,排查要按顺序来,不要乱试。我的固定流程是:

  1. 打印screener.url,手动在浏览器打开,确认 finviz 本身有没有结果。
  2. 如果浏览器有结果但代码没有,检查是不是分页没迭代完。
  3. 如果浏览器也没结果,逐个删减筛选条件,定位是哪个条件导致的。
  4. 检查视图编号和排序字段是否匹配,比如用 111 视图却按dividend排序,就会出问题。

这个顺序能覆盖 90% 的空结果场景。剩下 10% 通常是网络问题或者 finviz 临时调整了页面结构,等一会儿再试或者升级库版本。

5.2 字段缺失与列名变化

finviz 偶尔会调整返回的列名,比如把P/E改成PE,或者某个字段在某些视图下不返回。这时候stock['P/E']会抛 KeyError。稳妥的写法是用.get():

pe = stock.get('P/E') or stock.get('PE')

另外,不同视图返回的列不一样,如果你在代码里硬编码了某个列名,换视图就会崩。建议在转 DataFrame 之后,先打印df.columns确认一遍,再写后续逻辑。

5.3 请求频率与稳定性

finviz 对请求频率是有隐性限制的,短时间内大量请求可能被临时拒绝。我的经验是:单次筛选尽量把条件写全,减少请求次数,而不是分很多次小请求。如果确实需要多次请求,中间加个time.sleep(1)到2秒的间隔,实测下来比较稳。

还有一个技巧:把常用的筛选结果缓存到本地 CSV,下次跑的时候先读缓存,只有缓存过期或者条件变了才重新请求。这样既省请求,又加快了调试速度。

5.4 常见问题速查表

现象可能原因解决方向
返回空列表条件互斥或 token 无效打印 URL 手动验证,逐个删条件
KeyError列名变化或视图不匹配用 .get(),确认视图编号
数据只有 20 条分页未迭代用 for 循环迭代 Screener 对象
数值无法计算字符串带单位写清洗函数处理 B/M/K/%
请求被拒频率过高加 sleep,合并请求,用缓存
结果与网页不一致盘中数据变化收盘后跑,或固定时间点跑

5.5 几个我踩过的坑

第一个坑是把信号过滤当成静态条件。有次我写了个脚本,用ft=筛"突破均线"的标的,回测时发现结果好得离谱,后来才意识到ft=反映的是运行那一刻的状态,回测里根本不该用它。这个教训让我养成了区分"静态筛选"和"动态信号"的习惯。

第二个坑是忽略视图编号导致字段缺失。早期我图省事全用 111 视图,结果想算股息率时发现根本没这列,白白多写了一套请求逻辑。后来我固定用 131 视图做基本面筛选,字段全,省心。

第三个坑是数值清洗不彻底。有一次市值列里混进了-(表示无数据),我的清洗函数没处理,导致整列变成 object 类型,后续排序全乱。从那以后我的清洗函数第一行就是判断空值和-。

6. 把筛选器接入实际工作流的思路

6.1 筛选器在量化流程中的位置

筛选器适合做"粗筛",也就是从几千只标的里快速缩小到一个几百只的候选池。它的优势是快、字段全、不用自己维护数据源;劣势是条件粒度粗、无法做复杂计算。所以合理的分工是:finviz 粗筛出候选池,pandas 做精细过滤和因子计算,最后再交给策略逻辑。

我自己的流程是这样的:每天收盘后跑一次筛选器,把候选池存成 CSV;第二天开盘前读 CSV,用本地数据做二次过滤;盘中只做监控,不再请求筛选器。这样既利用了 finviz 的数据广度,又避免了实时请求的不稳定。

6.2 多组筛选条件的批量执行

如果你有多个策略,每个策略对应一组筛选条件,可以写成一个配置列表,循环执行:

strategies = [ {'name': 'dividend', 'filters': ['cap_large', 'fa_div_o3']}, {'name': 'growth', 'filters': ['cap_mid', 'fa_epsyoy_o20']}, {'name': 'value', 'filters': ['cap_large', 'fa_pe_u20']}, ] for s in strategies: result = Screener(filters=s['filters'], table='Valuation') rows = [dict(x) for x in result] pd.DataFrame(rows).to_csv(f"{s['name']}.csv", index=False)

这种批量执行要注意请求间隔,别一口气跑十几个策略,容易被限流。我一般每个策略之间 sleep 两秒,跑完一轮也就几十秒,可以接受。

6.3 结果落库与增量更新

候选池如果每天都要用,建议落成本地数据库而不是 CSV。CSV 的问题是每次全量覆盖,没法做历史对比。用 SQLite 就很合适,轻量、无需额外服务,还能按日期查询历史候选池。

落库的时候加一个snapshot_date字段,这样你可以对比"昨天筛出来的"和"今天筛出来的"有什么变化,哪些是新进池的、哪些被剔除了。这个变化本身就是一个有用的信号,比单看某一天的快照信息量大得多。

6.4 一个完整的可复用脚本骨架

把上面的东西串起来,一个可复用的脚本大概长这样:

import time import pandas as pd from finviz.screener import Screener def run_screen(filters, table='Valuation', order='-marketcap'): result = Screener(filters=filters, table=table, order=order) rows = [dict(x) for x in result] return pd.DataFrame(rows) def clean_df(df): # 针对具体列做清洗,这里省略细节 return df if __name__ == '__main__': filters = ['cap_large', 'fa_div_o3', 'sh_avgvol_o1000'] df = run_screen(filters) df = clean_df(df) df.to_csv('candidates.csv', index=False) print(f'共筛出 {len(df)} 只标的')

这个骨架的好处是职责清晰:run_screen只管拉数据,clean_df只管清洗,主流程只管编排。后续要加策略、加缓存、加落库,都在主流程里扩展,不会把拉数据和清洗的逻辑搅在一起。

7. 关于筛选器精度与数据时效的个人体会

finviz 筛选器的档位设计是它的特点也是它的限制。市值、成交量、股息率这些字段都是分档的,不是连续值。这意味着你没法精确表达"市值在 50 亿到 80 亿之间"这种需求,只能选最接近的档位,然后拉回来用 pandas 精筛。我一开始觉得这是缺陷,用久了反而觉得合理——筛选器的定位就是快速缩小范围,精度交给下游,各司其职。

数据时效方面,筛选器反映的是请求那一刻的状态,盘中会变。如果你的策略对时效敏感,建议固定在一个时间点跑,比如收盘后半小时,这时候数据已经稳定。我试过盘中跑和收盘后跑,结果差异不小,尤其是涉及涨跌幅和成交量的条件。所以做严肃分析时,我坚持收盘后跑,把结果存下来,第二天用存下来的数据,而不是第二天早上重新请求。

最后分享一个小技巧:如果你需要某个字段但不确定它在哪个视图里,可以先用 111 视图拉一只标的,看返回的字典有哪些键,再对照 finviz 网页上的列名去猜视图编号。这个方法比翻文档快,实测几次就能摸清字段和视图的对应关系。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询