资讯动态

Python+Selenium爬取问财网动态表格数据与pandas清洗实战

发布时间:2026/10/9 20:32:05 来源:尧图企业网站定制
1. 从零拆解问财网数据抓取的核心思路问财网这类平台本质上是一个自然语言驱动的选股与数据查询引擎。你在搜索框里输入“近三年净利润增长率大于20%的股票”它返回一张结构化的表格。对做量化研究、行业分析或者单纯想批量整理数据的人来说手动一页页复制显然不现实于是就有了用程序自动抓取的需求。我这次要聊的就是围绕这个场景用 Python 配合 Selenium 驱动 Chrome 浏览器把查询结果抓下来再用 pandas 做清洗和类型转换的完整过程。先说清楚这个内容适合谁看。如果你已经会一点 Python 基础语法知道列表和字典是什么但对动态网页抓取还没什么概念那这篇正好。如果你已经用过 requests 加 xpath 抓静态页面但一遇到 JavaScript 渲染的表格就抓瞎那这篇也能帮你补齐这块。问财网的结果表格是前端异步渲染出来的直接拿 requests 请求接口往往拿不到完整数据或者参数签名对不上所以走浏览器自动化这条路虽然慢一点但胜在稳定、直观、可控。核心关键词这里先点一下问财网、爬虫、Selenium、Chrome、pandas。整条链路是 Selenium 控制 Chrome 打开页面、输入查询条件、等待表格渲染、提取 HTML 或直接读取表格数据最后交给 pandas 做数据类型转换和后续分析。这里面每一个环节都有坑我会一个一个拆开讲。为什么选 Selenium 而不是 requests这是第一个要解释清楚的问题。requests 抓的是服务器返回的原始 HTML速度快、资源占用低但问财网的结果数据是通过 JavaScript 在浏览器里动态生成的。你用 requests 拿到的源码里表格位置可能只有一个空的 div 容器真正的数据在后续的 XHR 请求里。要模拟这些请求你得逆向分析接口参数、cookie、token一旦对方改了签名逻辑你的代码就废了。Selenium 的好处是它直接操作真实浏览器页面渲染成什么样它就抓什么样相当于你雇了一个机器人帮你手动操作稳定性高很多。代价是速度慢、吃内存但对于中小规模的数据抓取这个代价完全可以接受。再一个考量是反爬。问财网对高频请求是有风控的requests 那种毫秒级的连续请求很容易触发限制。Selenium 因为要等页面渲染天然就有间隔反而更像真人操作。当然这不意味着你可以无节制地抓后面我会讲怎么控制节奏。2. 环境搭建与工具选型的关键细节2.1 Chrome 与 ChromeDriver 的版本匹配问题Selenium 要驱动 Chrome必须保证 ChromeDriver 的版本和本机 Chrome 浏览器的版本对应。这是新手最容易卡住的地方。你装了个 Chrome 120却下了个 ChromeDriver 114跑起来直接报错提示 session not created。解决办法很简单打开 Chrome在地址栏输入查看版本信息的内部页面记下主版本号然后去 ChromeDriver 的官方下载页找对应版本。注意只需要主版本号一致即可比如 Chrome 是 120.0.6099.109你下 120 开头的 ChromeDriver 就行。从 Selenium 4.6 开始其实内置了一个 Selenium Manager能自动帮你下载匹配的驱动。我实测下来大部分情况下它能正常工作但网络环境不好的时候会卡住。所以我个人的习惯还是手动管理驱动把 chromedriver 放在项目目录下用代码指定路径这样最可控。from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(executable_path./chromedriver) driver webdriver.Chrome(serviceservice)注意ChromeDriver 的路径建议用相对路径或者配置到系统环境变量里写死绝对路径在换机器时会很麻烦。2.2 Python 依赖安装与 pandas 踩坑pandas 的安装看起来简单pip install pandas一行搞定但实际经常出问题。最常见的是网络原因导致的超时或者提示找不到匹配的版本。这时候可以换国内镜像源比如清华源pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple如果还是报 “could not find a version that satisfies the requirement”大概率是你的 Python 版本太老或者操作系统架构不对。pandas 现在对 Python 版本有要求太老的版本装不上新 pandas。建议用 Python 3.8 以上。另外在 PyCharm 里装 pandas有时候图形界面的包管理器会抽风直接在终端里用 pip 命令反而更靠谱。Selenium 的安装同理pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 浏览器启动参数的取舍启动 Chrome 的时候有几个参数值得关注。无头模式headless可以让浏览器在后台运行不弹出窗口适合部署到服务器上。但问财网这种页面无头模式下有时候渲染会出问题或者被识别出来。我建议调试阶段先用有头模式能看到页面在干什么方便排查。等逻辑稳定了再考虑切无头。from selenium.webdriver.chrome.options import Options options Options() # options.add_argument(--headless) # 调试阶段先注释掉 options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--window-size1920,1080)--window-size这个参数别小看。窗口太小页面可能会用移动端布局表格结构完全不一样你按桌面端写的选择器就全失效了。设成 1920x1080 是比较稳妥的做法。3. 页面元素定位与数据提取的实操要点3.1 等待策略为什么 time.sleep 是下策新手最爱用time.sleep(5)来等页面加载。这个做法能用但极其不优雅而且不稳定。网络快的时候你白等了几秒网络慢的时候 5 秒又不够。正确的做法是用显式等待WebDriverWait让 Selenium 轮询检查某个条件是否满足满足就立刻继续不满足就等到超时。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 15) table wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, table.result-table)))这里的 15 是最大等待秒数不是固定等待时间。实际可能 1 秒就返回了。presence_of_element_located只保证元素出现在 DOM 里不保证可见。如果表格是懒加载的可能还需要用visibility_of_element_located。实操心得问财网的表格渲染有个特点先出现一个加载动画然后表格才填充数据。如果你只等表格容器出现可能抓到的是空表。更稳的做法是等某一行具体的数据出现比如等第一个单元格的文本不为空。3.2 输入查询条件与触发搜索问财网的搜索框是一个 input 元素你需要先清空它再输入你的查询语句然后触发搜索。触发方式有两种一种是模拟回车键一种是找到搜索按钮点击。回车键更通用。from selenium.webdriver.common.keys import Keys search_box wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, input.search-input))) search_box.clear() search_box.send_keys(近三年净利润增长率大于20%的股票) search_box.send_keys(Keys.ENTER)这里有个细节clear()有时候对某些前端框架的输入框无效因为它们的值不是直接绑在 input 的 value 上。如果 clear 不起作用可以用全选加删除的方式search_box.send_keys(Keys.CONTROL, a) search_box.send_keys(Keys.DELETE)3.3 表格数据的提取两种思路对比提取表格数据有两条路。第一条是直接读取渲染后的 HTML用 pandas 的read_html解析。第二条是用 Selenium 逐个元素遍历自己组装成列表。两种方式各有优劣。pandas.read_html的优点是代码极简一行就能把表格转成 DataFrame。缺点是它依赖页面源码里的 table 标签结构如果表格是用 div 拼出来的现在很多前端框架都这么干read_html 就抓不到。问财网的表格我实测是标准 table 结构所以这条路走得通。import pandas as pd html driver.page_source tables pd.read_html(html) df tables[0] # 通常第一个表格就是结果表但read_html有个坑它会把页面上所有 table 都解析出来包括一些隐藏的、布局用的表格。你得根据列名或者行数判断哪个才是你要的。更稳的方式是先用 Selenium 定位到目标表格元素取它的 outerHTML再交给 read_html。table_element driver.find_element(By.CSS_SELECTOR, table.result-table) table_html table_element.get_attribute(outerHTML) df pd.read_html(table_html)[0]第二条路是手动遍历。找到表格的所有行 tr再遍历每行的 td取 text 内容。这种方式最灵活能处理各种奇葩结构但代码量大。rows table_element.find_elements(By.TAG_NAME, tr) data [] for row in rows: cells row.find_elements(By.TAG_NAME, td) if cells: data.append([cell.text for cell in cells]) df pd.DataFrame(data[1:], columnsdata[0])注意cell.text拿的是可见文本如果单元格里有隐藏元素或者样式影响可能拿不全。需要完整内容时用cell.get_attribute(textContent)。3.4 翻页与滚动加载的处理问财网的结果通常分页显示。要抓多页数据你得模拟点击下一页按钮等新数据加载完再抓一次。这里的关键是等待时机——点击之后不能立刻抓要等表格内容变化。一个可靠的判断方法是记录当前第一行的内容点击下一页后轮询等待第一行内容发生变化。import time first_row_before driver.find_element(By.CSS_SELECTOR, table.result-table tr:nth-child(2)).text next_btn driver.find_element(By.CSS_SELECTOR, a.next-page) next_btn.click() for _ in range(20): time.sleep(0.5) first_row_now driver.find_element(By.CSS_SELECTOR, table.result-table tr:nth-child(2)).text if first_row_now ! first_row_before: break如果页面是滚动加载而不是分页那就得用 JavaScript 控制滚动条driver.execute_script(window.scrollTo(0, document.body.scrollHeight))滚动之后同样要等待新内容出现。这种无限滚动的页面建议设置一个最大滚动次数避免死循环。4. pandas 数据清洗与类型转换实战4.1 抓下来的数据为什么全是字符串不管你是用 read_html 还是手动遍历抓下来的数据本质上都是文本。数字是字符串百分比是字符串日期也是字符串。直接拿来做计算比如求和、排序结果会乱七八糟。所以类型转换是必须的一步。先看看原始数据长什么样print(df.dtypes) print(df.head())你大概率会看到所有列都是 object 类型。像“净利润增长率”这种列值是 “23.5%” 这样的字符串。要转成数值得先去掉百分号再转 float。df[净利润增长率] df[净利润增长率].str.rstrip(%).astype(float) / 100注意这里除以 100 是因为百分比转小数。如果你后续只是比较大小不除也行但做加权计算时就得统一量纲。4.2 处理缺失值与异常符号问财网的数据里经常出现 “--” 或者 “-” 表示缺失还有 “暂无” 之类的文字。这些在转数值时会报错。稳妥的做法是先替换成 NaN。import numpy as np df df.replace([--, -, 暂无, ], np.nan) df[净利润增长率] pd.to_numeric(df[净利润增长率].str.rstrip(%), errorscoerce)errorscoerce的作用是遇到无法转换的值就设为 NaN而不是直接抛异常。这样即使有个别脏数据也不会中断整个流程。4.3 日期与时间的处理如果抓的是带日期的数据比如财报日期pandas 的to_datetime能帮你转成标准时间类型。df[报告期] pd.to_datetime(df[报告期], errorscoerce)转成 datetime 之后你就能按年份、季度做分组统计了。比如算某只股票近三年的平均增长率df[年份] df[报告期].dt.year yearly df.groupby(年份)[净利润增长率].mean()4.4 数据类型转换的常见报错与排查最常见的报错是ValueError: could not convert string to float。原因通常是字符串里混了非数字字符比如千分位逗号、空格、货币符号。处理办法是先用正则或者 str.replace 清理。df[市值] df[市值].str.replace(,, ).str.replace(亿, ).astype(float)还有一个坑是中文全角字符。有时候页面上的百分号是全角的 “”和半角的 “%” 不一样rstrip(%)去不掉。这时候得先统一替换df[净利润增长率] df[净利润增长率].str.replace(, %).str.rstrip(%)实操心得类型转换之前先df.sample(10)随机看几行再df[col].unique()[:20]看看有哪些特殊值。这一步花两分钟能省掉后面半小时的调试。5. 反爬应对与稳定性优化5.1 请求频率控制问财网对短时间内大量请求是有感知的。你如果一秒钟点十次搜索很快就会被限制访问表现为页面返回空数据或者直接跳转验证。控制频率的手段很简单每次操作之间加随机间隔。import random import time time.sleep(random.uniform(2, 5))随机间隔比固定间隔好因为固定间隔的模式太明显。2 到 5 秒的随机等待对中小规模抓取足够了。5.2 模拟真实浏览器特征Selenium 默认启动的 Chrome 有一些自动化特征比如navigator.webdriver属性为 true。有些网站会检测这个。可以通过启动参数和 CDP 命令来隐藏。options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) })这些操作能让浏览器看起来更像真人操作的。但我要强调这只是提高稳定性不是让你去突破什么限制。抓取行为本身要遵守网站的规则控制规模不要给对方服务器造成压力。5.3 异常重试与断点续抓网络请求总会有失败的时候。写爬虫一定要有重试机制。简单的做法是用 try-except 包住关键步骤失败就重试几次。def safe_get(driver, url, retries3): for i in range(retries): try: driver.get(url) return True except Exception as e: print(f第{i1}次尝试失败: {e}) time.sleep(3) return False对于多页抓取建议每抓完一页就把数据存一次存成 CSV 或者追加到列表里。这样即使中途崩了也不用从头再来。all_data [] for page in range(1, 11): # ... 抓取当前页 ... all_data.append(page_df) pd.concat(all_data).to_csv(result_partial.csv, indexFalse)5.4 常见问题速查表问题现象可能原因解决方向session not createdChromeDriver 版本不匹配检查主版本号是否一致表格抓到空数据等待时机不对数据未渲染完改用显式等待等具体数据出现类型转换报错字符串含特殊符号先 replace 清理再 astype页面跳转验证请求频率过高加随机间隔降低抓取速度无头模式下抓不到页面检测或无头渲染差异先用有头模式调试read_html 抓错表页面有多个 table先定位目标元素再取 outerHTML翻页后数据没变等待时间不够轮询检测首行内容变化6. 数据落地与后续分析衔接6.1 存成什么格式抓下来的 DataFrame最通用的存储格式是 CSV。优点是简单、通用Excel 和 pandas 都能直接打开。缺点是中文编码容易出问题。存的时候指定encodingutf-8-sig这样 Excel 打开不会乱码。df.to_csv(wencai_result.csv, indexFalse, encodingutf-8-sig)如果数据量大或者需要保留数据类型可以考虑存成 Parquet 或者 pickle。Parquet 压缩率高读取快适合后续反复分析。pickle 能完整保留 pandas 的数据类型但跨 Python 版本可能有兼容问题。df.to_parquet(wencai_result.parquet)6.2 多页数据合并的注意事项多页抓取时每页的列顺序理论上应该一致但实际可能因为某些行缺列而导致错位。合并之前最好检查一下各页的列名是否相同。for i, page_df in enumerate(all_data): if list(page_df.columns) ! list(all_data[0].columns): print(f第{i}页列名不一致: {page_df.columns})如果确实不一致用pd.concat的时候它会自动对齐列名缺失的填 NaN。但前提是列名本身要能对上。如果列名有细微差异比如多了个空格那就得先统一列名。df.columns df.columns.str.strip()6.3 从抓取到分析的完整链路数据抓下来、清洗完接下来就是分析了。比如你想筛选出净利润增长率连续三年大于 20% 的股票可以这样写# 假设 df 有 股票代码、年份、净利润增长率 三列 pivot df.pivot_table(index股票代码, columns年份, values净利润增长率) qualified pivot[(pivot 0.2).all(axis1)]这一步就把爬虫和数据分析串起来了。爬虫负责拿数据pandas 负责整理和筛选最后你得到一张干净的候选列表。实操心得抓取和分析最好分成两个独立的脚本。爬虫脚本只管抓抓完存成文件。分析脚本读文件做处理。这样调试分析逻辑的时候不用反复跑爬虫省时间也减少对目标网站的压力。7. 一些踩过的坑和实用建议第一个坑是页面结构变化。问财网的前端不是一成不变的今天用的 CSS 选择器过段时间可能就失效了。所以选择器尽量用相对稳定的属性比如表格的 class 名而不是依赖层级很深的 nth-child。如果发现抓不到数据了第一件事就是打开开发者工具看看目标元素的选择器是不是变了。第二个坑是登录态。问财网部分数据可能需要登录才能看全。Selenium 可以复用本地的 Chrome 用户数据目录这样你手动登录一次后续脚本就能带着登录态跑。options.add_argument(--user-data-dir/path/to/your/chrome/profile)但注意这个目录不能被正在运行的 Chrome 占用跑脚本前得先把 Chrome 完全关掉。第三个坑是内存泄漏。Selenium 跑久了Chrome 进程会占越来越多内存。如果是长时间大批量抓取建议每抓 N 页就重启一次 driver。if page % 50 0: driver.quit() driver webdriver.Chrome(serviceservice, optionsoptions)第四个坑是 pandas 的 SettingWithCopyWarning。当你从一个大 DataFrame 切片出一个小 DataFrame然后试图修改它pandas 会警告你可能改的不是原数据。解决办法是用.copy()显式复制。subset df[df[净利润增长率] 0.2].copy() subset[排名] range(1, len(subset) 1)最后说一个心态上的建议。写爬虫这件事技术只占一半另一半是耐心。页面加载慢、元素定位不到、数据格式乱七八糟这些都是常态。遇到问题别急打开浏览器开发者工具一步一步看一步一步试。我刚开始写的时候一个表格抓了一下午才搞定现在回头看那些坑都是必经之路。这套方案我用了挺长时间抓取问财网的各类财务数据、行情数据都能应付。核心就是 Selenium 负责拿渲染后的页面pandas 负责清洗和转换中间用显式等待保证稳定性用随机间隔控制节奏。代码不复杂但每个环节的细节都值得抠一抠。你把上面这些点都照顾到了基本就能稳定跑起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑