ARTICLE DETAIL

资讯详情

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

Python爬虫实战:抓取财经热点专题做趋势分析

Python爬虫实战:抓取财经热点专题做趋势分析 做爬虫这行最怕的不是目标网站把反爬做得多复杂而是技术练熟了之后手里一堆抓数据的本事却不知道该抓什么真正有价值的东西。我平时除了写代码也喜欢研究财经新闻的传播节奏尤其是机构媒体做的热点专题——比如第一财经的热点专题大盘。这类页面不是简单的资讯瀑布流而是编辑团队围绕资本市场的关键事件做的人工聚合标题、摘要、栏目归属、热度排序里都藏着机构视角下的“风向判断”。抓下来后不仅能看到当天市场在讨论什么还能通过一段时间的积累看出某个赛道的关注度是持续升温还是一日游。这篇博客就把整个过程完整拆开从页面结构分析、接口定位、代码实现到数据清洗、定时更新、撞上反爬怎么处理再到把抓下来的标题做成热词统计、辅助判断热点方向。适合刚学完 Python 基础、想找一个完整练手项目的朋友也适合已经写过几个爬虫、想从“能用”升级到“稳定、有分析价值”的开发者。整个项目用到的工具栈很简单——requests、BeautifulSoup、pandas、sqlite3外加一个 jieba 做分词统计都是 Python 生态里的常客环境用 Anaconda 或者纯 venv 都可以不挑机器。1. 先想明白为什么抓“热点专题”而不是抓普通资讯列表写爬虫之前我习惯先花一两个小时把目标站点的内容结构摸清楚而不是直接上手写代码。这个环节决定了爬虫的寿命和数据的分析价值值得认真对待。1.1 抓取目标的价值判断第一财经的普通资讯列表是机器按时间线推送的量大但乱一条三千字的深度稿和一条五十字的快讯混在一起抓下来之后清洗成本很高。热点专题则不同它的内容经过编辑筛选按主题聚合比如针对某次政策发布、某家头部公司财报、某个行业突发事件的专题页往往会把背景、评论、数据解读放在一个维度上。从数据分析的角度看这种编辑筛选过的数据有两个明显的优势噪声低能进专题的内容本身就代表编辑认为“值得关注”做热词统计时不需要做太多停用词清洗。时序价值高专题的更新频率比普通资讯低但每一篇都带时间戳可以用时间序列的方式跟踪一个话题的热度生命周期。另外专题页的标题和摘要措辞更讲究里面高频出现的公司名、行业词、政策词直接构成了“资本风向”的词云。我要抓的正是这个。1.2 技术选型requests 方案 vs Scrapy 框架 vs 无头浏览器这个项目只有我一个人维护目标明确是“稳定抓取一个站点的热点专题”数据量级一天几百条远没到需要分布式的程度。所以选型逻辑很简单用 Scrapy 当然可以但它的项目结构、中间件、Item Pipeline 对这个小任务来说偏重调试一个 xpath 还要启动整个爬虫进程效率不高。如果你已经在用 Scrapy也不是不行只是杀鸡用了牛刀。用 Playwright 加载浏览器渲染倒是能解决动态加载问题但开销大、部署要装浏览器内核如果只是追踪一个专题页的更新成本偏高。除非你发现目标数据只能通过 JS 渲染获得且没有接口否则不建议一上来就上无头浏览器。requests BeautifulSoup 的轻量组合这也是我这次实际采用的方案。代码逻辑直白出问题了直接单文件调试改起来快。它天然适合这个量级的数据抓取任务。用生活里的例子类比你要去菜市场买一家人一天吃的菜骑电动车去就够了不必把一辆冷链卡车开进菜市场。requests 就是那辆电动车灵活、轻快、坏了随手能修。2. 页面结构拆解与接口定位找到数据的真正出口写爬虫的第一步永远是“看页面”不是“写解析”。我按照从粗到细的顺序把目标页面的数据来源摸了一遍。2.1 从专题入口 URL 开始拆打开第一财经的热点专题大盘页面后先看地址栏里的 URL 结构。这类专题入口通常是www.yicai.com/news/gushi/类似的路径进入后会发现整个“大盘”其实是由多个 tab 或区块组成的比如“全部”、“深度”、“快讯”、“观点”等分类。我习惯先不做任何代码操作直接右键“查看网页源代码”用浏览器自带的开发者工具搜索几个有代表性的关键词比如某篇文章的标题。如果这个标题在 HTML 源码里直接出现说明内容是服务端渲染的requests 抓下来就能解析如果源码里搜不到或者只看到一个空的容器 div那就是前端 JS 动态渲染的得去 Network 面板里找接口。这一步的实操技巧是打开开发者工具切到 Network 面板勾选 Fetch/XHR 过滤然后刷新页面。观察这些异步请求的返回内容优先找不是 JS/CSS 文件的请求点开看 Preview 是不是 JSON 数据。2.2 用开发者工具剥开“双轨”数据逻辑实际观察下来这类页面往往是“双轨制”首屏是服务端渲染好的 HTML把关键内容直接给用户和搜索引擎看但第二次筛选、分页、按栏目切换时就开始走 XHR 接口。接口返回的 JSON 里有统一的列表结构比如{ data: [ { title: 热点事件标题, summary: 摘要内容, url: https://www.yicai.com/news/xxxxx.html, pubdate: 2025-01-20 08:30:00, channel: 股市 } ], total: 100 }这种结构对爬虫来说比解析 HTML 友好得多。我在实际项目里抓首屏 HTML 用于确认页面结构抓 JSON 接口用于批量拉取分页数据两条腿走路一个用作验证一个用作生产。2.3 字段设计我要留下什么数据字段设计决定了后面分析的边界。我最终只保留六个核心字段不贪多字段名类型说明title字符串文章标题用于热词统计和趋势分析summary字符串摘要清洗后可用于文本聚类url字符串原文链接用于去重和回溯pubdate日期时间发布时间用于时间序列分析channel字符串栏目/专题分类用于分组统计crawl_time日期时间抓取时间用于标识本轮抓取批次为什么不做深度内容抓取一是因为专题页的列表接口已经给了标题和摘要对“洞察风向”来说这些字段足够描述一个话题的热度二是因为正文页面的结构更杂抓取成本更高撞反爬的概率也更大。数据够用就行不要过度设计。3. 完整实操从环境准备到数据落库下面这部分是整篇博客的核心操作区。我会把每一步的命令、代码和判断逻辑都写出来尽量做到你拿着就能跑通。3.1 环境准备与依赖安装我的建议是新建一个干净的虚拟环境避免污染全局 Python。这里用 venv 示范python -m venv yicai_env source yicai_env/bin/activate # Windows 下用 yicai_env\Scripts\activate然后安装项目依赖一个命令搞定pip install requests beautifulsoup4 pandas sqlite3-helper jieba这里面的sqlite3-helper不是必需的Python 自带的sqlite3模块就够用。我实际项目里用的是标准库因为数据量不大不需要额外的 ORM。pandas 是给后续数据清洗和聚合统计用的如果你只需要存 CSV可以跳过。jieba 是中文分词库放到第 5 部分的趋势分析里用。3.2 请求头伪装与请求节奏控制爬虫遇到的问题十有八九不是解析失败而是请求被拒绝。被拒绝的原因很直接你的请求在服务端看来不像一个正常用户在访问。所以我在每轮请求里都设置了一组请求头这是最基础的工作import requests import random import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.yicai.com/, Connection: keep-alive, } def get_json(url, paramsNone, retries3): for attempt in range(retries): try: resp requests.get(url, paramsparams, headersHEADERS, timeout10) if resp.status_code 200: return resp.json() # 403/429 等状态码说明频率太高或请求被识别 print(f请求失败状态码: {resp.status_code}第 {attempt 1} 次重试) except requests.RequestException as e: print(f网络异常: {e}第 {attempt 1} 次重试) time.sleep(2 ** attempt random.uniform(0, 1)) return None这里有两个细节想多说一句。第一timeout必须设置不设超时的请求一旦网络抖动进程会一直等下去爬虫就像死了一样第二重试策略用的指数退避第一次等 2 秒第二次等 4 秒第三次等 8 秒不是固定的反复死磕给服务端留出恢复时间。3.3 解析逻辑拿到标题、摘要、链接和时间从接口拿到 JSON 之后解析逻辑其实很简单但我在项目里做了一层“字段容错”。因为运营后台改字段名是常事今天叫pubdate明天可能叫showtime。我在解析时用 dict.get 配合多个候选键名能取到哪个用哪个解析失败也不至于整个脚本崩掉def parse_article(item): title item.get(title) or item.get(headline) or summary item.get(summary) or item.get(desc) or url item.get(url) or item.get(link) or pubdate item.get(pubdate) or item.get(datetime) or channel item.get(channel) or item.get(cat) or 综合 return { title: title.strip(), summary: summary.strip(), url: url.strip(), pubdate: pubdate, channel: channel, crawl_time: time.strftime(%Y-%m-%d %H:%M:%S) }如果接口短期内不可用或者你只想抓首屏直接用 BeautifulSoup 解析 HTML 也同样可行。核心逻辑是找一个稳定的容器节点然后遍历里面的条目from bs4 import BeautifulSoup def parse_html(html): soup BeautifulSoup(html, html.parser) articles [] for item in soup.select(.news-item a): # 具体选择器以实际页面为准 title item.get_text(stripTrue) href item.get(href) if not href.startswith(http): href https://www.yicai.com href articles.append({title: title, url: href}) return articles提醒一下BeautifulSoup 的选择器最好在浏览器控制台里用document.querySelectorAll先验证一遍确认能选中想要的节点再写进代码。盲写选择器一改版就报废。3.4 数据存储CSV 与 SQLite 双写数据到手后我先写内存 DataFrame再做两步存储全量追加到 SQLite同时按当天日期导出一份 CSV 备查。这样做的原因是SQLite 适合长期积累、按字段查询而 CSV 方便临时用 Excel 打开看。import pandas as pd import sqlite3 def save_articles(articles): df pd.DataFrame(articles) if df.empty: return # 去重以 url 为唯一键 df df.drop_duplicates(subseturl, keeplast) # SQLite 同步 conn sqlite3.connect(yicai_articles.db) df.to_sql(articles, conn, if_existsappend, indexFalse) conn.close() # 按日期导出 CSV today time.strftime(%Y%m%d) csv_path fyicai_{today}.csv df.to_csv(csv_path, indexFalse, encodingutf-8-sig) print(f本轮新增 {len(df)} 条CSV 已保存至 {csv_path})注意这个utf-8-sig编码它不是随便选的。Excel 打开 UTF-8 编码的 CSV 时会乱码加 BOM 头后 Excel 能自动识别。这个小细节用 pandas 的人踩过坑的都懂。3.5 定时更新让爬虫自己跑起来我不喜欢手动跑脚本所以写了个简单的调度。考虑到 Windows 上计划任务和 Linux 上 crontab 的配置方式不一样我直接在 Python 脚本里用schedule库做了定时逻辑方便跨平台import schedule def job(): print(f{time.strftime(%Y-%m-%d %H:%M:%S)} 开始抓取) articles fetch_all_pages() save_articles(articles) print(f{time.strftime(%Y-%m-%d %H:%M:%S)} 抓取完成) schedule.every().day.at(09:00).do(job) schedule.every().day.at(12:00).do(job) schedule.every().day.at(18:30).do(job) while True: schedule.run_pending() time.sleep(60)三个时间点的选择也有讲究9 点前是盘前抓的是隔夜和早间资讯12 点是午间休市抓的是上午行情带来的解读18 点半是收盘之后抓的是全天复盘内容。这样节奏下来一天三个批次覆盖了资本市场最活跃的时段同时每次抓取之间间隔足够长不会对目标站点造成访问压力。4. 反爬应对与常见问题排查实录这个项目跑起来之后我没有收到过一条反爬警告但这不代表过程一帆风顺。事实上前两周我碰到的问题挺多的下面把最典型的几个场景和排查思路记录下来。4.1 HTTP 403 / 418 状态码不是对方在和你开玩笑刚开始跑脚本时我遇到 403 的频率很高。403 是服务器明确拒绝访问418 则是服务器说“我是一个茶壶”本质上是一个带有幽默色彩的反爬提示它更直接地告诉你你的请求被识别成爬虫脚本了。排查顺序我来排一下检查 User-Agent 是否完整不能是python-requests/2.x这种默认值。检查 Referer 是否正确很多站会校验当前请求是从哪个页面跳转过来的。检查请求频率如果一分钟内请求超过几十次风险很高。检查 Cookie 是否携带有些页面需要先访问首页拿到 Cookie 才能访问列表页。我自己项目里的解决方法是把请求频率压到非常保守的水平每次请求之间睡 3 到 5 秒并且随机化这个间隔。正常人读新闻也不可能每秒钟点一次下一页对吧模仿真实节奏是最好的伪装。4.2 数据字段缺失或乱码先查编码再查动态加载有几天我抓到 500 篇文章其中有 70 篇的标题是空的。后来一查是接口在返回数据时部分列表项把标题放在了嵌套子结构里顶层 key 只有空字符串。这种“潜伏缺失”比整段报错更难发现必须靠数据校验来兜底。我的做法是加了一条硬性过滤标题为空或长度少于 2 的记录直接丢弃。别小看这个规则它在后续做热词统计时避免了很多NoneType的坑。乱码问题相对好排查只需要关注网页编码格式。如果是 GBK 编码的页面你用 UTF-8 解码就会出现“锟斤拷”一类字样。判断方法是看响应头里的Content-Type里的charset字段或者看 HTML 头部的meta charset...。requests 的resp.encoding属性在部分情况下会猜错建议直接显式指定编码。4.3 请求频率过高的隐性危害我不建议在调试阶段用循环不加延时地疯狂打印日志。有一次为了测试分页功能我写了一个for i in range(50)的循环每页请求间隔只设了 0.1 秒结果跑了三页就被限流。从那以后我给自己定了一条规矩任何循环请求的代码写完之后必须检查里面有没有time.sleep没有就不过代码审查。做爬虫最基本的一条底线是不要干扰目标站点的正常服务。对于财经类网站数据更新时间决定了很多人的决策节奏你的高频请求如果导致站点变慢影响的是真实用户的体验。这个分寸感比任何技术细节都重要。4.4 常见问题速查表现象可能原因处理方法返回 403UA 被识别 / 缺少 Referer补充完整请求头降低频率返回 418请求频率过高随机延迟 3 到 8 秒加指数退避重试页面源码里搜不到标题数据由 JS 动态加载去 Network 面板找 JSON 接口中文乱码编码判断错误显式设置resp.encoding utf-8或对应编码字段缺失接口数据结构变化用 dict.get 多候选键名容错脚本长时间卡住未设置 timeout所有请求加timeout10URL 取不到完整地址相对路径未补全拼接域名前缀4.5 合规意识的个人观点这里说点技术之外但非常重要的事。爬虫本身不是灰色地带的代名词关键在两点一是抓取的对象是否是公开可访问的数据二是抓取频率是否在合理范围内。我做的这个项目只抓公开页面和国际通用的合理数据不碰任何登录后才能访问的接口也严格限制访问频率。建议你在自己的项目里也遵守同样的准则。读懂网站的服务条款和 robots.txt尊重数据源平台的规则这是从业者的基本职业素养也是这个领域能长期做下去的前提。5. 从数据到洞察热点专题还能怎么玩爬虫把数据存下来只是完成了第一步。它的真正价值体现在我们能从这个数据集合里看到什么趋势、洞察到什么信号。5.1 标题词频统计与热词提取我用 jieba 对标题做分词统计过滤掉常见的停用词比如“今天”、“最新”、“重磅”、“突发”这类修饰词留下公司名、行业名、政策关键词。代码很简单但效果直观import jieba from collections import Counter stopwords set([今天, 最新, 重磅, 突发, 什么, 怎么, 如何, 一个]) words [] for title in all_titles: for word in jieba.cut(title): word word.strip() if len(word) 2: continue if word in stopwords: continue words.append(word) counter Counter(words) for word, count in counter.most_common(30): print(word, count)跑完一轮之后当天出现频率最高的往往是市场正在激烈博弈的赛道。比如“半导体”连续多日居前说明板块关注度高企“长债”和“利率”频繁被提及说明宏观叙事正在往这个方向倾斜。这个热词榜就是“资本风向”的一个可量化切片。5.2 栏目热度对比与时序趋势我按 channel 字段把数据分成“股市”、“楼市”、“宏观”、“公司与行业”几类然后分别统计各栏目每天的抓取条数。条数上升通常意味着编辑在这方面的选题投入增加背后往往是市场关注度的转移。更进一步我把同一批关键词在时间维度上做滚动累计比如某个公司名连续 7 天出现在标题里然后用 pandas 画一条折线能直观看到它的热度生命周期。这类数据如果结合量化框架甚至可以做成简单的情绪因子作为一个补充信号源。你如果对量化感兴趣把这份数据作为素材写交易策略的信号输入也是一个挺顺滑的扩展路径。5.3 数据清洗到洞察的流程固化从原始 JSON 到可视化图表我的完整链路是requests 抓取 - pandas 清洗 - sqlite 存储 - jieba 分词 - Counter 统计 - matplotlib 画图。整个过程没有用到重型框架但每个环节都能独立复现这也是我推荐新手按这个路径练手的原因。顺手分享一个我在实际使用中发现的小技巧把热词统计结果按小时分桶可以捕捉到突发事件的传播节奏。比如某条政策在 10 点发布11 点的抓取批次里相关关键词可能还不多到 12 点的批次就会出现一个明显的频次峰值。这个峰值本身就是市场情绪快速升温的证据比单纯看新闻列表的排序变化要灵敏得多。收尾一点个人经验做这个爬虫项目到现在我最大的体会不是学习到了什么高级的反反爬技巧而是理解和尊重“数据节奏”这件事。资本市场的信息有它自己的节律——盘前静悄悄盘中沸腾盘后复盘沉淀。你用爬虫跟着这个节律走抓到的数据天然就是有结构的。反过来如果你一味追求快高频抓取不仅容易被限流还会抓到大量重复、低质的中间态内容反而增加了清洗成本。最后再分享一个小技巧脚本跑完不要急着关数据库SQLite 里那条crawl_time字段非常有用。万一某个批次的数据解析出错你可以用它把这一批数据筛出来重新处理或者在分析时排除掉。这个字段保留了所有历史痕迹相当于给你的数据仓库加了一个“快照回滚”能力。资本风向这东西周期越长看得越清楚数据积累得越久后面画出来的趋势线越有参考价值。
返回列表