简介一份面向Python初学者的网页标题抓取技术资料围绕requests与BeautifulSoup库实现指定页面全部文章标题的提取与本地保存。资源面向具备基本编程基础、对爬虫技术感兴趣的读者可应用于学习爬虫基础、快速获取网站信息等场景。包体为单个docx文档大小仅17KB内容紧凑但覆盖完整。文档从环境准备出发包含requests与beautifulsoup4的安装命令、完整爬虫代码、fetch_titles与save_titles等主要函数逐段讲解、主程序执行流程、运行示例以及目标网站抓取权限、请求频率控制、页面结构适配等实用注意事项能帮助读者快速搭建并调试自己的标题抓取脚本。目前已有174人学习使用适合用来补充爬虫入门的实践环节。1. 从抓文章标题入门为什么说这是最划算的爬虫起点处理过内容聚合、竞品监控或者单纯想给站点做备份的人大概率都经历过那个阶段站点页面成百上千人工复制标题和链接能把手点废复制回来的格式还乱成一团。这个用 requests 加 BeautifulSoup 的标题抓取小工具解决的正是「把网页里所有文章标题一次性抠出来存成干净文本」这件高频小事。它不依赖 Selenium 这类重量级浏览器自动化只发 HTTP 请求再解析 HTML速度很快、依赖极少适合有 Python 基础、想快速上手数据抓取的开发者。比起一上来就爬复杂接口、处理验证码抓标题是理解爬虫完整链路的理想样本——发请求、拿响应、定位节点、提取文本、落盘保存每一步都直观可见。你不需要了解框架机制跟着代码跑通一次就能把这套流程迁移到很多静态站的信息收集上。下面我把拆解过程、可复现代码和踩过的坑一次讲清楚。2. 核心选型requests 与 BeautifulSoup 在标题抓取中的分工2.1 为什么不用 urllib而选 requests 发请求Python 标准库里的 urllib 也能发 HTTP 请求但写起来啰嗦处理 headers、编码、重定向都要手工加代码。requests 把常见操作压缩成了一行requests.get(url)响应对象直接带.text、.status_code、.headers等属性对刚接触爬虫的人友好得多。我习惯在正式写逻辑前先验证目标 URL 的连通性。下面这段代码展示了 requests 的典型用法import requests url https://example.com # 替换成你的目标网址 response requests.get(url, timeout10, headers{User-Agent: Mozilla/5.0}) print(状态码:, response.status_code) print(响应编码:, response.encoding) print(内容长度:, len(response.text))逻辑说明requests.get会返回一个Response对象status_code是 HTTP 状态码200 表示成功。timeout10限制单次请求最多等 10 秒防止某个不可达域名把程序卡死。headers里带 User-Agent 是模拟浏览器访问不少站点会拒绝无 UA 的请求。参数说明timeout的单位是秒设成 5~15 都行太短容易被慢响应误杀太长程序会迟迟不报错。User-Agent字符串可以换但建议保留完整的浏览器标识头只写python-requests容易被某些站直接封掉。2.2 BeautifulSoup 选择解析器的依据BeautifulSoup 本身不做网络请求它只负责把 HTML 字符串变成可遍历的节点树。html.parser是 Python 内置解析器零依赖装好 BeautifulSoup 就能用lxml解析速度更快、容错更好但需要额外安装。原代码用的html.parser在绝大多数静态页面上完全够用只有当目标页面 HTML 严重不规范时才需要考虑换lxml。用soup.find_all(h2)能拿到页面里所有 h2 标签再看一个关键细节from bs4 import BeautifulSoup html_doc htmlbody h2标题一/h2 p正文内容/p h2 classpost-title标题二/h2 a href/news/3h2标题三/h2/a /body/html soup BeautifulSoup(html_doc, html.parser) titles [t.get_text(stripTrue) for t in soup.find_all(h2)] print(titles)逻辑说明find_all(h2)匹配的是所有 h2 标签不关心它嵌套在 a 标签里还是有 class 属性所以上面三个标题都会被抓到。get_text(stripTrue)把标签内部文本抽出来同时去掉首尾多余空白和换行省去后续清洗。参数说明h2是标签名参数想改成 h3、h1 或.post-title这类 class 选择器都行。get_text如果不带stripTrue标题文本里可能混着\n和空格之后写文件时每行会不干净。2.3 爬虫封装结构函数拆分比堆脚本更值得效仿原代码把「抓取」和「保存」拆成了两个独立函数这个习惯很关键。fetch_titles(url)只负责请求和解析返回一个字符串列表save_titles(titles, filename)只负责落盘。后续想改成存数据库、存 JSON 或者追加到已有文件完全不用动抓取部分的代码。# fetch_titles 返回的是列表调用方不用关心里面怎么实现的 def fetch_titles(url): response requests.get(url, timeout10) if response.status_code ! 200: print(f请求失败状态码: {response.status_code}) return [] soup BeautifulSoup(response.text, html.parser) return [t.get_text(stripTrue) for t in soup.find_all(h2)]逻辑说明函数内部先检查状态码不是 200 就提前返回空列表避免后续解析到一个错误页面。正常路径下解析和提取一气呵成返回纯文本列表。这样主程序只需要关心「拿到列表后打印还是保存」。参数说明response.status_code ! 200是一个硬判断实际上 301 重定向时 requests 默认会跟随跳转最终 status_code 可能是 200如果想禁止自动跟随需要给requests.get传allow_redirectsFalse这个参数在目标 URL 经常跳转到登录页时特别有用。3. 可完整复现的标题抓取脚本从安装到跑通3.1 环境准备与依赖安装建议先建一个干净的虚拟环境再装库避免把系统 Python 搞乱。Python 3.8 及以上版本都兼容这套代码。mkdir title-spider cd title-spider python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install requests beautifulsoup4逻辑说明第一行建项目目录第二行创建虚拟环境第三行激活它第四行安装两个核心库。pip install会自动拉取当前最新稳定版本不需要额外指定版本号。参数说明如果你用的是公司内网镜像源可以追加-i https://pypi.tuna.tsinghua.edu.cn/simple这类镜像地址加速安装。装完运行pip list确认requests和beautifulsoup4都在列表里。3.2 完整代码文件与运行说明下面是可运行的完整版本把原项目的逻辑整理成了更清晰的命令行入口并加了编码处理和文件写入保护import requests from bs4 import BeautifulSoup def fetch_titles(url): 从目标页面提取所有 h2 标题文本 headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36} try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 状态码不是 200 时抛异常 except requests.RequestException as e: print(f请求出错: {e}) return [] soup BeautifulSoup(response.text, html.parser) titles [t.get_text(stripTrue) for t in soup.find_all(h2)] return titles def save_titles(titles, filenametitles.txt): 将标题逐行写入文件UTF-8 编码 with open(filename, w, encodingutf-8) as f: for title in titles: f.write(title \n) print(f已保存 {len(titles)} 条标题到 {filename}) if __name__ __main__: url https://example.com # 替换为你的目标地址 titles fetch_titles(url) if titles: print(f共抓取到 {len(titles)} 条标题) for t in titles: print(t) save_titles(titles) else: print(没有抓取到任何标题请确认网页结构是否符合预期)逻辑说明raise_for_status()会把 4xx、5xx 响应直接变成异常抛出省去手写if status_code ! 200的流程。异常捕获覆盖了 DNS 解析失败、超时、连接被重置等网络层问题。文件写入用encodingutf-8避免 Windows 默认编码把中文标题写乱。参数说明timeout10是整体超时包括连接建立和响应读取的总时间。User-Agent字符串里的完整浏览器标识是常用的反爬规避手段抓静态页时必备。w模式会覆盖已有文件想追加到旧文件里请换成a。3.3 运行结果验证内容拿没拿到一眼看出在终端执行python spider.py后预期输出是这样的Fetching titles from https://example.com 共抓取到 12 条标题 某产品发布公告 团队招聘信息 2024年度报告 ... 已保存 12 条标题到 titles.txt如果输出是「没有抓取到任何标题」先别怀疑代码逻辑大概率是页面结构问题。打开目标网页右键查看源码搜索h2确认标题确实在 h2 标签里。文章列表页常混着 h1站点名、h3子标题需要根据实际情况把find_all(h2)改成find_all(h3)或者写成soup.select(article h2)这种更精确的 CSS 选择器。titles.txt的每一行就是一条纯净标题不带任何 HTML 标签。这个文件可以直接被表格工具打开也可以作为下一步数据处理的输入。4. 抓不到内容时的排查思路从状态码到页面结构的逐层定位4.1 状态码异常403、404、500 分别说明什么现象代码跑完只打印一行「请求出错」或者「请求失败状态码: 403」。原因分析403 表示服务器理解请求但拒绝执行最常见是缺失 User-Agent 或目标站的反爬策略拦截了非浏览器请求404 是页面不存在URL 写错或者页面已下线500 是服务器内部错误通常和你的请求无关过一会再试可能就恢复。解决办法403 优先补全 headers把完整的浏览器 UA 带上404 检查 URL 拼写尤其是把 HTML 实体字符或中文编码处理干净500 则加个重试逻辑用time.sleep(3)隔几秒再请求一次。4.2 页面结构不匹配标题不在 h2 标签里现象状态码 200网页内容正常返回但find_all(h2)结果为空列表。原因分析很多内容站的文章标题实际放在h1、h3里或者嵌套在div classarticle-titlea.../a/div这种结构中单纯找 h2 自然一无所获。解决步骤在浏览器里打开目标页点击右键「检查」选中你要的标题元素看它到底是什么标签、有没有特殊 class。然后把代码里的定位逻辑改成匹配实际结构。# 用 CSS 选择器同时匹配常见标题标签 titles soup.select(h1, h2, h3)上面的select方法接收 CSS 选择器逗号表示或能一次性覆盖多种标签。更针对性的做法是找标题所在的容器 class例如soup.select(.post-title a)提取链接文字同时还能拿到标题对应的 URL这对后续扩展很有用。另一个常见因素是页面内容由 JavaScript 动态渲染requests 拿到的 HTML 里根本不含标题文本。这种情况可以把soup.find_all(h2)输出前先print(soup.get_text()[:200])看看返回的源码里有没有任何有意义的正文。如果开头就是一段空壳 div说明是动态页面需要换 Playwright 之类渲染工具requests 这条路走不通。4.3 请求频率过高被封延时和重试是低成本解药现象前面几次运行正常某次突然所有请求都返回 403 或 429。原因分析短时间内连续请求触发了站点限流策略IP 被临时标记。爬虫守则第一条就是克制尤其目标站没给公共 API 时爬太快也是在给别人服务器制造压力。解决办法在循环抓取多个页面时在两个请求之间强制加入等待随机延时能有效降低被封概率。import time import random for url in url_list: titles fetch_titles(url) save_titles(titles, ftitles_{url.split(/)[-1]}.txt) time.sleep(random.uniform(2, 5)) # 随机等待 2~5 秒逻辑说明random.uniform(2, 5)生成一个 2 到 5 秒之间的随机浮点数每次等待时间不一样比固定time.sleep(3)更难被识别成机器行为。这个延时只适用于批量场景单独抓一页不需要加。参数说明延时范围根据目标站反应调整保守点用 5~10 秒。如果抓几十个页面就被封先检查 headers 是否完整再逐步扩大延时范围。4.4 编码问题导致的中文乱码响应编码怎么处理现象抓下来的标题在终端里是乱码比如「å\x85¬å\x91\x8a」这样的字符。原因分析requests 会从响应头推断编码但不少站点的Content-Type没写charset或者声明了错误的编码表。中文字符用 UTF-8 编码的页面被错误按 Latin-1 解码就变乱码。解决办法拿到响应后手动指定编码优先从 HTML 的 meta 标签读取或者直接强制 UTF-8。# 强制使用 UTF-8 解码响应内容 response.encoding utf-8 soup BeautifulSoup(response.text, html.parser)直接把response.encoding赋值为utf-8再访问response.text就会按这个编码解析。不过有些站点实际是 GBK 编码需要先确认再设置。判断方法是用response.encoding打印结果若不包含 utf-8尝试改成gbk后再对比输出。5. 把基础版变成可用工具三种进阶调整与验证方法5.1 从单个页面扩展到整站多个栏目原代码每次只能跑一个 URL实际使用中更常见的是扫一批页面。把 URL 列表写进程序循环调用fetch_titles并分批落盘是最直接的扩展方式。urls [ https://example.com/news, https://example.com/blog, https://example.com/events, ] for index, url in enumerate(urls, 1): print(f正在处理第 {index}/{len(urls)} 个页面: {url}) titles fetch_titles(url) if titles: save_titles(titles, filenameftitles_{index}.txt) time.sleep(random.uniform(2, 4))这个版本每个页面存一个独立文件避免不同栏目标题混到一起。如果你想汇总到一个文件里save_titles里的打开模式改成a模式追加即可。注意文件名带序号是为了防止覆盖上一轮结果这在批量运行时是重要的细节。5.2 同时抓标题和链接把 text 和 href 一起取单纯存标题对很多后续用途不够作者名、发布时间、链接这些信息往往是配套需求。用find_all(a)配合get(href)就能把链接也拿到items [] for a in soup.select(h2 a): # 定位 h2 里的 a 标签 title a.get_text(stripTrue) href a.get(href) if title and href: items.append(f{title}\t{href})逻辑说明select(h2 a)选择所有位于 h2 元素内部的 a 标签这是列表页最常见的标题结构。get(href)拿到相对路径比如/article/123.html要拼成完整 URL 可以用urllib.parse.urljoin(base_url, href)。参数说明如果网站用的相对路径urljoin可以自动补全域名这是写进文件前必须处理的环节。用\t作为字段分隔符之后导入表格工具时能直接分列。5.3 验证抓取结果文件对比与样本抽查保存完成后建议做一次人工验证别把脚本跑完就当工作结束了。打开titles.txt随机选三条标题去目标网站对应页面上搜索确认是否存在。如果三条中有任何一条找不到说明选择器可能误抓了付费内容、侧边栏推荐或其他噪声数据。另一个快速验证是统计结果条数与页面实际标题数的关系。比如文章列表页一页有 20 篇文章但find_all(h2)抓回了 25 条多出来的 5 条大概率来自推荐位、相关阅读这些区域。这时候把选择器从通用h2收紧到.post-list h2这类带作用域的写法过滤无效节点。我用这套流程抓过不少中大型资讯站最大的感受是选择器定位要在「抓全」和「抓准」之间找平衡。一开始把选择器写得太宽采集数据噪声大下游清洗成本高写得太窄漏掉页面结构调整后的部分数据过两周脚本可能就静默失效。从那以后我每次改完选择器都会保留一份抓取样本在本地隔几天重新跑一遍做回归对比确认页面没改版、脚本没失灵。这个习惯让我少踩了很多次「数据悄悄变少」的暗坑。希望帮到你。本文还有配套的精品资源点击获取