ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python爬虫+数据可视化:股票分析系统实现与踩坑复盘

Python爬虫+数据可视化:股票分析系统实现与踩坑复盘 做这个“基于Python爬虫的股票信息可视化分析系统”最初动机很朴素每天都有一堆财经数据要看但光看数字太费脑子Python爬虫加上可视化能让我把抓数据、清洗、整理、出图这一套流程自动化最后用看板把关键信息一屏展示出来。这篇文章就是我从零搭建这套系统的完整复盘从技术选型、爬虫实现到可视化看板再到踩过的一堆坑适合想用Python做数据分析与可视化实践的初学者也适合准备拿类似项目练手或做毕业设计的读者。1. 系统设计先把整体架构和模块边界想清楚1.1 一句话读懂系统的工作原理很多朋友拿到这类项目第一反应是“赶紧写爬虫”但我建议先把系统拆成四个环节数据采集 → 数据存储 → 数据分析 → 可视化展示。这个项目做的就是把这四个环节串成一条流水线。数据采集层负责从公开的信息源抓取数据数据存储层用SQLite这种轻量级数据库把抓回来的原始数据落盘避免每次都重新爬分析层用Pandas做清洗和计算比如算同比、环比、均值、相关性可视化层则用pyecharts把分析结果渲染成折线图、柱状图、热力图最后组合成一个可以交互的看板页面。这个流程想清楚之后整个项目的边界就明确了。比如我一开始根本没考虑做用户登录、权限管理这些功能因为分析系统的价值在于“把数据变可读”而不是做一个业务平台。边界越清晰后面写代码就越省心。1.2 技术选型的取舍为什么是PythonRequestspyecharts技术栈看起来是网上随手搜“免费python源码大全”就能拼凑出来的但真正落地时每一步选择背后都有理由。爬虫方面我选Requests而不是Scrapy。Scrapy的并发和调度确实强但个人项目里要抓的公开数据量没那么大Scrapy的项目结构反而让新手绕远路。Requests配合BeautifulSoup30分钟就能写出一个能跑的采集脚本足够应付大多数静态表格页面。存储方面直接用SQLite。有人觉得“正式的”系统应该上MySQL但单机分析项目里SQLite完全够用零配置、单文件、Python内置支持想迁移到MySQL时改下连接字符串就行。可视化我选pyecharts而不是Matplotlib。Matplotlib出的是静态图pyecharts生成的是可交互的HTML图表支持鼠标悬停、缩放、数据下钻。做数据分析看板交互感很重要但这只要求Python爬虫可视化界面不需要上Tableau或PowerBI那种商业工具。后面我会把整套代码拆开讲但建议别直接复制网上的源码包自己手敲一遍才能理解每个函数为什么存在。2. 数据采集层怎么拿到干净、合规、可用的数据2.1 数据源的选择公开、稳定、明确授权动手爬之前最重要的一件事是确认数据源合规。我这套系统聚焦的是上市公司年度报告里公开披露的财务指标、宏观统计部门发布的公开数据、行业协会发布的景气指数这类信息。这些数据本身就是面向公众发布的统计信息抓取和再分析的空间很清晰。这里必须强调一句不要去碰需要登录、需要付费授权、或者协议里明确禁止爬取的平台也不要尝试高频请求公开接口。做个人学习项目守住“公开数据、低频抓取、标明出处”这三个底线比任何技术都重要。选数据源还有一个标准稳定性。有些页面今天能访问明天改版你就要重新写解析逻辑。我建议优先选结构变化少的页面比如固定表格、固定参数的公开API。把数据源URL和字段说明记在一个配置文件里方便后期维护。2.2 通用采集模板RequestsBeautifulSoup抓取公开表格数据先给一个最通用的静态表格抓取模板这是我在多个项目里反复用的核心函数import requests from bs4 import BeautifulSoup import time from functools import wraps def retry(times3, delay2): 简单的重试装饰器遇到网络异常或反爬时自动重试 def decorator(func): wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: print(f第{i1}次请求失败: {e}) if i times - 1: time.sleep(delay) raise RuntimeError(请求重试多次仍失败) return wrapper return decorator retry(times3, delay3) def fetch_html_table(url, table_index0, encodingNone): 抓取页面中的第table_index个表格返回二维列表 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } resp requests.get(url, headersheaders, timeout10) # 优先用响应头里的charset识别不到再用apparent_encoding if encoding: resp.encoding encoding else: resp.encoding resp.apparent_encoding or utf-8 soup BeautifulSoup(resp.text, html.parser) table soup.find_all(table)[table_index] rows table.find_all(tr) data [] for row in rows: cells row.find_all([td, th]) # stripTrue顺手去掉单元格首尾空白比replace清空格更干净 data.append([cell.get_text(stripTrue) for cell in cells]) return data这个模板有几个细节值得讲。retry装饰器解决了爬虫最常见的“偶发失败”。抓公开数据时经常遇到连接被重置或超时如果只写一次请求脚本跑一半就挂了。我在实际项目里重试次数设为3次间隔3秒这样既能扛住偶发问题又不会因为过于频繁地重试给目标站点造成压力。resp.encoding resp.apparent_encoding or utf-8是抓中文页面时最关键的设置。Requests的encoding属性默认是从响应头推断的但很多中文页面响应头不写charset或者写的是西文编码直接解析就会出现乱码。apparent_encoding是Requests基于内容字节流猜测的编码对中文页面准确率更高。cells row.find_all([td, th])统一抓取数据单元格和表头单元格这样表格的首行自然就成了字段名不用单独处理。有了这个函数你可以把任意公开页面的第一个表格变成Python里的二维列表接下来就轮到Pandas做清洗。2.3 数据清洗与标准化脏数据比没数据更可怕抓回来的二维列表不能直接入库实际的表格里千奇百怪有空行、有合并单元格、有“--”表示缺数、有“1,234.56”这种带千分位的字符串还有百分比和小数混着来的情况。如果不过清洗后面画出来的图就是乱的。我的清洗流程分三步。第一步转成DataFrame并重命名列import pandas as pd def clean_table_to_df(raw_data, column_namesNone): if column_names: df pd.DataFrame(raw_data[1:], columnscolumn_names) else: # 第一行当表头 df pd.DataFrame(raw_data[1:], columnsraw_data[0]) # 去掉全空行 df df.dropna(axis0, howall) return df第二步是统一数值格式。公开表格里的数字经常是“1,234.56”这样的文本直接转float会报错。我用一个函数统一处理def to_float(x): 把带千分位逗号、百分号、--的文本统一转成数字 if x is None: return None if isinstance(x, str): x x.strip().replace(,, ).replace(%, ).replace( , ) if x in (, --, -, N/A): return None try: val float(x) except ValueError: return None # 如果原数据带%说明是百分比统一转成小数方便计算 if % in str(x) if False else False: return val / 100 return val return float(x)第三步是去重。同一页数据因为多次抓取可能会重复入库我在后面入库环节会用“日期指标名”做主键重复数据直接忽略这比清洗阶段去重更可靠。清洗这事看起来不性感但它是整个系统里最容易踩坑的地方。我见过太多项目因为列类型不对、空值没处理图表画出来一片乱码。记住一句话分析结果的可信度取决于数据清洗的认真程度。3. 数据存储与增量更新让系统不是一次性玩具3.1 SQLite表结构设计清洗完的数据存在SQLite里表结构我设计得尽量简单CREATE TABLE IF NOT EXISTS financial_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, indicator_name TEXT NOT NULL, -- 指标名称 region TEXT, -- 地区/板块 date_value TEXT NOT NULL, -- 数据日期 value REAL, -- 数值 unit TEXT, -- 单位 source_url TEXT, -- 数据来源 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(date_value, indicator_name, region) -- 去重约束 );这里把date_value存成文本而不是日期类型因为公开表格里的日期格式五花八门有的是“202401”有的是“2024-01”还有的是“2024/1/31”统一成文本之后在可视化阶段再统一格式化反而更灵活。UNIQUE(date_value, indicator_name, region)这个约束是防重复的终极方案。爬虫脚本重复执行时同一日期同一指标的记录插入会直接报唯一性冲突我就在插入逻辑里捕获异常并忽略实现幂等写入。3.2 入库逻辑与增量更新策略入库代码用Pandas配合sqlite3import sqlite3 from datetime import datetime def save_df_to_sqlite(df, db_pathstock_analysis.db): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS financial_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, indicator_name TEXT NOT NULL, region TEXT, date_value TEXT NOT NULL, value REAL, unit TEXT, source_url TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(date_value, indicator_name, region) ) ) insert_sql INSERT OR IGNORE INTO financial_data (indicator_name, region, date_value, value, unit, source_url) VALUES (?, ?, ?, ?, ?, ?) for _, row in df.iterrows(): cursor.execute(insert_sql, ( row.get(indicator_name), row.get(region), row.get(date_value), row.get(value), row.get(unit), row.get(source_url), )) conn.commit() conn.close()INSERT OR IGNORE配合唯一约束就是最简单的增量更新策略。第一次抓取把历史数据全量入库之后每天或每周再跑一次新数据自动插入老数据自动忽略。实际上这就是分布式爬虫的基本思路的缩小版——把“调度”和“抓取”解耦。个人项目不需要上Scrapy-Redis那种分布式框架但保留“增量”这个意识很重要。系统跑起来之后不需要每次都重爬数据越攒越多分析维度才丰富。3.3 定时执行的落地方式数据更新频率取决于你要分析什么。宏观指标月度更新行业景气度季度更新这种低频数据完全可以用系统自带的定时任务来跑。Windows下我用任务计划程序macOS/Linux用crontab每天凌晨2点执行一次爬虫脚本0 2 * * * cd /opt/stock_project /usr/bin/python3 scripts/update_data.py logs/crawl.log 21新手容易忽略日志。爬虫必须打日志至少要记录三点这次执行跑了多久、成功抓取多少条、失败重试了几次。没有日志第二天发现数据没更新你连从哪儿排查都不知道。4. 可视化分析层把数字变成能拍板的图4.1 图表选型不同分析场景用什么图可视化不是炫技是帮人更快理解数据。我的选型逻辑参考了“企业级数据可视化”常见做法按场景选图。分析场景推荐图表原因趋势变化长期看涨/跌折线图突出连续变化的方向和幅度同期对比不同板块/地区柱状图直观比较大小差距结构占比某指标构成饼图/环形图展示部分与整体的关系多指标相关性热力图快速发现两两相关关系单个指标预警仪表盘阈值一目了然比如要展示某个板块最近12个月的财务指标趋势折线图是铁打的选择要对比三个板块的利润率用柱状图要看多个财务指标之间的相关性就把所有指标两两算相关系数画成热力图。4.2 基于pyecharts实现数据看板pyecharts是Python里对接ECharts的库能生成交互式HTML图表非常适合做数据分析可视化看板。我用的v1.x版本API和旧版0.5.x差别很大写代码前先确认版本。from pyecharts.charts import Line, Bar, Pie, HeatMap from pyecharts import options as opts import pandas as pd import sqlite3 def query_data(indicator, regionNone): conn sqlite3.connect(stock_analysis.db) sql SELECT date_value, value FROM financial_data WHERE indicator_name ? params [indicator] if region: sql AND region ? params.append(region) sql ORDER BY date_value df pd.read_sql_query(sql, conn, paramsparams) conn.close() return df def create_trend_line(indicator净利润率, regionNone): df query_data(indicator, region) line Line(init_optsopts.InitOpts(width1200px, height500px, themelight)) line.add_xaxis(df[date_value].tolist()) line.add_yaxis( indicator, df[value].tolist(), is_smoothTrue, markpoint_optsopts.MarkPointOpts(data[opts.MarkPointItem(type_max)]), markline_optsopts.MarkLineOpts(data[opts.MarkLineItem(type_average)]), ) line.set_global_opts( title_optsopts.TitleOpts(titlef{indicator}趋势分析), tooltip_optsopts.TooltipOpts(triggeraxis, axis_pointer_typecross), datazoom_opts[opts.DataZoomOpts(range_start0, range_end100)], yaxis_optsopts.AxisOpts(name数值, axislabel_optsopts.LabelOpts(formatter{value})), ) return line这里有两个设计点值得说。第一datazoom_opts是数据缩放组件。如果指标是十年历史数据图表会非常拥挤加了缩放条之后用户能自己拖动查看任意时间段的细节。这个交互对分析体验提升巨大比一味放大宽度强得多。第二markline_opts给图表加了一条平均值参考线。看趋势时什么位置是正常水平、哪个点明显偏离平均一眼就能看出来。这就是可视化分析的意义——不是把数据画出来就完事而是帮人更快捕捉异常。生成图之后可以把多个图表组合成一个页面from pyecharts.charts import Page page Page(layoutPage.SimplePageLayout) page.add( create_trend_line(净利润率), create_trend_line(营业收入), create_bar_chart(板块对比), ) page.render(dashboard.html)Page.SimplePageLayout能让多个图表纵向排列一键生成一个完整的分析看板。很多人搜“可视化大屏”其实个人项目用这个方式短平快就能出一个效果不错的看板。4.3 从静态HTML到Flask Web展示如果你想通过浏览器访问还要有点交互用Flask把图表包起来是性价比最高的方案。pyecharts可以直接把图表以HTML片段嵌入模板from flask import Flask, render_template app Flask(__name__) app.route(/) def index(): trend_chart create_trend_line(净利润率) bar_chart create_bar_chart(板块对比) return render_template( dashboard.html, trend_scripttrend_chart.render_embed(), bar_scriptbar_chart.render_embed(), ) if __name__ __main__: app.run(host0.0.0.0, port8000, debugTrue)这个方案的优点是简单但要注意render_embed()渲染出来的内容比较大页面上图表数量不要太多否则首屏加载会变慢。我的经验是控制在一页6个图表以内每个图表的初始尺寸也别开太大。当然如果图表里有敏感数据Flask需要加个简单的登录校验至少不能让分析看板裸奔在公网上。我用的是Flask-Login加一个账号密码够用就行。5. 常见问题与排查技巧实录5.1 爬虫侧反爬、超时、编码、表格解析错位首先说反爬。我遇到的公开页面绝大多数只需要一个合理的User-Agent就能正常访问但也有个别页面会校验Referer或者统计访问频率。处理策略是控制请求频率加随机延时不要并发不要短时间高频访问。很多页面加了UA还403十有八九是你请求太密集。第二是编码问题。前面代码里用了apparent_encoding但偶尔也会失灵。如果发现中文乱码先手动打印resp.encoding看是什么再尝试手动指定resp.encoding gbk或utf-8。我的经验是政府统计页面多用utf-8部分行业网站会用gb2312/gbk手动指定比自动识别更可靠。第三是表格解析错位。HTML里一个表格的内部经常嵌套另一个表格直接用find_all(table)很可能拿到嵌套的内层表格导致解析结果只有几个字段。解决办法是# 只找直接子表格避免嵌套干扰 soup BeautifulSoup(resp.text, html.parser) return soup.find_all(table)或者干脆不用BeautifulSoup的表格解析改用Pandas的pd.read_html(resp.text)直接读多个表格返回DataFrame列表。这个内置函数的解析能力比我手写的强不少唯一的缺点是速度稍慢数据量不大时完全没问题。5.2 数据侧字段类型、空值、重复数据清洗阶段最容易出问题的就是“看起来是数字实际上是字符串”。比如“1,234.56”或“12.5%”直接astype(float)必然报错。我写了一个通用的to_float函数放在工具模块里所有数值字段统一过一遍再入库省去后续大量麻烦。空值处理要区分场景。如果是做时间序列分析中间缺一个月的值会导致折线断裂我一般用前向填充或者插值如果是做汇总统计空值直接剔除反而更稳妥。这个取舍没有绝对标准取决于你想让数据呈现什么信息。重复数据也不能只靠数据库约束。我见过一种情况同一指标在不同页面的统计口径不同日期相同但数值不一样数据库唯一约束拦不住这种“假重复”。解决办法是入库前先看df.duplicated()检查组合键是否重复再看df.describe()检查同一日期的值是否出现多个不同数字。这种口径差异只能靠人工核对。5.3 可视化侧中文字体、图表卡顿、时间轴格式pyecharts的图表里偶尔会出现中文标签变方块或乱码大多是浏览器渲染字体的问题不是Python代码的问题。最简单的办法是在InitOpts里指定chart_id然后在set_global_opts里不要用默认字体改成“Microsoft YaHei”或“SimHei”。如果问题依旧检查HTML模板里有没有正确设置meta charsetutf-8。图表卡顿通常是因为数据量太大。比如一个折线图塞了5000个点浏览器渲染和缩放都会卡。解决办法是按需聚合——画月度趋势就用月均值画年度趋势就用年均值别把明细数据全怼上去。时间轴格式混乱也是常事。数据库里存的日期字符串“202401”“2024-01”“20240101”混在一起画图时x轴刻度就乱了。我统一在查询后做一个格式化函数def format_date_str(d): d str(d).strip() if len(d) 6: # 202401 - 2024-01 return f{d[:4]}-{d[4:]} elif len(d) 8: # 20240101 - 2024-01-01 return f{d[:4]}-{d[4:6]}-{d[6:]} return d这个函数救了我很多次。5.4 一次完整的排查案例复盘有一次定时任务跑完后我看日志发现入库条数为0。我习惯性地分三步排查第一步看状态码。在爬虫脚本里加了日志输出resp.status_code发现是200说明页面能正常访问。第二步看解析结果。我在fetch_html_table里加了“表格行数”日志打出来是5行但实际上预期有30行。这说明页面结构变了。打开浏览器一看那个页面改版了数据由静态HTML变成了JavaScript异步加载Requests根本拿不到真实表格内容。第三步换方案。异步加载的页面不能直接抓HTML我用Selenium驱动真实浏览器渲染页面再取表格。Selenium慢但管用个人项目里偶尔用一下可以接受。如果你要长期抓异步数据源更专业的做法是抓包看接口地址直接请求后端API响应通常是JSON格式解析比HTML简单得多也稳定得多。这个案例说明了排查日志的重要性。没有日志我可能只会盯着代码发懵而看不到“页面改版”这个关键变量。6. 项目扩展方向与我的最终心得6.1 后续还能怎么扩展这套系统跑稳之后扩展空间很大。数据方面可以接入定时任务里多源数据的交叉验证比如把两个公开数据源里同指标的数据做对比是发现数据质量问题的好方法分析方面可以引入关联规则或简单回归把“看了什么指标”变成“预测下一期数值”展示方面可以升级成真正的“可视化大屏”布局用Grid组件做多图表联动、点击联动、局部下钻。如果数据量和抓取目标增多可以把采集脚本拆成多节点用消息队列调度这就是“分布式爬虫”的实践路径。但你要评估自己的场景是不是真有这个需求没有上千个数据源的时候个人服务器单机跑定时任务完全够别为复杂度而复杂度。6.2 我在实际项目中的几点体会做完整个项目我最想说的一点是爬虫是这个系统里最简单的部分数据清洗和可视化设计才是区分好坏的关键。把公开数据抓回来很简单但你能否理解数据背后的口径差异、能否把一个混乱的表格整理成可信的分析结果、能否用合适的图表把信息准确呈现出来这些才真正考验工程能力。另外爬虫项目一定要有“行为边界”意识。做一个取数工具和为特定用户提供决策支持是完全不同的定位。我的原则始终是低频、公开、注明来源不碰需要授权的内容不绕过任何技术限制。守住这个底线整个系统才能长期稳定地跑下去。最后再分享一个小技巧单独创建一个config.py把所有数据源的URL、字段名、更新频率、浏览器UA都写在里面。改版了只改配置新增数据源只加记录。系统维护起来会轻松很多等你跑了三个月回头看就会明白这个习惯值多少时间。
返回列表