Python爬虫五种主流方法:静态解析、动态渲染与接口调用全指南
发布时间:2026/10/3 0:05:18 作者:尧图编辑部 阅读量:1,286

我最早接触 Python 爬虫的时候想法特别单纯拿到网页源码正则表达式一把梭觉得“获取网页数据”就是“拿到 HTML 再抠内容”。等到真正开始面对各种网站才发现这条路远没有想象中那么直——有的网站把数据直接写在 HTML 里有的网站靠 JavaScript 动态渲染你看到的页面内容其实是一堆脚本运行完才出现的还有的网站你盯着一个表格实际背后是接口在悄悄返回 JSON。今天这篇文章我就把几年下来实际用过的 5 种方法按场景拆开讲清楚从原理到代码再到坑一次性说透。1. 动手之前先判断网页属于哪一类很多新手最容易犯的错不是不会写代码而是还没搞清楚目标页面是什么结构就急着去请求。结果 requests 请求回来的 HTML 和浏览器里看到的内容完全对不上然后开始怀疑人生。其实这大概率不是代码的问题是你和分析对象之间没有对齐认知。1.1 静态渲染、动态渲染还是纯接口网页数据大体分三类。第一种是服务端渲染数据在 HTML 源码里就能直接看到这种最友好requests 发一个 GET 请求拿回来的 text 里就有你要的数据。第二种是客户端渲染服务端返回的 HTML 只是一个空壳页面里的内容是 JS 脚本在浏览器里执行之后动态生成的你用 requests 直接抓抓回来的是“骨架”不是“肉”。第三种是纯接口型页面本身只是展示层真正有价值的数据藏在 XHR 或 fetch 请求背后的 JSON 接口里。判断方法很简单用浏览器打开目标页面右键“查看网页源代码”如果源码里能看到关键数据那是静态如果源码里只有一堆script标签正文数据一个都看不到那基本就是动态渲染或者接口加载。再看一眼开发者工具里的 Network 面板刷新页面重点看 XHR 和 fetch 类型的请求如果有一条请求返回的是 JSON 数据恭喜你找到接口了。1.2 五种方法的适用边界这五种方法之间不是谁替代谁的关系而是按“页面复杂程度”和“采集规模”分层的。方法适用场景上手难度运行成本requests BeautifulSoup服务端渲染的静态页面低极低requests-html轻量级动态页面不想上浏览器自动化低较低Selenium重度 JS 渲染、需要模拟交互中高Scrapy大规模采集、多页面、工程化中高中直接调接口能找到 JSON 接口的场景中极低我一直的建议是优先用最朴素的方法。能静态抓就不上渲染能拿接口就不去解析 HTML。把重型武器留给真正需要它的场景否则你的服务器内存和你的耐心都会很快耗尽。2. 方法一requests BeautifulSoup静态页面永远的主力这套组合我在生产环境里用了好几年至今依然是最常用的起步方案。requests 负责发请求BeautifulSoup 负责把拿回来的 HTML 解析成可查询的树结构各司其职干净利落。2.1 一个最小可用的抓取脚本以专门用来练习爬虫的书籍站点为例页面里的书名、链接都在一个固定的article节点里写起来很直观import requests from bs4 import BeautifulSoup url http://books.toscrape.com/ resp requests.get(url, headers{User-Agent: Mozilla/5.0}, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) for h3 in soup.select(article.product_pod h3 a): print(h3[title], h3[href])这段代码做了三件事带请求头访问页面、修正编码、用 CSS 选择器提取链接。这里有个非常容易被忽略的细节resp.encoding resp.apparent_encoding。有些网站没有在响应头里明确声明字符集或者声明了但实际内容用的是另一种编码直接打印中文数据可能是一堆乱码。用apparent_encoding是根据响应内容自动推断编码虽然会多花一点点时间但胜在稳。2.2 解析与提取的几个关键细节BeautifulSoup 的玩法主要分两种find/find_all和select。前者适合按标签和属性精确查找后者支持完整的 CSS 选择器语法写起来更像写页面样式我个人更推荐select。比如要提取一个链接的地址和文字h3[href]拿到的是属性值h3.get_text(stripTrue)拿到的是标签内的纯文本。这两个经常混着用注意别搞混。还有一个翻页的通用套路很多静态列表页的翻页按钮是一个“下一页”的链接你可以循环请求直到没有下一页为止。while True: soup BeautifulSoup(resp.text, html.parser) next_btn soup.select_one(li.next a) if not next_btn: break next_url urljoin(url, next_btn[href]) resp requests.get(next_url, headersheaders)这里必须用urljoin因为下一页链接往往是相对路径比如catalogue/page-2.html直接拼字符串很容易拼错。2.3 静态方案最常踩的两个坑第一个坑是数据根本没渲染出来。你抓回来一个静态页面的 HTML里面却找不到预期数据十有八九是目标站点把数据放在接口里页面只是前端展示壳。这时候别硬用 BeautifulSoup 去抠了果断跳到后面要讲的接口方案。第二个坑是请求头伪装不到位。有些网站会拦截不带User-Agent的请求返回 403 或者跳转到一个验证页面。我的经验是把User-Agent设置成当前主流浏览器的完整字符串必要时再加Accept-Language和Referer基本就能避免一大半基础反爬拦截。3. 方法二requests-html轻量动态页面的折中方案requests-html 是 requests 作者写的另一个库它的定位很有意思既能像 requests 一样发请求又能内置一个无头浏览器环境去执行 JavaScript。这意味着你可以用几乎和 requests 一样简单的 API去处理那些“只有一点动态渲染”的页面而不用立刻上 Selenium。3.1 为什么需要它有些页面的动态程度不算高只是用 JavaScript 从某个接口取数据然后渲染成 HTML。这种页面用纯 requests 抓不到但它的交互并不复杂没有点击、滚动、拖拽等操作上 Selenium 又显得杀鸡用牛刀。requests-html 的render方法在这种场景下特别合适——它会调用内置的 Chromium 去执行页面脚本然后把渲染完成后的完整 HTML 返回来。3.2 渲染与提取的完整示例以那个专门练手的名言网站为例它的/js/页面就是典型的 JS 渲染场景源码里只有空壳数据全靠脚本加载。from requests_html import HTMLSession session HTMLSession() resp session.get(https://quotes.toscrape.com/js/) resp.html.render(sleep2) for quote in resp.html.find(.quote .text): print(quote.text)注意sleep2这个参数。它表示在页面加载完后再等两秒目的是给脚本执行留出缓冲时间。如果删掉这行页面脚本可能还没跑完就已经开始提取数据了结果就是拿回一堆空列表。实际项目中这个等待时间可以根据页面复杂度调整但是没必要过度拉长毕竟每多等一秒你的整体抓取时间就多一秒。3.3 性能边界和替代方案requests-html 的render本质上是启动一个 Chromium 实例去渲染页面内存占用和启动耗时都不低而且第一次调用render时如果本地没有对应的 Chromium 内核它会自动下载容易让新手误以为程序卡死了。所以它只适合小批量、频率不高的场景。一旦你的目标数据量大、页面数量多我就建议要么找接口要么直接上 Selenium 自己管理浏览器实例而不是每个请求都靠 requests-html 去拉起一个渲染环境否则效率会非常难看。4. 方法三Selenium复杂交互和重度渲染的最终武器Selenium 是什么它本质上是浏览器的自动化驱动。它可以直接操控一个真实的浏览器窗口打开页面、点击按钮、输入文字、下拉滚动条然后拿到完整渲染后的内容。它不像 requests-html 那样只是“内置一个渲染内核”而是你真的可以看到浏览器在你面前跑。4.1 什么场景非它不可当目标页面需要登录、需要鼠标悬停、需要点击翻页、需要滚动加载甚至需要验证码交互时requests 和 requests-html 基本都失去了用武之地。你没法用一段简单脚本模拟真实的操作流但 Selenium 可以。这就是它被称为“最终武器”的原因。但要清醒一点Selenium 是五种方案里运行成本最高的一个。它要启动完整的浏览器进程内存占用轻松上几百兆并发能力也差。所以我的原则是只有当前面所有方法都搞不定时才上 Selenium。4.2 一个稳健的 Selenium 脚本写法这里给出一个带显式等待的成熟写法可以直接改着用from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC opts Options() opts.add_argument(--headless) driver webdriver.Chrome(optionsopts) try: driver.get(https://quotes.toscrape.com/js/) wait WebDriverWait(driver, 10) items wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, .quote .text)) ) for item in items: print(item.text) finally: driver.quit()WebDriverWait是 Selenium 场景里特别重要的构件。它会在指定时间内轮询页面直到某个条件成立比如元素出现。相比于写死time.sleep(5)显式等待在快的时候很快、在慢的时候也不容易误判是稳定性提升的关键。条件对象presence_of_all_elements_located表示页面里至少出现了一组匹配元素这个条件对列表页来说比“单个元素可见”更可靠。4.3 无头模式的隐藏坑--headless参数会让浏览器在后台运行不弹出窗口适合部署在服务器上。但无头模式下有些前端代码的行为会和有头模式略有差异——比如某些站点会检测navigator.webdriver属性或检查窗口尺寸导致无头模式下拿到的内容和正常浏览器不同。我的建议是本地调试阶段先不要开无头模式亲眼看看浏览器到底打开了什么页面、元素到底渲染成了什么样子等逻辑完全跑通之后再切到无头模式部署。另外Chrome 和 chromedriver 的版本必须匹配版本差太多会直接启动报错这是新手最常遇到的环境问题。5. 方法四Scrapy采集任务工程化的分水岭前面三种方法本质都是“脚本级”的一个 Python 文件从头跑到尾。一旦你要采集的网站有几十个分类、每个分类几十个页面、每条数据还要清洗和入库脚本就会变得越来越乱。这时候就该上 Scrapy 了。5.1 从脚本到框架解决的不只是速度Scrapy 是一个异步爬虫框架它自带调度器、下载器、解析器和管道你只需要关心两件事怎么写 Spider 规则、怎么在 Pipeline 里处理数据。它最大的优势其实不是快而是结构清晰。请求的调度、去重、重试、限速都内置了你写的是规则而不是实现这让项目的可维护性提升了一个量级。5.2 一个 Spider Item Pipeline 的最小闭环我写一个简化但功能完整的最小工程让你直观感受下框架的组织方式。首先是 Spiderimport scrapy class QuotesSpider(scrapy.Spider): name quotes start_urls [https://quotes.toscrape.com/js/] def parse(self, response): for quote in response.css(.quote): yield { text: quote.css(.text::text).get(), author: quote.css(.author::text).get(), }然后是 Pipeline在pipelines.py里定义class QuotesPipeline: def process_item(self, item, spider): # 这里可以做去重、清洗、入库等操作 print(item) return item最后在settings.py里启用管道ITEM_PIPELINES { myproject.pipelines.QuotesPipeline: 300, }这个结构跑起来之后你会看到每条名言数据都被打印出来了。放在实际项目里parse里可以继续yield scrapy.Request(next_url, callbackself.parse)去翻页Pipeline 里可以做数据库写入逻辑清晰后面维护起来非常舒服。5.3 并发与限速的平衡Scrapy 默认并发数是 16这个数字对很多小网站来说已经算比较激进了。我见过有人图快把并发调到 64结果没跑几分钟对方的防护就触发全部请求变成验证码页。真正稳妥的做法是在settings.py里设置合理的下载延迟CONCURRENT_REQUESTS 8 DOWNLOAD_DELAY 1.5DOWNLOAD_DELAY是多少秒Scrapy 会在每个请求之间至少等待这么久相当于一种熔断机制。这个值设在 0.5 到 2 之间既不至于抓得太慢也不至于让对方服务器压力过大。记住一条铁律爬虫的速度首先要对目标网站友好其次才是自己的效率。6. 方法五直接调接口最高效也最容易被忽略的路径这个方法是很多有一定经验的人反而不太常用的因为大家习惯了去解析 HTML。但实际上大量网站的页面数据都是通过接口返回的页面本身只是个壳。你打开浏览器开发者工具刷新页面耐心看一眼 Network 面板经常会发现一条 JSON 请求里包含了你想要的几乎所有数据。6.1 数据在接口里不在 HTML 里举个例子你看到页面上有一万个商品列表HTML 里可能只有第一屏的十几个剩下的数据是往下滚动时通过接口不断追加的。这时候如果你执着于解析 HTML不但拿不全还会因为页面结构变化而经常改代码。但如果你找到了那条分页接口直接用 requests 请求它每次返回的都是干净的 JSON解析成本直线下降。6.2 抓包定位接口的通用流程打开目标页面按 F12 进入开发者工具切到 Network 面板在筛选栏里选择 Fetch/XHR。然后刷新页面或者触发翻页操作观察新增的请求。找到返回内容为 JSON、且字段和页面上展示的数据对应的那条请求右键复制为 cURL再粘贴到 Postman 或直接转成 Python 代码。用公开测试接口演示一下import requests resp requests.get(https://httpbin.org/json, timeout10) data resp.json() print(data[slideshow][title])resp.json()是 requests 内置的 JSON 解析方法前提是响应内容确实是合法的 JSON 格式。如果是你会得到 Python 的字典或列表之后就是熟悉的字典操作了。接口方式还有个大优势通常请求量很小传输的是纯数据而不是整个 HTML 页面对对方服务器和你的带宽都更友好。6.3 接口方式的合规红线接口方式有一个必须反复强调的边界如果一个接口是公开的、没有鉴权、也没有明确禁止调用那你可以正常使用但如果接口有明显的签名参数、加密参数或者需要登录才能访问那就说明对方并不希望第三方直接调用这时候不要试图去破解签名或绕过鉴权。越过了这条线事情的性质就从“抓取公开数据”变成了“突破技术保护措施”风险完全不同。合法合规的爬虫应当尊重网站的使用条款访问 robots.txt控制请求频率不采集个人隐私信息不用于恶意竞争。练习时优先选择专门开放的测试网站和公开 API这样既能学到技术又不踩线。7. 五种方法之外的通用经验让脚本更稳的细节方法选对了代码写完了真正决定爬虫能跑多久的往往是那些看起来不起眼的细节。这些经验我在多个真实项目中反复验证过值得单独拿出来说。7.1 请求头与 Session 管理写爬虫一定要设置User-Agent这是最基本的礼貌和伪装。但真正复杂一点的站点还会校验请求头里其他字段比如Accept、Accept-Language、Referer。碰到这种情况最好的方式是用requests.Session()来管理多次请求Session 会自动保存 cookies也能让你统一设置请求头import requests session requests.Session() session.headers.update({User-Agent: Mozilla/5.0}) resp session.get(https://example.com)一个 Session 对象在多次请求之间保持连接复用的状态效率比每次都新建 requests 请求要高而且如果你需要登录后再采集Session 保持了登录状态的身份凭证后面所有请求都像是同一个浏览器发起的。7.2 超时、重试与编码陷阱请求必须设置timeout否则遇到一个不响应的站点你的脚本会一直挂在网络调用上看起来像死循环。重试机制也很重要网络请求偶尔失败是常态简单的做法是自定义一个带重试的函数import time import requests def fetch(session, url, retries3): for i in range(retries): try: resp session.get(url, timeout10) resp.raise_for_status() return resp except requests.RequestException: if i retries - 1: raise time.sleep(2 * (i 1))指数退避逻辑第一次失败等 2 秒第二次失败等 4 秒然后继续重试。这比固定等 2 秒要科学因为连续的失败往往说明对方服务器在限流或者暂时不可用你等得稍微久一点反而是对双方的保护。编码问题前面提过一次这里再强调遇到中文乱码第一反应是检查编码而不是粗暴地用正则去匹配。静态页面用resp.apparent_encoding接口响应如果宣告是 JSON 但内容不规范注意看响应头里的Content-Type是否带了charset参数。7.3 遵守规则控制频率给自己留后路最后一条饭桌级的经验永远不要让爬虫的速度超过目标网站的服务能力。很多反爬机制都是被无节制的爬虫逼出来的。作为开发者你的核心竞争力在于精准拿数据和稳健处理异常而不是暴力消耗带宽资源。我在实际项目里会给自己设几条硬规矩单线程请求默认延迟至少 0.5 秒并发压测只对自有服务器或明确允许压测的服务执行robots.txt 里明确禁止的路径绝对不碰页面数据结构变化后先停任务再排查而不是让它继续空跑。写在最后的一点体会我个人的习惯一直是能上轻量方案的绝不上重型武器能用 requests 解决的就别上 Selenium能找到公开接口的绝不去硬解析 HTML。因为每上升一个层级引入的开销和需要维护的细节都是指数级增加的。爬虫这个事难点从来不在“能把数据取下来”而在于“在复杂多变的网站环境里稳定地、合规地把数据取下来”。这五条路我都走过踩过的坑比代码多但正是因为踩过才知道哪条路上哪里有石头。希望这篇总结能让你在选路线的时候少走一段弯路。