简介:本资源是一套面向金融数据分析从业者、NLP研究者及量化初学者的实战型数据采集工具包,聚焦解决上市公司财务数据自动化获取难、反爬机制强、data2text任务缺乏高质量结构化语料等核心痛点。压缩包共9个文件(375KB),含5个txt日志与配置文件(如stocks.txt、failed_stocks.txt)、1个核心Python爬虫脚本(main.py)、1个README.md说明文档、1个PNG示例数据图、1个DOCX附赠资料及1个TXT说明文件,覆盖从环境配置、浏览器伪装、Cookie动态更新到异常处理的完整链路。已有55人学习下载,适用于构建财报摘要生成、智能研报系统等data2text下游任务。用户可直接运行Selenium自动化脚本抓取同花顺财务报表,复用预置的UA轮换、请求头伪造与浏览器行为模拟策略,显著提升在强反爬场景下的采集稳定性与可持续性。
1. 项目概述:从数据采集到文本生成的金融数据管道
最近在做一个金融领域的data2text项目,简单来说,就是让模型能读懂财务报表,然后自动生成像分析师一样的解读报告。这个想法听起来很酷,但第一步就卡住了:数据从哪来?我需要大量结构化的、干净的上市公司财务数据作为训练原料。市面上的数据API要么太贵,要么字段不全,要么更新不及时。于是,目光自然就转向了同花顺这类财经门户网站,它们的数据既免费又相对全面。但手动复制粘贴显然不现实,这就引出了我们今天的主题:如何构建一个稳定、可靠、能对抗网站反爬机制的自动化数据采集系统。
这个项目的核心,就是利用Selenium这个浏览器自动化工具,模拟真实用户的操作,从同花顺网站上将上市公司的三大报表(资产负债表、利润表、现金流量表)数据“抓”下来,并经过清洗和格式化,变成适合后续模型训练的规整数据集。整个过程听起来像是简单的“爬虫”,但在金融数据这个场景下,它远不止于此。你需要处理动态加载的JavaScript、应对频繁的登录验证和Cookie过期、伪装成正常用户访问以避免被封IP,还要保证数据抓取的准确性和完整性。这更像是一个小型的“数据工程”项目,每一步都充满了细节和陷阱。
如果你也在为寻找特定领域的数据源发愁,或者对如何用自动化工具高效获取网页数据感兴趣,尤其是面对那些反爬措施比较严格的网站,那么我在这趟“踩坑”之旅中总结的经验,或许能帮你省下不少时间。接下来,我会详细拆解整个系统的设计思路、关键技术的实现细节,以及那些在文档里找不到的实战心得。
2. 核心需求与方案选型:为什么是Selenium?
在动手写代码之前,明确需求和选择合适的工具至关重要。金融数据采集有其特殊性,这直接决定了我们的技术方案。
2.1 目标网站分析与核心挑战
同花顺的财务数据页面(例如,某个公司的深度资料-F10-财务概况页面)是典型的高度动态化的现代Web应用。数据并非直接写在HTML源码里,而是通过JavaScript异步加载(Ajax)渲染到页面上的。这意味着,如果你用传统的requests库直接去请求网页URL,拿到的只是一个空的页面骨架,看不到任何财务数字。
主要挑战包括:
- 动态内容加载:核心数据通过JS渲染,简单HTTP请求无效。
- 登录与会话维持:查看详细财务数据通常需要登录账号,且会话(Cookie)有有效期。
- 反爬虫机制:网站会检测异常访问行为,如高频请求、无头浏览器特征、缺少正常用户交互等,可能导致IP被暂时封锁或要求验证码。
- 页面结构复杂:数据分布在多个标签页(Tab)下,需要模拟点击切换。
- 数据格式化:抓取到的数据是夹杂在HTML标签中的文本,需要精确解析和清洗,转换为结构化的CSV或JSON。
2.2 工具选型:Selenium的胜出
面对这些挑战,我们有几个备选方案:
- Requests + 逆向工程:直接分析网站的数据接口(XHR/Fetch),模拟请求。这最高效,但同花顺的接口可能经过加密、携带复杂令牌(Token),逆向难度大,且接口一旦变动,维护成本高。
- Playwright/Puppeteer:新一代浏览器自动化工具,比Selenium更现代,API更优雅,性能也更好。但对于这个项目,其生态和资料量相对Selenium略少。
- Selenium:老牌、稳定、生态极其丰富的浏览器自动化工具。它通过WebDriver直接控制Chrome、Firefox等真实浏览器,能完美执行点击、输入、滚动等操作,看到的就是用户看到的最终页面。
我最终选择Selenium,基于以下几点考量:
- 完美解决JS渲染问题:Selenium驱动真实浏览器,页面中的所有JavaScript都会被执行,动态数据自然呈现,我们直接对最终DOM进行操作即可。
- 高度拟人化:可以轻松模拟登录、点击标签页、滚动页面等所有用户行为,极大地降低了被反爬系统识别为机器人的风险。
- 强大的元素定位能力:结合XPath、CSS Selector,可以精准定位到页面中任何一个数据单元格。
- 丰富的社区与资料:任何你遇到的问题,几乎都能在Stack Overflow或中文技术社区找到解决方案,这对于快速开发和问题排查至关重要。
- 灵活性:虽然性能上不如直接调用接口,但在应对复杂交互和反爬策略时,其“以不变应万变”的能力非常突出。
注意:Selenium的速度相对较慢,因为它要启动并渲染整个浏览器。如果对速度有极致要求,且网站接口易于逆向,
requests方案更优。但对于同花顺这类防护严密的金融站点,稳定性和成功率优先,Selenium是更稳妥的选择。
3. 系统设计与核心模块拆解
整个数据采集系统可以看作一个管道(Pipeline),我将其分为五个核心模块,如下图所示(此处用文字描述逻辑流程):
[启动] -> [浏览器驱动与伪装模块] -> [登录与会话管理模块] -> [数据导航与抓取模块] -> [数据解析与存储模块] -> [结束] | | | | |-- 反检测配置 |-- Cookie处理 |-- 页面元素遍历 |-- 文本清洗 |-- 用户代理(User-Agent)设置 |-- 自动更新/重登逻辑 |-- 多标签页/表切换逻辑 |-- 结构化存储(CSV/JSON)3.1 浏览器驱动与反爬伪装模块
这是整个系统的基石。目标不仅是启动浏览器,更是要让它“看起来”像一个真实的用户在操作。
核心配置项:
- 无头模式(Headless Mode)选择:开发调试时建议关闭无头模式(
headless=False),这样你能看到浏览器的所有操作,便于定位问题。在生产环境批量运行时可以开启,节省资源。但要注意,一些网站会检测无头模式。from selenium import webdriver from selenium.webdriver.chrome.options import Options chrome_options = Options() # 生产环境可开启 # chrome_options.add_argument('--headless') # 关闭“Chrome正受到自动测试软件控制”的提示栏,这个提示栏可能被网站检测到 chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) - 用户代理(User-Agent)伪装:使用最新的、常见的浏览器UA字符串,避免使用Selenium默认的UA。
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') - 禁用WebDriver属性:现代浏览器会暴露一个
navigator.webdriver属性,通常为true,这很容易被检测。我们需要通过CDP命令将其覆盖。driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' }) - 其他实用参数:
# 禁用图片加载,加速页面渲染(如果不需要图片) # chrome_options.add_argument('--blink-settings=imagesEnabled=false') # 禁用GPU加速,避免一些兼容性问题 chrome_options.add_argument('--disable-gpu') # 设置窗口大小,避免响应式布局导致元素定位失败 chrome_options.add_argument('--window-size=1920,1080')
实操心得:反爬是一场攻防战。上述配置能绕过大部分基础检测,但如果网站风控升级,可能需要更复杂的策略,如使用undetected-chromedriver这类专门对抗检测的库,或者引入随机延迟、模拟鼠标移动轨迹等。
3.2 登录与会话管理模块(Cookie持久化与更新)
对于需要登录的网站,管理会话是保证长期稳定运行的关键。我们不能每次运行脚本都手动登录。
方案:Cookie持久化
- 首次手动登录,保存Cookie:在调试模式下,运行脚本,程序会暂停,等待你手动在打开的浏览器窗口中完成登录(包括可能的验证码)。登录成功后,将驱动器的Cookies保存到本地文件。
import pickle import time def save_cookies(driver, path): # 等待登录完成,比如通过判断某个登录后才会出现的元素 time.sleep(20) # 给你手动操作的时间 with open(path, 'wb') as file: pickle.dump(driver.get_cookies(), file) print("Cookies saved successfully.") # 首次运行后调用 # save_cookies(driver, '同花顺_cookies.pkl') - 后续运行加载Cookie:再次运行脚本时,先加载本地Cookies并添加到浏览器,然后访问目标页面。如果Cookie有效,则直接进入登录状态。
def load_cookies(driver, path): try: with open(path, 'rb') as file: cookies = pickle.load(file) for cookie in cookies: # 添加前可能需要删除'expiry'字段,因为可能是浮点数导致错误 if 'expiry' in cookie: # 处理过期时间,或直接删除 del cookie['expiry'] driver.add_cookie(cookie) print("Cookies loaded successfully.") return True except FileNotFoundError: print("Cookie file not found. Need manual login.") return False driver.get('https://www.10jqka.com.cn/') # 先访问域名 time.sleep(2) if load_cookies(driver, '同花顺_cookies.pkl'): driver.refresh() # 刷新页面使Cookie生效 else: # 触发手动登录流程 input("请手动登录,完成后按回车继续...") save_cookies(driver, '同花顺_cookies.pkl') - Cookie失效处理:Cookie会过期。我们需要在抓取数据前,通过检查某个登录后特有的元素(如用户昵称显示)来判断当前是否仍处于登录状态。如果失效,则提示需要重新手动登录,并更新Cookie文件。
重要提示:Cookie包含敏感信息(会话令牌)。请勿将Cookie文件上传至公开的代码仓库(如GitHub)。务必将其添加到
.gitignore文件中。
3.3 数据导航与抓取模块
这是业务逻辑的核心。我们需要编写代码来“指挥”浏览器找到目标公司页面,并点击切换到正确的财务数据标签。
步骤拆解:
- 定位公司页面:通常我们有一个股票代码列表。同花顺的公司深度资料页有固定URL格式,例如
https://basic.10jqka.com.cn/股票代码/。直接拼接访问即可。 - 等待页面加载:使用Selenium的“显式等待”(WebDriverWait),这是最佳实践。它比固定的
time.sleep更高效、更稳定。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 访问公司页面 stock_code = '000001' # 平安银行 driver.get(f'https://basic.10jqka.com.cn/{stock_code}/') # 等待页面关键元素加载,比如“财务概况”这个Tab try: finance_tab = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, "//a[contains(text(), '财务概况')]")) ) print("公司页面加载成功。") except TimeoutException: print("页面加载超时或元素未找到。") - 切换财务数据Tab:找到“财务概况”、“资产负债表”、“利润表”、“现金流量表”等对应的链接或按钮,并点击。
finance_tab.click() time.sleep(2) # 等待Tab内容切换,可以结合等待条件优化 - 选择报告期:财务数据通常有多个报告期(如年报、季报)。需要定位下拉框(
<select>)并选择目标期数。from selenium.webdriver.support.ui import Select # 假设报告期下拉框的id是'report_period' period_select = Select(driver.find_element(By.ID, 'report_period')) period_select.select_by_visible_text('2023年年报') # 根据文本选择 # 或者 select_by_value('20231231') time.sleep(3) # 等待数据重新加载
实操心得:页面元素定位是Selenium编程中最耗时也最容易出错的部分。强烈建议使用浏览器的开发者工具(F12)的“检查”功能,结合Copy -> Copy XPath或Copy selector来获取元素路径。但自动生成的XPath可能很脆弱(例如包含变化的索引),最好能自己编写更稳定的相对XPath或CSS选择器。
3.4 数据解析与存储模块
抓取到数据所在的HTML表格后,我们需要将其“榨取”成结构化的数据。
- 定位数据表格:使用XPath或CSS选择器找到包含财务数据的
<table>元素。# 假设数据在一个id为`BalanceSheet`的table里 table = driver.find_element(By.ID, 'BalanceSheet') - 解析表格行与列:遍历
<tr>和<td>标签。data_rows = [] for row in table.find_elements(By.TAG_NAME, 'tr'): cols = row.find_elements(By.TAG_NAME, 'td') if cols: # 跳过空行或表头行(可能用<th>) row_data = [col.text.strip() for col in cols] data_rows.append(row_data) - 数据清洗:
- 处理空值和特殊字符:将
'-'、'--'转换为None或空字符串。 - 格式化数字:去除数字中的逗号(如
1,234.56)、百分号(5.6%),并将其转换为浮点数或整数。注意处理单位(如“亿元”)。 - 识别表头:通常第一行或前几行是表头,需要单独处理,作为DataFrame的列名。
- 处理空值和特殊字符:将
- 结构化存储:使用
pandas库将数据列表转换为DataFrame,然后保存为CSV或JSON文件。每个公司、每张报表最好单独存储,并包含股票代码和报告期作为元数据。import pandas as pd df = pd.DataFrame(data_rows[1:], columns=data_rows[0]) # 假设第一行是表头 filename = f'data/{stock_code}_资产负债表_2023年报.csv' df.to_csv(filename, index=False, encoding='utf-8-sig') # 用utf-8-sig避免Excel打开乱码
4. 核心代码实现与关键步骤详解
让我们将上述模块组合起来,看一个抓取某公司资产负债表的核心代码片段。
import pickle import time import pandas as pd from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException class ThsFinancialCrawler: def __init__(self, cookie_path='ths_cookies.pkl', headless=False): self.cookie_path = cookie_path self.driver = self._init_driver(headless) def _init_driver(self, headless): """初始化并伪装浏览器驱动""" options = webdriver.ChromeOptions() if headless: options.add_argument('--headless') options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) options.add_argument('user-agent=Mozilla/5.0...') # 替换为你的UA driver = webdriver.Chrome(options=options) # 执行CDP命令,隐藏webdriver属性 driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' }) return driver def login_or_load_cookie(self): """登录或加载Cookie维持会话""" self.driver.get('https://www.10jqka.com.cn/') time.sleep(3) try: with open(self.cookie_path, 'rb') as f: cookies = pickle.load(f) for cookie in cookies: if 'expiry' in cookie: del cookie['expiry'] self.driver.add_cookie(cookie) print("Cookie加载成功,刷新页面...") self.driver.refresh() time.sleep(3) # 验证登录是否成功 if self._check_login(): print("登录状态有效。") return True else: print("Cookie已失效。") raise Exception("Invalid Cookie") except (FileNotFoundError, Exception): print("需要手动登录。请在打开的浏览器窗口中完成登录。") input("登录完成后,在此按回车键继续...") with open(self.cookie_path, 'wb') as f: pickle.dump(self.driver.get_cookies(), f) print("新Cookie已保存。") return True def _check_login(self): """检查是否处于登录状态,例如查找用户昵称元素""" try: # 这里需要替换为登录后页面实际存在的元素选择器 self.driver.find_element(By.CLASS_NAME, 'user-name') return True except NoSuchElementException: return False def crawl_balance_sheet(self, stock_code, report_year='2023'): """抓取指定股票代码、指定年份的资产负债表""" url = f'https://basic.10jqka.com.cn/{stock_code}/' self.driver.get(url) print(f"开始处理股票 {stock_code}...") try: # 1. 点击‘财务概况’或直接进入财务页面 # 这里XPath需要根据实际页面调整 finance_link = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//a[contains(@href, '/finance/') or contains(text(), '财务')]")) ) finance_link.click() time.sleep(2) # 2. 切换到资产负债表Tab # 注意:页面内可能有iframe,需要先切换进去。这里假设没有或已处理。 balance_sheet_tab = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//*[contains(text(), '资产负债表')]")) ) balance_sheet_tab.click() time.sleep(3) # 等待表格加载 # 3. 选择报告期 # 找到下拉框,可能需要滚动到元素可见 select_element = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//select[@id='report_date']")) ) select = Select(select_element) # 选择对应年份的年报,这里逻辑需要根据下拉框选项调整 select.select_by_visible_text(f'{report_year}年年报') time.sleep(4) # 重要!等待数据异步加载完成 # 4. 定位并解析表格 table = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//table[@class='m-table']//tbody")) ) rows = table.find_elements(By.TAG_NAME, 'tr') data = [] for row in rows: cols = row.find_elements(By.TAG_NAME, 'td') if cols: row_data = [col.text for col in cols] data.append(row_data) # 5. 转换为DataFrame并保存 if data: # 假设第一行是表头(项目,期末余额,期初余额...) df = pd.DataFrame(data) output_file = f'./data/{stock_code}_balance_sheet_{report_year}.csv' df.to_csv(output_file, index=False, header=False, encoding='utf-8-sig') print(f"资产负债表数据已保存至 {output_file}") return df else: print("未找到表格数据。") return None except TimeoutException as e: print(f"在抓取{stock_code}时发生超时错误: {e}") # 可以截图保存现场,便于调试 self.driver.save_screenshot(f'error_{stock_code}.png') return None except Exception as e: print(f"抓取{stock_code}时发生未知错误: {e}") return None def close(self): """关闭浏览器""" self.driver.quit() # 使用示例 if __name__ == '__main__': crawler = ThsFinancialCrawler(headless=False) # 调试时关闭无头模式 try: if crawler.login_or_load_cookie(): # 可以遍历股票代码列表 df = crawler.crawl_balance_sheet('000001', '2023') print(df.head()) finally: crawler.close()关键步骤详解:
- 等待策略:代码中混合使用了
WebDriverWait(显式等待)和time.sleep(固定等待)。显式等待是首选,它只在条件满足时才继续,高效。但对于某些复杂的、非标准的Ajax加载,有时不得不使用短暂的sleep来确保数据渲染完成。这需要根据目标网站的具体行为进行调整和测试。 - 异常处理:
try...except块至关重要。网络不稳定、元素定位失败、页面结构微调都会导致程序崩溃。良好的异常处理能记录错误、跳过当前任务(如当前股票),继续执行下一个,保证批量任务的完成度。 - 页面结构变化:同花顺前端的任何改版都可能导致XPath失效。因此,定位器(如XPath表达式)应该尽可能使用相对路径和稳定的属性(如
id、name,或包含特定文本内容),避免使用绝对路径和易变的索引(如div[3]/table[2])。
5. 反爬策略进阶与实战避坑指南
即使做了基础伪装,在长时间、大批量抓取时,仍然可能触发网站的反爬机制。以下是我在实践中总结的进阶策略和常见问题。
5.1 高级反反爬策略
- 请求频率控制:这是最重要的原则。在循环抓取不同公司数据时,在每次请求之间加入随机延迟,模拟人类阅读和点击的间隔。
import random import time def random_delay(min_sec=2, max_sec=5): delay = random.uniform(min_sec, max_sec) time.sleep(delay) # 在抓取每个公司后调用 # random_delay(3, 8) - IP代理池:如果单个IP请求过于频繁被封,就需要使用代理IP。可以购买付费代理服务,或者使用一些免费的代理IP(但稳定性差)。在Selenium中设置代理:
chrome_options.add_argument(f'--proxy-server=http://{proxy_ip}:{proxy_port}') - 浏览器指纹多样化:除了UA,还可以随机化其他浏览器特征,如屏幕分辨率、时区、语言等。这可以通过CDP命令或加载特定插件来实现,但复杂度较高。
- 使用
undetected-chromedriver:这是一个专门为绕过Cloudflare等高级反爬和浏览器指纹检测而修改的ChromeDriver。如果你的基础Selenium脚本被频繁拦截,可以尝试换用它。import undetected_chromedriver as uc driver = uc.Chrome() - 验证码处理:如果遇到验证码,最简单的策略是遇到就停。在代码中检测验证码页面的出现(例如,查找验证码图片元素),一旦发现,就记录下当前任务,暂停程序,等待人工干预解决后,再继续运行。自动识别验证码(OCR或打码平台)成本高且不稳定,对于个人项目不推荐。
5.2 常见问题与排查技巧实录
以下是我在开发过程中遇到的一些典型问题及解决方法,整理成了速查表:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
NoSuchElementException元素找不到 | 1. 页面未加载完成。 2. XPath/CSS选择器写错了。 3. 元素在 iframe或shadow DOM内。4. 页面结构已更新。 | 1. 增加等待时间,使用WebDriverWait。2. 用浏览器开发者工具重新检查并验证选择器。 3. 使用 driver.switch_to.frame()切换到对应的iframe。4. 更新你的元素定位代码。 |
ElementNotInteractableException元素不可交互 | 1. 元素被遮挡(如弹窗)。 2. 元素在视窗外,需要滚动。 3. 元素是 disabled状态。 | 1. 关闭遮挡物。 2. 使用 driver.execute_script("arguments[0].scrollIntoView();", element)滚动到元素可见。3. 等待元素变为可交互状态。 |
| 页面加载慢或超时 | 1. 网络问题。 2. 网站服务器响应慢。 3. 页面资源(如图片、JS)过多。 | 1. 增加driver.implicitly_wait或WebDriverWait的超时时间。2. 在 ChromeOptions中禁用图片加载(--blink-settings=imagesEnabled=false)。3. 考虑使用代理。 |
| Cookie登录后很快失效 | 1. Cookie生命周期短。 2. 网站有额外的安全校验(如IP绑定)。 3. 保存/加载Cookie的格式有问题。 | 1. 定期(如每天)重新运行一次手动登录流程。 2. 尝试保持IP地址相对固定。 3. 检查 pickle保存和加载过程,确保expiry字段被正确处理。 |
| 被网站识别为爬虫,弹出验证码或拒绝访问 | 反爬系统检测到自动化行为特征。 | 1. 检查并完善浏览器伪装(见3.1节)。 2. 大幅降低请求频率,增加随机延迟。 3. 更换User-Agent。 4. 考虑使用 undetected-chromedriver。5. 终极方案:更换IP(代理)。 |
| 抓取到的数据是空的或格式错乱 | 1. 表格数据是异步加载的,抓取时机过早。 2. 定位到了错误的表格元素。 3. 数字中包含非标准字符(如全角空格、特殊单位)。 | 1. 在点击“选择报告期”后,增加足够的等待时间,或等待特定数据单元格出现。 2. 打印出抓取到的HTML片段进行调试。 3. 加强数据清洗逻辑,使用正则表达式匹配和替换。 |
独家避坑技巧:
- 多用
try-except,少用if:在遍历元素(如表格行)时,直接使用try-except包裹查找操作,比先判断元素是否存在更简洁健壮。 - 善用截图:当脚本在无头模式下运行出错时,你什么都看不到。在关键步骤后或异常捕获时,使用
driver.save_screenshot('debug.png')保存截图,能极大帮助离线调试。 - 分阶段测试:不要一次性写完整个爬虫。先测试能否打开页面,再测试能否登录,然后测试能否找到某个按钮,最后测试抓取一条数据。步步为营,容易定位问题。
- 尊重
robots.txt:在开始大规模抓取前,检查目标网站的robots.txt文件(如https://www.10jqka.com.cn/robots.txt),了解网站允许和禁止爬取的目录。遵守规则是长期可持续抓取的前提。
6. 数据预处理:为data2text任务做准备
抓取到的原始数据还不能直接喂给模型。对于data2text任务,我们需要将结构化的表格数据,转换成模型易于理解的“文本”格式。这个过程就是数据预处理。
6.1 数据清洗与规整化
- 合并多期数据:我们通常需要多个报告期的数据(如过去5年的年报)。将抓取到的多个CSV文件(每个文件代表一个公司一年的一张报表)进行合并,形成一个“宽表”,每一行是一个公司-年份,每一列是一个财务指标。
import os import pandas as pd all_data = [] for file in os.listdir('./data'): if file.endswith('_balance_sheet_2023.csv'): stock_code = file.split('_')[0] df = pd.read_csv(f'./data/{file}', header=None) # 假设无表头 # 这里需要复杂的逻辑来提取特定行(如“流动资产合计”、“负债合计”)并转置为列 # ... (具体解析逻辑取决于表格格式) # processed_series = ... 得到一个Series,索引是指标名,值是指标值 # processed_series['股票代码'] = stock_code # processed_series['报告期'] = '2023' # all_data.append(processed_series) final_df = pd.concat(all_data, axis=1).T # 合并所有Series final_df.to_csv('combined_balance_sheet_2023.csv', index=False) - 处理缺失值与异常值:某些公司可能缺少某些年份的数据,或数据存在明显错误(如负的资产)。需要进行填充(用前后年份均值)或剔除。
- 数据标准化:不同公司的资产规模差异巨大。直接使用绝对值(如“总资产:1,000,000万元”)不利于模型学习。通常需要进行标准化或归一化,例如,计算各个指标的同比增长率、占总资产的比例(共同比报表)等衍生指标。这些比率比绝对值更具可比性和信息量。
6.2 构建模型输入文本
data2text模型的输入通常是一段包含关键信息的“扁平化”文本。我们需要将规整好的结构化数据,转换成自然语言描述。
原始结构化数据(示例):
股票代码: 000001 报告期: 2023 流动资产合计: 4,500,000 (万元) 非流动资产合计: 3,200,000 (万元) 资产总计: 7,700,000 (万元) 流动负债合计: 3,800,000 (万元) ...转换后的模型输入文本(示例):
“平安银行(000001)2023年年报显示,其总资产达到77000亿元,较上年增长约10%。其中,流动资产为45000亿元,非流动资产为32000亿元。负债方面,流动负债为38000亿元...”构建脚本的核心思路:
- 设计模板:创建一个包含占位符的文本模板。
template = “{company_name}({stock_code}){report_year}年年报显示,其总资产达到{total_assets}亿元,较上年增长约{asset_growth_rate}%。其中,流动资产为{current_assets}亿元,非流动资产为{non_current_assets}亿元...” - 数据映射:将DataFrame中的每一行数据,填充到模板的对应占位符中。
for idx, row in final_df.iterrows(): input_text = template.format( company_name=row['公司名称'], stock_code=row['股票代码'], report_year=row['报告期'][:4], # 取年份 total_assets=row['资产总计'] / 10000, # 转换为亿元 asset_growth_rate=row['资产同比增长率'], # ... 填充其他字段 ) # 将input_text和对应的目标分析报告(需要人工标注或从其他渠道获取)配对,存入训练集文件 - 多样性:可以设计多个不同侧重点的模板(如侧重盈利能力、偿债能力、营运能力),从同一份数据生成多条训练样本,增加数据的多样性。
这个预处理流程是data2text任务成败的关键之一。高质量、一致性好的输入文本,能极大提升模型生成报告的可读性和准确性。整个从爬虫到预处理的管道搭建完毕后,你就拥有了一个可持续生产训练数据的“武器库”,为后续的模型训练打下了坚实的基础。
本文还有配套的精品资源,点击获取