Python爬虫入门实战:用requests库抓取静态网页图文数据

Python爬虫入门实战:用requests库抓取静态网页图文数据 1. 项目概述这到底是个什么样的爬虫项目先说结论这是一个用 Python 的 requests 库从零开始抓取静态网页图文数据的入门实战项目。很多人第一次接触爬虫就被各种高大上的框架吓住了——Scrapy、Selenium、异步协程、分布式节点——其实对于大量日常需求requests 库加几行解析代码就够用了。这个项目的核心价值在于用最少的工具、最清晰的思路把“网页数据是怎么从服务器跑到你硬盘里的”这件事彻底讲明白。静态网页这个词听起来很技术实际上你完全可以用生活经验去理解。你在浏览器里打开一个网页如果页面的内容在加载完成后就固定了不会因为你滚动、点击、刷新而动态改变这种网页十有八九就是静态页面。它的 HTML 代码里直接写死了标题、正文、图片链接你要做的就是把这段 HTML 拿下来再从中把需要的信息“抠”出来。反过来说如果你打开的是淘宝的商品列表、微博的热搜榜滚一下就会不断加载新内容那就属于动态网页需要另想办法。本项目只针对前者因为它是理解 HTTP 协议和网页结构最好的入门素材。这个项目适合谁说实话只要你有最基础的 Python 语法基础——会写 for 循环、会用变量、知道什么是字典和列表——就可以跟完整个流程。我会从环境搭建开始到请求发送、数据提取、图片批量下载最后讲一讲请求频率控制和常见反爬机制每一步都会交代清楚“为什么这么做”和“踩坑了怎么办”。即便你完全没写过爬虫按照文章里的代码和步骤操作也能跑出一个能真实使用的图文下载脚本。2. 整体设计思路拆解为什么选 requests 而不是其他方案2.1 从 requests 入手是最短的学习路径经常有人问我学爬虫是不是应该直接上 Scrapy我的建议一直是先把 requests 用熟再考虑框架。原因特别简单——Scrapy 是建立在对 HTTP 协议、选择器、爬取流程有基本理解之上的高级抽象它替你封装了大量细节但如果你不知道底层发生了什么事情出了问题你连排查方向都没有。requests 库的本质是什么呢它就是一个让 Python 帮你发 HTTP 请求的工具包。你在浏览器地址栏输入网址敲回车浏览器会向服务器发送一个请求服务器返回一堆 HTML 代码浏览器再把代码渲染成一个漂亮的页面。requests 能干的事情和这个流程几乎一样区别在于浏览器把结果渲染给人看requests 把结果交给程序处理。理解了这一点你就抓住了爬虫最底层的原理——向服务器要数据然后从返回的数据里提取你要的部分。拿这个项目涉及的任务来说我们需要什么需要向目标网站的页面发送一个 GET 请求拿到包含文字和图片链接的 HTML 代码然后用正则表达式或者解析库从 HTML 中提取数据。这个流程用 requests 配合 Python 标准库就能完整覆盖。如果你用 Scrapy你得先学会创建项目结构、理解 Spider 的写法、配置 Pipeline这些对新手来说都是干扰项。2.2 静态页面和动态页面的判断方法在动手写代码之前需要先确认目标页面到底是不是静态的。判断方法非常直接在浏览器里打开目标网页右键选择“查看网页源代码”在源码中搜索你肉眼能看到的关键文字。如果能在源码里找到说明这些文字是服务器直接返回的属于静态内容如果在源码里找不到但页面上却有那说明内容是页面加载后通过 JavaScript 动态填充的requests 直接请求获取不到。这个判断我会在每写一个爬虫前都做一遍不夸张地说它决定了你后续所有代码的写法。如果是静态页面requests 加 BeautifulSoup 就能拿到全部内容如果是动态页面你就得考虑分析页面的接口请求、使用 Selenium 模拟浏览器或者寻找页面的数据接口直接请求 JSON 数据。本项目的所有示例都是基于静态页面来讲解的这样可以把核心精力放在 requests 库本身的使用方法上不会被复杂场景分散注意力。2.3 爬虫项目的标准流程任何一个爬虫项目不管规模大小都逃不出下面这条链路分析目标页面结构、发送 HTTP 请求、解析响应内容、提取目标数据、保存数据到本地。很多人一上来就写代码结果等到解析环节才发现拿不到数据不得不回头重新分析页面浪费时间也消磨耐心。我自己的习惯是先在浏览器里手动打开页面花十分钟把页面结构和数据位置摸清楚再动手写第一行代码。针对图文数据这个具体场景整个链路可以进一步细化。文字内容通常在 HTML 的标签内比如标题在h1标签里正文在p标签里图片地址则一般在img标签的src属性里也可能隐藏在a标签的href属性中。接下来要做的就是用 Python 代码把这些 HTML 标签按规则匹配出来拿到文字和图片链接最后把文字存成文本文件或 Markdown 文件把图片一张张下载到本地文件夹。3. 环境准备与 requests 库核心用法3.1 基础环境搭建Python 安装与虚拟环境你要运行 requests 库前提是电脑上有 Python 环境。我遇到过很多新手在第一步就卡住了其实流程不复杂。去 Python 官网下载对应你操作系统的安装包装的时候有一个特别关键的选项——Add Python to PATH一定要勾上。勾上后系统才能直接在命令行里识别python命令否则你装完了却无法启动。装好以后建议顺手创建一个虚拟环境。虚拟环境是干嘛用的你可以把它理解成一个独立的工具箱里面放的各种 Python 库只在这个项目里生效不会污染系统全局环境。这样不同项目用了不同版本的依赖库互相之间不会打架。创建方法很简单mkdir web-scraper-project cd web-scraper-project python -m venv venvWindows 系统下用venv\Scripts\activate激活虚拟环境macOS 或 Linux 用source venv/bin/activate。激活后命令行前面会出现(venv)字样表示你已经进入了独立的 Python 环境。这时候再用 pip 安装依赖就干净了。3.2 requests 库安装与最核心的四个方法在虚拟环境激活的前提下安装 requests 只需要一条命令pip install requests安装完成后可以用pip list确认是否安装成功。接下来是重头戏——requests 库最常见的用法。我先整理一个对照表把请求方法、对应场景、关键参数一次说清楚方法作用典型使用场景requests.get()向目标 URL 发送 GET 请求获取网页 HTML、下载图片文件requests.post()向目标 URL 发送 POST 请求提交表单、登录模拟、API 调用requests.Session()创建会话对象跨请求保持 Cookie需要登录态、连续访问多个页面的场景requests.head()只获取响应头部信息探测链接是否可用、查看响应头对于爬取静态网页图文数据这个项目90% 的情况你只需要用到requests.get()。我给你写一个最小可用的请求模板import requests 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 } url https://example.com/page/1 response requests.get(url, headersheaders, timeout10) print(response.status_code) print(response.encoding) print(response.text[:500])这里headers里面的User-Agent是告诉服务器“我是哪个浏览器”很多网站会拒绝没有这个字段的请求。timeout参数设置了超时时间单位是秒防止服务器一直不回消息导致程序卡死。response.status_code是 HTTP 状态码200 代表请求成功403 代表被拒绝访问404 代表页面不存在429 代表请求太频繁被限流了。这些状态码的含义后面排查问题时要经常用到。3.3 从响应对象中获取你需要的数据拿到response对象以后有几个属性是你必须熟悉的。response.status_code判断请求是否成功response.encoding是服务器返回的编码方式response.text是解码后的文本内容response.content是原始字节数据response.headers是响应头信息。编码问题在这里特别容易踩坑。有些网站的编码方式是 gb2312 或者 gbk而 requests 默认会用响应头里的编码来解码。如果服务器给的编码信息不准确response.text就会出现乱码。我的处理方式是先用response.apparent_encoding拿到自动检测的编码结果再手动指定编码response.encoding response.apparent_encoding这个操作能解决 90% 的乱码问题。至于图片下载需要用response.content拿到的是二进制数据然后以wb模式写入本地文件img_response requests.get(img_url, headersheaders, timeout10) with open(image.jpg, wb) as f: f.write(img_response.content)核心思路就是文本用response.text图片音频等二进制文件用response.content。4. 实操核心环节完整爬取一个静态页面的图文数据4.1 页面分析与解析策略现在我假设你已经选好了目标网站并且在浏览器里确认过页面里的文字和图片链接都直接存在于 HTML 源码中。接下来的一步是分析 HTML 结构。我推荐的做法是在浏览器里按 F12 打开开发者工具用左上角那个箭头图标点击页面上的目标元素然后在查看器里找到对应的 HTML 标签。解析 HTML 的方式有三种常用选择正则表达式、BeautifulSoup、lxml 的 XPath。很多人推荐直接上 BeautifulSoup因为它对新手友好、容错率高。但我觉得有一点必须先说清楚正则表达式适合简单的、结构固定的内容提取比如某个特定格式的链接BeautifulSoup 适合处理复杂的嵌套标签结构XPath 在处理表格和列表时优势明显。本项目的图文数据场景我建议直接使用 BeautifulSoup语法直观代码可读性也好。安装与引入pip install beautifulsoup4from bs4 import BeautifulSoup soup BeautifulSoup(response.text, html.parser)第二个参数指定了解析器html.parser是 Python 标准库自带的不需要额外安装性能上对于静态页面完全够用。4.2 提取标题和正文文字内容架设目标页面的结构大概是这样的article h1 classarticle-titlePython 爬虫入门实战/h1 div classarticle-content p这是第一段文字/p p这是第二段文字/p /div img src/uploads/cover.jpg alt封面图 img src/uploads/content-1.jpg alt内容图 /article提取标题用soup.select_one()方法参数是 CSS 选择器title_tag soup.select_one(h1.article-title) title title_tag.get_text(stripTrue) print(title)get_text()方法提取标签中的文字内容stripTrue会自动去掉首尾空白字符。提取正文部分先定位到包含正文的div然后找出里面所有的p标签content_div soup.select_one(div.article-content) paragraphs content_div.find_all(p) article_text \n.join(p.get_text(stripTrue) for p in paragraphs)这里有一个很关键的小知识find_all()返回的是一个可迭代的列表每个元素是一个p标签对象调用它的get_text(stripTrue)拿到该段落的文字。最后用换行符拼接起来就成了一个完整的纯文本格式的文章内容。4.3 图片链接提取与完整 URL 拼接图片标签和文字标签不太一样图片地址在src属性里。提取方式如下img_tags content_div.find_all(img) img_urls [] for img in img_tags: src img.get(src) if src: if src.startswith(http): img_urls.append(src) else: img_urls.append(https://example.com src) else: print(发现无 src 属性的 img 标签)第二部分是图片链接拼接。静态页面里图片地址常常是相对路径比如/uploads/cover.jpg它指的是“当前域名下的 uploads 目录”。如果不处理直接下载肯定报错。所以我在代码里做了个判断如果地址以 http 开头说明是完整 URL直接使用否则补上站点的根域名。在做这个步骤时我强烈建议你把img_urls先打印出来人工检查几个链接能否在浏览器中打开。这看起来笨拙但能帮你快速发现 URL 规律是否判断正确比代码报错后再排查高效得多。4.4 图片批量下载与延迟控制拿到图片链接列表后接下来就是逐个下载。这里最容易忽视的是下载间隔。如果一次性快速请求几十张图片极容易触发网站的反爬机制表现就是请求返回 429 状态码Too Many Requests请求太多被限流了。我建议每张图片之间至少间隔 0.5 到 1 秒用time.sleep()控制import time import os os.makedirs(images, exist_okTrue) for index, img_url in enumerate(img_urls): try: img_response requests.get(img_url, headersheaders, timeout10) if img_response.status_code 200: file_name fimages/image_{index:03d}.jpg with open(file_name, wb) as f: f.write(img_response.content) print(f已下载: {file_name}) else: print(f下载失败: 状态码 {img_response.status_code} - {img_url}) except requests.RequestException as e: print(f请求异常: {img_url} - {e}) time.sleep(0.5)index:03d是一个格式化写法把序号补成三位数字这样文件名可以按顺序排列。try-except是不可省略的网络环境千变万化任何一个请求失败都不应该导致整个程序崩溃。你要做的是记录失败信息让它继续跑下去。4.5 完整代码整合与运行把上面所有环节整合到一起一个完整的静态网页图文爬虫脚本大概长这样import os import time import requests from bs4 import BeautifulSoup BASE_URL https://example.com/article/1 DOWNLOAD_DIR images 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 } def fetch_html(url): response requests.get(url, headersheaders, timeout10) response.raise_for_status() response.encoding response.apparent_encoding return response.text def parse_article(html): soup BeautifulSoup(html, html.parser) title soup.select_one(h1.article-title).get_text(stripTrue) content_div soup.select_one(div.article-content) paragraphs content_div.find_all(p) article_text \n.join(p.get_text(stripTrue) for p in paragraphs) img_urls [] for img in content_div.find_all(img): src img.get(src) if src: img_urls.append(src if src.startswith(http) else BASE_URL.rsplit(/, 1)[0] / src.lstrip(/)) return title, article_text, img_urls def download_images(img_urls, save_dir): os.makedirs(save_dir, exist_okTrue) for index, img_url in enumerate(img_urls): try: resp requests.get(img_url, headersheaders, timeout10) if resp.status_code 200: file_name os.path.join(save_dir, fimage_{index:03d}.jpg) with open(file_name, wb) as f: f.write(resp.content) print(f已下载: {file_name}) else: print(f失败: {resp.status_code} - {img_url}) except requests.RequestException as e: print(f异常: {img_url} - {e}) time.sleep(0.5)这样一个模块化组织的代码每个函数只负责一件事后续如果要扩展到多页面爬取只需要在外面套一个循环就行。5. 高频异常排查从 429 到超时的完整处理方案5.1 429 状态码意味着什么最近两年只要一搜爬虫相关的内容几乎总能看见 exceeded retry limit, last status: 429 too many requests 这行字。429 表示你在单位时间内的请求次数超过了服务器允许的上限是网站对你的一种善意提醒慢一点别把服务器打挂了。触发 429 的原因无非以下几种请求频率过高短时间内发出大量请求没有设置合理的请求头服务器识别出你是脚本同一个 IP 被大量请求关联被临时加入了限流名单。其中请求频率过高是最主要的原因。解决思路也有一个从简单到复杂的递进过程。第一层每次请求之间加time.sleep(1)把 QPS每秒查询数降下来。第二层设置指数退避的重试策略——如果遇到 429第一次等 2 秒重试第二次等 4 秒第三次等 8 秒以此类推。这样既不会把服务器打死也能在浪头过去后自动恢复。import time import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry( total5, backoff_factor1, status_forcelist[429, 500, 502, 503], ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter)这种方法可以做到遇到 429 自动等待并重试不需要手动干预。5.2 连接超时和读取超时的区别requests.get()里的timeout参数它既管连接超时连不上服务器也管读取超时服务器一直不返回数据。如果不设置程序会一直等着表现就是“卡死”。建议统一设置一个 10 秒的超时够宽裕也不会无限等待。超时的根本原因通常是目标网站响应慢或者你的出口网络到目标服务器之间的链路不稳定。解决手段包括重试机制、通过代理 IP 更换出口、错峰访问等。一般建议先加重试机制如果依然频繁超时再考虑代理。5.3 请求头伪装不只是 User-Agent新手最容易犯的错误是只设置User-Agent结果请求依然被拒绝。除开 User-Agent网站还会看Referer告诉服务器你从哪个页面跳转过来的、Accept-Language浏览器支持的语言、Accept-Encoding支持的压缩格式。很多网站的图片防盗链就是检查Referer如果发现请求来源不是本站页面就直接拒绝。遇到这种情况在请求图片时加上img_headers { User-Agent: headers[User-Agent], Referer: BASE_URL, }这是实战中非常有用的一个技巧很多图片下载失败都是因为少了这个头。5.4 图片下载后无法打开的问题图片文件下载下来了但打开的时候提示文件损坏或者格式不对。这个问题的原因一般是下载的是重定向后的 HTML 页面而不是图片本身图片不是 JPEG 格式但被强制保存成了 .jpg下载过程中网络中断导致文件不完整。排查手段很简单下载完成后检查文件大小如果文件只有几 KB大概率下载到了一个源。另一个检查办法是用 Python 读出文件的前几个字节JPEG 文件的开头是\xff\xd8PNG 是\x89PNG通过文件头判断真实的文件格式再决定扩展名。5.5 编码导致乱码的问题网页返回的中文变成了一堆拉丁字符或者问号这不是网络问题是解码方式不对。HTML 页面在head标签里一般会标明charsetutf-8或者charsetgbk但有些服务器返回响应时没有在 HTTP 头里写清楚编码requests 推测失败后就用了默认编码。前面提到过使用response.apparent_encoding让 requests 根据页面内容自动猜测编码能解决大多数乱码问题。如果还不行就在解析 HTML 时手动指定编码response.encoding utf-86. 爬虫的边界意识与工程化建议6.1 爬虫的合规使用与驯服边界爬虫本身是中性工具但使用必须建立在不损害目标服务正常运营的前提下。我个人的实践准则是只为学习和研究使用请求频率控制在个位数每秒以内保持在对服务器几乎无感的状态只爬取公开数据不碰需要登录才能访问、明确设置访问权限的内容不将爬取的数据用于商业用途尤其是涉及个人信息的私密数据。6.2 从单页爬虫到完整工程的扩展建议这个项目跑通之后它不应该只是一个一次性脚本而应该成为你进一步探索的基石。后续可以做的扩展方向很多把单页提取逻辑封装成类支持传入不同的 URL 和选择器加入多页面循环自动翻页爬取整个栏目把数据保存逻辑从文本文件升级为 SQLite 或 MySQL引入 logging 模块记录爬取日志而不是用 print 输出任务量大了以后再用协程或者队列做并发。我在实际项目中最常建议的成长路线是先写一个能跑的通的单页脚本再把它重构为多页面循环再接入数据库最后才考虑并发。每一步都要确保前面的基础扎实了再往前走不要一上来就追求写一个“高并发分布式爬虫”。6.3 最后分享一个我自己很受用的经验调试爬虫代码时永远先打印后请求。也就是说拿到响应后不要急着写一大串解析代码先把response.status_code、response.url、response.text[:200]打印出来看看。确认数据真的拿到了再动手写解析逻辑。这个小习惯帮我在排查问题上省下了大量时间希望你也能从中受益。