
我最早接触网络爬虫就是想知道怎么把网页上那些散落的数据变成自己能用的表格。当时网上资料很零散今天学个requests明天看个正则后天又被验证码卡住始终串不成一条完整的链路。后来在项目里真正用爬虫解决了业务问题才慢慢把“网页抓取、数据解析、反爬绕过”这整条流程摸透。这篇内容就是把这些经验整理出来从HTTP基本原理讲到动态渲染页面抓取再到常见反爬机制的识别与应对适合刚学完Python基础语法、想用爬虫做点实际事情的读者也适合那些写了不少脚本但总是被反爬拦住的朋友对照排查。爬虫这件事本质上就是模拟浏览器和服务器对话。浏览器能干的爬虫也能干只是需要更精细一点要伪装得像、要懂得解析、要知道服务器在什么情况下会拒绝你。把这些搞明白写一个稳定爬虫和写一个demo之间差别非常大。1. 爬虫第一步先搞懂网页是怎么交数据的很多人一上来就写代码requests请求一发拿到HTML就开始正则提取结果页面上明明有数据正则却匹配不到或者请求两次就被封IP。这些问题的根源往往不在解析代码而在最底层的HTTP理解上。1.1 HTTP请求的本质是一问一答爬虫的每一次抓取本质就是一次HTTP请求。浏览器访问网页时会向服务器发送一个请求头Headers服务器根据请求头里的信息决定返回什么内容。这里面有几个关键字段值得优先关注User-Agent告诉服务器“我是谁”也就是浏览器版本、操作系统这些信息。服务器最早、最基础的反爬就是从这儿开始的如果请求里没有UA或者UA是一个奇怪的字符串直接拒掉。Referer告诉服务器“我从哪儿来”比如从百度点进来的链接Referer就是百度。有些网站会校验这个字段防的是盗链但爬虫也经常被误伤。Cookie用来维持状态。登录态、浏览记录、部分反爬验证结果都存在里面。新手最容易犯的错是用了requests的默认UA。那个UA长什么样呢大概是python-requests/2.31.0服务器一眼就能识别这不是浏览器直接返回一个验证码页或者403。所以第一步老老实实把浏览器里的UA复制过来或者准备一个UA池轮流用。1.2 状态码和响应内容是服务器给你的暗号拿到响应之后不要急着解析先看两个东西状态码和响应头。状态码是服务器最直接的反馈200正常内容给你了301/302重定向requests默认会跟随但有些场景需要自己处理403禁止访问基本是被反爬拦了404页面不存在可能是URL拼错了418这个有意思是“我是茶壶”网站用这个状态码来嘲讽爬虫实际含义就是我识别到你了不想给你数据429请求太频繁触发了限流503服务器暂时不可用也可能是被WAF临时拦截响应头里的Content-Type也很重要。如果返回的是text/html那正常解析HTML如果返回的是application/json说明这个接口是JSON格式直接解析JSON就行别费劲去正则HTML了如果返回的是application/javascript说明可能是个JS动态渲染的接口得另想办法。实操经验拿到响应先print一下resp.text的前500个字符确认了内容再写解析代码。真写复杂项目时先把Response对象保存到本地文件反复调试解析逻辑别每次都要重新请求服务器否则调试十次就触发反爬了。2. 环境准备和工具选型这步省事是为后面省命爬虫开发环境不需要多复杂但版本问题踩一次坑就能浪费半天。这里把我的配置思路完整说一下。2.1 Python版本和虚拟环境Python 3.9以上基本都是可以正常工作的我推荐3.10或3.11因为很多新库对旧版本支持不够友好比如某些反爬破解库用到了新的语法特性3.8跑不起来。建议用虚拟环境管理依赖别一股脑全装到全局环境里。爬虫项目A可能用了requests 2.28项目B可能用了httpx 0.24全局混装迟早出问题。用venv或者conda都可以命令很简单# 创建虚拟环境 python -m venv venv # 激活Windows venv\Scripts\activate # 激活macOS/Linux source venv/bin/activate # 安装核心依赖 pip install requests beautifulsoup4 lxml pandas2.2 核心库选型各司其职爬虫涉及的工具我按用途分了三类请求库requests是最常用的简单直观但有些复杂的反爬场景需要用httpx或者curl_cffi后者能模拟浏览器的TLS指纹专治一些比较严格的WAF。解析库BeautifulSoup4适合快速上手语法简单lxml是底层加速器解析速度比纯Python实现快很多如果数据在接口返回的JSON里直接用Python内置的json模块。动态渲染处理Selenium和Playwright。网站如果数据是JS动态加载的HTML里看不到真实数据就用这两个库模拟真实浏览器。另外推荐一个调试神器httpbin.org。这是个公开的测试网站你请求它它会原样返回你的请求信息。写爬虫之前先往httpbin一跑看看自己的Headers伪装得怎么样非常直观。import requests response requests.get(http://httpbin.org/headers, timeout10) print(response.json()) # 返回内容里能看到requests实际发送了哪些请求头2.3 抓包工具是爬虫的眼睛写爬虫之前先学会看浏览器开发者工具。按F12打开切到Network面板刷新页面就能看到页面发出的所有请求。这里面最重要的信息是请求URL、请求方式GET还是POST、请求头、请求体、响应内容。判断这个网站是不是JS动态渲染方法也很简单用浏览器打开页面CtrlU查看源代码搜索某个关键数据字段如果能搜到说明是服务端渲染的直接用requests就能抓如果搜不到但页面上明明有数据那就是JS动态渲染需要考虑Selenium/Playwright或者去Network里找接口地址直接调用。注意按F12看Network是正经调试手段所有浏览器都自带跟破解、绕过无关这只是数据分析师、研发人员日常排查问题的基础操作。3. 网页抓取requests库的正确打开方式requests库用起来很简单但真正写得优雅、稳当需要注意很多细节。这里把几个核心场景拆开讲。3.1 带Headers的请求伪装得像一点直接在requests.get()里加headers参数。建议把所有要用的请求头定义成一个字典方便复用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: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } url https://example.com/list response requests.get(url, headersheaders, timeout10) print(response.status_code) print(response.text[:1000])这里重点讲timeout参数。很多入门教程根本不提timeout但实际请求中如果目标服务器没有响应requests会一直挂着程序卡在那里不动。加了timeout之后超过设定时间就抛异常可以引导程序切换到备用方案。3.2 Session对象让请求保持“记忆”如果爬虫需要在同一个会话里发多个请求比如先登录、再访问个人中心页请求之间需要共享Cookie。这时候不能每次都用requests.get()要使用Sessionimport requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ..., }) # 第一次请求服务器会在响应头里设置Cookie session.get(https://example.com/login_page) # 第二次请求Session自动带上Cookie data session.get(https://example.com/my_data, timeout10) print(data.text[:1000])Session还有一个隐藏作用降低被识别风险。因为Session保存了连接池对同一个网站不会每次请求都重新建立TCP连接持续会话更接近真实浏览器的行为模式。3.3 动态渲染页面的处理方案如果requests怎么也拿不到数据页面HTML里没有目标字段那基本可以判断是JS渲染。这时候有两种处理方式第一种找到背后的数据接口。大多数网页的动态数据都是通过XHR请求从后端API获取的浏览器Network面板里找到那个返回JSON数据的请求直接调用这个接口。这是最优解速度快、数据干净、不依赖浏览器。第二种使用Playwright模拟浏览器。适合那种接口加密特别复杂、或者需要在页面环境里执行JS才能生成数据的场景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, timeout30000) page.wait_for_selector(.item-list) html page.content() browser.close()Playwright是这两年的主流选择比Selenium安装简单速度也更快。如果你处理的网站比较简单Selenium依然能用但新项目我建议直接上Playwright。3.4 抓取二进制文件图片和文件下载这里插入一个实用的场景抓取文件时要设置streamTrue然后按块写入本地文件避免一次性把大文件加载到内存里import requests response requests.get(https://example.com/file.pdf, streamTrue, timeout30) with open(download.pdf, wb) as file: for chunk in response.iter_content(chunk_size1024): if chunk: file.write(chunk)提示如果目标URL是下载行为而非页面跳转响应头里通常会有Content-Disposition字段包含文件名信息。下载前先检查这个字段可以对文件进行合理命名。4. 数据解析三板斧正则、XPath和JSON解析拿到网页源码或接口返回的数据之后就要进入解析环节。很多人只学了一两种解析方式遇到某种结构就得硬磨其实三种方案各有适合场景全掌握了才顺手。4.1 正则表达式字符串界的万能手术刀正则适合处理简单、规律明显的字符串提取。比如从一段文本里提取所有电话号码、邮箱、日期。但正则不适合解析嵌套结构的HTML——因为标签嵌套有层级正则写起来复杂不说还容易漏匹配。几个高频正则示例import re # 提取所有链接 links re.findall(rhref(.*?), html_text) # 提取邮箱 emails re.findall(r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}, text) # 提取多行文本 pattern re.compile(rdiv classtitle(.*?)/div, re.S) titles pattern.findall(html_text)这里有个重要参数re.S即DOTALL它让.能匹配换行符。HTML代码通常有很多换行不加re.S跨行的内容往往匹配不到。4.2 XPath和BeautifulSoupHTML结构解析的黄金组合XPath是一门在XML/HTML文档中查找信息的语言写起来像路径导航理解成本低。结合lxml库解析速度远超正则from lxml import html tree html.fromstring(html_text) titles tree.xpath(//div[classtitle]//a/text()) hrefs tree.xpath(//div[classtitle]//a/href)XPath常用语法// 在任意层级查找/ 从根节点开始查找或者表示子节点//a[idtop] 查找id为top的a标签//div[contains(class, item)] 查找class属性里包含item的divtext() 提取文本内容href 提取属性值BeautifulSoup的写法更符合Python习惯对新手友好from bs4 import BeautifulSoup soup BeautifulSoup(html_text, lxml) titles soup.select(div.title a) for title in titles: print(title.text) print(title.get(href))CSS选择器soup.select其实比find/find_all更好用因为语法和前端一样写起来简洁。如果要筛选属性是“以某字符串开头”用select(a[href^https])就能搞定。实操心得大规模爬取建议直接用lxml解析BeautifulSoup虽然写了“lxml”作为解析器但封装层本身还有性能开销。数据量达到十万级以上时差距非常明显。4.3 JSON解析接口返回数据的标准玩法现在的网站大量使用前后端分离架构数据通过JSON格式在接口间传递。拿到接口返回的数据后用json.loads()转成字典就能像操作普通Python字典一样提取数据import json data json.loads(response.text) # 假设数据结构是 {data: {list: [{title: xxx}, ...]}} for item in data[data][list]: print(item[title])如果JSON嵌套层级很深可以用jsonpath库简化代码from jsonpath import jsonpath titles jsonpath(data, $.data.list[*].title) print(titles)这种写法的好处是路径表达清晰而且JSON文档结构调整时只要路径大致不变代码改动量也很小。4.4 数据清洗与结构化存储解析出来的数据别急着存先做一轮清洗。常见问题包括换行符和空格、HTML实体字符 、等、数据不一致比如有的记录用了“暂无”表示缺失、类型转换把字符串型数字转成int/float。清洗完成后数据可以存成CSV、JSON文件或者导入SQLite/MySQL。这里给一个用pandas转DataFrame并将数据写入CSV的通用例子import pandas as pd data_list [ {title: Python爬虫实战, price: 59.00}, {title: 数据分析入门, price: 49.00}, ] df pd.DataFrame(data_list) df[price] df[price].astype(float) df.to_csv(books.csv, indexFalse, encodingutf-8-sig)注意编码写的是utf-8-sig不是utf-8。因为Excel打开CSV时如果文件没有BOM头中文会乱码。这个坑我踩过不止一次。5. 反爬机制全景拆解识别哪些套路在拦你浏览器看完页面很容易爬虫却总被半路拦住。原因就是服务器对“非人类访问”设置了重重关卡。理解这些关卡是写稳定爬虫的核心。5.1 请求头校验第一道门槛服务器会通过User-Agent、Referer、Accept-Language、Accept-Encoding等字段判断请求是不是来自真实浏览器。应对方案是尽量复用浏览器的完整请求头而不是只加一个UA。有一些网站还会校验Sec-Fetch-* 系列字段比如Sec-Fetch-Site、Sec-Fetch-Mode这些是浏览器自动带的代表请求来源和模式。直接用requests发请求时这些字段默认不存在严格校验的网站就会拒绝。解决方法有两条手动在headers里补全这些字段配合浏览器开发者工具里的真实值。用curl_cffi这样的库它能模拟浏览器的TLS握手特征和请求头顺序更接近真实浏览器。from curl_cffi import requests response requests.get(https://example.com, impersonatechrome120, timeout10) print(response.status_code)5.2 IP和请求频率控制最常见也最直接的封禁手段服务器记录每个IP的访问频率超过阈值就暂时或永久封禁。这是最经典的反爬策略。应对的核心原则是“降低访问频率、增加随机性”每次请求之间随机sleep 1-3秒同一时间只跑一个爬虫任务不并发轰炸每天抓取总量控制在一定范围内部分场景需要准备代理IP池轮换使用import time import random for page in range(1, 20): response session.get(fhttps://example.com/list?page{page}, timeout10) parse(response.text) time.sleep(random.uniform(1.5, 3.5))5.3 验证码和滑块验证人机识别的升级版当网站发现可疑流量会弹出验证码要求验证。图形验证码可以用打码平台解决滑块验证现在大多是基于行为轨迹分析需要模拟人类拖拽的加速度、停顿等特征已经不是简单坐标偏移能搞定的了。对于个人学习场景我的建议是尽量避免对抗验证码。爬取前先评估这个网站是否值得花这么大力气很多情况下换个数据源、调整请求频率温和一点根本不会触发验证码。5.4 数据混淆与JS加密高阶反爬的常态有些网站返回到浏览器的数据是加密或混淆过的需要在JS环境里解密才能得到最终数据。这时候常规requests已经不够用了通常需要深入分析JS代码在Python里还原解密逻辑或者直接用Playwright在浏览器环境里等解密完成再读取结果。这类反爬的专业性很强通常只在特定网站出现。给一个建议初学阶段遇到这种网站先跳过别死磕先建立整体爬虫能力等基础扎实了再研究JS逆向。5.5 识别反爬并调整策略的实战思路实际场景中反爬往往不是单一手段而是层层叠加的。遇到一个网站要从简单到复杂依次排查先用requests 基础Headers请求看看返回什么如果返回403或验证码补充更多请求头字段如果请求能通了降低频率测试是否可以稳定抓取如果页面HTML里没有数据去Network找接口直接调用接口如果接口参数加密评估是否可以绕过或者需要JS破解最后才考虑Playwright模拟浏览器这套流程能帮你避免“一上来就开大Playwright”很多场景其实requests就够用了。6. 一个完整的入门实战抓取公开书单页面并整理成表格光说不练假把式下面用一个完整示例把前面讲的请求、解析、存储串起来。假设目标是抓取一个公开图书排行榜页面的书名、作者、价格和评分。6.1 目标分析与请求构造打开页面后先按F12看Network确认数据是不是直接在HTML里。如果在直接用requests请求import requests from lxml import html import pandas as pd import time import random BASE_URL https://example.com/books?sortratingpage{} 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,en;q0.8, } all_books [] for page in range(1, 6): response requests.get(BASE_URL.format(page), headersHEADERS, timeout10) if response.status_code ! 200: print(f第{page}页请求失败状态码{response.status_code}) break tree html.fromstring(response.content) # 假设每个图书条目长这样 # div classbook-item # h3 classtitlexxx/h3 # span classauthoryyy/span # span classprice¥59.00/span # span classrating8.9/span # /div items tree.xpath(//div[classbook-item]) for item in items: title item.xpath(.//h3[classtitle]/text())[0].strip() author item.xpath(.//span[classauthor]/text())[0].strip() price item.xpath(.//span[classprice]/text())[0].replace(¥, ) rating item.xpath(.//span[classrating]/text())[0] all_books.append({ title: title, author: author, price: float(price), rating: float(rating), }) print(f第{page}页完成累计 {len(all_books)} 条) time.sleep(random.uniform(1, 2))6.2 数据导出与适配Excel数据全部抓取后用pandas清理一下再输出df pd.DataFrame(all_books) df df.sort_values(rating, ascendingFalse) df.to_csv(books_ranking.csv, indexFalse, encodingutf-8-sig) print(df.head(10))这样一个入门级爬虫就完整闭环了发送请求 → 解析HTML → 提取字段 → 清洗 → 存储。整个过程大约100行代码却覆盖了爬虫最核心的流程。6.3 反爬温和策略的补充如果这个页面有基础的反爬每次请求间隔再拉大一点并且把上面的示例封装成函数随机化Headers等。实际工程实践中爬虫代码本身只占三成工作量其余七成都在应对反爬、异常处理和数据质量保证上。7. 常见问题与排查技巧实录写爬虫过程中最痛苦的往往不是不会写而是“明明看着没错怎么就报错了”。这里把我常遇到的几个高频问题整理一下。7.1 请求超时或连接被重置现象requests抛Timeout异常或ConnectionError。 排查步骤先确认本地网络正常浏览器能正常访问目标网站检查是否加了timeout参数没有加先加上用curl命令在命令行请求一次对比结果如果是连接被重置大概率是TLS指纹被识别试用curl_cffi7.2 状态码403但浏览器可以访问这是最常见的反爬拦截。逐个排查User-Agent是否正确设置Headers是否完整Referer、Accept、Accept-Language是否缺Cookie —— 有些网站需要先访问首页种Cookie才能访问内页请求频率是否过快先降低频率再试是否触发了WAF等待一段时间后再试7.3 响应内容被gzip压缩后乱码requests库会自动解压gzip和deflate压缩但有时网站返回的Content-Encoding不是标准值或者返回了没有头部信息的压缩内容就会出现乱码。解决办法先尝试resp.content而不是resp.text手动加headers里Accept-Encoding: identity表示不接收压缩内容但部分服务器的某些接口不支持也需要灵活处理# 先输出前500个字符来判断是否乱码 print(response.text[:500]) # 如果乱码试试用content解码 try: text response.content.decode(utf-8) except UnicodeDecodeError: text response.content.decode(gbk, errorsignore)7.4 元素用XPath解析不到用浏览器开发者工具复制XPath在代码里运行却取不到值。原因可能是页面的HTML与运行时HTML不完全一样复制的是浏览器渲染后的DOM而requests拿到的是原始HTML如果数据是JS动态加载的原始HTML里就没有这个元素网页有iframe嵌套目标内容在子frame里XPath写的绝对路径太脆弱稍有变化就失效建议用相对路径特征属性来定位比如//div[contains(class, item)]而不是/html/body/div[3]/div[2]/div[1]。7.5 数据抓取中Excel打开CSV乱码这是编码问题。写入时用encodingutf-8-sig而不是utf-8。如果已经生成文件了可以用记事本打开后另存为带BOM的UTF-8格式。7.6 端口被占用或内存溢出爬虫长期运行可能会遇到这个问。如果是多线程并发需要控制线程数量加队列管理如果是有人量数据被读取进内存建议边爬边存不要攒在内存里最后一次性写入。排错建议把爬虫每一步的输出都print出来。requests之后print状态码解析之后print前10条数据存储之后print文件路径。只用这一个习惯就能解决80%的调试困惑。8. 几个写爬虫的高质量习惯技术之外我更想分享几个长期写爬虫形成的工作习惯。这些习惯不一定体现在代码上却决定了爬虫能不能长期稳定运行。8.1 先看robots.txt再决定怎么爬规范做法是先查看目标网站的robots.txt比如 https://example.com/robots.txt 里面会写明允许和禁止抓取的路径。这不是法律强制但体现了对网站所有者的尊重也能帮你避开一些敏感数据区域。8.2 尽量在低峰时段运行抓取行为本身会给服务器带来负载。建议把耗时任务安排在凌晨2点到6点之间配合更长的请求间隔既不影响目标网站正常用户体验也能降低被限制的概率。8.3 做好日志记录别抓完就完事了爬虫一旦跑起来就不太可能完全不出错。建议启动任务时把每次请求的URL、状态码、异常信息、抓取条数都记录到日志文件里。排查问题时一搜日志就有线索而不是靠回忆。import logging logging.basicConfig( filenamecrawler.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) logging.info(page 1 fetched, got 20 books) logging.error(page 2 failed, status: 403)8.4 数据校验要前置抓取完不是立刻入库。先做一轮完整性校验比如关键字段是否为空、数量是否在预期范围、价格和评分的类型是否正确。校验规则写得越细后面清洗数据时越省事。用爬虫去拿数据就好像跟网站做一场谈判。你了解对方的规则越多拿到的数据就越多被封的风险也越低。先把基础流程搞稳再慢慢接触更多的反爬类型这个过程本身就是学爬虫最有价值的部分。