尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Django爬虫实战:股票分红数据抓取与展示系统

Django爬虫实战:股票分红数据抓取与展示系统 简介这是一套基于Django框架的股票分红数据爬虫与展示系统源码面向具备Python与Django基础的金融数据爱好者、个人投资者及研究人员用于解决分红数据获取分散、查询与可视化不便的问题。压缩包共约2000个文件以1627个py源码为主体辅以156个html模板、87个js脚本、15个css样式及若干c、h、txt、md等文件整体约71.85MB目录结构完整便于按模块阅读与二次开发。系统通过分页爬虫从东方财富抓取股票分红数据采用随机User-Agent与延迟策略模拟真实访问降低被封风险数据经Django ORM存入SQL Server可在settings.py中配置并支持查询、导出与图表展示。已有257人学习下载读者可据此掌握爬虫编写、ORM建模、数据库配置与前端图表联动的完整链路也可直接运行项目获取分红数据并做进一步分析。1. 从一份分红数据说起Django 爬虫与展示系统到底解决什么问题每年年报季做量化研究或者给客户做投顾报告的人都会遇到同一个麻烦想拿到一批上市公司过去几年的现金分红明细打开交易所或财经门户一家一家翻公告、复制粘贴几十家还能忍几百家就是纯体力活。更麻烦的是分红数据不像股价那样有现成的标准接口它散落在公告、财报摘要、分红送转专题页里格式还不统一。这时候一个「基于 Django 框架的股票分红数据爬虫和展示系统」的价值就出来了——它把「抓取—清洗—入库—展示」这条链路用一套 Web 系统串起来爬虫负责把分红数据从公开页面拉下来Django 负责存储、管理后台和前端展示你打开浏览器就能按股票代码、按年度筛选查看。这个方向适合两类人一类是刚学完 Django 基础、想找一个真实数据场景练手的开发者股票分红数据字段清晰、结构规整比爬电商评论更适合入门另一类是有实际数据需求的研究者自己搭一套比买数据便宜而且字段可以按自己的口径定制。需要提前说清楚的是分红数据属于公开披露信息抓取时控制频率、遵守目标站点的 robots 约定是这套系统能不能长期跑下去的前提。下面我按自己搭这类系统的顺序把选型、爬虫、入库、展示和踩过的坑讲一遍。2. 技术选型与数据源拆解为什么是 Django requests SQLAlchemy 思路2.1 为什么展示层选 Django 而不是 Flask很多人第一反应是爬虫加展示Flask 更轻。但分红数据这个场景有几个特点决定了 Django 更省事。第一你需要一个后台来管理股票列表、手动触发爬取、查看抓取日志Django 自带的 admin 几乎零成本就能给你一套增删改查界面Flask 要自己拼。第二分红数据表结构相对固定Django 的 ORM 配合 migration 管理表变更很顺字段加一个「除权除息日」不用手写 SQL。第三你要做的是「展示系统」涉及模板渲染、分页、筛选Django 的模板和分页器开箱即用。选型上我一般这样配Django 负责 Web 层和 ORM爬虫部分用 requests 做同步抓取数据量不大时完全够用如果目标站点是 JS 渲染的再上 selenium 兜底。存储用 Django ORM 直接落库不额外引入 SQLAlchemy避免两套 ORM 打架——热搜里提到的 sqlalchemy 储存爬虫数据在纯爬虫项目里合适但既然已经用了 Django就没必要再叠一层。组件选型理由Web 框架Djangoadmin 后台、ORM、模板、分页齐全爬虫请求requests分红页面多为服务端渲染同步够用动态页面selenium少数 JS 渲染页面兜底存储Django ORM SQLite/MySQL与框架一体免维护两套 ORM定时任务django-crontab 或系统 cron按季度/年度触发抓取2.2 分红数据源长什么样字段怎么定动手前先把数据源摸清楚。上市公司分红数据通常来自几个地方交易所公告页、财经门户的分红送转专题、以及财报里的利润分配章节。以常见的分红送转专题页为例一张表里一般有这些列股票代码、股票简称、送转比例、派息金额每 10 股派多少元、股权登记日、除权除息日、分红年度、方案进度。建模时我建议把「方案」和「股票」拆成两张表用外键关联。原因是同一只股票多年会有多条分红记录一对多关系清晰。核心字段设计如下# models.py from django.db import models class Stock(models.Model): code models.CharField(max_length10, uniqueTrue, verbose_name股票代码) name models.CharField(max_length50, verbose_name股票简称) def __str__(self): return f{self.code} {self.name} class Dividend(models.Model): stock models.ForeignKey(Stock, on_deletemodels.CASCADE, related_namedividends) year models.CharField(max_length10, verbose_name分红年度) plan models.CharField(max_length100, verbose_name分红方案, blankTrue) cash_per_10 models.DecimalField(max_digits10, decimal_places4, nullTrue, blankTrue, verbose_name每10股派息) record_date models.DateField(nullTrue, blankTrue, verbose_name股权登记日) ex_date models.DateField(nullTrue, blankTrue, verbose_name除权除息日) progress models.CharField(max_length20, blankTrue, verbose_name方案进度) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (stock, year, plan) # 防止重复抓取这里几个参数值得说cash_per_10用DecimalField而不是FloatField因为金额做统计时浮点误差会累积分红金额尤其敏感unique_together是防重复抓取的关键爬虫重跑时用get_or_create就不会产生脏数据日期字段允许null因为很多「预案」阶段还没有确定登记日。字段定完跑python manage.py makemigrations和migrate表就建好了。3. 爬虫实现从列表页到详情页的抓取链路与入库3.1 用 requests 抓列表页并解析分红行分红专题页通常是「列表页 分页参数」的结构先抓列表拿到每只股票的分红行再决定要不要进详情页。下面是一个最小可跑的抓取函数用 requests 加 BeautifulSoup 解析表格# spider/dividend_spider.py import requests from bs4 import BeautifulSoup from decimal import Decimal from datetime import datetime HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_page(url, paramsNone): 抓取单页超时和状态码都要处理 resp requests.get(url, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止中文乱码 return resp.text def parse_table(html): 解析分红表格返回字典列表 soup BeautifulSoup(html, html.parser) rows [] for tr in soup.select(table.dividend-table tbody tr): tds [td.get_text(stripTrue) for td in tr.find_all(td)] if len(tds) 6: continue rows.append({ code: tds[0], name: tds[1], year: tds[2], plan: tds[3], cash_per_10: tds[4], ex_date: tds[5], }) return rows逻辑说明fetch_page里apparent_encoding这行是血泪经验很多财经站返回头不带 charset不设就会抓到一堆乱码timeout10必须有否则某个页面卡住整个任务就挂住。parse_table用 CSS 选择器定位表格tds长度校验是防止表头或空行混进来。参数上HEADERS里的 UA 建议定期更新长期用同一个老 UA 容易被识别。3.2 清洗字段并写入数据库抓到原始字符串后不能直接入库日期格式、金额单位、空值都要处理。下面这段负责清洗和落库# spider/save.py import re from decimal import Decimal, InvalidOperation from datetime import datetime from .models import Stock, Dividend def clean_decimal(text): 从10派3.50元这类文本里抽出数字 if not text: return None m re.search(r(\d\.?\d*), text) if not m: return None try: return Decimal(m.group(1)) except InvalidOperation: return None def clean_date(text): 把 2024-06-12 / 2024/06/12 / 20240612 统一成 date if not text: return None text text.strip().replace(/, -) for fmt in (%Y-%m-%d, %Y%m%d): try: return datetime.strptime(text, fmt).date() except ValueError: continue return None def save_rows(rows): saved 0 for r in rows: stock, _ Stock.objects.get_or_create( coder[code], defaults{name: r[name]} ) obj, created Dividend.objects.get_or_create( stockstock, yearr[year], planr[plan], defaults{ cash_per_10: clean_decimal(r[cash_per_10]), ex_date: clean_date(r[ex_date]), }, ) if created: saved 1 return saved逻辑说明clean_decimal用正则从「10派3.50元」里抠数字因为不同站点文案不统一直接float()会翻车clean_date兼容三种常见日期写法解析失败返回None而不是抛异常保证一条脏数据不会中断整批入库。get_or_create配合模型里的unique_together重跑爬虫时已存在的记录不会重复插入这是这套系统能反复跑的基础。3.3 分页抓取与频率控制列表页往往有几十上百页需要循环翻页。这里的关键不是代码多复杂而是频率控制# spider/run.py import time import random from .dividend_spider import fetch_page, parse_table from .save import save_rows BASE_URL https://example.com/dividend/list def run(max_pages50): total 0 for page in range(1, max_pages 1): html fetch_page(BASE_URL, params{page: page}) rows parse_table(html) if not rows: break # 没有数据说明到末页了 total save_rows(rows) time.sleep(random.uniform(1.5, 3.5)) # 随机间隔别用固定值 return total逻辑说明time.sleep(random.uniform(1.5, 3.5))用随机间隔而不是固定 2 秒固定间隔的请求节奏太规律容易被判定为机器行为if not rows: break是翻页终止条件比硬编码总页数更稳因为总页数会变。参数上max_pages设个上限是后悔药防止目标站分页异常导致无限循环。抓取任务建议挂到 cron 上按季度跑而不是实时跑分红数据更新频率本来就低。4. Django 展示层列表筛选、详情页与后台管理4.1 用 ListView 做分红列表和筛选数据入库后展示层要解决「按股票代码查、按年度筛、分页看」三件事。Django 的ListView配合get_queryset就能覆盖# views.py from django.views.generic import ListView from .models import Dividend class DividendListView(ListView): model Dividend template_name dividend/list.html paginate_by 20 context_object_name dividends def get_queryset(self): qs Dividend.objects.select_related(stock).order_by(-ex_date) code self.request.GET.get(code) year self.request.GET.get(year) if code: qs qs.filter(stock__code__icontainscode) if year: qs qs.filter(yearyear) return qs逻辑说明select_related(stock)是关键优化列表里要显示股票简称不加这行每渲染一行就查一次 Stock 表20 条就是 20 次额外查询数据量上来后页面明显变慢。paginate_by 20控制每页条数get_queryset里从 GET 参数取筛选条件icontains支持代码模糊匹配。模板里用{% for %}渲染表格分页用{% include pagination.html %}复用即可。4.2 后台管理与手动触发抓取Django admin 稍微配置一下就能当数据管理后台用# admin.py from django.contrib import admin from .models import Stock, Dividend admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display (code, name) search_fields (code, name) admin.register(Dividend) class DividendAdmin(admin.ModelAdmin): list_display (stock, year, plan, cash_per_10, ex_date, progress) list_filter (year, progress) search_fields (stock__code, stock__name) raw_id_fields (stock,)逻辑说明list_filter让你在后台按年度和进度快速筛search_fields支持按股票代码搜raw_id_fields在股票数量多时避免下拉框加载几千个选项卡死页面。如果想在后台加一个「立即抓取」按钮可以写一个自定义 admin action 调用第 3 章的run()函数但要注意抓取是耗时操作直接放在请求里会超时常见做法是丢给后台任务或单独脚本跑。4.3 前端展示的一个小细节模板里渲染金额时别直接输出cash_per_10用{{ d.cash_per_10|default:- }}处理空值否则页面上会出现一堆None。日期同理用{{ d.ex_date|date:Y-m-d|default:- }}。这些细节不影响功能但决定这套系统看起来是「能用」还是「像半成品」。5. 避坑与排查这套系统最容易翻车的五个地方5.1 抓回来全是乱码现象入库后股票简称显示成锟斤拷或问号。原因目标页面响应头没声明编码requests 默认按 ISO-8859-1 解码。解决在fetch_page里显式设resp.encoding resp.apparent_encoding或者直接指定resp.encoding utf-8抓之前先print(resp.encoding)确认一下。5.2 重跑爬虫后数据翻倍现象同一只股票同一年的分红记录出现两条。原因没用get_or_create或者模型里没加unique_together直接create每次都插新行。解决模型层加唯一约束写入层统一走get_or_create两个一起上才保险光靠代码约束挡不住并发或手动补数据。5.3 列表页越翻越慢现象分红列表翻到后面几页加载要好几秒。原因get_queryset里没做select_related模板每行访问d.stock.name都触发一次查询典型的 N1。解决查询集加select_related(stock)如果还慢用 Django Debug Toolbar 看实际 SQL 条数正常应该是个位数。5.4 日期字段入库报错现象某条记录入库时抛ValidationError或直接存成None。原因目标站日期格式不统一有的写「2024年06月12日」strptime匹配不上。解决clean_date里多兼容几种格式加%Y年%m月%d日分支解析失败返回None并记日志别让一条脏数据中断整批。5.5 爬虫跑着跑着被封现象抓了几十页后返回 403 或验证码页。原因请求频率太高或 UA 太固定。解决随机间隔、轮换 UA、控制单次抓取页数必要时加代理池——但分红数据更新慢把抓取频率降到一天一次甚至一周一次基本不会触发风控没必要为了快把站点惹毛。6. 进阶技巧用 Django 的聚合查询把分红数据变成可用指标系统能展示原始分红记录只是第一步真正有用的是把它变成能对比的指标。比如「某只股票连续分红年数」「近三年平均每股派息」这些用 Django ORM 的聚合就能算不用把数据导出去用 pandas。# analytics.py from django.db.models import Count, Avg, Sum from .models import Dividend def stock_summary(code): qs Dividend.objects.filter(stock__codecode, cash_per_10__isnullFalse) return qs.aggregate( yearsCount(year, distinctTrue), avg_cashAvg(cash_per_10), total_cashSum(cash_per_10), ) def continuous_dividend_ranking(min_years3): 连续分红年数达标的股票按平均派息排序 return (Dividend.objects .values(stock__code, stock__name) .annotate(yearsCount(year, distinctTrue), avg_cashAvg(cash_per_10)) .filter(years__gtemin_years) .order_by(-avg_cash))逻辑说明aggregate对单只股票做汇总values annotate做分组统计Count(year, distinctTrue)里的distinct很重要同一股票同一年可能有多条方案记录不去重年数会虚高。continuous_dividend_ranking这个查询可以直接喂给前端做一个「分红榜」页面比单纯罗列记录有价值得多。再进一步如果你想让前端不刷新就能看到最新抓取结果可以上 Django Channels 做 WebSocket 推送抓取任务完成后主动推一条消息给页面。但这个属于锦上添花数据更新频率低大多数场景下拉刷新就够了别为了技术而技术。我自己搭这类系统最大的教训是一开始总想把爬虫写得又快又全结果维护成本全花在对抗反爬上反而核心的展示和分析没做好。后来改成低频抓取、把精力放在数据清洗和指标计算上系统才真正用起来。分红数据这种低频更新的场景稳比快重要得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表