requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
发布时间:2026/10/25 0:00:19 锦皓数字建站

简介一份以实战为导向的Python爬虫入门资源面向具备基本编程基础、希望快速掌握网络数据抓取的初学者可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档完整演示从网页中提取标题的流程涵盖GET请求发送、状态码判断、文档解析、标签定位等基础步骤并延伸介绍了CSS选择器与异常处理思路。资源为单个docx文档体积仅14KB内容紧凑无冗余便于离线阅读随堂实践。文档中提供了可复制的完整代码包括依赖库安装命令、请求失败时的状态码提示、遍历并打印标题文本的具体写法并建议读者自行替换URL进行验证同时通过示例强调实际操作需遵守robots.txt规则、控制访问频率避免对目标网站造成过大负载帮助读者建立规范的爬虫开发习惯。目前已有133人浏览学习适合作为爬虫入门的第一份手边参考也适合作为教学辅助材料快速上手。1. 从Requests到BeautifulSoup一次只抓的Web Scraping入门不少人学爬虫第一反应是找复杂的框架、上分布式采集结果被环境配置劝退。我的建议正好相反先用 requests 发一个 HTTP GET 请求再用 BeautifulSoup 从返回的 HTML 里挑出全部标签把这条链路跑通再谈其他。requests 负责把网页内容原样搬回来BeautifulSoup 负责从这堆带尖括号的文本里精准切出标题文字两者分工明确加起来不到二十行代码。这篇笔记适合有基础 Python 语法、想快速上手网络数据采集的人代码可以直接复制运行重点放在参数怎么调、失败时看什么输出。2. 搭建环境与第一个请求requests的选型理由与关键参数2.1 为什么是 requests 而不是 urllibPython 标准库里其实自带 urllib也能发 HTTP 请求但处理 Cookie、重定向、请求头拼接时要写不少样板代码遇到需要维持会话的站点更是麻烦。requests 把这些底层细节统一封装成 get、post、put 这类方法调用一个方法对应一个动作代码可读性高很多。对于入门阶段requests 几乎是社区公认的首选。另一个值得知道的点是requests 底层用的是 urllib3它把连接池复用、TLS 握手、自动解压这些脏活全接管了。你看到 response requests.get(url) 这一行背后其实是 DNS 解析、TCP 连接、发送请求头、接收响应体的完整流程。理解这一点对排查问题很有用——比如状态码返回 200但 response.text 里是一段「访问异常」提示问题大概率不在 requests而在对方服务器识别出这不是浏览器发出的请求。2.2 安装与最小请求三步验证网络通不通安装很简单在终端里执行下面这行命令即可pip install requests beautifulsoup4我习惯先在虚拟环境里操作避免把全局 Python 环境弄乱。装好后别急着写完整爬虫先跑一个最小请求验证网络链路通不通import requests url http://example.com response requests.get(url, timeout10) print(response.status_code) print(response.text[:200])这段代码的作用是发送 GET 请求并打印状态码和响应体前 200 个字符先确认目标网站真的能访问。status_code 等于 200 说明请求成功如果打印出 403 或 406说明服务器拒绝了这次请求后面需要加请求头伪装浏览器。先把这一步跑通再往下做解析能省很多排查时间。2.3 响应对象的关键成员拿数据前先读懂它requests.get() 返回的是一个 Response 对象里面有几个成员在爬虫里几乎天天要用。response.status_code 是 HTTP 状态码response.text 是响应体按推测编码解码后的字符串中文网站偶尔会解码错误需要手动指定 encodingresponse.content 是原始字节下载图片或二进制文件时用它response.headers 里能看到 Content-Type、Server 字段判断返回内容到底是什么类型。params {page: 1, limit: 20} response requests.get(url, paramsparams, timeout(3, 10)) print(response.url)params 参数会自动把字典拼成查询串追加到 URL 后面比如 ?page1limit20这样不用手动拼接字符串。timeout 推荐传元组 (3, 10)前一个数是连接超时后一个数是读取超时单位都是秒。只传一个数的话连接和读取共用同一个值如果服务器一直不返回数据脚本可能长时间卡住这是新手常踩的坑。不同状态码在爬虫场景里的含义也值得记一下状态码含义爬虫里常见场景200请求成功正常返回页面内容301/302重定向requests 默认自动跟随403被拒绝缺少 UA 或触发反爬策略404地址不存在链接失效或写错路径429请求过于频繁触发限流需要放慢速度500服务器内部错误目标站本身出问题重试也未必有用实际调试时优先看 response.text 的开头几百字而不是只看状态码。很多反爬页面为了稳定会故意返回 200但内容已经换成了验证提示。提示如果目标页面需要登录才能访问requests.get() 里还可以加 cookies 参数或者先用 requests.post() 提交表单获取会话后续章节的 Session 对象会更适合这种场景。3. 解析HTML提取标题find_all与CSS选择器的搭配用法3.1 find_all 与 Tag 对象从h1标签开始提取BeautifulSoup 的核心作用是把 HTML 文本解析成一棵标签树。soup.find_all(h1) 会返回一个列表列表里每个元素是一个 Tag 对象代表页面上的一个标签。要从 Tag 里取出纯文字用 get_text() 方法。from bs4 import BeautifulSoup html_text htmlbodyh1第一个标题/h1h1第二个标题/h1/body/html soup BeautifulSoup(html_text, html.parser) h1_tags soup.find_all(h1) for h1 in h1_tags: print(h1.get_text(stripTrue))这里我用了一段静态 HTML 字符串做演示效果和解析真实网页响应是一样的。get_text(stripTrue) 会去掉标题首尾的空白字符和换行避免打印出来带着一大串空格。注意 strip 参数只清理首尾不会动标题内部的空格如果正文里有连续空格需要另做清洗。find_all 还可以加参数缩小范围比如 find_all(h1, limit3) 只取前三个结果find_all(h1, class_title) 只查找 class 为 title 的 h1 标签。这里 class_ 后面必须带下划线因为 class 是 Python 的保留关键字直接写 class 会报语法错误。我见过不少人在这一步被卡住报错信息里明确提示了这一点。3.2 select() 与 CSS Selectors层级定位比循环查找更省事find_all 按标签名和属性筛选时还够用但遇到「某个区域下的某类元素」这种层级关系写起来就啰嗦了。比如要抓 id 为 content 的 div 里面所有 h2用 find_all 要先定位 div 再在结果上继续 find_all代码会多一层嵌套。soup.select() 方法接受 CSS Selectors 语法可以直接用一行表达式完成同样的定位。# 选取 class 为 title 的所有 h1 soup.select(h1.title) # 选取 id 为 header 的容器内部的 h1 soup.select(#header h1) # 选取带 href 属性的所有 a 标签 soup.select(a[href]) # 选取 class 为 content 的 div 下的直接子元素 p soup.select(div.content p)select() 返回的同样是 Tag 对象列表取值方式和 find_all 没有区别。实际项目里我通常两个方法混用先用 select() 把范围锁定到某个区块再在区块内用 find_all() 提取同类型的多个元素。CSS Selectors 的另一个好用的地方是属性选择器比如抓所有带 download 属性的链接一行就能搞定不用写循环去判断。3.3 完整脚本与三个边界情况空列表、嵌套标签和重复标题把前面的内容串成一个完整脚本加上异常处理和编码兜底import requests from bs4 import BeautifulSoup url http://example.com try: response requests.get(url, timeout10) response.raise_for_status() except requests.exceptions.RequestException as e: print(请求失败, e) exit() if text/html not in response.headers.get(Content-Type, ): print(目标不是 HTML 内容拒绝解析) exit() response.encoding response.apparent_encoding soup BeautifulSoup(response.text, html.parser) h1_tags soup.select(h1) print(f共找到 {len(h1_tags)} 个 h1 标题) for h1 in h1_tags: title h1.get_text(stripTrue) if title: print(title)raise_for_status() 会在状态码为 4xx 或 5xx 时主动抛异常这样就不用每次手动判断状态码。response.encoding 先赋成 apparent_encoding这是从页面内容里推测出的编码比 requests 默认按响应头猜测的方式更可靠。脚本最后额外判断了 h1 是否为空文本避免页面里存在空标签时打印一堆空白行。边界情况要提前想清楚。第一页面上可能一个 h1 都没有返回空列表代码不报错但也没有输出这不代表爬虫坏了而是页面结构问题。第二h1 内部可能嵌套了 a 链接或 spanget_text() 会连同子标签的文字一起取出拿到的是拼接后的完整文本。第三一个页面有多个 h1 时脚本会全部打印如果你只想要第一个改成 soup.select_one(h1) 即可。注意解析器我默认用 html.parser它不需要额外安装第三方库。lxml 解析速度更快但要单独装编译好的二进制包入门阶段没必要增加这个变量。4. 爬虫避坑指南五个会把新手逼疯的翻车现场这一章完全来自实战中反复遇到的故障。每个问题都按「现象 → 原因 → 解决」展开照着排查能省下很多翻论坛的时间。4.1 状态码200但抓到的是提示页现象requests.get() 返回的状态码是 200打印 response.text 却发现里面不是目标内容而是「您的访问异常」或一道验证页面。原因这是反爬策略的常见手法。服务器判断请求头里的 User-Agent 不像浏览器就返回一个兜底页面但状态码仍然保持 200方便前端正常渲染。只看状态码完全发现不了问题。解决先打印 response.text 的前 200 个字符看一眼实际内容。然后给请求加上浏览器请求头至少带上 User-Agentheaders { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } response requests.get(url, headersheaders, timeout10)设置 UA 后大部分提示页问题都能解决。如果还不行可能需要先访问首页拿到 Cookie 再请求目标页这时就该用下一章的 Session 对象了。4.2 中文标题乱码encoding 的黑匣子现象抓回来的英文和数字正常但中文标题全部变成乱码比如「å»¶」这种无法辨认的字符。原因requests 在解码响应体时会根据响应头里的 charset 推断编码。有些网站响应头里没写 charsetrequests 会回退到一个默认值中文页面在这种默认编码下解码必然出乱码。解决手动指定编码优先用 apparent_encoding它是从页面内容字节特征推测出来的response.encoding response.apparent_encoding也可以在抓取前先查看响应头里的声明print(response.headers.get(Content-Type))如果里面有 charsetutf-8 之类直接赋给 response.encoding 也行。注意一点乱码问题出在解码环节BeautifulSoup 本身没有责任soup 解析的是已经解码的字符串。4.3 find_all 结果为空动态渲染的锅现象代码没有任何报错response.status_code 是 200但 soup.find_all(h1) 返回空列表或者只能拿到页面框架里那几个无关标题。原因页面内容由 JavaScript 在浏览器端动态渲染requests 拿到的 HTML 只是空壳骨架真实标题是脚本执行后生成的Requests 不执行 JavaScript自然抓不到。还有一种情况是标题不在 h1 里而是用 div 套了大量样式来模拟标题的视觉效果。解决用浏览器打开目标网页右键「查看网页源代码」对比源代码和渲染后页面。如果源代码里没有你要的数据说明是动态渲染需要改用 Selenium 或 Playwright 这类浏览器自动化工具。如果数据在源代码里但标签不是 h1就调整选择器比如改成 soup.select(div.article-title)。4.4 抓几次后被拒绝403 和 429 的限流现象脚本前两次运行正常第三次开始连续返回 403或者返回 429 Too Many Requests。原因短时间高频率的请求触发了服务器的限流机制。即使你伪装了 UA单位时间内访问次数太多服务器同样会识别为脚本行为。解决控制抓取节奏在每次请求之间加延时常见的做法是 time.sleep(2) 到 time.sleep(5)。同时查看目标网站的 robots.txt路径一般写在域名根目录下比如 http://example.com/robots.txt里面会明确列出哪些路径不允许爬取。做技术练习时尽量选择允许采集的站点。4.5 脚本卡死不动timeout 缺失的后遗症现象requests.get() 执行后长时间没有反应脚本像挂了最后才抛出一个超时异常。原因没有显式设置 timeoutrequests 会一直等待服务器响应。有些服务器接受连接后不返回数据连接就一直挂着脚本也就卡住了。解决所有请求都显式设置 timeout推荐用元组形式区分连接和读取超时response requests.get(url, timeout(3, 10))再把每个请求包进 try-except捕获 requests.exceptions.RequestException批量任务里配合循环实现失败重试。从那以后我每次写爬虫都强制检查三个地方有没有 timeout、有没有异常捕获、有没有控制频率缺一个都不上线。5. 让爬虫更健壮Session、Headers与批量抓取三步进阶前面的脚本每次请求都是独立的遇到需要登录或维持会话的站点就会失效。requests 的 Session 对象能保持连接和 Cookie配合自定义 Headers 可以模拟一个稳定的浏览器会话。下面是一个批量抓取多个页面标题的完整示例import requests from bs4 import BeautifulSoup import time session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) urls [http://example.com, http://example.com/about, http://example.com/contact] all_titles [] for url in urls: try: response session.get(url, timeout(3, 10)) response.raise_for_status() response.encoding response.apparent_encoding soup BeautifulSoup(response.text, html.parser) titles [h.get_text(stripTrue) for h in soup.select(h1) if h.get_text(stripTrue)] all_titles.extend(titles) time.sleep(2) except requests.exceptions.RequestException as e: print(f抓取失败{url}原因{e}) print(f共采集到 {len(all_titles)} 个标题) print(list(dict.fromkeys(all_titles)))Session 会复用底层连接连续请求同一个站点时比每次新建连接更快也更不容易被识别为异常流量。headers.update() 把 UA 统一设置到会话级别后续每个请求自动携带。time.sleep(2) 是给服务器留出的喘息时间批量采集时这个间隔建议只增不减。验证环节我一般做两步第一步对比采集数量和页面实际数量是否一致不一致说明选择器漏了元素或页面有动态内容第二步对标题列表去重检查是否有重复抓取。跑完这套流程一个入门级的标题采集器就算真正能交付了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。