
学爬虫的人第一个念头大多是“把网页上的数据弄下来”。这个念头本身没有错但真正动手之后你会发现爬虫的门槛从来不在“怎么把页面抓下来”而在“抓下来之后怎么处理”以及“哪些能抓、哪些不能抓”。这一节我打算用最基础的方式带你把一条合规抓取、解析、存储的链路完整走一遍。先说明白这里说的“爬虫入门”不是教你写一个能横扫全站的采集工具也不是把某个平台的数据搬空。那是非常危险的做法技术上也许不难但法律和道德上会把你拖进深坑。我们这一节的目标很朴素——学会用 Python 里的 requests 和 BeautifulSoup 这两个基础库从公开网页中提取信息把它整理成结构化的数据存下来同时搞清楚整个过程中哪些红线不能碰。这套内容适合谁一句话刚接触 Python、想把“网页上的文字表格图片”变成“自己能处理的干净数据”的人。如果你已经有 Python 基础但对网络请求、HTML 结构、XPath 这些东西还是似懂非懂那这一节的内容同样能帮你把地基打牢。我们用到的工具非常轻量不需要重型框架打开一个编辑器就能跑。1. 理解爬虫的本质它就是在模拟一个人浏览网页1.1 爬虫的三个基础组件抓取、解析、存储如果把爬虫拆开来看其实它只干了三件事。第一是“请求”——把目标网页的地址发给服务器服务器把 HTML 内容返回给你第二是“解析”——从这一大段 HTML 标签里把你想要的那部分文字、链接、属性等等信息摘出来第三是“存储”——把摘出来的数据按照统一的格式写入本地文件或数据库。这三步听起来简单但每一环都有大量细节。比如“请求”环节你不仅要发请求还要考虑带什么样的请求头、服务器返回了什么样的状态码、内容用了什么编码再比如“解析”环节HTML 本身就是一堆嵌套的标签你得决定是用正则硬抠、用 BeautifulSoup 按标签找、还是用 XPath 按路径取到了“存储”环节数据清洗会让很多新手崩溃明明页面上显示“¥ 199.00”抓下来却变成了“¥\u3000199.00”这中间全是编码和清洗的坑。我们这一节用一句话总结爬虫的运行逻辑模拟浏览器的行为获取服务器返回的内容从非结构化数据中提取结构化信息。一旦你把这个逻辑吃透了后面学 Scrapy、学异步爬虫、学分布式爬虫都只是在“怎么更快更稳地完成这三步”上做优化本质不会变。注意这里说的“模拟浏览器行为”指的是技术层面的模拟而不是让你伪装成真人去突破对方设置的技术屏障。两者有本质区别后面我会单独讲。1.2 合规不是口号而是要落地的三条线很多教程会把“合规”当成一句话带过但实际上合规是爬虫这个行当里的生命线。我把它拆成三条具体可执行的原则你写代码的时候随时对照。第一条线叫“看规则”。绝大多数正规网站都会在域名根路径下放一个 robots.txt 文件用标准格式写明哪些路径允许爬虫访问、哪些不允许。比如你访问/robots.txt会看到类似Disallow: /admin/这样的规则。动工之前先读这个文件这是最基本的尊重。有些网站还会在服务条款中明确写明禁止爬虫采集那这种站点就坚决不碰。第二条线叫“看身份”。爬虫请求时应该带上明确的 User-Agent 标识自己是爬虫而不是伪装成一个完全不存在的浏览器版本去绕过检测。这与反爬对抗完全是两码事——合规爬虫主动亮明身份频率控制在合理范围服务器允许就继续不允许就停下来。第三条线叫“看频率”。哪怕一个网站允许爬虫访问也不代表你可以每秒发几十个请求。服务器的带宽和数据库资源都是有限的你一个人的脚本可能把整个站点拖垮。新手最容易犯的错就是写了个死循环忘了加延时结果几分钟内把目标站点打挂。合规爬虫的标准是用尽可能低的频率获取足够的数据同时主动设置请求间隔。这三条线下你再去看网上那些“反爬虫绕过技巧”就会发现大部分内容根本不该出现在合规爬虫的语境里。遇到封锁就退让、降速、换公开接口或者直接放弃这个数据源这才是正路。1.3 工具选型Requests BeautifulSoup 还是直接上 Scrapy入门阶段我的建议非常明确先用requests BeautifulSoup手动写不要一上来就上 Scrapy。原因有两点。第一Scrapy 虽然是业界主流的爬虫框架性能好、功能全但它的学习曲线会分散你的注意力。你要同时理解引擎、下载器、中间件、Item Pipeline 一大堆概念很容易陷入“会用框架但不知道底层在干嘛”的状态。而 requests 和 BeautifulSoup 组合简单直接每一个环节都能看得见摸得着。第二手工写一遍请求、解析、存储的流程之后你才能真正理解框架帮你做了什么。比如 Scrapy 里一个不起眼的AutoThrottle设置背后其实是对请求频率的自动控制你要是不知道手动控制请求间隔有多麻烦就不会理解这个功能的价值。这是一个非常经典的对比方案requests BeautifulSoupScrapy上手难度低适合入门中高需要理解框架机制请求控制完全手动直观内置并发与限速解析方式BeautifulSoup 灵活但稍慢Selector 基于 lxml性能高适合场景小规模、学习、快速原型大规模采集、分布式部署调试便捷度高打印即所见相对复杂2. 核心细节拆解从 GET 请求到数据定位2.1 前置动作先判断目标值不值得抓选什么样的网站练手是新手面临的第一道选择题。我的建议是选一个静态页面、结构稳定、没有登录墙的公开信息站。比如图书榜单、菜谱、名词解释、开源文档这类站点就非常合适。一定不要选什么第一是电商平台它们的数据是商业核心资产反爬措施极其严密你一开始碰大概率会被封 IP第二是需要登录才能看到内容的站点因为那部分内容属于用户私有数据采集有法律风险第三是含有个人信息的数据比如手机号、身份证号、住址哪怕页面公开也不该抓。实操中你还要注意站点是否经常改版。有些个人博客非常典型今天用这个主题明天换那个模板一周后 HTML 结构全变了你的解析代码就得跟着重写。所以说第一版的爬虫代码最好只针对单个页面写等流程跑通了再考虑扩展多页面。别一上来就想搞一个大而全的采集系统那不现实也不必要。2.2 请求头里的门道为什么 User-Agent 是关键参数用 requests 发请求时最常被忽略的就是请求头。很多新手直接写requests.get(url)把请求头发出去然后被服务器返回 403 拒绝却不知道问题出在哪。服务器是怎么识别爬虫的第一眼看的就是 User-Agent。浏览器发请求时User-Agent 会带上一长串版本信息比如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36。而 requests 库默认的 User-Agent 是python-requests/2.31.0服务器一眼就能看出来这不是浏览器直接拒绝。所以爬虫的第一步就是给 requests 加上一个常见的浏览器 User-Agent。这段代码非常重要import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } response requests.get(https://example.com, headersheaders, timeout10)注意这里有个timeout参数我建议你每次请求都带上它。不设置超时意味着如果目标服务器一直不响应你的脚本会一直挂在那里像一块石头一样卡死不仅浪费资源还影响调试效率。设置 10 秒超时请求超过 10 秒就抛异常程序能继续往下走。2.3 响应里的编码问题一个让无数人抓狂的细节请求发出去之后服务器返回的response.text就是网页正文。但这里有个隐藏的天坑编码。HTTP 响应头里的charset参数并不总是可靠的。有些旧站点根本不在响应头里声明编码只会在 HTML 的meta标签里写meta charsetutf-8而 requests 库默认会用ISO-8859-1去解码结果中文全变成乱码。解决办法是主动检查并设置正确的编码import requests response requests.get(https://example.com, timeout10) # 优先从响应头获取编码没有就用 apparent_encoding 猜测 if response.encoding is None or response.encoding.lower() iso-8859-1: response.encoding response.apparent_encoding text response.textapparent_encoding是 requests 基于内容字节统计推断出来的编码对中文页面通常很准确。但要注意这个推断是基于整个文本内容的如果页面内容很短推断结果可能不准。另一个思路是直接从response.content字节数据中按指定编码解码比如response.content.decode(utf-8)你自己控制编码过程。2.4 解析方式对比正则、BeautifulSoup、XPath 怎么选拿到 HTML 之后问题就变成了“怎么从一堆标签里把数据抠出来”。我见过三种主流方案每种都有各自的应用场景。正则表达式是最原生的方式直接对字符串做模式匹配。它的优点是灵活、不需要额外库缺点是写起来容易出错尤其是 HTML 这种带嵌套结构的文本正则很难处理复杂嵌套关系。我建议新手不要用正则解析 HTML容易把自己绕晕。BeautifulSoup 是最适合入门的方式。它会把 HTML 解析成一棵树然后你可以通过find()、find_all()这样的方法按标签名、属性、class 名称来查找元素。它最大的优点是容错性强即使 HTML 写得不规范也能尽量解析正确。缺点是纯 Python 实现性能上比 lxml 慢不少。XPath 是相对进阶一点的方式它把 HTML 看成 XML 树通过路径表达式来定位节点。比如//div[classbook]//h2/a/text()这样的写法可以直接提取出 class 为 book 的 div 下所有 h2 里的 a 文本。XPath 表达式简洁、功能强大配合 lxml 解析器速度很快很多实际项目都用这种方式。新手理解 XPath 需要一点时间但一旦掌握效率提升非常明显。我个人的建议是入门用 BeautifulSoup 建立直觉熟悉之后切换到 lxml XPath。两者在 requests 生态里都能很好地工作你大可不必纠结使用哪个。2.5 数据清洗与存储抓到之后别急着高兴解析出来的数据往往是脏的。比如文本前后有换行符和空格、价格数字里混入了货币符号、时间格式五花八门。这些都需要在存储之前清洗掉。清洗的核心方法就三板斧strip()去掉首尾空白、replace()替换不需要的字符、正则提取想要的模式。等你处理的数据类型多了你会发现清洗浪费的时间往往比抓取和解析加起来还多。存储方面入门阶段只需掌握两种。CSV 是通用性最强的格式Excel 和 Pandas 都能直接打开注意写入时要加上encodingutf-8-sig否则 Excel 里中文会乱码SQLite 是单文件数据库适合数据量稍大、需要查询的场景Python 内置 sqlite3 模块不需要额外安装。3. 实操环节写一个真正能跑的合规爬虫3.1 目标选择用公开测试站练手这里我挑一个非常适合入门的公开目标quotes.toscrape.com一个专门为爬虫学习准备的公共测试站点。它没有登录墙没有复杂的验证码内容就是一些名人名言和作者信息。最关键是它欢迎爬虫访问我们在这个站点上练习既安全又能完整覆盖所有核心知识点。动工之前按惯例先看一下它的 robots.txt内容很简单基本没什么限制。然后随便打开一个页面观察它的 HTML 结构。你会发现每条名言都被包含在一个div标签里class 叫quote里面又有一个span classtext存放名言正文一个span classauthor存放作者名字还有一个a标签指向作者详情页。3.2 三步走请求、解析、打印结果第一步写请求逻辑。获取首页 HTML设置好 User-Agent 和超时检查状态码是否为 200。这个步骤上面已经铺垫过直接组合起来即可。第二步写解析逻辑。用 BeautifulSoup 把 HTML 转成对象然后找到所有div下的quote块from bs4 import BeautifulSoup soup BeautifulSoup(response.text, html.parser) quotes soup.find_all(div, class_quote) for quote in quotes: text quote.find(span, class_text).text author quote.find(span, class_author).text # tags 是链接列表先取文本即可 tags [tag.text for tag in quote.find_all(a, class_tag)] print(text, author, tags)看到结果打印出来的那一刻你会觉得爬虫原来并不神秘。但要注意一个小细节find_all(div, class_quote)中class_后面有个下划线因为class是 Python 的关键字这个写法是 BeautifulSoup 固定的 API不能省略。第三步暂时用打印代替存储先确认解析逻辑没问题。这一步非常关键做全量爬取之前先小范围验证能避免你把错误数据写进文件。3.3 分页逻辑与频率控制优雅地抓取多页数据首页只有 10 条名言要抓更多就得处理翻页。观察测试站的 URL 规律首页是/page/1/第二页是/page/2/依次类推。有些站点翻页是通过 URL 参数实现的比如?page2原理都一样——确认规律后用一个循环生成不同的 URL 即可。分页爬取的完整流程大概是这样的import time base_url https://quotes.toscrape.com/page/{}/ all_quotes [] for page_num in range(1, 5): url base_url.format(page_num) resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f第 {page_num} 页请求失败跳过) continue soup BeautifulSoup(resp.text, html.parser) for quote in soup.find_all(div, class_quote): text quote.find(span, class_text).text author quote.find(span, class_author).text all_quotes.append({text: text, author: author}) print(f第 {page_num} 页抓取完成累计 {len(all_quotes)} 条) time.sleep(2) # 每页之间休息 2 秒这里有两个值得留意的地方。第一是状态码判断请求失败时不中断程序打印一句提示然后继续下一轮。第二是time.sleep(2)我在每页请求之间强制休息两秒——不要小看这个设置它能让你的爬虫在目标服务器眼里显得“礼貌”得多也能避免因为请求过快被封。在真实项目中这个间隔时间要根据目标网站的响应速度和规模动态调整一般建议 1 到 5 秒之间。3.4 存储到文件把中间结果变成最终成果看到解析结果正常之后再把它写入文件。Python 自带的 csv 模块足够用import csv with open(quotes.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[text, author, tags]) writer.writeheader() writer.writerows(all_quotes)这里用encodingutf-8-sig是为了兼容 Excel如果你用记事本打开文件出现中文乱码多半就是这里漏掉了。newline则是为了避免 csv 模块在 Windows 平台下写出多余空行的问题。到这一步你已经完成了一个完整的合规爬虫发送请求、解析 HTML、翻页、频率控制、存储文件。麻雀虽小五脏俱全。4. 常见问题与排查技巧实录4.1 请求被拒状态码 403这是新手遇到最多的报错。看到 403第一反应别想着“怎么绕过去”而是先自查三件事User-Agent 设置了吗请求频率是不是太高了目标站点是不是明确禁止爬虫如果 User-Agent 没问题试着把请求间隔拉长到 5 秒以上再跑一次。有时候服务器只是临时限制休息几分钟就恢复了。如果是目标站点明确禁止那就是你自己选错了目标换数据源才是正解。4.2 解析结果为空明明页面上有数据这种情况 80% 是因为页面里的数据是用 JavaScript 动态加载的。你用 requests 拿到的只是服务器返回的初始 HTML数据还没渲染上去。判断方法很简单用浏览器右键查看网页源代码搜索你要的数据如果源码里没有那就是动态渲染的。动态页面的合规解决方案不是硬写一个无头浏览器去模拟点击而是先找找页面有没有公开的 API 接口。很多网站虽然页面是动态的但数据都是通过一个 JSON 接口返回的直接用 requests 请求那个接口解析 JSON 反而比解析 HTML 更简单。这也是我在实际项目中很常用的思路。4.3 中文乱码问题乱码的原因基本都集中在编码环节。按这个顺序排查先看response.encoding是什么如果是ISO-8859-1这类非中文编码就手动改成utf-8或使用apparent_encoding如果response.text正常但写入文件后乱码检查 CSV 写入时的编码是不是用了utf-8-sig。4.4 数据里有大量空白和杂项处理这种问题不要用正则一刀切要分析来源。如果是标签内部文本自带的前后空白strip()就能解决如果是 HTML 里的换行符和缩进先把文本按行切割去掉空行再拼接如果是像 “\u3000” 这样的全角空格直接用replace(\u3000, )清除。4.5 爬虫被限流页面突然打不开这几乎是每个爬虫学习者的必经之路。第一次遇到时很容易慌但请记住你的需求永远没有别人的服务器稳定重要。正确的做法是立即停止脚本等待几分钟检查自己的请求频率向目标网站表达友好。如果你在限制解除后继续高频访问那就不再是“技术问题”而是“态度问题”了。我把这些典型问题整理成一个速查表方便你以后对照现象可能原因解决方向403 拒绝请求头缺失或频率过高设置 UA降低频率中文乱码解码编码不匹配手动指定 utf-8解析为空动态脚本渲染寻找公开 JSON 接口文本带空白HTML 源码含格式字符strip() replace()请求超时网络波动或服务器慢设置 timeout异常重试数据量过大没有分页控制限制页数调节 sleep我在实际使用中发现很多人在爬虫入门阶段踩的坑不是因为代码逻辑有多复杂而是因为太急于求成跳过了“先看规则、再定方案、小范围验证、最后全量执行”这个过程。其实你只要慢下来把一个页面完整地走通再考虑扩展整个流程会顺畅得多。最后再分享一个小技巧写完爬虫后随手把目标页面的 HTML 保存一份到本地当作调试用的“快照”。后续解析代码改来改去不需要每次重新请求线上服务器直接用本地文件测试就行既快又安全。这个习惯我一直保持到现在非常管用。