Python爬虫实战:用requests和BeautifulSoup高效抓取网页标题 简介面向具备基本编程基础、对 Python 爬虫感兴趣的开发者文档围绕“抓取网页文章标题”任务展开系统演示了用 requests 发起 HTTP 请求、BeautifulSoup 解析 HTML并通过 find_all(h2) 提取标题后保存到本地 txt 文件的完整流程。内容包含库的安装命令、fetch_titles 与 save_titles 函数的逐段讲解、运行实例与结果展示还提醒读者遵守 robots.txt 与网站使用条款、在请求频繁时加入延时并讨论如何根据页面结构调整选择器、做好异常处理与数据存储、灵活应对反爬策略等问题对初学者搭建真实可扩展的爬虫脚本很有参考价值。资源为单个 docx 文档压缩包大小约 17KB轻量易读便于随时查阅对照。已有 176 人学习浏览适合跟随示例亲手实践并在此基础上调整目标站点与选择器逐步构建自己的抓取工具。1. 从文章标题抓取聊起一个能跑通的最小爬虫骨架日常接需求时我经常遇到“帮我把某几个栏目的文章标题整理成表格”这类活儿。手动复制当然能做但页面一多就让人崩溃。用 Python 写个爬虫去抓标题是最快能落地、也最容易让新手建立信心的场景。这篇文章要拆的是一份标题抓取资源基于 requests 发起请求用 BeautifulSoup 解析页面里的标题标签最后把结果写到本地文本文件。它能解决“批量获取文章列表、快速做内容盘点、给后续数据分析做前置采集”这类具体问题。适合有基础 Python 语法、想上手爬虫但还没系统学过的人也适合需要快速抄一个简化版采集脚本的熟手。我会把完整代码、参数含义、适配真实网页的调整方法以及我实际踩过的几个坑都放出来。读完你能得到一个可以直接改 URL 就跑的脚本也能知道标题不在预期标签里时该怎么兜底。2. 最小可用版本requests 取网页BeautifulSoup 拆标题2.1 为什么选 requests BeautifulSoup而不是别的组合做标题抓取这种轻量采集技术选型的原则是“够用就好”。requests 是 Python 生态里最成熟的 HTTP 客户端API 简单get 一下就能拿到响应文本BeautifulSoup 则是把 HTML 解析成可遍历对象的老牌库对新手特别友好查找标签的写法很像是在写自然语言。有人会问怎么不用 urllib我的判断是 urllib 虽然内置但处理 Cookie、超时、请求头时要写的样板代码更多。还有人会推荐 Scrapy可抓标题这种单页、低频的任务上 Scrapy 有点大材小用。requests 加 BeautifulSoup 的组合代码量最少调试时哪里出错一眼就能看到出了问题也容易在社区搜到答案。这套组合的边界也要说清楚它适合页面内容在 HTML 源码里直接可见的静态页面。如果目标网站是前端 JS 动态渲染数据requests 拿到的 HTML 里根本没有标题那种情况我在第四章会讲怎么绕。2.2 最小可运行版本完整代码与参数说明先看一个能直接跑的脚本。它做的事很简单请求指定 URL找到页面里所有h2标签提取文本打印出来并保存。import requests from bs4 import BeautifulSoup def fetch_titles(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } try: response requests.get(url, headersheaders, timeout10) except requests.RequestException as e: print(f请求失败: {e}) return [] if response.status_code ! 200: print(f页面返回异常状态码: {response.status_code}) return [] soup BeautifulSoup(response.text, html.parser) title_tags soup.find_all(h2) return [tag.get_text(stripTrue) for tag in title_tags] def save_titles(titles, filenametitles.txt): with open(filename, w, encodingutf-8) as f: for title in titles: f.write(title \n) if __name__ __main__: url https://example.com titles fetch_titles(url) if titles: print(f共抓取 {len(titles)} 个标题) save_titles(titles) print(已保存到 titles.txt) else: print(未获取到标题)这段代码里有几个参数值得单独说。headers里的User-Agent是标识客户端类型的字符串。很多网站会拦截不带 UA 的请求或者直接返回 403加了这一项能伪装成普通浏览器降低被拒概率。timeout10是请求超时时间单位是秒超过这个时间还没响应就主动放弃避免脚本卡死。response.status_code ! 200是状态码检查。200 表示正常其他常见状态码比如 302 是重定向、404 是地址不存在、403 是禁止访问。这一层判断虽然简单但能让你在第一时间知道是网站拒绝了你还是解析逻辑出了问题。soup.find_all(h2)返回所有符合条件的标签对象get_text(stripTrue)提取标签内的纯文本stripTrue会去掉首尾空白和换行。这里有个限制它默认只认h2真实网站的标题结构未必如此第三章细说。2.3 从控制台输出到本地落盘文件处理的细节很多入门教程会在控制台打印完就结束但实际采集场景里结果往往要交给下游使用所以落盘是刚需。上面代码里我用了open(filename, w, encodingutf-8)其中w模式表示覆盖写入每次运行都会重建文件。如果希望多次运行结果不互相覆盖可以改成a追加模式。encodingutf-8这一项不能省。Windows 下 Python 默认编码可能是 gbk写中文标题时会直接 UnicodeEncodeError导致程序中断。指定 utf-8 之后文件内容也能被大多数编辑器和后续处理工具正常读取。写文件时我习惯在每条标题末尾加\n保证每个标题占一行。如果你后续想把标题导入 Excel这个格式可以直接用 Excel 的“分列”功能处理。如果想存成 CSV可以用 csv 模块替代直接写文件但对纯标题场景文本文件已经足够清晰。3. 适配真实网页标题不在 h2 里时怎么定位3.1 先用浏览器开发者工具确认标题的真实结构把示例代码往真实网站上一跑最常见的情况就是“脚本没报错但标题列表是空的”。原因几乎都是同一个目标网站的标题标签根本不是h2。我一般的做法是先用浏览器打开目标页面按 F12 打开开发者工具点左上角的箭头图标再点击页面上的任意一个标题DevTools 会直接高亮对应的 HTML 元素。这一步能精确看到标题包在什么标签里、带什么 class、有没有嵌套结构。比如有的网站标题是h1 classarticle-title有的是a classtitle-link还有一些是h3搭配内层a。拿到真实结构后再回头改爬虫的查找语句。这个过程是整个适配工作的核心不要猜网页结构要看结构。3.2 多选择器兜底从 find_all 到 select确定真实结构之后可以用更精准的方式定位。下面这段代码演示了如何通过 CSS 选择器查找标题并做多套选择器兜底。def fetch_titles_flexible(url): headers {User-Agent: Mozilla/5.0} response requests.get(url, headersheaders, timeout10) soup BeautifulSoup(response.text, html.parser) # 按优先级尝试不同的选择器哪个命中用哪个 selectors [ h2, h3 a, a.title-link, .article-title ] for selector in selectors: elements soup.select(selector) if elements: print(f使用选择器 {selector} 命中 {len(elements)} 个标题) return [el.get_text(stripTrue) for el in elements] return []soup.select接收 CSS 选择器h3 a表示 “h3 内部的 a 标签”.article-title表示classarticle-title的标签。按顺序逐个尝试哪个先找到结果就用哪个这种兜底策略能应对同一站点不同页面结构不完全一致的情况。get_text(stripTrue)在嵌套结构下会把子标签里的文字也拼进来。如果标题里混有分类名、日期等其他信息你要在循环里加上过滤逻辑比如只保留长度在 5 到 100 字之间的文本。这一步没有统一标准完全看目标页面的实际结构。3.3 去重、排序与乱码处理页面标题可能出现重复比如置顶文章在列表里出现两次。文本范围抓取时通常需要去重。最简单的方式是转换为集合再变回列表def clean_titles(titles): seen set() result [] for title in titles: title title.strip() if title and title not in seen: seen.add(title) result.append(title) return result这段代码通过一个set记录已出现的标题既去掉了空字符串也保留了第一次出现的顺序。很多列表页的标题本身就是按时间倒序排列的所以我不建议在做完解析后轻易重新排序保持页面原始顺序往往更符合业务直觉。乱码是另一个高频问题。requests拿到响应后会用response.encoding去解码但某些网站的响应头没声明编码或声明的编码与实际不符就会出现满屏–之类的乱码。这时可以采用以下策略response.encoding response.apparent_encodingapparent_encoding是从响应内容里推测出的编码准确率通常不错。但注意用它之后要在实际项目中多做几个页面的验证因为推测不是百分百可靠。建议在脚本里先打印response.encoding和response.apparent_encoding做个对比再决定用哪种方式解码。4. 标题爬虫避坑四个我翻过车的真实问题4.1 访问被拒返回 403 Forbidden现象运行脚本后控制台输出页面返回异常状态码: 403一个标题都没抓到。原因绝大部分情况是请求没有带 User-Agent。网站服务器认出了请求不是来自真实浏览器直接拒绝了。另一个可能是请求频率太高被服务器临时封禁。解决先加上模拟浏览器的请求头这是最简单的止损手段。如果还不行就把目标网站的robots.txt找出来看一眼路径通常是域名/robots.txt确认你要抓的路径没有被明确禁止。再不行就把请求之间的间隔拉长加入time.sleep(2)这类延时。遇到 403 我从不死磕单次请求优先从“伪装程度”和“请求频率”两个方向同时调整。4.2 页面源码里根本没有标题现象代码不报错状态码是 200但soup.find_all(h2)返回空列表打印结果为空。原因目标页面是前端框架动态渲染的标题数据由 JavaScript 在浏览器端发起异步请求然后渲染到页面上。requests 拿到的只是空壳 HTML里面没有实际数据。解决优先去浏览器的开发者工具里看 Network 面板找到返回标题数据的 XHR 或 Fetch 请求。直接请求那个数据接口通常能拿到结构更干净的 JSON 数据这也比解析 HTML 更稳定。如果接口加密复杂才考虑用自动化工具但那是更重的方案对入门场景来说找接口是性价比最高的路径。4.3 中文标题乱码成谜现象标题内容变成新闻一类看不懂的字符。原因response.encoding从响应头读到的编码与页面实际使用的编码不一致。常见于没有声明 charset 的站点或者声明了 gbk 但实际是 utf-8 的页面。解决用我在第三章提到的response.apparent_encoding兜底。更稳妥的做法是检查一下 HTML 里meta charset...标签的实际声明手动设置响应编码。比如页面是 GBK 编码就写response.encoding gbk再继续解析。这类问题没有万能钥匙遇到乱码就多打印几个响应头信息能少走弯路。4.4 请求太频繁账号或 IP 被限制现象脚本连续运行一段时间后突然所有请求都返回 503 或验证码页面要求输入验证码才能继续。原因短时间内的请求数量超过了网站设定的阈值被识别为爬虫行为。解决降低请求频率加入随机延时而不是固定延时。比如time.sleep(random.uniform(1, 3))模拟人工操作的自然间隔。对于大量页面可以分批跑每批之间停一段时间。此外采集数据也要尊重目标网站的条款明确禁止采集的内容不要强行突破。我是把它当成长期运行脚本的基本礼仪来看的。5. 把脚本加固成可复用小工具重试机制、增量保存与参数调节5.1 请求重试应对网络抖动与临时限制网络请求很难做到百分百成功断网、服务器 5xx、超时都可能导致单次抓取失败。与其失败后手动重跑不如在脚本里内置重试逻辑。import time import random import requests from requests.adapters import HTTPAdapter def fetch_with_retry(url, headers, retries3, timeout10): session requests.Session() session.mount(https://, HTTPAdapter(max_retriesretries)) try: response session.get(url, headersheaders, timeouttimeout) response.raise_for_status() return response.text except requests.RequestException as e: print(f第 {retries} 次重试后仍失败: {e}) return None这里用requests.Session配合HTTPAdapter设置最大重试次数raise_for_status()会在状态码为 4xx 或 5xx 时主动抛出异常触发重试逻辑。注意重试不是无脑重复初次请求失败后下一次请求前等待一个随机间隔能避免在服务器过载时立刻加重负担。我会建议重试次数设为 3超过 3 次直接把 URL 记录下来等这一轮跑完再单独处理。5.2 增量保存只抓新标题不重复落盘当脚本需要定时运行时全量覆盖写入会丢掉之前抓到的标题追加写入则会产生大量重复。这时增量保存是更实用的策略。做法是先读取本地已有的标题集合再过滤掉已经存在的部分。def load_existing_titles(filenametitles.txt): try: with open(filename, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) except FileNotFoundError: return set() def incremental_save(titles, filenametitles.txt): existing load_existing_titles(filename) new_titles [t for t in titles if t not in existing] with open(filename, a, encodingutf-8) as f: for title in new_titles: f.write(title \n) return len(new_titles)load_existing_titles把文件里已有的标题读成一个集合incremental_save先计算差值再把新标题追加写入文件返回值是新写入的数量。这个函数可以直接接在上一章的去重逻辑后面防止同一标题被反复保存。用追加模式a而不是覆盖模式是为了保住历史数据。5.3 参数调节清单超时、间隔、编码、请求头最后整理一份我在实际使用中会反复调整的参数清单。网上很多现成代码跑不动多数是因为这些参数没有针对目标页面做适配。参数常见取法作用timeout10~30 秒避免请求长时间无响应导致脚本挂起请求间隔随机 1~3 秒降低服务器压力防止封禁encoding页面 meta 声明或 apparent_encoding解决中文乱码User-Agent本机浏览器的 UA 字符串提高请求通过率选择器浏览器 DevTools 实测得到的 CSS 选择器精确定位标题抓标题这类任务真正花时间的往往不是写爬虫而是调这些参数。比如我上次做一个列表页采集标题标签有三种情况我先后试了五个选择器才确认了稳定的定位方式。从那以后我每次写爬虫都强制自己先打开浏览器看结构再决定代码怎么写。建议你也保留这个习惯能省掉不少排查时间。希望这篇文章能帮你顺利抓到自己想要的内容。本文还有配套的精品资源点击获取