news 2026/9/8 1:08:07

Python爬虫实战:requests+BeautifulSoup采集基金会公开项目数据全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:requests+BeautifulSoup采集基金会公开项目数据全流程

做数据采集这行,最怕的不是目标网站结构有多复杂,而是你根本不知道自己到底要采什么、采回来能干什么。最近我完成了一轮基金会公开项目数据的深度采集,整个过程没有上重型框架,就是Python爬虫里最常用的requests加BeautifulSoup,再配合一些调度和清洗技巧,把过去几年这些机构对外公示的项目清单、金额、周期、区域、受益群体等核心字段全部结构化落地。这篇文章就是这次实践的完整复盘。

为什么值得写出来?因为这类需求其实非常典型:研究公益行业资金流向的分析师、想做资助项目库的团队、甚至是只学过Python基础但想知道“爬虫在真实项目里到底怎么落地”的开发者,都会遇到一模一样的场景——目标网站是公开的,数据是零散分布在列表页和详情页里的,你需要自己设计一套采集方案把它变成一张干净的表。所以这篇内容不会只扔代码,我会把选型逻辑、解析细节、反爬节奏、排错过程全部讲透,保证你看完能直接照做,而且能避开我踩过的坑。

1. 项目背景与采集目标拆解

1.1 为什么需要采集基金会公开项目数据

基金会的公开项目数据,指的是各基金会在官方网站、信息公示栏目或者公开数据平台中主动向社会公开的信息,通常包括项目名称、项目简介、资助金额、受益对象、实施区域、项目周期、项目状态、执行机构等。这些信息的共同特点是:数量不小、分散在不同页面、格式不完全统一,但每一行都有分析价值。比如你想研究某个公益领域近三年的资金分布,或者想给资助方做一个“同类项目都在做什么”的摸底报告,靠人工一页页复制粘贴是根本不现实的。

我这次的目标也很直接:把选定范围内基金会官网的公开项目列表抓下来,进入每个项目的详情页补全字段,最后汇总成一张结构化的数据表。这个需求在业内其实很常见,只不过很多人一上来就想着“搞个大而全的爬虫系统”,结果光环境配置和框架选型就折腾了好几天,反而忽略了最核心的数据解析。我的经验是:先用手工方式打开几个页面,搞明白信息在哪、长什么样,再写代码去模拟这个浏览过程,效率会高得多。

1.2 采集范围与字段定义

开工之前先定义清楚“采什么”非常关键。我这次划定的范围是公开项目公示页中可公开访问、不涉及隐私和个人敏感信息的数据。字段层面,我设计了一个主表,所有内容围绕项目ID进行关联:

字段名称字段类型说明示例
project_idstring项目唯一标识P20230001
project_namestring项目名称乡村儿童阅读支持计划
fieldstring项目所属领域教育
amountfloat资助金额(万元)120.5
regionstring实施区域云南省
beneficiarystring受益对象乡村小学生
durationstring项目周期2023.01 - 2023.12
statusstring项目状态已结项
summarystring项目简介为……提供阅读资源
source_urlstring详情页URLexample.org/project/123
updated_atstring抓取时间2024-06-01

看到这个表你就明白,这不是一个“随便抓抓”的活,而是要从半结构化页面里提炼出相对规整的业务字段。字段设计早一点想清楚,后续的数据清洗会省掉大量重复劳动。我建议你也按这个思路,在项目开始前先拿Excel手工录入5到10条真实数据,把字段名、格式、取值统一好,这比写到一半再回头改字段要靠谱得多。

1.3 项目目标与非目标

这个项目我只定了三条目标:第一,能自动遍历列表页,拿到所有项目的详情链接;第二,能进入每个详情页抓取核心字段并清洗入库;第三,支持增量更新,不是一次性用完就扔。非目标也很明确:不做全互联网的泛采集,不做需要登录或涉及非公开信息的抓取,不追求每秒几十个请求的高并发。把非目标提前列出来,能防止项目做着做着就跑偏。

2. 技术选型与运行环境准备

2.1 为什么用 requests 而不是更容易“上头”的 Scrapy

很多看了爬虫教程的朋友上来就想用Scrapy或者分布式爬虫框架,但以这类基金会官网的数据量来说,通常就是几千到几万条项目记录,单机、串行、加上适量延时,完全够用了。Scrapy的功能确实强大,但它的学习曲线和调试成本并不低,尤其是项目里还涉及各种动态字段、异常重试、页面结构临时变化,用轻量方案反而更灵活。

我个人的建议是:数据量在十万条以下、目标站点不超过十个、字段以文本和数字为主,直接用 requests + BeautifulSoup 就非常舒服。requests负责发HTTP请求,BeautifulSoup负责解析HTML,两个库加一起不超过一百行核心代码就能把整个列表页和详情页的逻辑跑通。分布式爬虫和消息队列这些,留到真正遇到“单机采集时间不可接受”或者“目标站点分散并且数量庞大”的情况再上,不要为了技术炫技而过度设计。

2.2 环境准备与依赖安装

这次实践我使用Python 3.8以上的版本,建议你直接装3.10或3.11,语法兼容性和第三方库支持都更省心。如果电脑上还没有Python环境,建议先去python.org下载对应平台的安装包,安装时记得勾选“Add Python to PATH”,这是新手最容易漏掉的一步,漏了会导致命令行里敲python毫无反应。

依赖方面只需要四个核心库,执行这条命令就能装齐:

pip install requests beautifulsoup4 lxml pandas openpyxl
  • requests:负责网络请求;
  • beautifulsoup4:负责HTML解析;
  • lxml:用C语言实现的解析器,比默认的html.parser快不少;
  • pandas 和 openpyxl:最后数据清洗和导出Excel用。

如果你是在Linux服务器上操作,可能还需要处理pip权限问题,最简单的做法是加--user参数,或者用一个虚拟环境。我习惯用python -m venv venv创建虚拟环境,然后source venv/bin/activate激活,这样不会把依赖装乱。Windows下激活命令是venv\Scripts\activate,注意PowerShell有时会拦脚本,改成CMD运行就没问题。

2.3 合规与频率控制的基线

这节我必须多说几句,因为爬虫翻车绝大多数不是技术问题,而是节奏问题。我给自己定的规矩有这几条:第一,先看目标的robots.txt,路径是https://目标域名/robots.txt,这里会明确告诉你哪些路径不允许抓取;第二,请求头里带上真实的User-Agent,表明请求来源;第三,两个请求之间至少间隔2到5秒随机延时,绝不做突发式并发;第四,抓下来的数据只用于个人研究或内部整理,不做二次转售或公开传播。

提示:合规不是一句空话。很多基金会网站的公开信息本身就是为了方便公众查阅,在合理频率下采集一般不会有问题,但如果你把对方服务器压垮了,那性质就完全变了。控制自己的请求节奏,既是自我保护,也是对这个行业的尊重。

3. 页面结构分析与数据解析实现

3.1 先手工拆解URL规律

拿到一个目标网站,我做的第一件事不是写代码,而是打开浏览器,手动把列表页、详情页都翻一遍,然后在开发者工具里看URL结构。大多数基金会官网的项目展示栏目会遵循一个很典型的模式:

  • 列表页:https://example-foundation.org/projects?page=1
  • 详情页:https://example-foundation.org/project/123.html

这里的规律是:列表页用查询参数控制分页,详情页用路径参数定位单条记录。找到这个规律后,爬虫骨架就清楚了:先从列表页提取所有详情页的URL,再逐个请求详情页并解析字段。

我建议你用浏览器“查看源代码”而不是直接看渲染后的页面来判断信息位置,因为很多信息虽然能通过JavaScript动态加载,但初始HTML里其实已经有完整数据。如果底层的HTML结构是接口动态返回的,那就要去Network面板里找XHR请求,看看是不是有个JSON接口在提供数据——那种情况反而更好解析。

3.2 列表页信息的提取

列表页上通常能看到项目名称、所属领域、发布时间和详情链接,这些信息往往被包在类似<div class="project-list">的容器里。用BeautifulSoup做解析,核心思路是先定位所有项目条目,再对每个条目做字段提取:

import requests from bs4 import BeautifulSoup 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 parse_list_page(page_url): resp = requests.get(page_url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") items = [] for card in soup.select("div.project-item"): title_tag = card.select_one("h2 a") if title_tag is None: continue detail_url = title_tag.get("href") if detail_url.startswith("/"): detail_url = "https://example-foundation.org" + detail_url items.append({ "project_name": title_tag.get_text(strip=True), "detail_url": detail_url, "field": card.select_one(".category").get_text(strip=True) if card.select_one(".category") else "", }) return items

这段代码里有两个容易被新手忽略的点。第一是resp.encoding = resp.apparent_encoding,很多网站没有在响应头里声明编码,或者声明得和实际内容不一致,用apparent_encoding能根据页面字节内容推断真实编码,避免中文乱码。第二是detail_url的拼接判断,页面里的链接经常是相对路径,必须以域名拼接成绝对URL,否则后续请求会直接失败。

提取完列表页数据后,可以做一层打印或写入临时文件,先确认拿到的详情链接数量是否正确。我通常会先跑第一页,然后手工抽查一两条链接能不能在浏览器里打开,确认没问题再进入批量阶段。

3.3 详情页核心字段解析

详情页是字段最丰富的地方,一个典型的详情页会把项目的资助金额、实施区域、受益对象、项目周期、状态都以“标签+值”的形式列在页面中部。这种情况下,不要依赖页面里某个绝对位置,而是要根据标签文本去定位对应的值,这样即使页面上多了个推荐阅读模块,也不会干扰解析逻辑。

def parse_detail_page(detail_url, project_id): resp = requests.get(detail_url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") field_map = {} # 假设详情页用 dl/dt/dd 结构展示字段 for dt in soup.select("dl dt"): label = dt.get_text(strip=True) dd = dt.find_next_sibling("dd") value = dd.get_text(strip=True) if dd else "" field_map[label] = value summary_node = soup.select_one(".project-intro") return { "project_id": project_id, "amount": parse_amount(field_map.get("资助金额", "")), "region": field_map.get("实施区域", ""), "beneficiary": field_map.get("受益对象", ""), "duration": field_map.get("项目周期", ""), "status": field_map.get("项目状态", ""), "summary": summary_node.get_text(strip=True) if summary_node else "", "source_url": detail_url, }

很多字段并不是每次都能取到,所以我在代码里统一做了缺省处理,取不到就给空字符串,等清洗阶段再做统一处理。这里的核心思路是“尽力而为”而不是“一次到位”,尤其面对历史数据时,早期项目详情页可能字段只有一两项,程序不能因为缺字段就崩溃。这也是我把字段解析写成一个独立函数的价值所在——每换一个目标网站,只需要改动这个函数就能适配。

3.4 数据清洗与结构化输出

原始解析结果基本是字符串,比如“资助金额:120.5万元”“项目周期:2023.01-2023.12”,这些内容虽然人眼看得懂,但对数据分析来说并不合适。所以我在采集之后增加了一个清洗层,专门做三件事:把金额文本转成数值、把日期区间标准化、把区域和受益对象的空值填成“暂未公示”。

import re def parse_amount(text): if not text: return None match = re.search(r"([\d.]+)", text) if match: return float(match.group(1)) return None

金额字段我统一以“万元”为单位存储,因为不同页面上可能出现“120.5万元”“1,205,000元”“120.5万”等不同写法,直接以万元为单位,再在清洗函数里做一次单位换算,后续做统计时就不会出现单位不一致的问题。日期区间我用正则拆成年和月,分别保存为开始日期字段和结束日期字段,比原始字符串更适合做时间趋势分析。

清洗完成后,我用pandas把数据组装成DataFrame,再导出为Excel文件。这个小流程看起来不起眼,但它决定了下游分析和可视化能不能顺利进行。我建议每抓取完成一批数据就先做一次清洗和汇总,不要等到全部抓完再处理,因为那样一旦发现某个字段解析有误,返工成本会非常高昂。

4. 调度策略与反爬规避的实操细节

4.1 请求头伪装与随机延时

爬虫和正常浏览之间唯一的区别,就是它把浏览行为自动化了。因此调度策略的核心原则,就是让程序的行为尽量接近一个人工操作者的节奏。我这边使用了一个User-Agent池,每次请求从池子里随机选一个,避免固定UA被识别;每次请求之间使用2到5秒的随机延时,让访问间隔没有规律。

import random import time USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/120.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) ... Firefox/121.0", ] def make_session(): session = requests.Session() session.headers.update({"User-Agent": random.choice(USER_AGENTS)}) return session def safe_request(session, url, retries=2): for attempt in range(retries): try: resp = session.get(url, timeout=10) if resp.status_code == 200: return resp except requests.RequestException: pass time.sleep(3 + attempt * 3) return None

用Session还有一个隐藏好处:它能复用底层的TCP连接,对于连续访问同一个域名的情况,速度会比每次新建连接快不少,同时对目标服务也更友好。延时方面我建议不要用固定值,固定值在日志里会呈现非常规整的访问节奏,反而容易触发风控,随机范围在2到5秒之间,数据量特别大的时候可以放宽到3到8秒。

4.2 断点续采与URL去重

批量采集最害怕的事情之一,是跑到一半程序崩了,然后你又得从头来一遍。所以我在代码里加了一个“已抓取URL集合”,每次成功解析完一个详情页,就把URL写进本地文件。下次启动时,把这个文件加载进内存,遇到已经抓过的URL就直接跳过。这个机制同时解决了两个问题:一是断点续采,二是避免重复请求消耗不必要的资源。

import os def load_done_urls(path="done_urls.txt"): if not os.path.exists(path): return set() with open(path, "r", encoding="utf-8") as f: return set(line.strip() for line in f if line.strip()) def mark_done(url, path="done_urls.txt"): with open(path, "a", encoding="utf-8") as f: f.write(url + "\n")

实际运行过程中,我会先把列表页所有详情链接收集到一个待抓取队列文件里,然后再启动详情页抓取,这样即使程序中断,我也知道还有哪些链接没处理。对这个项目来说,队列文件用最简单的纯文本文件就够了,每条URL一行,没必要引入Redis或者消息队列。

4.3 数据持久化与增量更新

数据落地我用的是“每天一个独立文件”的方式,文件名加上当天日期,防止覆盖上一轮采集结果。每一轮抓取结束后,我会把所有天文件合并成最新的总表,同时保留历史文件方便回溯。增量更新的逻辑也很简单:列表页越靠前的内容越新,所以增量更新时只需要抓前几页,然后和已有数据里的project_id做一次比对,去重合并即可。

import pandas as pd def merge_existing(path="foundation_projects.xlsx", new_df=None): if os.path.exists(path): old_df = pd.read_excel(path) combined = pd.concat([old_df, new_df], ignore_index=True) combined = combined.drop_duplicates(subset=["project_id"], keep="last") else: combined = new_df combined.to_excel(path, index=False)

这里的去重键是project_id。我建议在采集阶段就保证project_id生成规则稳定,比如从详情页URL里提取数字部分作为ID,这样同一个项目无论被哪一轮抓取,都能稳定对应到同一条记录。

5. 常见问题与排查思路实录

5.1 请求被拦截,返回403或非200状态码

最常见的错误场景之一,就是爬到一半突然收到大量403状态码。我的排查步骤通常是这样:先确认是不是目标站点已经检测到高频访问,如果是,就加大延时、轮换User-Agent;如果仍然不行,再把请求头补上Referer、Accept-Language等字段,伪装得更像真实浏览器。还有一种情况是访问了robots.txt禁止的路径,这种就要检查代码逻辑,主动调整采集范围。

如果目标站做了更严格的防护,单靠requests就很难突破了。但以我这次采集的基金会网站来看,它们本身是面向公众提供查询服务的,只要把频率控制在人类操作范围内,基本不会走到这一步。记住,能用降低频率解决的问题,就别急着上更复杂的工具。

5.2 解析结果为空或字段丢失

解析返回空结果,十有八九是页面结构变了,或者你的CSS选择器写得太严格。排查方法很简单:把当前页面的HTML保存到本地文件,然后用解析库一点点调试,看选择器到底匹配到了什么。我吃过一次亏,某天发现所有项目名称字段全为空,最后定位到原因是运营在页面里加了一个“截止报名”的小标签,把原来的h2 a结构挤到了另一个层级。

注意:调试解析逻辑时一定要用本地保存的HTML文件,而不是每次都去请求线上环境。一方面速度快,另一方面不会因为反复请求给目标站带来压力。这也是好爬虫和坏爬虫之间很明显的区别。

5.3 中文乱码问题

乱码问题的根源是编码判断错误。requests拿到响应后,会先看响应头里的charset,但很多老网站并不会正确声明。我习惯在每个请求后主动设置编码:先用resp.encoding = resp.apparent_encoding,如果还乱,就直接看页面源码里<meta charset="...">标签,手动指定。对中文字符来说,最常见的两个编码是utf-8和gbk,知道了目标站用哪种,基本就不会再乱码。

resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding text = resp.text

5.4 其他踩过的坑

还有几个小坑值得记录。一是列表页数量和详情页数量不一致,往往是列表页里包含了置顶项目或者重复项目,解析时需要额外做一次URL去重。二是采集过程中出现超时异常,requests默认的timeout有时候不够用,尤其是详情页图片多、页面重的场景,把timeout调到15秒左右更稳妥。三是大量数据写Excel时内存占用过高,解决办法是每抓500条就写一次磁盘,不要等全部抓完再一次性写入。

还有一个容易被忽略的细节:如果详情页的数据是通过JavaScript异步加载的,直接解析HTML会什么都拿不到。这时候去浏览器的Network面板里找XHR请求,看能不能直接命中接口。如果找到的是一个返回JSON的接口,基本上就是一马平川了,用resp.json()直接解析,比解析HTML简单十倍。

6. 项目运营经验与后续扩展方向

6.1 从这次采集里沉淀出的几条经验

做完这个项目,我最大的体会是:数据质量远比采集速度重要。很多爬虫教程喜欢强调并发多高、抓得多快,但真实采集场景里,你最终交付给业务方或自己分析的是一张可信的数据表,而不是一堆抓下来的网页。所以我在每个环节都设置了校验点:列表页解析完先看条数,详情页解析完先看字段覆盖率,清洗完先看金额和日期分布。任何一步发现异常,就停下来人工对比页面,而不是让程序不管三七二十一跑完。

另一点是日志一定要留。我用最基本的logging库,把每次启动、每完成一个页面、每发生一次异常都记录下来。这个习惯在数据量小的时候看不出价值,但项目维护一个月后,你会非常需要日志来定位“昨天怎么少抓了十个项目”这种问题。日志文件按天滚动,保留最近三十天,占用磁盘空间很小。

6.2 后续可以继续做的方向

这个项目的代码结构本身就留了扩展空间。如果你想把框架搭得更完整,可以在现有基础上做几件事:第一,接入定时调度工具,每天凌晨自动跑一次增量更新;第二,把输出从Excel升级成SQLite数据库,查询和增量合并会更方便;第三,采集结束后对接大模型做自动摘要,把项目简介压缩成几句可读性更强的文本,方便后续生成行业月报。更远的扩展方向,是把采集范围从单一基金会扩展到多个数据源,这时候就可以考虑用轻量的任务队列来管理不同站点的采集任务,但核心解析逻辑依然可以复用这一套。

最后再分享一个小技巧:别把爬虫代码写成一个几百行的脚本堆到底。我习惯把网络请求、页面解析、数据清洗、数据存储分别拆成独立函数,这样每次换目标站,只需要重写“页面解析”这一层,其余逻辑可以原封不动地迁移。爬虫这个行当,代码更新迭代特别快,结构清晰一点,能给未来的自己省下大量返工时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 1:07:20

前端直接渲染后端超大高精度SVG:放弃Echarts后的完整实践方案

接手一个前后端分离项目时&#xff0c;后端同事直接把一批超大高精度SVG矢量图丢了过来&#xff0c;问我前端能不能直接渲染&#xff0c;还强调“别再用Echarts硬转&#xff0c;精度扛不住”。一开始我还觉得奇怪&#xff0c;Echarts不是有SVG渲染器吗&#xff0c;后来真上手才…

作者头像 李华
网站建设 2026/9/8 1:02:55

n8n实现混合数据RPA:GUI与API自动化整合方案

1. 项目概述&#xff1a;n8n中的混合数据RPA挑战在自动化流程设计领域&#xff0c;n8n作为开源工作流自动化工具正获得越来越多企业的青睐。最近我在为一个跨境电商客户设计库存管理系统时&#xff0c;遇到了一个典型场景&#xff1a;需要同时操作本地ERP软件的图形界面(GUI)和…

作者头像 李华
网站建设 2026/9/8 1:01:47

三段式爬虫管道设计:列表-详情-附件解耦采集架构

做采集项目这些年&#xff0c;我踩过最深的坑&#xff0c;不是反爬严&#xff0c;也不是解析难&#xff0c;而是把“抓列表”“抓详情”“下附件”全塞在一个脚本里&#xff0c;几百行代码串成一坨&#xff0c;跑到一半报错&#xff0c;从头再来。后来我把这套流程重构成“列表…

作者头像 李华
网站建设 2026/9/8 0:59:23

Android技术负责人实战:架构、性能与合规的三重权衡

这些年带 Android 团队&#xff0c;越来越觉得“技术负责人”这个头衔的分量不在代码量&#xff0c;而在判断力。架构、性能、合规&#xff0c;这三座大山每个单拎出来都能写好几本书&#xff0c;但实际工作中它们往往是缠在一起的——你做了一个漂亮的组件化改造&#xff0c;结…

作者头像 李华
网站建设 2026/9/8 0:58:38

Adobe Bridge 2025安装全攻略:从环境准备到素材高效管理实战

装Adobe Bridge这件事&#xff0c;听起来比Photoshop、Premiere这种大软件简单多了&#xff0c;结果我上周帮朋友新电脑装2025版&#xff0c;硬是折腾了两个多小时。卡进度条、提示磁盘空间不足、装完双击没反应&#xff0c;各种状况轮着来。后来我把整个流程从头到尾捋了一遍&…

作者头像 李华
网站建设 2026/9/8 0:58:28

有效SEO策略制定全流程:从关键词研究到技术优化实战指南

很多做网站的朋友都来问过我同一个问题&#xff1a;SEO到底怎么才能做出效果&#xff1f;市面上讲SEO的内容浩如烟海&#xff0c;今天教你一招&#xff0c;明天告诉你一个秘籍&#xff0c;可真到自己上手的时候&#xff0c;往往还是一头雾水。我觉得核心问题不在于你懂不懂某个…

作者头像 李华