
Python 爬虫获取网页数据的 5 种方法刚上手 Python 爬虫的时候我也有过一段迷茫期网页明明就摆在浏览器里可一到代码里就各种拿不到数据。要么返回空页面要么直接报错要么拿到一堆看不懂的 HTML 标签。后来才慢慢想明白爬虫最核心的问题从来不是“会不会用某个库”而是“这个网页的数据到底藏在哪里又该用哪种方式把它取出来”。这篇文章就把我实际项目中用过的 5 种获取网页数据的方法整理出来——从最基础的 requests 请求到 BeautifulSoup 解析再到 lxml 的 XPath 提取最后是 Selenium 和 Playwright 这类处理动态页面的方案。每种方法我都会说清楚适用场景、核心代码、容易踩的坑以及我自己的选型经验。无论你是刚准备写第一个爬虫的新手还是已经写过一些脚本、想系统梳理思路的进阶用户这篇内容都能帮你少走不少弯路。1. 五种方法的能力边界与选型逻辑1.1 为什么同样抓网页需要用五种不同方法很多人以为爬虫就是“发个请求、拿回 HTML、用正则抠数据”三步走。但真实情况远比这个复杂静态页面可以直接解析 HTML动态页面需要等待 JavaScript 执行接口型网站的数据藏在 XHR 请求里还有的网站做了各种反爬限制。同一个 URL在不同场景下适合它的数据获取方式完全不同。我习惯把网页数据获取拆成两个层面第一层是“把数据拿回来”第二层是“把数据提取出来”。requests、urllib 负责第一层BeautifulSoup、lxml、正则表达式、Selenium 负责第二层。而实际方法的选择主要取决于三个因素数据是否在初始 HTML 中、数据是否需要浏览器渲染、网站的反爬强度。这里给你一个我常用的选型判断逻辑先在浏览器里按 F12 打开 Network 面板刷新页面看第一个请求返回的内容里有没有你要的数据。如果有就用请求库加解析库如果没有看 XHR 接口里能不能直接拿到 JSON能的话就模拟接口都拿不到大概率是 JavaScript 动态渲染这时候再上浏览器自动化工具。1.2 五种方法的能力对比为了帮助新手快速建立全局印象我把五种典型方案整理成了一个对照表后面每个方法再逐一展开。方法核心工具适用场景依赖学习成本标准库请求法urllib简单静态页面、无第三方库环境无低会话请求法requests绝大多数静态页面、带 cookie 的页面第三方低DOM 解析法BeautifulSoup需要灵活遍历 HTML 结构第三方低结构化查询法lxml XPath复杂 HTML、频繁提取特定节点第三方中动态渲染法Selenium / Playwright前端渲染页面、需模拟用户操作第三方高另外还有一种常被忽略但效率极高的方式——直接模拟接口请求拿 JSON 数据。严格来说它不能算“解析网页”但在实战中我遇到的大多数数据获取需求最后都落到了这个方案上。第 6 部分我会单独聊它。在实际项目里这几种方法不是互斥的我经常在同一个爬虫里组合使用requests 拿页面、BeautifulSoup 解析结构、遇到动态内容再局部调用 Playwright。理解每种方法的边界才能组合出最合适的方案。2. 请求层基础requests 与 urllib 的完整用法2.1 requests 的核心请求链路requests 是我日常使用频率最高的 HTTP 客户端库它比标准库 urllib 好用太多API 设计简洁处理 cookie、session、重定向都省心。这里有一段完整的请求示例我标注了关键参数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, Accept-Language: zh-CN,zh;q0.9, Referer: https://example.com/, } session requests.Session() session.headers.update(headers) response session.get( https://example.com/list, params{page: 1, size: 20}, timeout(5, 15), ) response.encoding response.apparent_encoding html response.text这段代码里有两个容易忽略的细节第一个是timeout(5, 15)前面是连接超时、后面是读取超时防止某个网站响应慢导致整个任务卡死。第二个是response.encoding response.apparent_encoding很多网站的 charset 声明与实际内容不一致强制用 apparent_encoding 可以从内容里探测真实编码能有效避免中文乱码。requests 最实用的能力是 Session 对象。它会自动保存服务端下发的 cookie并且在你调用session.get()时自动携带。这意味着你只需要先访问一次登录页再提交表单后续的请求就会自动带上登录态不需要手动维护 cookie 字典。提示requests 本身不执行 JavaScript。如果页面数据是通过 JS 异步加载的requests 拿到的 HTML 里不会有这些数据。遇到这种情况请直接跳转到第 5 部分或第 6 部分。2.2 urllib没有第三方库时的备选方案有些公司的生产环境出于安全考虑不允许随便安装第三方包或者你只是临时写个一次性脚本这时候可以用 urllib 完成任务。标准库方案稍微啰嗦一些但核心思路是一样的from urllib import request, parse url https://example.com/search params parse.urlencode({q: python 爬虫}) full_url f{url}?{params} req request.Request( full_url, headers{ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), }, methodGET, ) with request.urlopen(req, timeout10) as resp: html resp.read().decode(utf-8, errorsignore)需要特别提醒的是errorsignore这个参数。urllib 读取的原始字节流解码时遇到非法字符会抛 UnicodeDecodeError加上 ignore 可以跳过这些字符保证拿到完整数据而不是中断报错。urllib 也支持携带 cookie但需要手动创建http.cookiejar.CookieJar并配合HTTPCookieProcessor使用。代码比 requests 复杂不少日常开发我还是推荐优先用 requests只有在环境受限时才退回到 urllib。2.3 请求层最容易被忽略的“反爬三件套”新手拿 requests 请求网页最常遇到的就是 403 Forbidden 或 418 Im a teapot。这两个状态码基本宣告了你的请求被识别为爬虫。我的经验是请求头里至少要带好“反爬三件套”User-Agent、Referer、Accept-Language。User-Agent 告诉服务器你是什么浏览器Referer 告诉服务器你从哪个页面跳转过来Accept-Language 表明你的语言偏好。很多网站的反爬系统会同时校验这几个字段缺一个就可能命中风控规则。建议把常用的浏览器 UA 字符串存成一个列表每次请求随机取一个降低被连续识别为同一客户端的概率。还有一个细节是请求频率。我刚开始写爬虫时喜欢在循环里快速请求结果不到十次就被封了 IP。后来养成一个习惯每次请求之间加随机延时不要用time.sleep(1)这种固定值而是用time.sleep(random.uniform(1, 3))让请求间隔像是真人操作。3. BeautifulSoup从 HTML 树里提取数据的经典方案3.1 三种解析器的选择逻辑requests 拿到响应文本后数据还是“一张纸”而 BeautifulSoup 的作用是把这张纸变成一棵可以灵活操作的树。BeautifulSoup 本身不做网络请求它只负责解析。它依赖的底层解析器有三种html.parser、lxml、html5lib。解析器速度容错性依赖html.parser中等一般标准库自带lxml快较好需安装 lxmlhtml5lib慢极好需安装 html5lib我日常基本都用 lxml 作为解析器因为它速度最快容错性也够用。只有当页面 HTML 写得极不规范、lxml 都在报警告时我才会换 html5lib 试试。创建 BeautifulSoup 对象的标准写法如下from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml)3.2 核心选择器用法与实战示例BeautifulSoup 最常用的方法就三个find()、find_all()、select()。前两个是按标签名或属性查找第三个是支持 CSS 选择器风格的查找。我实际写代码时用得最多的是select()因为 CSS 选择器表达能力强而且和前端同事沟通时的语法完全一致。举个例子现在要提取一个商品列表页里所有商品名称和价格HTML 结构大概是这样的div classgoods div classitem h3 classtitle无线蓝牙耳机/h3 span classprice199.00/span /div div classitem h3 classtitle机械键盘/h3 span classprice399.00/span /div /div对应的提取代码是items soup.select(div.item) for item in items: title item.select_one(h3.title).get_text(stripTrue) price item.select_one(span.price).get_text(stripTrue) print(title, price)这里有个我踩过很多次的坑get_text()会取出节点内所有文本包括内部的空格和换行所以一定要传stripTrue。另外如果某个商品标题缺失select_one()会返回 None调用.get_text()时就报 AttributeError。稳妥的做法是先判断是否为 None 再取值title_node item.select_one(h3.title) title title_node.get_text(stripTrue) if title_node is not None else BeautifulSoup 还有一个很实用的能力是修改节点。你可以直接node[class] new-class修改属性或者node.string.replace_with(新文本)替换文本。配合相关库把清洗后的 HTML 导出成 Markdown 或纯文本的场景这个能力很省事。4. lxml 结合 XPath高效定位复杂节点的利器4.1 XPath 的核心语法速查当页面结构越来越复杂纯靠 BeautifulSoup 的 CSS 选择器会写出很长一串嵌套选择维护成本高。这时候我会切换到 lxml 的 XPath。XPath 是一门专门用来在 XML/HTML 文档中定位节点的语言很多浏览器插件都支持直接调试写起来比 CSS 选择器更灵活。下面这张表是我常用的 XPath 表达式速查建议收藏表达式含义//div从任意位置找所有 div 节点//div[classitem]找 class 属性等于 item 的 div//div[contains(class, item)]找 class 属性包含 item 的 div//a/href取所有 a 节点的 href 属性//span/text()取所有 span 节点的直接文本//div[idlist]//ul/li[2]取 id 为 list 的 div 下第二个 li//h3[position()3]取前两个 h3 节点lxml 的用法分为两步先etree.HTML(html)构建一个 Element 对象再调用xpath()方法执行表达式。元素对象与 BeautifulSoup 的树不同它更像是一个“节点指针”可以直接操作。4.2 用 XPath 处理翻页与列表抓取举一个真实案例。我需要抓取一个资讯网站的文章列表每个列表页有 10 篇文章结构是div.article-list div.article-item每篇文章有标题链接和发布时间。用 XPath 的写法如下from lxml import etree tree etree.HTML(html) articles tree.xpath(//div[contains(class, article-item)]) for article in articles: title article.xpath(.//a[contains(class, title)]/text()) link article.xpath(.//a[contains(class, title)]/href) pub_time article.xpath(.//span[contains(class, time)]/text()) if title: print(title[0].strip(), link[0] if link else , pub_time[0] if pub_time else )注意这里 XPath 返回的都是列表即使只有一个匹配项也包在列表里所以取值时用[0]取出第一个元素或者先判断列表是否为空。这个特性和很多新手的直觉不一样很容易漏掉索引导致报错。XPath 还有一个非常好用的技巧——用contains()做模糊匹配。很多网站的 class 名是动态生成的比如classtitle title-202501直接用classtitle匹配不上但用contains(class, title)就能精准命中。这个函数在应对自动化生成的样式名时几乎是必备技能。注意XPath 表达式中如果用双引号包裹属性值表达式内部就不能再嵌套双引号否则会语法报错。建议外层用单引号、内层用双引号或者反过来统一风格。5. 动态渲染页面Selenium 与 Playwright 的完整方案5.1 Selenium 的配置与等待策略当页面数据依赖 JavaScript 动态渲染requests 和普通解析就失效了。比如某数据可视化平台列表内容是根据用户操作实时拼接出来的初始 HTML 里只有一个空壳。这时候需要用一个真正的浏览器去执行 JS然后读取渲染后的 DOM。Selenium 是老牌的浏览器自动化工具核心思路是启动一个 WebDriver 实例让它打开浏览器访问页面再通过代码模拟点击、滚动、等待最后读取页面内容。下面是一段最基础的 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 options Options() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) try: driver.get(https://example.com/dashboard) wait WebDriverWait(driver, 10) items wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, div.card)) ) for item in items: print(item.text) finally: driver.quit()这段代码里有几个关键点。第一是--headlessnew无头模式可以在后台运行浏览器不弹出窗口适合服务器环境。第二是显式等待WebDriverWait它比time.sleep()可靠得多会自动检测元素是否出现既不浪费等待时间也不会过早读取。新手最常见的错误是在driver.get()后立刻用driver.find_element()去抓数据这时候 JS 可能还没执行完返回空列表。我的建议是只要页面里有 AJAX 数据一律用显式等待不要用固定 sleep。5.2 Playwright更现代的浏览器自动化选择Selenium 用久了你会发现它有两个痛点一是版本兼容问题Chrome 更新后 WebDriver 可能失效二是 API 设计比较老操作链式写法不够直观。所以我近两年新项目基本迁移到了 Playwright。Playwright 的安装和使用都非常顺滑一条命令可以同时安装库和对应的浏览器pip install playwright playwright install chromium用 Playwright 抓取动态页面的代码比 Selenium 简洁很多from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://example.com/dashboard, wait_untilnetworkidle) page.wait_for_selector(div.card) cards page.query_selector_all(div.card) for card in cards: print(card.inner_text()) browser.close()wait_untilnetworkidle会等到网络请求基本停止后再继续对大部分 AJAX 页面够用。如果页面是无限滚动加载Playwright 还提供了mouse.wheel模拟滚动每滚一段等待新数据出现这个能力在抓取瀑布流页面时非常实用。Playwright 还有一个杀手级功能是自动等待。比如你调用page.click()或page.fill()时它会自动等待目标元素可交互不需要你手动写显式等待。这一点比 Selenium 省心太多也是我迁移的主要原因。提示浏览器自动化工具虽然强大但执行效率和资源占用都远高于 requests同一时间能启动的浏览器实例有限。能走接口和静态解析的页面优先用前几种方法只有确认必须渲染 JS 时再上浏览器方案这是爬虫性能优化的基本思路。6. 补充方案逆向接口直取 JSON 数据6.1 从浏览器 Network 面板定位真实数据源这个方法其实是无心插柳发现的。有次我要抓一个数据展示页面的列表Selenium 跑得很慢而且页面还时不时弹出滑块验证。我随手打开了浏览器的 Network 面板刷新页面后看到了一大堆 XHR 请求点开一个居然直接返回了结构清晰的 JSON里面就是页面上的所有列表数据。从那之后我抓数据的第一优先级就变成了先看 Network再写代码。定位真实接口的方法很简单在 Network 面板筛选 Fetch/XHR 类型逐个看响应 Preview找到包含页面核心数据的那个请求。然后右键复制为 cURL再用工具转换成 Python 代码。6.2 模拟接口请求的注意事项接口模拟的好处是拿到的是干净的结构化数据不用再费劲解析 HTML。但模拟接口也有一些反爬风险最大的问题是请求签名。很多网站会在请求参数里带上 sign、token 之类的签名这些参数是前端 JS 用密文生成的直接复制无法复用。处理签名有一个比较通用的思路先看签名参数是纯前端生成还是需要请求一个 token 接口换取。如果是后者就用 requests 先请求 token 接口拿到后再拼到数据接口请求头里。我遇到过一个实际案例token 的过期时间是 30 分钟我就在爬虫里维护了 token 缓存和过期时间判断避免每次都重新获取。还要注意请求参数的时效性。有些接口的 params 里带有时间戳或页码信息直接复制旧链接会在服务器端校验失败。所以逆向接口时代码里最好动态生成时间戳相关参数不要写死。接口模拟是我目前生产效率最高的一种方式但也要克制使用尤其是并发请求时要控制频率。我的经验是并发控制在 5 个以内随机延时保持在 0.5 到 2 秒之间这样既能保证效率又不至于对目标服务器造成压力。7. 常见问题与排查技巧实录7.1 状态码 403 与反爬机制绕过思路403 是爬虫最常遇到的状态码意味着服务器理解了请求但拒绝访问。这里我按经验把常见的 403 场景和解决思路整理成了一个表现象可能原因对策首次请求就 403缺少 User-Agent 或 UA 被识别补齐请求头用真实浏览器 UA请求几次后 403请求频率过高触发风控增加随机延时降低并发带 Cookie 请求后 403登录态过期或会话被标记重新登录检查 token 有效性带特殊 Header 后 403Header 顺序或特殊字段不合法复制浏览器完整的请求头并精简还有一个经常被忽略的情况是网站对请求的 Accept、Accept-Encoding 等头也有校验。有的反爬系统不检查具体值而是检测这些头是否存在或者顺序是否异常。遇到排查不出来原因的 403直接下载当前浏览器的完整请求头一行一行对比往往能找到问题。7.2 编码乱码、超时与连接重置乱码问题的主要原因我前面提过——response.encoding 判断错误。解决方式就是显式设置编码。还有一个技巧优先从 HTML 的 meta 标签中读取 charset如果没有再使用apparent_encoding。超时的处理我一般分两类单个请求超时用 timeout 参数控制整个爬虫任务超时用全局信号量或超时线程控制。requests 的 timeout 参数只控制单次请求如果页面内容很多、读取时间很长建议用流式响应streamTrue分批读取或者干脆跳过该 URL 记录日志。“Connection reset by peer” 这种错误通常出现在 HTTPS 握手阶段常见于某些网站对特定地区的 IP 或特定 TLS 指纹做了拦截。遇到这种情况可以尝试更换 HTTP/2 支持库或者调整客户端的 TLS 版本。不过实话实说这类反爬比较高级普通入门项目很少遇到先不用纠结。7.3 故障排查速查表我把平时排查爬虫问题的顺序整理成了一个清单碰到问题照着走就行先用浏览器打开目标页确认页面数据是否存在排除页面本身为空的情况。复制目标 URL 到 Postman 或 curl 中访问确认 URL 和请求头是否正确。打印响应状态码和响应前 500 个字符判断返回的是否是预期的 HTML/JSON。如果返回是 HTML 但解析为空检查页面数据是否由 JS 动态加载。如果是动态加载转头看 XHR 接口里的 JSON 数据。如果接口有签名参数回到前端 JS 逆向签名逻辑。最后才考虑用浏览器自动化方案兜底。这个顺序本质上对应了前面讲到的五种方法从最轻量到最重量级依次尝试。大部分场景在第 3 步或第 5 步就能解决。7.4 我的一些个人经验做爬虫这些年我最大的体会是不要一上来就写代码先把目标页面的数据结构搞清楚。花十分钟看 Network 面板可能省下两个小时调试浏览器自动化的时间。第二个体会是要有纪律性——请求频率、延时、失败重试、日志记录这些“不性感”的东西才是项目稳定运行的关键。我见过太多爬虫写起来容易、跑几天就挂最后全是在补反爬的处理。还有一个建议如果你打算长期做数据采集建议从一开始就建立一个简单的数据存储方案比如把抓到的数据统一保存成 JSON 或写入 SQLite不要把抓取和存储混在同一个脚本里。这样即使解析逻辑改了历史数据还在不用重新跑一遍全量抓取。最后模拟接口、控制频率、做好异常兜底是我认为爬虫项目里最重要的三件事。从 requests 到 Playwright工具可以换但这三个原则始终不变。希望这份总结能帮你少踩一些我当年踩过的坑。