ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python爬虫入门到实战:网页数据采集与反爬应对完整指南

2026/9/8 1:39:22 拓冰建站 浏览量
Python爬虫入门到实战:网页数据采集与反爬应对完整指南 简介面向希望快速上手 Scrapy 框架的 Python 爬虫学习者这份资源以 BBS 论坛作为实战目标演示了从请求发起、页面解析到结构化数据提取与存储的完整流程并将爬虫逻辑、数据字段定义、处理管道与项目配置分层组织在一个小型工程中便于初学者理解框架各组件之间的协作关系。压缩包共 14 个文件其中 6 个 py 源码文件是核心涵盖蜘蛛程序、数据模型、管道处理与设置模块6 个 pyc 为对应编译产物可用于运行表现对照另有 1 份 docx 说明文档和 1 个 cfg 配置文件帮助解决环境配置与启动问题。资源包整体仅 18KB轻量易读解压后即可查看代码结构学习负担小。目前已有 5615 人学习下载适合爬虫入门者边读源码边动手实践也适合作为课程设计、毕业设计或小型论坛数据采集项目的参考模板。 大概每个和数据打交道的人都经历过这种时刻需要在几十上百个网页里找同一类信息手动打开、复制、粘贴重复操作一下午到头来还容易抄错行。我第一次认真决定写爬虫就是因为要整理一批公开的新闻列表和对应的正文链接页面数量不算多但手工整理实在太折磨人。后来花了小半个晚上把流程做成了脚本十分钟跑完所有页面那种成就感是挺直接的。爬虫抓取网页数据本质上就是用程序模拟浏览器去访问网页、拿到HTML或接口返回的数据再按需求抽取成结构化信息。它不神秘也不该被神化。它解决的是批量的、重复的、规则明确的网页数据采集问题。这篇文章适合想把手动复制粘贴时间省下来的运营、数据分析、后端开发以及所有想系统入门爬虫的人。我会从边界意识、环境选型、完整示例、反爬认知到工程化稳定运行把一条能直接落地的路径讲透。1. 动手之前先确定“能爬”和“该爬”的边界1.1 爬虫只是自动化的浏览器不是绕过规则的黑魔法很多人一听“爬虫”下意识觉得它是某种灰色工具实际上它的技术内核非常朴素客户端发一个HTTP请求服务器返回响应内容程序再对内容做解析。这个过程和你在浏览器里打开一个网页没有本质区别区别仅在于浏览器还会渲染CSS、执行JS而爬虫脚本通常只关心最终的HTML或JSON数据。既然本质是“自动化的浏览器”那么评价一次爬取行为是否合适也完全可以套用现实生活的常识。你进一家实体店随手翻阅货架上的公开宣传册这很正常但如果你蹲在门口把进出的人挨个登记或者趁店员不注意翻进仓库拍照那就是另一回事了。爬虫的边界类似动手之前先问自己三个问题数据是不是公开可见的请求频率会不会给服务器造成压力采集后的用途是否正当三条都过了再写代码也完全不迟。我个人建议入门阶段只抓“完全公开、无需登录、服务端不排斥程序访问”的页面。这类页面足够你练熟整套流程也不会给自己埋雷。等能力上来之后如果需要采集登录后才能看到的数据优先确认是否有官方API或授权渠道而不是一上来就想怎么绕过限制。1.2 Robots协议与访问节奏先看规则再动手判定一个站点是否欢迎自动访问最直接的依据是/robots.txt。在站点域名后拼上这个路径比如https://example.com/robots.txt就能看到类似User-agent: *、Disallow: /admin/这样的说明。它在一定程度上代表了站点对自动访问的态度。遵守它不是为了应付谁而是为后续稳定抓取创造条件——一个明确声明禁止抓取的路径服务器大概率也有对应的访问控制。另一个新手最容易忽略的点是访问节奏。服务器最怕的往往不是某个路径被爬了而是一瞬间涌来大量高频请求把带宽和数据库拖垮。我自己踩过一次坑刚开始学爬虫时写了个循环去抓某个小型公开站点的列表页忘记加任何间隔结果跑到第100多个请求时页面开始返回503。后来我把请求间隔拉长到1秒左右任务就再没出过问题。一个可靠的参考值动态列表页的请求间隔设在0.5到1秒批量任务放在2到3秒如果目标站点体量小、响应慢间隔还要更保守。学会在代码里用time.sleep()控制节奏这比任何UA伪装都管用。2. 轻量环境与工具链Requests加两个解析库就够了2.1 为什么入门选Requests而不是Scrapy爬虫的生态其实很丰富有Scrapy这种重型框架也有Playwright这类浏览器自动化工具。但对新手来说我最推荐从requestslxml起步。原因很简单Scrapy虽然功能强大但它的调度器、中间件、管道、Item等概念对初学者是一大堆黑箱调试链路长出了问题很难定位到底卡在哪一层。而Requests只有几十行代码就能跑通一次完整的请求所见即所得报错也直观适合建立正确的心智模型。当你用轻量方案跑通几个项目再回头看Scrapy才能理解它到底解决了哪些重复劳动。到那时你会发现分布式调度、自动重试、限速器其实都是你手写过的逻辑只是框架帮你标准化了。所以入门阶段的重点不是“用最强的工具”而是“用最简单的工具把原理吃透”。执行环境上一个纯净的Python 3.9以上版本即可依赖安装就两条命令pip install requests lxml pandaspandas不是必装项但后面做数据清洗会用到建议一次装好。解析库我通常只用lxmlXPath和CSS选择器都能支持速度也足够快。2.2 用浏览器开发者工具找到真正返回数据的请求写爬虫最耗时的一步往往不是写代码而是定位“数据到底在哪个请求里”。很多人会复制浏览器地址栏的URL但实际请求发出后页面上的内容可能是由好几个接口拼接出来的。正确的做法是打开开发者工具F12切到Network标签刷新页面然后逐个看请求的Response内容。具体来看列出的请求类型里要重点找两种一种是文档类型的请求也就是最原始的HTML页面另一种是Fetch/XHR类型的接口通常返回JSON或局部HTML。在浏览器里按CtrlF搜索你要抓取的关键字比如新闻标题、商品价格看哪个请求的响应里包含这个关键字那个就是目标。这时候右键复制它的Request URL和需要的请求头存下来备用。尤其要注意请求头里的User-Agent和Referer后面模拟请求时大概率要用到。很多新手在这一步就迷路了看到Network列表里几十个请求不知道选哪个。记住一个原则——优先找返回内容和你肉眼看到的页面数据一致的那个请求别管名字叫什么。3. 跑通第一个完整任务请求、解析、落盘的全链路3.1 请求阶段带超时、带状态检查的GET假设目标是一个公开的资讯列表页页面上有新闻标题、链接和发布时间。整个爬虫可以拆成三段先请求拿HTML再解析抽字段最后保存结果。请求阶段是最容易翻车的环节。网上很多老教程只写一行requests.get(url)然后直接resp.text这在网络不稳定的环境下会频繁报错。一个更健壮的写法是这样import requests url https://example.com/news/list headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 状态码非200时主动抛异常 print(resp.text[:500])这里的timeout10必须加否则遇到无响应的连接脚本可能一直挂在那里。raise_for_status()会在状态码为4xx或5xx时直接抛异常方便你尽早发现异常页面而不是把一个错误页面当成正常内容去解析。3.2 解析阶段XPath比正则更适合处理HTML拿到HTML之后很多新手第一反应是用正则去匹配内容。说实话正规的HTML结构解析尽量不要用正则因为标签嵌套、属性顺序变化很容易让正则在不知不觉中写出一堆脆弱的匹配规则。我更推荐用XPath它像一门“专门查HTML的语言”在lxml里可以快速、稳定地定位节点。from lxml import etree tree etree.HTML(resp.text) items tree.xpath(//div[contains(class, news-item)]) for item in items: title_node item.xpath(.//h2/text()) link_node item.xpath(.//a/href) date_node item.xpath(.//span[classdate]/text()) title title_node[0].strip() if title_node else link link_node[0] if link_node else date date_node[0].strip() if date_node else print(title, link, date)这里有几个细节要留意。XPath的text()返回的是一个列表哪怕只有一个匹配也要用下标取.//开头的写法表示从当前节点往下查找不要漏掉点号。调试XPath最方便的方式是直接在浏览器Console里用$x(//div[contains(class, news-item)])确认能选中节点后再写进脚本能省下不少来回试错的时间。3.3 落盘阶段CSV、SQLite还是JSON解析得到的数据一定要落盘才算真正“抓取成功”。三种常见存储方式各有适用场景我按需求判据给出一张对照表存储方式适用场景优点缺点CSV数据量小、后续用Excel或pandas分析直观、通用字段嵌套复杂时不方便JSON数据结构化程度高、需要对接程序层级清晰、可嵌套人眼查看不够友好SQLite数据量大、需要频繁查询和去重支持SQL、单文件易管理需要多学一点SQL语法入门阶段我建议优先用CSV加pandas导出尤其注意编码参数import pandas as pd records [] # 假设已在循环中取出多个 title, link, date records.append({title: title, link: link, date: date}) df pd.DataFrame(records) df.to_csv(output.csv, indexFalse, encodingutf-8-sig)有人会问为什么用utf-8-sig。直接写UTF-8会在Excel里打开时出现中文乱码utf-8-sig会在文件头写入BOM标记Excel和其他常用表格软件都能正确识别。这个细节是我第一次给同事交数据时踩坑踩出来的当时他打开CSV满屏乱码我十分尴尬。4. 请求头、反爬与动态页面为什么脚本拿不到肉眼可见的数据4.1 三个最常见的异常响应403、418与重定向同样一个页面浏览器能打开脚本却频繁遇到403、418这是新人必问的问题。403表示服务器拒绝请求通常意味着服务端识别出你不是一个正常浏览器418则更直白它是Im a teapot很多站点拿它作为反爬拦截的占位状态码看到它基本可以确认你的请求特征已经被风控盯上了。还有一个容易遇到的是重定向循环脚本在几个URL之间跳来跳去最终返回一个200但内容和你想要的完全无关。我整理了一份解决对应关系的表格状态码常见原因首选处理方式403User-Agent缺失或请求特征明显补充常用浏览器请求头418请求频率过高被风控拦截立刻停止任务拉长请求间隔301/302循环登录态缺失或协议跳转异常检查是否启用了不合适的重定向需要提醒的是看到418不要去硬刚。很多新手的第一反应是“伪装得更像一点”但服务端做风控更多看的是行为特征和统计规律。最有效的做法是停一会儿、降低频率、把代码从“暴力抓取”改成“礼貌访问”基本都能恢复正常。4.2 把请求头写得像真实用户但别陷入“完美伪装”误区模拟真实用户的请求头通常需要关注几个字段User-Agent、Accept、Accept-Language、Referer。其中User-Agent最重要它直接告诉服务器“我是哪种浏览器”很多站点只做这一项检查。其余字段可以按浏览器的实际请求头原样复制。但有几句话我必须说不要追求“完全还原浏览器的所有header指纹”。服务端通常看的是分布规律而不是某个header字段是否带全。比如你用一个静态UA配合固定间隔做低频抓取可能比每次随机换UA却以每秒10个请求高频访问更安全。把心力和时间花在控制访问频率、遵守robots协议、处理好数据质量上性价比要高得多。另外强调一点不要伪造过度的浏览器环境。有的教程让你连Sec-Ch-UA、Accept-Encoding也一并带上如果脚本自身压缩解码没做对反而会引入更多问题。用最朴素的方式正确表达“我是一个真实用户”就够了而不是扮演一个“各方面完美但我自己都控制不住的浏览器”。4.3 动态渲染页面数据根本不在HTML里早期网页的数据基本都在HTML源码里爬虫抓下来直接用XPath解析就行。但现在的页面越来越多地采用前后端分离架构HTML骨架里几乎没有真实数据内容是在浏览器里执行JavaScript后异步渲染出来的。这种页面有一个很明显的判断方法在浏览器里右键查看网页源代码搜一下你肉眼看到的某个关键字如果源代码里搜不到基本可以断定数据是异步加载的。遇到这种情况入门阶段最推荐的方式不是立刻上浏览器自动化而是切到开发者工具的Network-Fetch/XHR面板找到真正返回数据的那个JSON接口直接请求这个接口。接口通常返回结构清晰的JSON解析甚至比HTML还要简单。浏览器自动化工具如Playwright是后续可以学的方向但引入它意味着更大的开销和更复杂的等待策略对新手并不友好。判断接口时也有一个技巧把Network面板的请求按返回体大小排序往往那个返回体特别大的就是承载核心数据的接口。5. 从能跑到稳定跑异常重试、限速去重与踩坑修复5.1 异常捕获与退避重试别做永动机写爬虫的初期脚本跑一半挂掉是家常便饭。要么网络超时要么服务器返回5xx要么页面结构变化导致解析空指针。不做重试数据就会漏做重试太频繁又会加重服务端负担。一个比较稳妥的模式是“有限次数退避重试”每次失败后等待时间递增超过最大次数才放弃。import time import requests def fetch_with_retry(url, headers, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp except (requests.RequestException, ValueError) as exc: print(f第 {attempt 1} 次请求失败: {exc}) if attempt max_retries - 1: time.sleep(2 * (attempt 1)) # 2秒、4秒、6秒递增 return None注意这里没有写成无限重试。无限重试最可怕的后果是对端服务已经恢复不过来了你的脚本还在每隔1秒打一次最终拖出更大规模的封禁。实际项目中重试两次失败就应进入告警流程让人来处理而不是让机器硬顶。5.2 限速、URL去重与日志稳定运行的三个底座要让爬虫能够无人值守地跑上一整晚有三件事必须在动手前规划好。限速是最基本的也是很多人忽略的。不管你的目标任务量有多大都不应该一次性把所有请求打出去。用time.sleep(random.uniform(0.5, 1.5))这样的小随机间隔比固定间隔更贴近真实用户的行为模式对服务端也更友好。URL去重解决的是“重复抓取”的问题。实际上分页列表经常会出现同一个链接出现在多个页面或者下一页的入口跳回上一页的情况。一个简单的办法是维护一个已抓取URL的集合请求前先做判断already_seen set() for url in urls: if url in already_seen: continue resp fetch_with_retry(url, headers) if resp: already_seen.add(url) # 做解析和保存 time.sleep(random.uniform(0.5, 1.5))日志则是排查问题的第一手资料。用Python内置的logging模块把每次请求的URL、状态码、耗时和异常记录到文件里一旦任务中断你能立刻知道它停在哪一步、遇到了什么错误。不要只靠print()print在终端上一刷屏历史信息就找不回来了。5.3 三个反复踩的坑编码乱码、相对路径与死循环第一个坑是编码问题。有的网站响应里没有明确charsetrequests默认猜测可能出错导致抓下来的文本乱码。常规解法是先看响应头里的字符集拿不准时用resp.apparent_encoding让程序自动推断然后显式设置resp.encoding resp.apparent_encoding第二个坑是翻页时的相对路径。列表页里的下一页链接经常是./news?page2这类相对地址如果你只拿到一个相对路径直接请求就会404。正确的做法是用urllib.parse.urljoin把相对路径和当前页面地址拼接成完整URL这一步极其容易漏漏掉的后果就是爬完第一页之后全部分页失效。第三个坑是翻页终止条件。有些站点的“下一页”按钮在最后一页依然存在只是指向当前页或空页面如果不做判断脚本会在同一页上反复抓取形成一个死循环。稳妥的做法是在循环体里限制最大翻页数同时检查新页面提取到的列表项数量如果下一页的内容和上一页完全一致就主动break。在我实际跑过的任务里这三个坑出现频率极高解决难度都不大但每个都足以让一个新手折腾半小时以上。重点是遇到问题先想“是不是结构变了”“是不是缺了拼接”而不是急着改解析规则。最后分享一个小习惯每次写完爬虫先不要让它跑全量抽一个最小范围的目标跑一轮确认字段完整、编码正常、耗时合理再放开批量运行。这轮验证能帮你拦截掉绝大多数半夜告警。爬虫真正的难点从来不在于发请求而在于把边界、频率、数据质量三个基础项做扎实。本文还有配套的精品资源点击获取