简介:这份PDF面向具备一定Python基础、希望系统掌握网络爬虫设计与实现的开发者与在校学生,以股票信息抓取为完整案例,讲解从需求分析到系统落地的全过程。内容围绕页面采集、页面解析、连接处理、数据存储与数据挖掘等模块展开,涉及Scrapy框架定向抓取、Requests多线程处理URL、BeautifulSoup解析网页,以及PostgreSQL数据存储与可视化呈现,并给出软硬件环境配置与可行性分析,适合作为课程设计或项目实践的参考。资源包共1个PDF文件,约1.2MB,为期刊论文排版,结构完整、便于打印研读。目前已有486人学习,可帮助读者理清爬虫系统的模块划分与框架选型思路,理解增量采集、结构化存储与数据二次加工的实现要点,并对照文中环境要求快速搭建实验环境。
1. 从一份“股票信息总是慢半拍”的需求说起
做毕设选题时,很多同学会把“Python 爬虫”四个字直接当题目交上去,结果开题就被导师问住:你爬什么、爬来干什么、爬完怎么证明它有用?我见过一个比较典型的翻车现场,某同学交了一版“通用网页爬虫”,功能列表写了采集、解析、存储,答辩时被追问“你的系统解决了谁的什么问题”,答不上来。后来他把题目收窄成“定向抓取股票披露信息并做可视化呈现”,整份方案立刻立住了。这份方案要解决的就是这件事:用 Python 搭一套从定向采集、结构化解析、数据挖掘到前端浏览的完整链路,让披露后的股票信息能进系统、能被管理员维护、能被投资者看到走势。它适合正在做爬虫类毕设或课设的学生,也适合需要交付一份数据采集小系统的从业者参考。
2. 需求分析与技术选型:五个模块怎么切、为什么这么选
2.1 先把功能边界划清楚
这套系统的核心不是“能爬”,而是“爬完之后数据是活的”。按原料里的划分,系统拆成五个模块:股票网页采集、股票网页解析、数据挖掘、股票信息维护、股票信息浏览。前三个是数据链路,后两个是使用链路。采集模块负责快速且增量地拿到股票网页;解析模块把网页里的表格、字段抽成结构化记录;挖掘模块对基础数据做二次加工,比如涨跌幅排序、成交量异常标记;维护模块给管理员做增删改查和数据还原;浏览模块给投资者看大盘走势、个股日走势和智能推荐。
这里有个容易被忽略的设计点:增量采集。股票信息是持续披露的,如果每次全量重爬,既慢又容易被目标站点限流。通常的做法是给每条记录留一个r_time入库时间字段,采集前先查库里该股票代码的最新时间,只抓比它新的页面。这个字段在原料的gp_info和gp_details表里都有,说明原方案也是按这个思路留的口子。
2.2 技术栈的取舍逻辑
原料里给的技术组合是 Python + Requests + BeautifulSoup + Scrapy + Flask + PostgreSQL + Nginx,前端用 Vue,B/S 结构。这套组合不是随便堆的,每一层都有明确职责:
| 层次 | 选型 | 承担职责 | 选它的理由 |
|---|---|---|---|
| 采集调度 | Scrapy | 定向抓取、请求调度、去重 | 自带调度器和去重机制,适合按 URL 规则批量抓 |
| 请求连接 | Requests 多线程 | 补充请求、验证码页、非结构化页 | 写法直白,多线程处理 URL 列表效率高 |
| 页面解析 | BeautifulSoup | HTML 结构化抽取 | 容错好,表格类页面提取字段方便 |
| Web 服务 | Flask | 提供接口和页面路由 | 轻量,毕设规模够用,上手快 |
| 数据库 | PostgreSQL | 存用户、基础股票、详细行情 | 字段类型丰富,时间类型和数值类型规范 |
| 前端 | Vue | 图表渲染与交互 | 数据驱动视图,配合图表库做走势图顺手 |
需要说明一点:Scrapy 和 Requests 同时出现不是冗余。Scrapy 适合“按规则批量抓列表页”,Requests 适合“对单个详情页做定制请求、带自定义请求头”。原料里特别提到headers参数用来伪装用户代理,这就是 Requests 层的活。两者分工,采集模块才不会写成一锅粥。
2.3 运行环境与性能指标
原料给的软件环境是:服务端 Windows 8 及以上或 CentOS 7.4 及以上,Nginx 1.6 及以上,Python 2.7 及以上(实际开发用 Python 3.6),数据库 PostgreSQL 9.3 标准版。硬件底线是 1 GB RAM,推荐 2 GB。这些数字放在今天看偏低,但作为毕设交付的基线是合理的——它说明这套系统不依赖高配机器,评审时不会因为“跑不起来”被扣分。
性能指标原料写了两条硬约束:数据处理时间控制在 3 秒内,页面加载与响应时间不超过 2 秒,兼容 Chrome、火狐、IE、360,分辨率不超过 1920×1080。这两条要写进验收标准里,后面第 6 章会讲怎么测。
提示:Python 2.7 在原料里出现是因为原文写作时间较早。实际动手时直接用 Python 3.x,Requests、BeautifulSoup、Scrapy、Flask 在 3.x 下都成熟,别为了“忠于原文”去装 2.7,那是给自己找麻烦。
3. 总体架构与数据库设计:B/S 结构下的角色与表结构
3.1 三层结构与两个角色的职责划分
系统按 B/S 结构组织,分表现层、业务层、数据层。表现层是 Vue 页面,业务层是 Flask 提供的接口,数据层是 PostgreSQL。角色只有两个:用户和管理员。用户侧功能包括注册登录、个人信息管理、大盘走势、股票日走势分析、股票智能推荐、股票价格走势分析、股票整体走势分析;管理员侧包括管理员登录、系统用户管理、数据还原管理、股票信息管理、股票日走势管理。
这个角色划分的关键在于“数据维护”和“数据浏览”分离。用户只能看,管理员才能改。原料里写得很明确:基础数据经清洗聚合入库后展示到基础数据库,管理员可以对数据做删除错误数据、修改股票信息、新增数据、定向查询。这条边界如果模糊,比如让普通用户也能触发重爬,系统就会变成一个谁都能往里灌数据的黑匣子,答辩时很难解释权限模型。
业务流程上,注册和登录走的是标准校验链路:Web 端提交 → 服务端校验唯一性和密码一致性 → 入库 → 反馈。登录成功后跳转首页,失败提示重新输入。管理员的操作则是“页面操作 → 数据传输至数据库 → 校验 → 反馈最新信息至 Web 端”。这两条时序在原料的 3.2 节有描述,落地时建议画成时序图放进论文,比纯文字清楚。
3.2 三张核心表怎么设计
原料给了三张表:用户信息表gp_user、股票基础数据表gp_info、股票详细数据表gp_details。字段设计上有几个值得说的点。
gp_user里Id是 64 位整型主键,username、password、Email、Tel、address都是 varchar,Role是 2 位整型用来区分用户和管理员,State是 2 位 varchar 表示当前状态,r_time记录注册时间。这里Role用整型而不是字符串是合理的,权限判断时直接比数值,省去字符串匹配。
gp_info是基础行情表,主键shares_id,股票代码shares_f12、名称shares_f14、最新价shares_f2、涨跌幅shares_f3、涨跌额shares_f4、成交量shares_f5、成交额shares_f6、振幅shares_f7、最高shares_f15、最低shares_f16、今开shares_f17、昨收shares_f18、换手率shares_f8、市盈率shares_f9、市净率shares_f23、量比shares_F10,外加state和r_time。字段名带f编号是抓取源字段映射的痕迹,落地时建议在解析层做一次“源字段 → 业务字段”的映射字典,别让前端直接面对shares_f12这种名字。
gp_details是详细行情表,记录时间shares_time、最低价、当前价、最高价、均价、成交量、成交金额、成交单价、state、r_time。注意原料里这张表有多个字段都叫shares_price,这是原文的笔误,实际建表时必须改成shares_low_price、shares_cur_price、shares_high_price之类,否则建表直接报错。这是血泪经验:论文里的表结构可以照抄思路,但字段名一定要自己过一遍。
建表语句示意如下,以gp_info为例:
CREATE TABLE gp_info ( shares_id BIGSERIAL PRIMARY KEY, -- 主键,自增 shares_f12 VARCHAR(525) NOT NULL, -- 股票代码 shares_f14 VARCHAR(525), -- 股票名称 shares_f2 VARCHAR(525), -- 最新价格 shares_f3 VARCHAR(525), -- 涨跌幅度 shares_f4 VARCHAR(525), -- 涨跌额 shares_f5 VARCHAR(525), -- 成交量(手) shares_f6 VARCHAR(525), -- 成交额 shares_f7 VARCHAR(525), -- 振幅 shares_f15 VARCHAR(525), -- 最高 shares_f16 VARCHAR(525), -- 最低 shares_f17 VARCHAR(525), -- 今天开盘价 shares_f18 VARCHAR(525), -- 昨收 shares_f8 VARCHAR(525), -- 换手率 shares_f9 VARCHAR(525), -- 市盈率(动态) shares_f23 VARCHAR(525), -- 市净率 shares_F10 VARCHAR(525), -- 量比 state VARCHAR(1) DEFAULT '1', -- 数据状态,1 有效 0 失效 r_time TIMESTAMP DEFAULT now() -- 入库时间,增量采集依据 );参数说明:shares_id用BIGSERIAL自增,避免手动维护主键;所有行情字段用 varchar 是原料的选择,好处是抓取时不用做类型转换、脏数据不会导致入库失败,坏处是后续做数值计算要在查询时CAST。我一般会在挖掘模块里统一转成数值再算,而不是改表结构,因为改表会牵动解析层。state默认'1'表示有效,管理员“删除”数据时实际是置'0',这样数据还原功能才有东西可还原——这是原料里“数据还原管理”能成立的前提。
3.3 采集与解析的衔接设计
采集模块拿到网页后,解析模块用 BeautifulSoup 抽取字段。衔接的关键是“解析结果先落临时结构,再统一入库”。如果边解析边入库,一条记录解析失败会污染整批数据。通常的做法是解析函数返回字典列表,入库前做一次字段完整性校验,缺关键字段(股票代码、名称)的记录直接丢弃并记日志。这个日志就是后面排查问题的依据。
4. 核心模块实现:采集、解析、挖掘与可视化的落地细节
4.1 定向采集与增量判断
采集模块用 Scrapy 做定向抓取。核心是定义好起始 URL 和翻页规则,并在parse里把详情页链接交给回调。增量判断放在入库前:按股票代码查gp_info里最大的r_time,只处理比它新的页面。示意代码如下:
import scrapy from datetime import datetime class StockSpider(scrapy.Spider): name = "stock" allowed_domains = ["example-stock-site.com"] start_urls = ["http://example-stock-site.com/list?page=1"] def parse(self, response): # 列表页:提取每只股票的详情页链接 for link in response.css("a.stock-detail::attr(href)").getall(): yield response.follow(link, callback=self.parse_detail) # 翻页:按预设停止条件终止,避免无限递归 next_page = response.css("a.next::attr(href)").get() if next_page and self.page_count < 50: self.page_count += 1 yield response.follow(next_page, callback=self.parse) def parse_detail(self, response): # 详情页:抽取字段,交给管道做增量判断和入库 yield { "code": response.css(".code::text").get(default="").strip(), "name": response.css(".name::text").get(default="").strip(), "price": response.css(".price::text").get(default="").strip(), "crawl_time": datetime.now().isoformat(), }这段代码里有两个设计说明。第一,allowed_domains限定域名,防止爬虫跑偏到外站,这是定向爬虫和全网爬虫的分界线。第二,翻页用page_count做硬上限,对应原料里“达到预设停止条件时自动终止”的要求。停止条件不能只靠“没有下一页”,因为有些站点会返回空页而不是 404,硬上限是兜底。parse_detail里所有字段都给了default="",是为了让缺字段的页面也能产出记录,再由管道层统一过滤,而不是在解析层抛异常中断整条链路。
Requests 多线程那一层,主要用在需要自定义请求头的场景。原料提到headers里的user-agent用于伪装身份,落地时把 UA 放进配置,别硬编码在代码里:
import requests from concurrent.futures import ThreadPoolExecutor HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch(url): # 单页请求,超时和重试是必须的,否则线程会卡死 for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=5) resp.raise_for_status() return resp.text except requests.RequestException: if attempt == 2: return None def fetch_all(urls): # 线程池控制在 8 以内,过高容易被目标站点限流 with ThreadPoolExecutor(max_workers=8) as pool: return list(pool.map(fetch, urls))参数说明:timeout=5是单请求上限,超过就重试;max_workers=8是经验值,再高对毕设规模没有收益,反而增加被封的风险。fetch返回None表示三次都失败,调用方要把这类 URL 记进失败列表,而不是静默丢弃。
4.2 解析层的字段映射与清洗
解析层用 BeautifulSoup 把 HTML 表格转成字典。原料里gp_info的字段名带f编号,说明源头字段和业务字段不是一一对应的,所以解析层要维护一张映射表:
from bs4 import BeautifulSoup FIELD_MAP = { "股票代码": "shares_f12", "股票名称": "shares_f14", "最新价": "shares_f2", "涨跌幅": "shares_f3", "成交量": "shares_f5", "成交额": "shares_f6", } def parse_table(html): soup = BeautifulSoup(html, "html.parser") rows = soup.select("table.stock-table tr") records = [] for row in rows[1:]: # 跳过表头 cells = [c.get_text(strip=True) for c in row.find_all("td")] if len(cells) < len(FIELD_MAP): continue # 列数不足,视为脏行 record = {FIELD_MAP[k]: v for k, v in zip(FIELD_MAP, cells)} records.append(record) return records设计说明:FIELD_MAP把中文表头映射到数据库字段,好处是页面表头顺序变了只要改映射,不用动入库逻辑。rows[1:]跳过表头是表格解析的常规操作。列数校验放在入库前,脏行直接跳过,避免一条坏数据让整批入库回滚。清洗环节还包括去重和除错,原料 3.1 节写了“去重除错”,落地时按股票代码 + 采集时间做唯一约束最省事。
4.3 数据挖掘与可视化呈现
挖掘模块做的是二次加工。原料提到的用户侧功能里,大盘走势、日走势、智能推荐、价格走势、整体走势,本质都是对gp_info和gp_details的聚合查询。比如“股票价格走势分析”是把多只股票的价格平均值加权后用折线堆积图展示,纵轴个股单价、横轴时间,不同价格区间用不同颜色分层。这个图的数据来源就是gp_details按shares_time分组后的均价序列。
智能推荐不需要复杂模型,按涨跌幅、换手率、量比做加权排序就能给出候选列表,毕设规模完全够用。关键是推荐结果要能解释——页面上标出“按近 5 日涨幅排序”,比一个黑盒分数更经得起答辩追问。可视化用 Vue 配合图表库,接口返回 JSON,前端只负责渲染,别把聚合逻辑写在前端,否则数据量一大页面就卡,2 秒响应指标守不住。
5. 避坑与常见问题:采集、入库、可视化三段最容易翻车的地方
5.1 采集被限流或返回空页
现象:跑了几十页之后请求全部超时,或者返回 200 但内容是空壳。原因通常是请求频率过高触发站点限流,或者 UA 被识别。解决:把线程池降到 4 到 8,请求之间加随机间隔,UA 从配置读取并准备多个轮换;同时给fetch加重试上限,失败 URL 落盘,下一轮单独补抓,而不是让整批任务挂掉。
5.2 字段错位导致数据串行
现象:入库后“最新价”列里出现股票名称。原因:页面表格某几行有合并单元格或空列,按位置取td就会错位。解决:解析时优先按表头文本定位列索引,而不是按固定顺序;对列数异常的行直接跳过并记日志。这个坑在表格类页面里非常常见,我一般会在解析函数里先打印前三条记录人工核对,确认映射对了再跑全量。
5.3 增量采集判断失效
现象:每次跑都全量重爬,或者新数据进不来。原因:增量依据的r_time用了数据库默认值,而采集时间没写进去,导致比较永远成立或永远不成立。解决:入库时显式写入采集时间,增量查询用“该股票代码下最大采集时间”,并且给(shares_f12, r_time)建联合索引,否则数据量上来后这条查询会拖慢整批任务。
5.4 可视化接口拖垮页面
现象:走势图加载超过 2 秒,切换时间范围时页面卡死。原因:前端一次拉全量明细,或者后端每次请求都做全表聚合。解决:接口按时间范围分页或预聚合,把日走势的聚合结果落到中间表,前端只拉聚合后的序列;图表数据量控制在千点以内,超出就降采样。原料给的 2 秒响应指标,靠的就是这一层预聚合。
5.5 权限边界被绕过
现象:普通用户能调到管理员的维护接口。原因:接口只在前端隐藏了按钮,后端没校验Role。解决:所有写操作接口统一加角色校验装饰器,Role不等于管理员直接返回 403。这个坑答辩时最容易被问,因为评审老师往往会直接改 URL 试。
6. 验收与答辩:怎么用一张检查表证明系统真的能跑
验收这件事,我的习惯是先把原料里的硬指标翻译成可执行的检查项,再逐条跑一遍留证据。原料给了两条性能约束:数据处理时间 3 秒内、页面响应 2 秒内;兼容 Chrome、火狐、IE、360;分辨率不超过 1920×1080。这些不能只写在论文里,要变成一张验收表,每项后面附截图或日志。
| 验收项 | 检查方法 | 合格标准 | 留证方式 |
|---|---|---|---|
| 采集完整性 | 选 3 只股票,人工核对页面与库内记录 | 关键字段无缺失、无错位 | 对比截图 |
| 增量采集 | 连续跑两次,看第二次入库条数 | 第二次只入库新增记录 | 两次日志 |
| 解析正确性 | 抽查 10 条记录逐字段比对 | 字段映射全部正确 | 抽查表 |
| 数据处理耗时 | 记录一次完整采集到入库的时间 | 3 秒内 | 日志时间戳 |
| 页面响应 | 浏览器开发者工具看接口耗时 | 2 秒内 | 网络面板截图 |
| 权限隔离 | 用普通用户 token 调管理员接口 | 返回 403 | 接口返回截图 |
| 浏览器兼容 | Chrome、火狐、IE、360 各开一次 | 页面布局与图表正常 | 四张截图 |
| 数据还原 | 管理员删除一条再还原 | 记录恢复且状态正确 | 操作前后对比 |
答辩时的技巧是“先给结论再给证据”。开场用一句话说清系统解决了什么:定向抓取股票披露信息,经解析、挖掘后以走势图呈现,管理员可维护、用户可浏览。然后直接打开验收表,挑采集、增量、权限三项现场演示,因为这三项最能体现设计深度。性能指标不用现场跑全量,展示日志时间戳即可。如果被问到“为什么用 Scrapy 又用 Requests”,就按第 2 章的分工回答:批量列表页交给 Scrapy 的调度器,需要自定义请求头的详情页交给 Requests,两者职责不重叠。
还有一个容易被忽略的点:把失败日志和脏数据过滤记录也放进答辩材料。评审老师看到你考虑了“抓不到怎么办、抓到脏数据怎么办”,对系统完整度的判断会明显不同。我一般会准备一页“异常处理清单”,列出限流、字段错位、增量失效三类问题的处理策略,这比多讲十个功能点都管用。
从那以后我每次带人做这类采集系统,都强制先写验收表再写代码,因为指标一旦落到表里,实现时就不会跑偏,答辩时也有据可查。希望帮到你。
本文还有配套的精品资源,点击获取