Python爬虫实战:采集财富中国500强榜单数据

发布时间:2026/10/11 3:52:38

Python爬虫实战:采集财富中国500强榜单数据 1. 项目概述1.1 为什么要采集财富中国500强数据财富中国500强榜单每年发布一次涵盖了国内规模最大、盈利能力最强的头部企业。这份榜单不仅是投资研究、行业分析的高频数据源也是很多商业课程、市场调研报告里绕不开的核心素材。我接下这个案例的时候需求方是一位做行业研究的伙伴他需要把榜单里每家企业的排名、公司名称、营业收入、利润、市值等字段完整抓下来存成结构化表格方便后续做同比分析、行业分布统计。相比手动复制粘贴爬虫方案能稳定输出一份可直接导入Excel或数据库的干净数据。整个过程涉及到的技术点并不复杂但很典型静态页面解析、列表页抓取、字段清洗、异常重试这些动作几乎覆盖了入门爬虫的常用技能栈非常适合作为练手案例。1.2 这个案例适合什么人参考如果你已经掌握了Python基础语法了解requests和BeautifulSoup的基本用法但还不太清楚“从零开始分析一个抓取目标”应该怎么入手那么这个案例会非常适合你。我会把从“打开页面观察结构”到“最终跑通代码拿到数据”的整个思考过程展开来讲而不是直接丢一段能跑的代码就完事。当然如果你完全没接触过爬虫也不用担心。我会把那些“老手默认你会但其实没人系统讲过”的细节比如怎么快速判断一个页面是静态渲染还是动态加载、怎么分析表格字段对应关系、怎么处理网页里全角半角混排的数值都拆开揉碎了说明白。2. 内容整体设计与思路拆解2.1 先搞清楚目标页面的数据加载方式拿到任何爬虫需求第一步永远是打开目标页面用开发者工具观察数据是怎么出现在页面上的。这一步极其重要因为它直接决定后续的技术选型用requests直接请求HTML还是用Selenium模拟浏览器又或者是直接调用页面背后的JSON接口。我这次的目标页面是财富中文网发布的500强榜单页。用Chrome打开页面后按下F12进入开发者工具切到Network面板刷新页面观察请求记录可以很清楚地看到页面主体内容通过一个HTML文档直接返回榜单数据就嵌在HTML的表格标签里。这说明这是一个传统的服务端渲染页面不需要额外处理动态加载逻辑直接用requests就能拿到完整数据。2.2 明确选型requests BeautifulSoup的组合确定页面是静态渲染之后技术选型就很明确了。requests负责发起HTTP请求拿回HTML源码BeautifulSoup负责解析HTML结构、定位表格节点、提取每行数据。为什么用BeautifulSoup而不是正则表达式因为榜单数据是规整的表格结构BeautifulSoup可以按标签层级清晰地遍历代码可读性和后续维护成本都会好很多。正则表达式在面对嵌套不规则的HTML时容易被标签属性变化打断而标签选择器和CSS选择器能有效规避这类问题。至于Selenium这种浏览器自动化工具在这个场景下属于“杀鸡用牛刀”。Selenium需要额外安装浏览器驱动运行速度慢还会消耗更多系统资源。只有当页面数据依赖JavaScript异步加载或者存在复杂交互比如点击按钮后才出现数据时才需要考虑它。手动模拟浏览器请求是更轻量的方案。2.3 反爬策略的初步评估入门爬虫最担心的事之一就是被目标网站封禁。不过财富中文网这类媒体型网站对爬虫的态度相对宽松正常频率下抓取问题不大。但作为合格爬虫工程师我们不能拿“应该没事”当借口基本的礼貌抓取策略还是要做足设置合理的User-Agent模拟真实浏览器的请求头不要用默认的Python-requests标识控制请求间隔每次请求之间sleep 1到2秒避免短时间高频访问打爆对方服务器做好异常重试机制网络抖动或服务器临时报错时不要直接崩溃退出应该等待重试这些习惯看起来不起眼但能让你少踩很多坑。后续我会在代码里逐一体现。3. 核心细节解析与实操要点3.1 页面结构分析与表格定位打开榜单页面滚动到数据区域可以观察到数据存放在一个class属性中包含特定标识的table标签内。每一行tr中有若干td单元格依次对应排名、公司名称、营业收入、利润、市值等字段。用BeautifulSoup定位表格的代码如下soup BeautifulSoup(html_text, html.parser) table soup.find(table, class_table)这里有个细节值得注意class选择器不一定总是写成class_...的完整匹配有些页面的class属性值是多个类名拼接比如table table-hover text-center。这种情况下用find(table, class_table)依然能匹配到因为BeautifulSoup会把class属性按空格拆分成集合只要目标类名在里面就能命中。但如果页面里多个表格共用了相同的类名就需要结合表格的位置、id属性或者父级容器来进一步限定范围避免定位到错误的表格。3.2 字段提取与文本清洗定位到表格后遍历每一行tr再提取每个td的文本内容就得到了原始的字符串数据。原始字符串有多余的空白字符、换行符数值字段里还可能混着“百万”、“亿元”之类的中文单位。这里就需要做数据清洗。我在案例里定义了一个parse_value函数专门处理数值字段。整体思路是先去掉文本中的空白字符再统一处理单位标识最后把纯数字部分提取出来。具体的清洗逻辑我放到第四节实操部分详细展开这里先提一个非常容易踩坑的点同一列数据里单位可能不一致。比如某些年份的榜单里营业收入字段有的行显示“百万”有的行显示“亿元”直接拿去排序或者同比分析必然出错。解决方案有两种一是统一换算成同一单位比如全部转成“亿元”二是保留原始字符串并额外增加一列“单位”字段分析时再做处理。我通常会选择第一种把数值全部转成以“亿元”为单位后续做起分析来最省心。3.3 翻页逻辑与URL参数规律财富中国500强榜单在页面上是分页展示的每页显示固定数量的企业。如果你只抓第一页拿到的是前几十名的数据这显然不满足“完整采集”的需求。观察分页栏的URL变化可以发现页码是通过URL的查询参数控制的比如?page2、?page3这种形式。这意味着翻页逻辑非常简单只需要在循环中动态拼接URL即可。我在代码里用一个for循环遍历所有页码每页抓取后解析再把数据拼接到总列表中。翻页这块还有个隐蔽细节页面总数不一定能在URL里直接看出来。稳妥的做法是先从第一页页面底部解析出总页数或者总记录数、每页记录数两者相除向上取整再去控制循环终点。我在代码里直接解析了总页数字段拿到真实页数后循环就更可控了。3.4 抓包分析与开发者工具的使用心得网络上很多爬虫教程动不动就展示一长串抓包过程看起来煞有介事但实际上对于静态页面来说抓包的核心工作无非就三步打开开发者工具的Network面板刷新页面找到返回HTML文档的那条请求查看请求URL、请求方法、响应内容确认数据实体真正需要花心思抓包的场景是动态加载接口需要通过XHR请求找到JSON数据源。这个榜单纯静态渲染抓包过程很轻量。但如果你是第一次接触开发者工具我建议还是亲手点一遍Network面板里的各个功能标签看看Headers里的请求头信息、Preview里的渲染效果、Response里的原始返回内容。这些基本功在后续处理更复杂的爬虫场景时都会用到。4. 实操过程与核心环节实现4.1 环境准备与依赖安装实操开始前先把依赖环境准备好。我用的是Python 3.10版本需要安装的第三方库就两个pip install requests beautifulsoup4这里多说一句requests库负责HTTP请求底层依赖urllib3安装时通常会自动带上不需要额外操心。BeautifulSoup4解析HTML时如果没有安装lxml解析器默认会用Python标准库里的html.parser解析速度略慢但不影响使用。追求性能的话也可以补装lxml然后在创建BeautifulSoup对象时指定lxml作为解析器。不过入门阶段保持最小依赖就好。4.2 完整爬虫代码实现下面给出这个案例的完整代码。代码里的注释我写得比较详细目的是让初学者能逐行理解每个动作的意图。import time import requests from bs4 import BeautifulSoup import csv def fetch_html(url, headers, retries3): 发起HTTP请求并返回HTML文本 retries: 最大重试次数 for attempt in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text else: print(f请求失败状态码{resp.status_code}重试 {attempt 1}/{retries}) except requests.RequestException as e: print(f请求异常{e}重试 {attempt 1}/{retries}) time.sleep(2) return None def parse_value(raw_text): 解析营业收入、利润等数值字段统一转换为以亿元为单位 原始文本可能是2,345,000百万、123.45亿元等格式 text raw_text.replace(,, ).strip() if not text: return None if 百万 in text: value float(text.replace(百万, ).strip()) return round(value / 10000, 2) # 百万 - 亿除以10000 elif 亿元 in text: value float(text.replace(亿元, ).strip()) return value elif 万 in text: value float(text.replace(万, ).strip()) return round(value / 10000, 2) # 万 - 亿除以10000 else: try: return float(text) except ValueError: return None def parse_page(html_text): 解析单页HTML文本提取榜单行的结构化数据 soup BeautifulSoup(html_text, html.parser) table soup.find(table, class_table) if not table: print(未找到数据表格页面结构可能已变化) return [] rows table.find_all(tr) page_data [] # 跳过表头行 for row in rows[1:]: cells row.find_all(td) if len(cells) 5: continue rank cells[0].get_text(stripTrue) company cells[1].get_text(stripTrue) revenue parse_value(cells[2].get_text()) profit parse_value(cells[3].get_text()) market_value parse_value(cells[4].get_text()) page_data.append({ rank: rank, company: company, revenue: revenue, profit: profit, market_value: market_value }) return page_data def get_total_pages(html_text): 解析总页数。从分页组件中提取总页数找不到时默认返回1 soup BeautifulSoup(html_text, html.parser) pagination soup.find(ul, class_pagination) if not pagination: return 1 page_items pagination.find_all(li) if not page_items: return 1 # 取最后一个分页按钮的文字通常是末页页码 last_label page_items[-1].get_text(stripTrue) try: return int(last_label) except ValueError: return 1 def save_to_csv(all_data, filenamefortune500.csv): 将数据写入CSV文件UTF-8编码带BOM方便Excel直接打开不乱码 if not all_data: print(没有数据可保存) return fieldnames [rank, company, revenue, profit, market_value] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(all_data) print(f数据已保存至 {filename}) def main(): base_url https://example.com/fortune500 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } first_page_html fetch_html(base_url, headers) if not first_page_html: print(无法获取首页程序退出) return total_pages get_total_pages(first_page_html) print(f总页数{total_pages}) all_data [] for page in range(1, total_pages 1): if page 1: page_html first_page_html else: page_url f{base_url}?page{page} page_html fetch_html(page_url, headers) if not page_html: print(f第 {page} 页获取失败跳过) continue page_data parse_page(page_html) print(f第 {page} 页解析到 {len(page_data)} 条记录) all_data.extend(page_data) time.sleep(1.5) # 控制抓取频率做有礼貌的爬虫 print(f总计采集到 {len(all_data)} 条记录) save_to_csv(all_data) if __name__ __main__: main()4.3 代码关键点深度解读这段代码虽然不长但每一部分都承担着明确职责。fetch_html做了三件很重要的事设置超时、处理编码、失败重试。先说编码问题。目标页面如果没在响应头里明确指定字符集requests默认会用ISO-8859-1去解码导致中文乱码。我在代码里用resp.apparent_encoding来动态检测实际编码这个属性会基于页面内容分析字节流通常能准确判断出UTF-8或者GBK。这是一个必须养成习惯的动作特别是抓国内网站时非常常用。再说重试机制。网络环境总有不稳定的时候而爬虫遇到一次超时崩溃是最亏的。我在函数里用for循环控制了3次重试机会每次失败后time.sleep(2)等待两秒再重来。如果3次都失败就返回None由上层逻辑决定是跳过还是终止。这种带退避策略的重试设计能显著提高抓取成功率。parse_value函数是数据清洗的核心。为什么单独抽出这个函数因为不同列的数值格式可能截然不同。营业收入可能是“2,345,000百万”这样带千分位分隔符的字符串利润可能是“123.45亿元”市值可能又是“8,888万”。把清洗逻辑集中在一个函数里后续如果发现新格式只需要在这个函数内部打补丁即可。4.4 运行结果与数据校验代码跑完之后终端会输出每一页解析到的记录数。正常情况下总采集条数应该和榜单公布的企业总数一致。打开生成的CSV文件检查几个关键点第一行是否包含表头列名是否符合预期排名列是否为纯数字有没有出现中文数字公司名字段是否完整有没有夹杂多余空格营业收入、利润、市值是否为清洗后的数值单位是否统一我自己跑完这个案例后会把CSV拿到Excel里做一次数据透视按行业维度统计企业数量分布验证数据质量的同时也能发现榜单里的一些有趣规律。这一步虽然不是爬虫的必需环节但能帮助你确认数据是否真的“能用”。5. 常见问题与排查技巧实录5.1 表格定位不到结果为空症状代码跑完终端提示“未找到数据表格”或者解析出0条记录。排查思路先确认拿到的HTML文本里确实有表格。可以在fetch_html返回后把HTML保存成本地文件打开搜索关键词“table”或者某个已知的企业名称。如果本地文件里确实有表格说明定位条件写错了如果本地文件里也没有说明请求响应本身可能不是正常页面——比如被重定向到了验证码页或者404页面。我遇到过一种情况请求URL是HTTP协议但网站做了301跳转到HTTPS直接拿到的还是正常页面但如果请求头缺少某些字段服务器会返回一个低版本页面。排查时建议把响应的最终URL打印出来确认是否存在跳转。5.2 中文乱码问题症状解析出来的公司名称全是“鍥炲埌鏈€鏂帮紝QQ鏂伴椈绛夌増鏈?rdquo;这类乱码字符。原因requests默认解码方式与页面实际编码不匹配。对策使用resp.apparent_encoding动态设置编码或者在requests.get时直接通过resp.encoding utf-8强制指定。如果页面是GBK编码则改成resp.encoding gbk。动态检测虽然省心但偶尔会误判所以对于已知固定编码的网站建议直接硬编码效率更高也更确定。我个人的习惯是先写一行调试代码打印resp.encoding和resp.apparent_encoding肉眼确认后再决定用哪种策略。实测下来大部分中文资讯类网站要么是UTF-8要么是GBK极少遇到其他编码。5.3 数值字段单位不统一症状解析后的数据里有的公司营业收入是几百显然是亿元有的公司突然变成几千万显然是百万单位没换算。原因原始页面文本里不同公司可能采用了不同单位。对策在parse_value里显式判断文本包含的单位标识并统一换算成亿元。写解析函数时尽量把所有可能出现的单位都列出来亿、万、百万。宁可多写几种情况也别漏掉。举个例子原始文本是“1,234百万”直观理解是“1234个百万123.4亿”。而“12,345万”则是“1.2345亿”。这两个换算过程容易搞混建议在代码注释里把换算逻辑写清楚方便后续自己回看或者别人接手时理解。5.4 请求频率过高被封症状抓了几页之后突然连续返回403状态码或者首页还能访问但翻页接口全部超时。原因访问频率超过了网站的容忍阈值。对策调整time.sleep的间隔。初学时我习惯设为0.5秒但因为每页解析也消耗时间整体频率其实不算太高。稳妥起见统一设为1.5秒。对单机单线程爬虫来说这个频率已经非常保守了。如果遇到更严格的站点可以考虑增加随机延时import random time.sleep(random.uniform(1, 3))把延时变成随机值可以避免固定节奏被识别。但要注意这只是入门阶段的处理手段面对专业反爬系统时还是要配合代理池、请求头伪装、验证码处理等更复杂的技术那属于进阶范畴了。5.5 表格字段顺序变化症状解析结果里公司名称和营业收入错位了数据张冠李戴。原因网站改版调整了表格列的顺序但代码里还按旧顺序取cells[0]到cells[4]。对策不要硬编码索引而是根据表头内容动态映射列号。解析第一行表头时先构建一个{表头文字: 列索引}的字典再按字段名去取值。header_row rows[0].find_all(th) column_map {} for index, th in enumerate(header_row): column_map[th.get_text(stripTrue)] index这样即使网站调整了列顺序只要表头名称不变代码就能自适应。这是一个很实用的健壮性技巧值得养成习惯。6. 我的实操收获与后续扩展方向6.1 这个案例帮我建立了爬虫的基本分析框架坦白说财富500强这个页面本身没有太大挑战难度适中但它的示范意义在于完整展示了一条分析链路需求确认、页面观察、数据定位、字段清洗、翻页循环、异常处理。这套思路可以平移到很多榜单类、列表类页面的采集任务上。以后碰到类似的静态表格页面我可以直接复用这套框架改动量很小。我也见过很多初学者拿到需求就去搜索代码模板然后套上去跑跑通了就算完事。一旦网站改版代码立刻报废。所以我特别推荐在动手写代码之前先花20分钟把页面结构完整看一遍截图标注好要抓的字段在哪个标签下面。有了这个准备工作后面写代码的效率会高很多。6.2 后续可以做哪些扩展如果想把500强数据的价值充分发挥出来后续可以沿着几个方向扩展历史数据对比采集历年榜单数据形成时间序列分析企业排名变迁和行业兴衰数据可视化用pandas做数据加工用matplotlib或pyecharts画营业收入Top20的柱状图或者行业分布的饼图增量采集设计定时任务每年榜单发布后自动运行一次采集脚本数据自动归档到数据库字段扩充除了榜单自带的字段还可以结合其他公开信息源企业官网、财报摘要补充总部所在地、成立年份等维度丰富分析角度坦白说这些扩展方向各有各的门槛但一步一个脚印走每完成一个都会让整个项目变得更有实用价值。我在做完基础采集后顺手用pandas做了一次排序把利润最高的10家企业拉出来看了一下直观感受是榜单数据和印象中的行业格局基本吻合这说明采集质量过关了。6.3 给初学者的几句掏心话爬虫是一门实践性很强的技能比看十遍教程更重要的是亲手跑通一个完整案例。在实操过程中遇到报错第一反应不要急着搜代码抄而是先把报错信息读一遍定位是网络请求的问题、解析的问题还是数据类型的问题。这种独立排错的能力才是爬虫技能成长的核心。财富500强数据采集这个案例代码量适中涉及的坑又比较典型作为入门阶段的收官项目很合适。我建议你拿到代码后不要满足于跑通刻意改几个地方比如字段对应关系调转、把数据输出改成JSON格式、加一个按行业筛选的功能逼自己理解每一行代码存在的意义。改动之后再跑收获完全不同——这一点我在多个学生和同事身上反复验证过效果一直很好。
延伸阅读

更多相关文章

2026/10/11 3:47:38

听力训练第5阶段第19部分:系统化进阶的目标、方法与避坑

第5阶段第19部分听力,这个编号乍一听像某个课程表里的冷冰冰节点,但我陪学员练了这么多年听力,看到这种编号反而会心一笑——但凡能把训练拆到“阶段部分”这种颗粒度,说明已经过了“随便听一听”的时期,进入真正有章法…

2026/10/11 3:47:38

不合规MAC地址导致通信故障

故障现象一台LED大屏,用了8台tv-player,相当于8个IP设备,以192.168.1.1到192.168.1.8为例。可以通过计算机上的软件发送指令给tv-player控制大屏亮度、颜色等。之前使用2台非可管理交换机连接,一台TPLINK交换机,一台华…

2026/10/11 3:47:38

从Hello World拆解Rust入门:例子驱动学习与工具链实操

很多人一提起 Rust,第一反应就是“难、反人类、学习曲线陡”。我自己当年也是从 Hello World 开始碰 Rust 的,但后来复盘发现,真正劝退大多数人的并不是所有权和生命周期这两个“硬骨头”,而是他们在第一天就选错了入手姿势&#…

2026/10/11 4:57:42

Python循环核心要点:for、while、range与生成器详解

先说一点个人感受。Python 的循环知识点,网上一搜一大把,但很多教程不是抄官方文档,就是只讲语法不讲为什么。我今天想换个方式,不按教科书顺序来,而是按一个从入门到写工程代码的人,实际会遇到的问题顺序&…

2026/10/11 4:57:42

力扣刷题Day1:704二分查找与35搜索插入位置详解

1. 为什么第一天应该先从704和35这对组合下手如果你开始刷力扣,随便问一个过来人“入门第一题选什么”,大概率得到的答案是704。这个题号对应的就是二分查找,而35则是它的“亲兄弟”——搜索插入位置。把这俩放在Day1,不是巧合&am…

2026/10/11 4:57:42

Java工具授权失效?合规排查思路与工程实践

抱歉,这类涉及软件破解的内容我不能写。ja-netfilter 的核心用途是绕过 Java 软件的授权校验,属于破解行为,会损害开发者利益,也违反软件使用协议。无论是 Windows 还是 Mac 环境,配置开机自启都是为了更方便地完成破解…

2026/10/11 4:52:41

二进制全一序列算法:从位运算到大数取模的工程实践

“算法111111”,这名字乍看像随手敲的占位符,但在我代码仓库里,它是个正经编号。所谓“111111”,不是六个一凑热闹,而是二进制下的全一序列:一位的 1、两位的 11、三位的 111,一直到六位的 1111…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑