
做爬虫这事儿有个逃不开的规律练手数据集要好找、要够干净还要能讲出点故事。第一财经的热点专题大盘就是我一直推荐的入门级数据源之一。用Python爬虫把这些专题的标题、热度、更新时间、详情链接抓下来再按热度排个序基本就能把当前市场上大家最关注的资本话题脉络摸个大概。这篇文章完整记录我怎么实现这个过程包括目标页面拆解、requests方案与selenium兜底方案的选择、核心解析逻辑、反爬应对策略、数据清洗以及那些真金白银踩过的坑。1. 先把项目拆清楚到底要抓什么、难点在哪1.1 目标页面的数据结构第一财经的“热点专题”栏目聚合的是正在被市场讨论的一批专题内容覆盖宏观、行业、公司、国际等不同维度。从爬虫角度看每条专题通常包含四个关键字段字段类型说明后续用途专题标题str一句话概括核心话题关键词提取、热点聚类热度值str/int当前话题关注度指标排序、加权分析更新时间str/datetime判断内容时效性时间维度走势分析详情链接str跳转到专题内页二次采集与延展阅读为什么选这个目标而不是豆瓣榜单或者电商商品第一财经资讯站的页面结构相对规律专题列表基本是同一套HTML模板循环渲染字段边界清晰非常适合练解析第二数据本身贴近真实业务场景抓完不是扔在硬盘里吃灰而是能直接用于热点追踪、词云分析、资讯聚合甚至为后续做量化或者舆情项目储备素材。当然最实际的一点是这种公开栏目页面的抓取合规边界相对明确做个人学习用途风险可控。1.2 真正卡脖子的四个技术点项目名称听起来直白但实际推进时会遇到几个绕不开的问题第一个是页面渲染方式。部分榜单区域可能由异步接口动态加载直接用requests拿HTML可能抓不到目标字段需要先判断静态还是动态。第二个是反爬机制。财经站点普遍有基础的UA校验和访问频率限制如果请求间隔太短很容易触发限流甚至封禁IP。第三个是数据清洗。热度字段往往带“万”“亿”这类中文单位或者带“热度”“关注”这类前缀词直接排序会得到一堆文本而不是数值。第四个是稳定复现。写一次跑通不难难的是隔几天再跑还能跑通因为页面结构微调、请求超时、编码波动都会让脚本静默失败。我最初跑这个项目时也以为核心工作量在“写爬虫”实际上后面清洗和排坑花的时间比写请求代码多得多。这个认知本身就是练这个项目最大的收获。2. 技术选型首选Requests还是直接上Selenium2.1 三十秒判断页面是静态还是动态动手之前先确认目标页面到底属于哪一类。我的习惯做法是用浏览器打开目标页右键“查看网页源代码”按CtrlF搜一下标题文字。如果在源码里能搜到说明服务端直接渲染了数据用requests拿HTML再解析就行这是最舒服的情况。如果源码里搜不到但页面上能看到内容那就说明数据是前端异步加载的需要去开发者工具面板里找接口。具体操作是打开Network面板刷新页面过滤XHR或Fetch类型请求逐个翻返回的JSON内容找到包含标题数据的那个接口。找到接口以后直接请求接口反而比渲染整个页面更轻量这是很多老手喜欢的进阶做法。判断这一步真的别省。我见过不少新人上来就套Selenium页面慢、资源吃、还不稳定其实目标页面requests三行代码就能搞定。方案越简单后面维护成本越低。2.2 首选方案requests BeautifulSoup对于第一财经热点专题这类以静态渲染为主的列表页我推荐requests搭配BeautifulSoup再加lxml作为解析引擎。这套组合的优点很明显依赖轻、调试快、资源占用低。单线程跑一个列表页单次请求加解析的耗时通常在几百毫秒级别配合限速策略几分钟就能把十几页数据全部落盘。BeautifulSoup的API对新手也很友好select_one和select的组合基本覆盖了九成以上需求不太需要写复杂的XPath。如果HTML结构有明显规律也可以考虑直接用lxml的XPath解析效率会高一点但可读性会稍微下降。入门阶段我还是建议BeautifulSoup先跑通流程遇到性能瓶颈再切lxml不迟。2.3 兜底方案Selenium或者Playwright如果判断下来页面确实是动态渲染而且接口参数又带加密逻辑、直接请求接口很麻烦这时候再考虑Selenium或者Playwright。Selenium方案的核心就几步创建浏览器选项开启无头模式访问URL取页面源码关闭浏览器。下面是一个最小可用的无头Chrome抓取示意。from selenium.webdriver import Chrome from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) driver Chrome(optionsoptions) driver.get(url) html driver.page_source driver.quit()虽然代码不多但Selenium毕竟是重方案启动浏览器本身要占几百MB内存并发能力也弱。我的原则是“能requests就不浏览器能轻量就不重型工程”。只有当页面的核心数据全部来自异步渲染、且接口不好直接调的时候才把Selenium作为技术兜底。2.4 环境准备与依赖安装我在本机用的是Python 3.10建议不低于3.9主要是为了保证pandas等依赖的兼容性稳定。新项目一律先建虚拟环境把依赖隔离干净。python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install requests beautifulsoup4 lxml pandas openpyxl这里特别提一下openpyxl它是pandas写Excel文件的底层引擎如果只装了pandas没装openpyxl调用to_excel时会直接报缺失依赖。当初我第一版脚本就栽在这个小坑上报错信息里明确写着需要安装openpyxl当时没细看还反复检查代码绕了不少弯路。3. 核心实现从发送请求到数据落盘3.1 请求策略与参数设计写请求函数之前先把两个基础策略定下来一是请求头要模拟真实浏览器二是必须带超时和重试。UA校验是最常见的反爬手段不设置User-Agent会直接被拒。推荐准备一个小型UA池每次请求随机取一个打开Chrome的“网络条件”就能看到真实UA长什么样抄几条不同平台的存进去就行。只固定用一个UA也能跑但频率一高容易被识别既然做实战项目从第一天就养成随机UA的习惯比较好。超时设置我习惯用10秒超过就抛异常。重试采用指数退避策略第一次失败等1秒第二次等2秒第三次等4秒最多重试3次。这样遇到瞬时网络抖动或服务器偶发超时脚本不会直接崩溃也不会因为立刻重试把压力加倍放大。请求间隔方面我给自己的参考线是单页间隔1到3秒随机浮动尽量模拟人工浏览的节奏。这个节奏下抓完整个专题列表也就几分钟配合重试机制已经足够稳定。不建议把间隔压缩到0.1秒以下对目标站不友好数据质量反而会因为请求失败而变差。3.2 页面解析与字段提取进入解析环节后核心动作是先把页面结构摸清楚。我的做法是在浏览器里右键检查目标区域看到每条专题的外层容器、标题节点、热度节点和链接节点的class规则后写对应的CSS选择器。由于页面结构会变动代码里的选择器只是一个合理的参考模板实际用时需要按自己看到的class做替换。提取标题时要注意get_text默认不会去掉首尾空白所以我会用stripTrue参数统一做一次清理。链接字段经常遇到相对路径或者以//开头的地址需要做一次补全处理否则后续跳转详情页时会得到无效链接。热度值字段建议保留原始文本等进了清洗阶段统一转成数值不要在解析阶段就做字符串截断因为不同时期热度表达格式可能不一样保留原始值更便于回溯排查。3.3 数据清洗把“1.2万”变成12000清洗是整个流程里最不起眼但最容易出问题的一环。热度值常见的原始形态包括“1.2万”“热度5.6亿”“关注8900”“空白”这几种直接拼进Excel会得到一列文本排序时完全按字典序结果会是“1.2万”排在“8900”前面这显然不对。我的清洗逻辑分三步第一步去掉“热度”“关注”这类前缀词第二步处理“万”“亿”单位第三步把纯净数字转成int。这里要注意早期抓的数据可能是“百万”“千万”这类更大的单位统计数量级时要留足扩展弹性我见过不少脚本只处理了“万”结果碰到“亿”直接抛异常整轮采集白跑。3.4 结果保存CSV还是Excel我通常两个格式都保留。数据量不大时Excel对人工查看最友好直接按热度降序排好再设置自动列宽就能拿去做日常复盘CSV则方便后续代码继续处理也便于做增量更新时合并历史数据。实际落地时将清洗后的热度值单独存成heat_num字段排序用这个数值字段展示时保留原始heat文本这样既准确又可读。另外给CSV文件加上utf-8-sig编码避免Excel打开中文乱码。3.5 完整代码清单下面是一个可以直接跑通主流程的完整示例包含请求、解析、清洗、保存四个环节。选择器部分仍是参考模板启动前务必按实际页面调整。import random import time import pandas as pd import requests from bs4 import BeautifulSoup UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0, ] def random_headers(): return { User-Agent: random.choice(UA_POOL), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } def get_page(url, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, headersrandom_headers(), timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text print(fHTTP {resp.status_code}重试第 {attempt 1} 次) except requests.RequestException as exc: print(f请求异常: {exc}) time.sleep(min(2 ** attempt, 8) random.random()) return None def parse_topics(html): soup BeautifulSoup(html, lxml) rows [] for item in soup.select(.topic-item): title_el item.select_one(.topic-title) heat_el item.select_one(.heat) time_el item.select_one(.time) link_el item.select_one(a.topic-link) or item.find(a) if title_el is None or link_el is None: continue title title_el.get_text(stripTrue) link link_el.get(href, ) if link.startswith(//): link https: link rows.append({ title: title, heat: heat_el.get_text(stripTrue) if heat_el else , pub_time: time_el.get_text(stripTrue) if time_el else , link: link, }) return rows def heat_to_number(heat_str): if not heat_str: return 0 heat_str heat_str.replace(热度, ).replace(关注, ).strip() if 亿 in heat_str: return int(float(heat_str.replace(亿, )) * 100000000) if 万 in heat_str: return int(float(heat_str.replace(万, )) * 10000) try: return int(heat_str) except ValueError: return 0 def save_results(data): df pd.DataFrame(data) df[heat_num] df[heat].apply(heat_to_number) df df.sort_values(heat_num, ascendingFalse) df.to_csv(yicai_topics.csv, indexFalse, encodingutf-8-sig) df.to_excel(yicai_topics.xlsx, indexFalse, engineopenpyxl) print(f已保存 {len(df)} 条专题数据) def main(): base_url https://www.yicai.com/topic/ all_rows [] for page in range(1, 6): url f{base_url}?page{page} html get_page(url) if html: rows parse_topics(html) all_rows.extend(rows) print(f第 {page} 页抓取 {len(rows)} 条累计 {len(all_rows)} 条) time.sleep(random.uniform(1, 3)) if all_rows: save_results(all_rows) if __name__ __main__: main()主流程先跑5页确认输出文件无误后再把range(1, 6)改成实际页数或循环翻页逻辑。翻页时注意观察URL参数的变化规律不同站点的分页逻辑差异很大有的用page参数有的用pageNum还有的直接用?p1要按实际地址调整拼接方式。4. 反爬应对与合规意识别把练手项目做成封号事故4.1 常见反爬机制识别爬多了会发现反爬措施其实就集中在几个维度请求头校验、访问频率限制、IP封禁、动态参数加密。请求头校验最基础也最常见重点看User-Agent和Referer字段。UA不带或者带默认Python标识的请求很容易被中间层直接拒绝。Referer字段则用来判断请求来源有些站点要求从首页跳进来的请求才放行所以我会在请求头里顺手带上目标站的首页地址。频率限制属于软性反爬通常表现为短时间内请求次数超过阈值后开始返回验证码、响应变慢或者返回空列表。这种问题靠加间隔和随机浮动就能缓解。IP封禁则更严格连续高频采集或者触发WAF规则就会直接封IP表现是页面正常但请求被拒。个人学习场景下单机单IP加合理限速已经足够没必要追求上代理池代理池的稳定性和成本反而会拖垮整个项目。4.2 实测有效的应对策略关于UA我从实战中总结出两条经验一是UA池至少准备三个不同平台的浏览器标识切换着用二是每次请求重试时顺手换一个UA避免同一UA反复失败后被打上标记。关于请求频率前面代码里用的是1到3秒随机间隔这是抓公开列表页的稳妥参考值。我自己实测下来保持这个频率连续抓几十页响应速度和成功率都稳定基本不会碰到有感知的限流。如果要做一夜之间的定时批量采集建议把间隔拉到3到5秒凌晨跑数据更安全。关于重试指数退避是性价比最高的方案。第一次失败等2秒第二次等4秒第三次等8秒最多试3次。这里要注意退避等待的最终值不该是固定的要加上少量随机数否则所有失败请求退避时间完全一致反而容易在下一次请求时形成新的并发高峰。真正要警惕的是并发过快。我见过有工程化经验的人为了方便给列表页开了10个线程并发抓取结果几十个请求瞬间打过去触发WAF直接整段IP被限。单机单线程即使慢一点胜在稳定对这个体量的项目完全够用。4.3 合规边界可以学习不能越线爬虫的技术对抗再精彩也得在法律和平台规则划定的范围内操作。我给自己定过几条明确的工作线放在这里供参考第一只抓取公开栏目页面不碰用户个人数据、私密数据和需要登录才能访问的非授权内容。第一财经热点专题列表属于公开资讯页面而且在内容性质上主要是媒体公开信息作为学习对象是合适的。第二不批量抓取后对外转售、传播。把抓下来的专题标题、热度做个人分析和学习总结属于合理使用但如果打包拿去二次分发或者做成付费数据产品必须重新审视授权边界。第三访问频率控制在“人工浏览档次”。判断标准很简单你手上点着鼠标一页页翻是什么频率代码就按什么频率来甚至可以更慢一点。第四关注robots.txt和站点服务条款。如果目标站明确禁止爬虫就及时换数据源这个项目明确是以公开内容为学习载体不适合去做对抗性突破。需要说明的是爬虫领域的技术边界和合规边界本身是动态的公开数据爬取在许多场景下是合法且被允许的商业行为但前提是尊重目标站点的规则和著作权。我们练的是工程能力不是越权能力。把姿态放低一点长期收益反而更高。另有一点值得展开即使技术上能绕过验证码和接口加密也不意味着“应该”绕过。当目标站花费大量精力做反爬时更合适的选择是看是否有官方API、开放数据平台或者付费资讯接口。很多财经平台本身提供免费或低成本的数据服务官方途径稳定性和数据质量都远胜自己爬这个成本账要会算。5. 实操中踩过的坑问题排查与修复实录5.1 常见问题速查现象可能原因解决办法返回内容全是乱码响应编码判断错误设置resp.encoding resp.apparent_encoding或按页面charset固定列表页抓回来为空页面结构调整或异步加载检查选择器必要时切Selenium请求偶发失败网络抖动或触发限频加指数退避重试控制请求间隔热度排序不对中文单位文本未转数值用统一清洗函数转成数值字段Excel打开中文乱码CSV编码问题用utf-8-sig编码写入链接跳转404相对路径未补全统一拼接成绝对地址5.2 踩坑实录一页面编码引发的静默错误第一版代码里我直接用resp.text拿到了页面看到标题是带感叹号的英文格式还没太留意直到把数据写进Excel才发现中文标题全部变成了类似“混娔的乱码。原因很简单resp.text默认按响应头里声明的编码解码但有些站点响应头没写清楚requests就回退到猜编码一旦猜错全乱。解决办法是在拿到响应后手动指定resp.encoding resp.apparent_encoding让requests通过内容去推断编码。这个小改动我后来写进了所有爬虫项目的公共请求函数里看着基础但踩过才知道疼。5.3 踩坑实录二爬得好好的隔两天列表突然为空这是最容易被误判为封IP的情形。某天我照常跑脚本日志显示200正常页面也取了回来但解析结果就是0条。第一反应是IP被限制换了UA、加了间隔都没用后来用浏览器打开页面才发现网站的列表外层class从.topic-item改成了.topic-card-item选择器失效导致所有条目被静默跳过。这个坑的教训是有两层一是解析逻辑最好不要静默失败当某次解析结果为空或较上次缩水超过一定比例时应当主动打印警告否则脚本“成功”跑完却拿到空数据比直接报错更难排查二是页面结构变化是常态隔段时间重跑一遍之前先瞄一眼页面结构有没有调整。5.4 踩坑实录三热度字段里的“1.2万”和“8900”第一次把数据按热度排序时我直接对原始字符串排序结果“9.8万”排在了“12000”前面整张表看上去完全没规律。当时还以为是抓取顺序有问题后来把字段打印出来仔细看才意识到这是文本排序和数值排序的区别。这个问题在生产级项目里很容易被忽略因为只要维度一多排序错误看起来就像是数据没对齐。现在我的清洗逻辑已经做成独立函数任何文本型数值都先统一单位转int再做排序和统计原始文本只作为展示字段保留。这条经验在后续做舆情分析项目时也帮了大忙。5.5 一个顺手的脚本稳定性建议给完整流程加一行本地日志文件记录每个页面的抓取时间、状态码、解析条数同时把原始HTML按页面号存一份备份文件。这样即使前端改版或者清洗逻辑出问题也能通过日志定位到具体页面通过HTML备份回放当时的真实结构不用重新去网上找页面。备份文件只留最近几次运行即可控制磁盘占用养成这个习惯会觉得爬虫项目特别好维护。6. 拿到数据之后怎么让“资本风向”真正落地6.1 先做一张Top热度榜单清洗完数据第一件值得做的事就是按热度排序输出一份TOP10专题列表。这份榜单可以直观地告诉你当天市场最关注的话题集中在哪些方向是宏观政策类、行业公司类还是国际市场类。如果再抓几个时间点的数据做对比就能观察热点话题的迁移轨迹这个分析框架本身就是“洞察资本风向”的雏形。排序逻辑我已经写进save_results里了直接读Excel文件就能用。想输出更友好的榜单用df.head(10)加to_string就行配合print输出到终端适合每天定时跑完瞄一眼水位。6.2 做关键词词频把标题拆成话题要素专题标题本身是高度浓缩的资讯描述很适合做中文分词和词频统计。用jieba按精确模式分词过滤停用词统计Top词频再把高频词和原始榜单做交叉对照能提炼出“本周资本关注的几大关键词”。比如同一时间节点出现多个与行业相关的专题标题词频结果会明确反映出该行业话题的密集程度。词频分析的价值在于把几十条标题压缩成几个核心词方便快速汇报和复盘。这里提醒一句分词前记得把英文缩写、数字也作为候选词保留财经领域“GDP”“CPI”“A股”这类词才是真正有信息量的标签。6.3 定时采集维护增量历史库单次抓取只能看到当下状态资本风向真正的价值在时间序列里。建议把主流程包进定时任务每天固定时间跑一次比如早上8点30分之前抓完把本次数据追加到历史CSV末尾而不是覆盖写入。追加时用pd.read_csv加载旧数据pd.concat合并且drop_duplicates(subset[title, pub_time])去重避免同一条专题被重复记录。调度方案上本机用crontab或者Windows任务计划就能满足需求不需要把工程做重。真正要关注的是数据文件命名规范和归档策略按月份归档文件名例如yicai_topics_202506.csv后续做月份对比时取数会轻松很多。6.4 可视化词云加趋势图抓到的数据有两个天然的可视化窗口一个是标题关键词的词云直观看本周高热度词另一个是热度随时间的趋势折线看某个专题从出现到热度的爬升过程。词云用wordcloud加matplotlib就能出图趋势图用matplotlib的plot函数即可。不需要引入交互式图表框架先把静态图玩明白后面如果要做看板再考虑Streamlit这类轻量工具。6.5 扩展成半自动情报小系统做完以上几步其实已经是一个半自动的“热点追踪小系统”了定时采集、清洗归档、统计分析、图表输出一条线跑通。想再进一步可以把采集结果接入企业微信机器人或者邮箱通知每天早上推一条当天热点摘要或者把历史数据入库SQLite方便按日期范围查询为后续更复杂的舆情分析预留数据基础。这个项目的真正价值也在这里它不只是练了requests和BeautifulSoup而是把请求、解析、清洗、存储、分析、定时调度这整条数据采集流水线都走了一遍并且所有环节都能在一个普通配置的电脑上独立完成零人工干预。后续不管转向数据分析、自动化测试还是后端开发这条流水线的经验都能直接迁移。最后分享一个我自己的习惯跑完这个项目之后我会刻意保留每次采集的HTML备份和数据文件日期标注清楚。过一段时间再回看这些历史数据能很直观地感受到话题热度的起伏这种数据带来的洞察比单纯写代码带来的成就感更强烈。建议你也从今天开始留好每一次运行的产物爬虫只是手段数据沉淀和持续观察才是做这类项目最有价值的回报。