☰
Python Selenium实战:金融数据爬虫构建与反爬策略详解
2026/9/29 16:56:14 网站建设 项目流程

简介:本资源是一套面向金融数据分析从业者与NLP工程师的实战型数据采集工具包,聚焦于上市公司财务报表结构化数据获取这一高频刚需场景,专为data2text任务(如自动生成财报摘要、分析报告)提供高质量原始数据支撑。压缩包共9个文件(375KB),含5个文本配置与日志文件(如stocks.txt、failed_stocks.txt)、1个核心Python爬虫脚本(main.py)、1个Markdown说明文档(README.md)、1个示例可视化PNG(data-example.png)及1个Word附赠资料(含扩展应用提示)。已有55人学习下载,内容覆盖Selenium驱动浏览器自动化控制、同花顺网站动态渲染页面解析、Cookie自动轮换机制与UA/行为指纹级反爬伪装策略,完整实现从目标URL调度、登录态维持到财务指标表格精准提取的全流程闭环。用户可直接部署运行,快速获得清洗后的结构化财报数据,并基于附赠文档延伸至自然语言生成环节。

1. 项目缘起:从“数据饥渴”到“精准投喂”的实战需求

在金融科技和量化分析领域,数据是绝对的血液。无论是构建预测模型、进行风险分析,还是完成像“data2text”(数据到文本生成)这样的前沿任务,第一步永远是获取高质量、结构化的原始数据。我最近在做一个关于上市公司财报文本自动生成的项目,核心就是需要海量的、标准化的财务报表数据作为“饲料”。理想很丰满,但现实是骨感的——公开的金融数据API要么收费昂贵,要么数据维度不全;而像同花顺、东方财富这类财经门户网站,虽然数据丰富直观,但并没有提供友好的批量下载接口。

这就是典型的“数据饥渴”场景。手动复制粘贴?面对成百上千家上市公司、动辄十几年的财报数据,这无异于天方夜谭。于是,自动化数据采集,或者说网络爬虫,就成了唯一可行的技术路径。但金融网站的反爬虫机制通常比较严密,简单的requests库请求很容易被识别和封锁。这时,Selenium这类浏览器自动化工具的优势就凸显出来了——它能模拟真实用户的操作,从打开浏览器、登录、翻页到点击下载,几乎可以复刻人类的所有交互行为,从而有效绕过许多基于行为识别的反爬策略。

本项目正是基于这样的实战需求诞生的:利用Python的Selenium库,自动化抓取同花顺网站上的上市公司财务报表数据,并完成初步的清洗与格式化,为后续的data2text模型训练提供可直接使用的结构化数据集。整个方案不仅实现了核心的抓取功能,更重点解决了金融数据抓取中的两大痛点:Cookie会话维持与更新,以及对抗反爬虫的浏览器伪装策略。下面,我就把这套从零搭建、踩过无数坑的完整方案拆解给你。

2. 技术选型深析:为什么是Selenium,而不是Requests或Playwright?

面对一个数据采集任务,工具选型是第一步,也是决定后续开发效率和成功率的基石。市面上主流的Python爬虫方案大致有三类:Requests/BeautifulSoup静态爬虫、Selenium浏览器自动化、以及新兴的Playwright/Pyppeteer。为什么在这个项目里,我坚定地选择了Selenium?

2.1 静态爬虫(Requests)的局限性

Requests库效率极高,直接发起HTTP请求获取HTML,再配合BeautifulSoup或lxml进行解析,是爬虫的经典组合。对于同花顺这类网站,它行不通的核心原因在于数据渲染方式。现代网站大量使用JavaScript动态加载内容,财报数据表格很可能是在页面加载完成后,通过AJAX请求获取JSON数据,再由前端JS渲染到页面上的。使用Requests抓取到的初始HTML源码,只是一个“空壳”,里面并没有我们需要的表格数据。虽然可以通过分析网络请求,找到那个返回数据的API接口进行直接请求(即“抓包”),但这存在几个问题:

  1. 接口参数复杂:金融数据接口的查询参数往往包含加密令牌(token)、时间戳、复杂编码的股票代码等,逆向分析成本高。
  2. 接口稳定性差:这类内部API变动频繁,且没有公开文档,一旦网站前端升级,接口可能失效或变更。
  3. 反爬升级:针对API接口的反爬(如请求头校验、频率限制、参数签名)可能比网页端更严格。

因此,对于JavaScript重度渲染的网站,静态爬虫要么无能为力,要么需要投入大量精力进行逆向工程,维护成本很高。

2.2 Selenium的不可替代性

Selenium的核心思想是“所见即所得”。它通过WebDriver控制一个真实的浏览器(如Chrome, Firefox)实例,等待页面完全加载、所有JS执行完毕后再获取完整的DOM内容。这就完美解决了动态渲染的问题。对于同花顺财报页面,Selenium可以:

  • 模拟点击:点击“财务报表”选项卡,选择“资产负债表”、“利润表”等。
  • 模拟选择:选择报告期(如“年报”、“季报”)、时间范围。
  • 获取完整数据:在页面所有交互完成后,直接获取渲染好的表格HTML,简单解析即可。

更重要的是,Selenium模拟的是真实用户行为。它会产生正常的浏览器指纹、加载所有资源(图片、CSS、JS)、管理Cookie和Session。这使得它能够相对自然地绕过那些基于“访问者是否使用浏览器”的初级反爬检测。

2.3 与Playwright的对比

Playwright是微软开源的新一代浏览器自动化工具,它更快、API更现代,并且原生支持多浏览器和无头模式。它确实是Selenium的有力竞争者。但在本项目语境下,我选择Selenium基于以下几点考量:

  • 生态与资料成熟度:Selenium历史悠久,社区庞大,遇到任何稀奇古怪的问题,几乎都能找到解决方案或讨论。这对于需要快速稳定上线的生产级数据采集项目至关重要。
  • 伪装成熟度:围绕Selenium的“反反爬虫”技巧(如隐藏WebDriver特征、使用特定浏览器配置)已经形成了非常成熟的实践方案,有大量现成的代码片段和扩展库(如undetected-chromedriver)可供参考或直接使用。
  • 项目依赖与可控性:Selenium的核心驱动是浏览器本身,只要浏览器稳定,它就很稳定。Playwright虽然打包了浏览器,但作为一个较新的项目,在复杂网络环境或特定系统上可能遇到未知兼容性问题。

注意:这并不意味着Playwright不好。对于全新的项目,如果你追求极致的性能和现代API,Playwright值得尝试。但本项目的目标是稳定、可靠地长期采集金融数据,Selenium经过时间检验的稳定性和丰富的“反反爬”生态是更优选择。

3. 环境搭建与核心配置:打造一个“隐形”的浏览器

工欲善其事,必先利其器。使用Selenium的第一步不是写代码,而是配置一个足够“像人”的浏览器环境。一个裸奔的Selenium驱动浏览器,会有很多特征被网站检测到,从而被识别为自动化脚本。

3.1 基础环境准备

首先,安装必要的Python库:

pip install selenium pandas openpyxl webdriver-manager
  • selenium: 核心自动化库。
  • pandas: 数据处理和分析神器,用于将抓取的表格数据转换为DataFrame并保存。
  • openpyxl: 支持pandas将数据写入Excel的.xlsx格式。
  • webdriver-manager:强烈推荐!它可以自动下载和管理与本地浏览器版本匹配的ChromeDriver或GeckoDriver,省去手动查找和配置驱动路径的麻烦。

3.2 Chrome浏览器的高级配置(反检测核心)

直接使用webdriver.Chrome()会启动一个带有明显自动化特征的浏览器。我们需要通过ChromeOptions添加一系列参数来消除这些特征。

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options def create_stealth_driver(): chrome_options = Options() # 1. 基础隐身:不显示“Chrome正受到自动测试软件控制” chrome_options.add_argument('--disable-blink-features=AutomationControlled') # 2. 禁用自动化标志(至关重要) chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) # 3. 修改navigator.webdriver属性(通过CDP) # 这部分需要在driver创建后执行,见后续代码 # 4. 使用常规用户数据目录(非默认临时目录) # 这样可以保存登录Cookie,避免每次都需要登录 # chrome_options.add_argument(f"user-data-dir={os.path.expanduser('~')}/ChromeProfileForCrawler") # 注意:首次使用需手动登录一次,后续即可自动保持登录状态 # 5. 其他实用参数 chrome_options.add_argument('--no-sandbox') # 在Linux服务器或无GUI环境下常用 chrome_options.add_argument('--disable-dev-shm-usage') # 解决共享内存问题 chrome_options.add_argument('--start-maximized') # 启动时最大化,更像真人操作 # chrome_options.add_argument('--headless') # 无头模式,不显示GUI,适合服务器。但有些反爬能检测无头模式,慎用。 # 6. 使用webdriver-manager自动管理驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=chrome_options) # 7. 执行CDP命令,覆盖navigator.webdriver属性 driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' }) # 8. 也可以移除其他自动化特征,如“window.chrome” driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(window, 'chrome', { get: () => undefined }); ''' }) return driver

3.3 关于无头模式(Headless)的取舍

无头模式不显示浏览器界面,节省资源,适合服务器环境。但越来越多的反爬系统能够检测无头浏览器。对于同花顺这类重要网站,我建议在开发调试阶段使用有头模式,确保所有流程跑通。在部署到生产环境时,可以尝试无头,但如果频繁被屏蔽,则需要回退到有头模式,并可能借助Xvfb(虚拟显示缓冲区)在无GUI的服务器上运行。

3.4 Cookie的持久化与智能更新

Cookie是维持登录状态的关键。上述代码中注释掉的user-data-dir参数就是方法之一:指定一个固定的用户数据目录,Selenium会使用该目录下的Chrome配置文件,其中包含了Cookie、本地存储等。你只需要用这个配置的浏览器手动登录一次同花顺,以后启动就会自动保持登录。

但这种方法有个缺点:Cookie会过期。金融网站出于安全考虑,登录会话有效期可能只有几小时或几天。因此,我们的爬虫必须具备Cookie失效检测与自动更新能力。

一个更健壮的策略是:

  1. 尝试使用现有Cookie访问:用保存的Cookie(可以从user-data-dir读取,或序列化到文件)初始化请求,访问一个需要登录的页面(如“个人中心”)。
  2. 检测登录状态:检查页面元素或URL,判断是否跳转到了登录页。
  3. 自动登录:如果未登录,则调用自动登录函数。这需要你事先准备好账号密码,并使用Selenium模拟输入和点击登录按钮。务必注意账号安全,不要将密码硬编码在代码中,应使用环境变量或加密配置文件。
  4. 保存新Cookie:登录成功后,获取新的Cookie并持久化保存。

这套逻辑增加了程序的复杂性,但保证了长时间运行(如爬取全市场股票多年数据)的稳定性。

4. 同花顺财报页面结构与抓取策略解析

在开始编码前,我们必须像侦探一样,仔细分析目标网站的结构和行为逻辑。同花顺的上市公司财报页面(例如http://basic.10jqka.com.cn/600519/finance.html)是典型的单页应用(SPA)风格,数据通过异步加载。

4.1 页面交互流程分析

  1. 导航到财报页:输入股票代码,进入公司主页,再点击“财务分析”或类似标签。
  2. 选择报表类型:页面内通常有选项卡,如“资产负债表”、“利润表”、“现金流量表”。点击后,页面主体区域会通过AJAX加载对应的表格。
  3. 选择时间范围:有下拉菜单或按钮可以选择报告期类型(年报、季报)和具体年份、季度。
  4. 数据渲染:选择完成后,表格数据会动态填充。表格通常是标准的HTML<table>,但可能包含复杂的表头(合并单元格)和大量数据行。

4.2 数据定位策略

Selenium提供了多种元素定位方式(ID, Name, Class Name, XPath, CSS Selector)。对于结构稳定的生产级爬虫,CSS Selector和XPath是主力。

  • CSS Selector:速度快,语法简洁,适合基于class、id、属性的简单定位。例如,定位所有报表选项卡:driver.find_elements(By.CSS_SELECTOR, ".finance-nav li")。
  • XPath:功能强大,可以基于层级关系、文本内容进行定位,在复杂结构中更灵活。例如,定位包含“资产负债表”文本的按钮:driver.find_element(By.XPATH, "//button[contains(text(), '资产负债表')]")。

一个关键技巧:等待。动态加载意味着元素不是立即可用的。必须使用“显式等待”(Explicit Wait),让Selenium等待特定条件成立后再进行操作,这是编写稳定爬虫的基石。

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 错误做法:直接查找,可能因元素未加载而抛出异常 # table = driver.find_element(By.ID, 'myTable') # 正确做法:显式等待 try: # 等待最多10秒,直到ID为'ProfitStatementNewTable0'的表格出现 wait = WebDriverWait(driver, 10) table_element = wait.until(EC.presence_of_element_located((By.ID, 'ProfitStatementNewTable0'))) print("利润表表格加载完成") except TimeoutException: print("等待表格超时,可能页面结构已变化或加载失败") # 这里可以加入重试或错误处理逻辑

4.3 应对页面变化的策略

网站前端可能会改版。我们不能让爬虫因为一个按钮的class名变了就彻底崩溃。策略如下:

  1. 多层定位:不要依赖单一的、可能变化的属性(如一个特定的class名)。尝试使用更稳定的属性组合,或者通过父级容器相对定位。
  2. 文本内容定位:像“资产负债表”、“年报”这类文本内容是相对稳定的,使用XPath的text()函数定位是不错的选择。
  3. 定期维护与监控:将关键元素的定位器(Selector)集中放在配置文件中。爬虫运行时,可以加入健康检查:尝试访问几个已知页面,检查关键元素是否存在,如果失败则发出警报。
  4. 容错与重试:对于关键操作(如点击选项卡),如果失败(元素未找到、点击无效),可以加入重试机制,并记录日志。

5. 核心抓取流程代码实现与详解

理论分析完毕,现在进入实战编码环节。我们将把整个抓取过程模块化,构建一个健壮、可维护的爬虫。

5.1 主控流程设计

整个抓取任务可以抽象为以下步骤:

  1. 初始化:创建伪装好的浏览器驱动,加载Cookie或尝试自动登录。
  2. 股票代码遍历:读取一个股票代码列表(如从文件或数据库)。
  3. 单股票处理: a. 访问该股票的财报主页。 b. 循环处理每一种需要的报表类型(资产负债表、利润表、现金流量表)。 c. 对于每种报表,循环选择需要的报告期(如近5年的年报)。 d. 抓取当前报表和报告期下的数据表格。 e. 解析表格HTML,转换为结构化数据(如pandas DataFrame)。 f. 保存数据到文件(CSV或Excel),最好按股票、报表类型、年份组织。
  4. 异常处理与日志:任何步骤失败,都应记录错误(股票代码、错误信息),并尽可能继续处理下一个股票或报表,而不是让整个程序崩溃。
  5. 资源清理:所有任务完成后,关闭浏览器驱动。

5.2 代码模块拆解

模块一:驱动管理与登录

import pickle import os import time from selenium.common.exceptions import TimeoutException, NoSuchElementException class FinanceCrawler: def __init__(self, headless=False): self.driver = create_stealth_driver() # 使用前面定义的函数 self.cookie_file = 'session_cookies.pkl' self.login_url = 'https://stockpage.10jqka.com.cn/' self.is_logged_in = False def load_cookies(self): """从文件加载Cookie并添加到驱动""" if os.path.exists(self.cookie_file): with open(self.cookie_file, 'rb') as f: cookies = pickle.load(f) for cookie in cookies: # 添加前可以删除'expiry'字段避免格式错误 if 'expiry' in cookie: # 检查cookie是否过期 if cookie['expiry'] < time.time(): print("Cookie已过期,需要重新登录") return False self.driver.add_cookie(cookie) print("Cookie加载成功") return True return False def save_cookies(self): """将当前驱动的Cookie保存到文件""" cookies = self.driver.get_cookies() with open(self.cookie_file, 'wb') as f: pickle.dump(cookies, f) print("Cookie保存成功") def check_login_status(self): """检查当前是否已登录,访问一个需要登录的页面验证""" self.driver.get('https://my.10jqka.com.cn/') # 假设这是个人中心页 time.sleep(2) if "login" in self.driver.current_url or "登录" in self.driver.page_source: return False return True def login(self, username, password): """执行自动登录(需谨慎,确保符合网站条款)""" print("开始执行登录...") self.driver.get(self.login_url) # 注意:这里需要根据同花顺实际登录页面的HTML结构来定位元素 # 以下为示例,实际元素ID/Class需要你自行查看页面源码确定 try: wait = WebDriverWait(self.driver, 10) # 假设有账号密码输入框和登录按钮 user_input = wait.until(EC.presence_of_element_located((By.ID, 'username'))) pass_input = self.driver.find_element(By.ID, 'password') login_btn = self.driver.find_element(By.ID, 'loginBtn') user_input.send_keys(username) pass_input.send_keys(password) login_btn.click() # 等待登录成功跳转 time.sleep(5) if self.check_login_status(): self.save_cookies() self.is_logged_in = True print("登录成功") return True else: print("登录可能失败") return False except Exception as e: print(f"登录过程发生异常: {e}") return False

模块二:报表数据抓取器这是最核心的部分,负责与页面交互并提取数据。

def fetch_financial_report(self, stock_code, report_type='balance', year=2023, report_period='year'): """ 抓取指定股票、报表类型、年份和报告期的数据 :param stock_code: 股票代码,如 '600519' :param report_type: 报表类型,'balance'(资产负债表), 'income'(利润表), 'cashflow'(现金流量表) :param year: 年份 :param report_period: 报告期,'year'(年报), 'quarter1'(一季报)等 :return: pandas DataFrame 或 None """ # 1. 构建并访问该股票的财报页面 base_url = f'http://basic.10jqka.com.cn/{stock_code}/finance.html' self.driver.get(base_url) time.sleep(3) # 初始页面加载等待 # 2. 映射报表类型到页面操作 report_map = { 'balance': {'tab_text': '资产负债表', 'table_id_prefix': 'BalanceSheetNewTable'}, 'income': {'tab_text': '利润表', 'table_id_prefix': 'ProfitStatementNewTable'}, 'cashflow': {'tab_text': '现金流量表', 'table_id_prefix': 'CashFlowNewTable'} } if report_type not in report_map: print(f"不支持的报表类型: {report_type}") return None tab_info = report_map[report_type] # 3. 点击对应的报表选项卡 try: # 使用XPath通过文本内容定位选项卡 tab_xpath = f"//div[contains(@class, 'finance-nav')]//a[contains(text(), '{tab_info['tab_text']}')]" tab_element = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, tab_xpath)) ) # 有时元素被遮挡,用JavaScript直接点击更可靠 self.driver.execute_script("arguments[0].click();", tab_element) print(f"已切换到{tab_info['tab_text']}") time.sleep(2) # 等待选项卡内容加载 except TimeoutException: print(f"找不到或无法点击'{tab_info['tab_text']}'选项卡") return None # 4. 选择报告期(例如选择‘2023年年报’) # 这里逻辑较复杂,因为同花顺的日期选择器可能是下拉菜单或一组按钮 # 需要你具体分析页面。以下是一个假设性的示例: try: # 假设有一个下拉框选择年份 year_select = Select(self.driver.find_element(By.ID, 'year_select')) year_select.select_by_visible_text(str(year)) time.sleep(1) # 假设有按钮选择报告期类型 period_btn_xpath = f"//div[@class='period-selector']//button[contains(text(), '{'年报' if report_period=='year' else '季报'}')]" period_btn = self.driver.find_element(By.XPATH, period_btn_xpath) period_btn.click() time.sleep(2) # 等待数据重新加载 except Exception as e: print(f"设置报告期时出错: {e}") # 有时网站默认就是最新年报,可以不进行操作 # 5. 定位并等待数据表格出现 # 表格的ID可能是动态的,如'BalanceSheetNewTable0',需要观察规律 # 这里我们尝试用前缀匹配 try: wait = WebDriverWait(self.driver, 15) # 使用XPath的starts-with函数匹配ID前缀 table_element = wait.until( EC.presence_of_element_located((By.XPATH, f"//table[starts-with(@id, '{tab_info['table_id_prefix']}')]")) ) except TimeoutException: print(f"等待{table_info['tab_text']}数据表格超时") # 可以尝试截图保存现场,便于调试 self.driver.save_screenshot(f'error_{stock_code}_{report_type}.png') return None # 6. 获取表格HTML并解析 table_html = table_element.get_attribute('outerHTML') # 使用pandas的read_html直接解析HTML表格,非常方便 try: df_list = pd.read_html(table_html, flavor='html5lib') # html5lib解析器容错性好 if df_list: df = df_list[0] # 通常第一个表格就是我们需要的数据 # 添加元数据列,便于后续识别 df['股票代码'] = stock_code df['报表类型'] = report_type df['年份'] = year df['报告期'] = report_period df['抓取时间'] = pd.Timestamp.now() print(f"成功抓取 {stock_code} {year}年{report_type}数据,形状: {df.shape}") return df else: print(f"解析表格HTML失败,未找到表格") return None except Exception as e: print(f"使用pandas解析表格时出错: {e}") return None

模块三:主循环与数据存储

def crawl_batch_stocks(self, stock_list, report_types, years): """批量爬取多只股票的数据""" all_data = [] for stock_code in stock_list: print(f"\n开始处理股票: {stock_code}") for rpt_type in report_types: for year in years: print(f" 正在抓取 {rpt_type} {year}...") df = self.fetch_financial_report(stock_code, rpt_type, year) if df is not None: all_data.append(df) else: print(f" 抓取失败,跳过") # 礼貌性延迟,避免请求过快 time.sleep(random.uniform(1, 3)) # 合并所有DataFrame并保存 if all_data: final_df = pd.concat(all_data, ignore_index=True) # 保存到Excel,每个报表类型一个sheet with pd.ExcelWriter(f'financial_reports_{pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")}.xlsx') as writer: for rpt_type in report_types: type_df = final_df[final_df['报表类型'] == rpt_type] if not type_df.empty: # 简单清理列名(原始表格可能有合并表头带来的多层索引) type_df.columns = [str(col).strip() for col in type_df.columns] type_df.to_excel(writer, sheet_name=rpt_type[:31], index=False) # Excel sheet名最多31字符 print(f"所有数据已保存至Excel文件") return final_df else: print("未抓取到任何有效数据") return None def close(self): """关闭浏览器驱动""" if self.driver: self.driver.quit() print("浏览器驱动已关闭")

6. 反爬虫对抗进阶与稳定性优化

即使使用了伪装浏览器,面对专业的金融数据网站,我们仍可能遇到封锁。以下是一些进阶的对抗和优化策略。

6.1 请求频率与行为随机化

即使模拟了浏览器,过于规律和频繁的请求仍然会被识别为机器人。

  • 随机延迟:在每个关键操作(如点击、翻页、切换股票)之间加入随机等待时间,模拟人类阅读和思考的间隔。time.sleep(random.uniform(2, 5))。
  • 随机操作序列:不一定严格按照“A->B->C”的顺序。例如,在抓取不同报表时,可以随机排序。
  • 限制并发与总量:避免短时间内抓取过多股票。可以设置每日抓取上限,或将任务分散到不同时间段执行。

6.2 使用代理IP池

这是应对IP封锁最有效的手段之一。当网站检测到某个IP请求异常并封锁后,我们可以切换另一个IP继续工作。

  • 集成代理:Selenium可以通过ChromeOptions配置代理。
    chrome_options.add_argument(f'--proxy-server=http://{proxy_ip}:{proxy_port}')
  • 代理池管理:你需要一个可靠的代理IP来源(付费服务或自建)。在爬虫中集成代理池逻辑,在请求失败(特别是返回403、429状态码)时自动更换代理。
  • 注意:很多公开免费代理速度慢、不稳定,不适合用于需要稳定登录状态的金融数据抓取。

6.3 应对验证码

这是终极挑战。同花顺可能在频繁操作后弹出验证码(滑块、点选、文字等)。

  • 降低触发概率:通过上述行为随机化和频率控制,尽量减少触发验证码的可能。
  • 人工介入:对于长期运行的爬虫,可以设计一个“报警-人工处理”机制。当检测到验证码页面时,程序暂停,发送通知(如邮件、短信),等待人工处理并输入验证码后,程序再继续。这需要将浏览器实例保持在无头模式或可远程访问的状态。
  • 第三方打码平台:对于简单的图形验证码,可以接入打码API,自动识别。但对于复杂的交互式验证码,成本高且识别率有限。

6.4 日志、监控与断点续爬

一个健壮的工业级爬虫必须有完善的日志和故障恢复机制。

  • 详细日志:记录每个步骤的开始、结束、成功与否,以及任何异常信息。使用Python的logging模块,将日志输出到文件和控制台,便于事后排查。
  • 进度保存:将已成功抓取的股票代码、报表类型、年份实时保存到一个进度文件(如JSON或数据库)中。当程序因网络中断、崩溃或其他原因重启时,可以先读取进度文件,跳过已抓取的内容,实现“断点续爬”。
  • 健康检查:定期(如每抓取20只股票)访问一个简单的测试页面,检查网络连通性和登录状态是否正常。

7. 数据清洗与预处理:为data2text任务做准备

抓取到的原始表格数据往往是“脏”的,不能直接用于模型训练。data2text任务要求输入是干净、结构化的数据。以下是一些常见的清洗步骤:

7.1 处理表头同花顺的表格为了显示美观,表头可能是多层合并单元格。pd.read_html解析后,可能会产生多级索引(MultiIndex)的列名,或者将部分表头误读为第一行数据。

  • 检查列名:打印df.columns查看结构。如果是MultiIndex,需要将其扁平化,例如取最后一级作为列名,或手动拼接。
  • 重命名列:将中文列名转换为有意义的英文缩写或标准字段名,便于后续处理。例如,“流动资产合计” ->total_current_assets。

7.2 处理缺失值与异常格式

  • 识别缺失值:财报表格中,可能用“--”、“-”或空白表示无数据。pd.read_html通常会将它们转换为NaN。需要检查并统一。
  • 处理数字格式:数字可能带有千位分隔符(逗号)、百分号(%)或单位(如“亿元”)。需要使用字符串操作去除这些符号,并转换为数值类型(float)。
    def clean_numeric_column(series): # 去除逗号、空格、百分号、中文单位 series = series.astype(str).str.replace(',', '').str.replace(' ', '') series = series.str.replace('%', '').str.replace('亿元', 'e8').str.replace('万元', 'e4') # 尝试转换为数值,无法转换的设为NaN return pd.to_numeric(series, errors='coerce') for col in df.select_dtypes(include=['object']).columns: df[col] = clean_numeric_column(df[col])

7.3 数据重塑与对齐不同年份、不同报表的数据需要对齐到统一的结构。

  • 统一时间索引:将“年份”和“报告期”合并成一个标准的时间戳索引,如2023-12-31代表2023年年报。
  • 纵向堆叠:我们抓取的是多个时间点的截面数据。对于模型训练,通常需要时间序列数据。可以将同一股票同一报表的不同年份数据,按时间顺序排列。
  • 横向合并:最终可能需要将资产负债表、利润表、现金流量表的数据,根据股票代码和时间,合并成一张宽表,每一行包含一家公司在某个时间点的全部财务指标。

7.4 输出为模型友好格式清洗完成后,根据下游data2text模型的要求输出数据。

  • CSV/JSON Lines:最常见的序列化格式,每行一个样本(如一家公司一年的数据)。
  • 结构化字段:每个样本可以是一个字典,包含stock_code、date、report_type以及各个财务指标字段。
  • 文本化:data2text的输入虽然是结构化数据,但模型可能需要文本形式的描述。我们可以将关键指标(如营业收入、净利润)及其同比变化,用自然语言模板简单描述,作为辅助输入。例如:“{公司}在{年份}年实现营业收入{营收}亿元,同比增长{增长率}%。”

8. 实战中的坑与经验总结

在开发和运行这个爬虫的几个月里,我踩过了几乎所有能踩的坑。这里分享几条血泪教训,希望能帮你节省大量时间。

8.1 元素定位失效:网站改版了怎么办?这是最常见的问题。我的策略是“防御性编码”加“监控告警”。

  • 不要写死定位器:将所有XPath/CSS Selector字符串作为配置项,集中管理在一个config.py或JSON文件中。
  • 使用相对定位和模糊匹配:优先使用包含文本内容的XPath(contains(text(),'...'))或模糊属性匹配(starts-with(@id,'...')),这比依赖一个特定的class名更稳定。
  • 建立元素健康检查:在爬虫主循环开始前,用一个测试股票代码跑一遍所有关键步骤(点击各个选项卡、选择日期),如果任何一步失败,立即发出警报(邮件、Slack等),而不是等到爬了几百个股票后才发现。

8.2 浏览器崩溃或驱动失联Selenium WebDriver与浏览器之间的连接有时会不稳定,特别是在长时间运行且网络波动的情况下。

  • 使用try...except包裹核心操作,并设计重试逻辑。如果发生WebDriverException,可以尝试重新初始化驱动和浏览器(当然,这会丢失当前页面的状态)。
  • 实现会话恢复:如前所述,通过Cookie持久化,即使浏览器崩溃重启,也能快速恢复到登录状态。
  • 使用driver.service.process监控:可以检查浏览器进程是否存活,如果死了,就重新启动。

8.3 数据解析错误:表格结构不统一即使在同一网站,不同公司的财报表格细微结构也可能有差异(比如少数栏目不同)。

  • 宽容的解析器:pd.read_html(flavor='html5lib')比默认的lxml容错性更好。
  • 数据验证:解析后,检查DataFrame的列数是否在预期范围内,是否存在大量NaN列。可以建立一个“预期列名”的白名单,过滤掉无关的列。
  • 保存原始HTML:在解析失败时,除了截图,最好将出问题的表格HTML原始代码也保存下来,供后续离线分析和调试。

8.4 法律与伦理边界最后,也是最重要的一点。

  • 遵守robots.txt:检查同花顺的robots.txt文件,尊重网站关于爬虫抓取频率和目录的限制。
  • 限制抓取速度:这是最基本的礼貌,也是对自身IP的保护。不要对网站服务器造成压力。
  • 数据用途:抓取的数据应用于个人学习、研究或内部分析。切勿用于商业售卖、发布或从事任何可能侵犯网站权益的活动。公开的数据并不等于可以无限制地商业化利用。
  • 用户代理标识:在ChromeOptions中设置一个合理的User-Agent,并在请求头中可能包含一个联系方式,以示友好。
    chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 MyResearchBot/1.0 (contact: your-email@example.com)')

这个项目从零到一构建了一个针对复杂金融网站的数据采集系统,它远不止几行抓取代码那么简单,涉及浏览器伪装、会话管理、反爬对抗、异常处理、数据清洗等一系列工程化问题。实际部署时,你可能还需要考虑任务队列(如Celery)、分布式抓取等更复杂的架构。但无论如何,本文提供的核心思路和代码模块,已经为你打下了一个坚实可靠的基础。记住,爬虫是与网站运维人员的一场“动态博弈”,保持代码的灵活性、可维护性和友好的抓取习惯,才能让你的数据管道行稳致远。

本文还有配套的精品资源,点击获取

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

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

立即咨询