资讯详情

资讯详情

零基础学爬虫:Python网页数据自动化采集与反爬实战指南

简介面向Python零基础人群的爬虫技术入门PDF指南覆盖requests、BeautifulSoup、Selenium三大核心工具从静态网页抓取到动态页面渲染破解、常见反爬策略应对循序渐进讲解爬虫架构与实战案例适合刚接触编程的新手以及希望提升数据采集能力的技术爱好者。资源为单个PDF文档大小441KB内容紧凑包含清晰架构对比图、可直接运行的代码示例和分阶段进阶路线便于随时查阅、动手实践。已有307人学习下载。指南系统拆解爬虫五大组件——URL管理器、网页下载器、网页解析器、存储器与调度器的职责与协作方式配合电商比价、舆情监控等典型应用场景让读者快速建立整体认知。实战部分提供三个由易到难案例新闻标题抓取、JavaScript渲染页面的Selenium破解以及代理IP轮换、随机延迟、验证码应对等反爬措施同时明确Robots协议与数据合法使用的边界帮助读者高效采集数据的同时避免触碰法律红线。1. 爬虫不是黑魔法零基础也能做出的网页数据自动化采集零基础学爬虫最大的障碍不是语法而是不知道一个完整流程长什么样。爬虫技术说白了是用代码替你完成「打开网页、提取内容、存成表格」这套重复动作也就是网页数据自动化采集。它解决的是人力整理要一小时、脚本三秒钟的差距比如每天汇总几个公开信息页的最新列表手工复制粘贴又累又容易漏写一个脚本挂着定时跑就能稳定拿到结构化数据。这篇笔记面向完全零基础的读者但不会让你从变量类型开始背。路线是先跑通一个请求再学会从网页里提取字段把结果存下来最后处理反爬和中断。整个流程走完你能独立完成一个可交付的小采集工具。它不适合你去碰需要绕认证、高并发或涉及敏感数据的场景——那不是入门该做的事。2. 先把最小闭环跑通requests 请求与响应处理2.1 环境准备Python 与三件套零基础第一件事不是背语法而是先把「能发出请求」这件事跑通。我一般建议用 Python 3.8 以上的版本自带 pip装依赖只用一行命令python --version pip install requests beautifulsoup4 lxmlrequests 负责网络请求beautifulsoup4 负责解析 HTMLlxml 是底层解析引擎让 BeautifulSoup 解析速度更快。新手别急着上 scrapy它功能全但抽象层级多出错后不好排查。先用手动三件套把每一步看清楚之后再上框架是水到渠成的事。如果之前没装过 Python直接在官网装稳定版安装时勾选 Add to PATH。这一步不做后面python命令会提示找不到。装完依赖有个常见的坑Windows 下如果同时装了多个 Python 版本记得用python -m pip install而不是裸pip install否则包可能装到另一个解释器里导入时报 ModuleNotFoundError。这个小问题能卡住新手半小时先提前绕开。2.2 发送第一个 GET 请求一切爬虫的起点都是模拟浏览器向服务器要页面。requests 把这件事封装成了一个函数调用最小可运行代码是这样的import requests url https://example.com/list # 换成你要抓的页面 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout10) print(resp.status_code) print(resp.encoding) print(resp.text[:500])headers 里的 User-Agent 是身份标识很多站点会直接拒绝不带 UA 的请求后面避坑章还会再展开。timeout10 是超时控制单位秒防止目标服务器无响应时脚本一直卡死。resp.status_code 是状态码200 表示正常403、404、500 各有各的解法别急着换代码先看它。resp.text 是解码后的页面文本resp.content 是原始字节两者在编码错乱时会表现出明显差异这也是新手最早遇到「乱码」的地方。接着加一个判断把「请求成功」和「请求失败」分开处理if resp.status_code 200: html resp.text print(页面长度:, len(html)) else: print(请求失败:, resp.status_code)这个 if 看着简单但能让脚本在遇到异常页时立刻暴露问题而不是把错误页面当正常数据解析最后得到一堆空列表找不出原因。很多翻车现场就是从「不检查状态码直接解析」开始的。2.3 带参数请求与 POST 表单真实抓取场景很少直接访问首页更多是带着查询关键词、页码这类参数。requests 对 GET 参数的写法是把参数放在字典里params {keyword: python, page: 1} resp requests.get(url, paramsparams, headersheaders, timeout10) print(resp.url) # 看看最终请求的 URL 长什么样params 字典会被自动做 URL 编码中文关键词不用手动处理这也是比手动拼字符串更可靠的原因。打印 resp.url 可以看到最终拼出来的地址排查参数有没有生效时这一行很有用。遇到登录后提交表单这类场景需要换成 POSTdata {username: demo, password: demo1234} resp requests.post(url, datadata, headersheaders, timeout10)这里有个选型问题要讲清楚能用 GET 就不上 POST因为 POST 常涉及登录态和校验字段。零基础阶段如果遇到必须登录才能看到数据的页面我的建议是直接跳过换一个公开数据的页面练手。爬虫入门练的是流程不是解锁权限。如果同一个会话里需要连续访问多个页面比如翻页用 Session 对象更稳session requests.Session() session.headers.update(headers) for page in range(1, 6): resp session.get(url, params{page: page}, timeout10) print(page, resp.status_code)Session 会帮你维护 Cookie 和默认请求头翻页时不容易被服务器当作「新访客」反复校验。底层原因无状态请求每次都要重新握手有状态会话能保留服务器下发的身份标记很多站点的翻页逻辑正是依赖这个标记。2.4 响应编码与抓包诊断文本编解码是爬虫新手遇到的第一堵墙。现象是打印出来全是「以」这类符号。原因很简单服务器返回的是字节流requests 根据响应头猜测编码猜错了用错误编码去解码中文自然变成乱码。常见做法是手动指定resp.encoding resp.apparent_encodingapparent_encoding 是 requests 用字符检测库从字节内容里反推出来的编码比响应头里写的可靠。缺点是检测需要额外耗时在批量请求里每个都做会变慢所以只在发现乱码时用。另一个更快的路子是直接用resp.content.decode(utf-8, errorsignore)前提是你确定目标页面是什么编码。最靠谱的诊断方式是用浏览器开发者工具的 Network 面板。刷新页面找到对应请求看 Response Headers 里的 Content-Type它通常标注了 charset...。这比任何猜测都准。当脚本行为和浏览器不一致时对比浏览器请求和脚本请求的差异是最快的定位方式。编码问题的核心是「字节到字符串的转换规则必须和页面实际编码一致」先学会用 resp.encoding 和 resp.apparent_encoding 做诊断不乱码了再往下走。3. 从 HTML 里把数据抠出来三种解析方案怎么选3.1 正则适合小字段别硬刚整页正则是最直接的提取手段适合目标明确、结构简单的字段。比如提取标题、价格、日期这类短文本不需要额外解析整个页面。最小用法是这样import re pattern rh2[^]*classtitle[^]*(.*?)/h2 matches re.findall(pattern, resp.text, re.S) print(matches)这里.*?是非贪婪匹配表示尽量少地匹配字符避免把一个标签里的多个标题吞掉。re.S让点号能匹配换行符因为 HTML 里标签之间经常有换行和缩进。这个 pattern 的写法比较脆弱页面结构一变就可能取不到值所以只适合一眼能看清规律的字段。价格这类数字字段正则反而更稳因为不需要关心周围标签长什么样def extract_price(text): m re.search(r(\d\.?\d*), text.replace(,, )) return float(m.group(1)) if m else None这段先去掉千分位逗号再提取带小数的数字返回浮点数。注意正则的结果永远是字符串要转成 float 才能做数值比较。那么什么时候别用正则页面结构复杂、标签嵌套深、需要提取一对多关系的时候。用正则解析整个 HTML 是典型的误用标签没闭合或层级变化就会静默出错。我的判断标准是取两三个字段正则要从列表里循环抽取结构数据老老实实上解析器。3.2 BeautifulSoup容错强新手首选BeautifulSoup 把 HTML 解析成可查询的对象树标签不完整、层级错乱它也能容错处理。这是新手最稳的选择代码读起来也接近自然语言from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) items soup.select(li.item) # CSS 选择器选取所有 class 含 item 的 li for item in items: title_el item.find(h2, class_title) title title_el.get_text(stripTrue) if title_el else link_el item.find(a) link link_el.get(href) if link_el else print(title, link)select 用的是 CSS 选择器语法li.item表示带 item 这个 class 的 li 元素。find 是查找第一个匹配的子元素class_ 带下划线是因为 class 是 Python 关键字。get_text(stripTrue) 能把元素里的文本取出来并去掉首尾空白比手动 replace 换行要省事得多。这里有个细节值得强调取属性用get(href)而不是[href]因为后者在属性不存在时会抛 KeyError前者返回 None。这个习惯能让脚本在面对不完整数据时多扛几次不至于一遇到坏数据就中断。find_all 和 find 的区别是返回列表还是单个元素循环场景里常用 find_all 取全部单值场景用 find。3.3 XPath批量同构页面效率更高当目标页面是「列表页 详情页」这种高度同构的结构XPath 比 BeautifulSoup 更适合。它用路径表达式定位节点写一次就能套用到几百个页面上from lxml import html tree html.fromstring(resp.text) rows tree.xpath(//li[contains(class, item)]) for row in rows: title row.xpath(.//h2[contains(class, title)]/text()) link row.xpath(.//a/href) print(title[0] if title else , link[0] if link else )//li表示在任意层级找 licontains(class, item) 是因为实际 class 可能还有别的值精确匹配会漏掉。.//h2里的点表示从当前节点往下找/text()取文本内容/href取属性值。XPath 的返回值永远是列表所以没匹配到是空列表而不是报错用title[0] if title else 兜底。一个实用技巧在浏览器 Elements 面板右键目标元素选择 Copy XPath能拿到浏览器生成的绝对路径但那种路径往往又长又脆。我一般只借用它确认层级关系然后自己手写成带 contains 的相对路径这样页面结构小调整时不容易全盘失效。BeautifulSoup 和 XPath 不是互斥的同一个页面里可以混用我常把 XPath 用在大列表提取把 BeautifulSoup 用在单条详情解析。3.4 选型顺序与空元素兜底如果你拿到一个页面不知道怎么选按这个顺序判断第一看字段数量。两三个简单字段直接正则。第二看结构是否稳定同构。列表循环、每项字段一致优先 XPath 或 BeautifulSoup。第三看页面是否规范。标签闭合完整两者都行残缺页面BeautifulSoup 容错更好。比选型更重要的是「空元素兜底」。爬虫抓取的数据不可能每个字段都在缺标题、缺链接、缺价格太常见了。如果不做保护一个 None 就能让整个循环崩溃。我习惯在每个字段的提取上都走「先判断后取值」的写法def safe_text(item, tag, class_name): el item.find(tag, class_class_name) return el.get_text(stripTrue) if el else 这个函数的作用是把「找元素、取文本、防缺失」打包成一步。日志里打印空字段的数量能帮你评估页面结构的变化幅度。如果一批数据里空值突然变多先怀疑选择器过期而不是数据本身的问题。这是爬虫调试里最容易被忽略的信号。4. 数据到手之后清洗、存储与编码的五个硬骨头4.1 清洗把爬下来的文本变成可用数据网页里拿到的文本几乎都带换行、缩进、全角空格直接存库会让后续分析变得很痛苦。我习惯在解析阶段就把清洗做掉而不是等全部爬完再处理。一个通用清洗函数长这样import re def clean_text(value): if value is None: return return re.sub(r\s, , value).strip()\s匹配所有连续空白字符包括空格、换行、制表符统一替换成单个空格后再去掉首尾。这样处理后从 HTML 里带出来的杂乱格式就被抹平了。价格、数量这类字段还要做类型转换字符串转 float日期字符串统一格式def normalize_price(value): text clean_text(value).replace(¥, ).replace(,, ) return float(text) if text else 0.0清洗阶段最常见的问题是「隐式脏数据」看起来正常实际上是全角数字、特殊空格、隐藏字符。先跑一次小批量把字段打印出来看一眼确认没有这些再放全量。省得最后分析时发现数值全对不上。4.2 写入 CSV中文乱码的根源与 utf-8-sigCSV 是爬虫落地数据最通用的格式Excel、数据库、pandas 都能直接读。写出一个不乱码的 CSV 只需要记住两个参数import csv with open(data.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([title, price, link]) for row in rows: writer.writerow([row[title], row[price], row[link]])newline 是必须的否则在 Windows 下每行后面会多一个空行。encodingutf-8-sig 是中文乱码的后悔药普通 utf-8 写入的文件 Excel 打开时默认按本地编码猜经常乱码而 utf-8-sig 会在文件开头写入 BOM 标记Excel 一看就知道用 UTF-8 解。这是零基础最容易踩的编码坑没有之一。字段较多时用 DictWriter 按字段名写入更不容易错位with open(data.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, price, link]) writer.writeheader() writer.writerows(rows)DictWriter 直接接受字典列表fieldnames 定义了列顺序writeheader 写表头。rows 里多出来的字段会被忽略缺的字段会留空不会因为某一行缺字段导致整体错列。4.3 结构化落地JSON 与 SQLite数据量不大、又希望保留嵌套结构的时候JSON 是最舒服的落地格式。中文写入时必须设置 ensure_asciiFalse否则会被转成\uXXXX序列人完全没法读import json with open(data.json, w, encodingutf-8) as f: json.dump(rows, f, ensure_asciiFalse, indent2)indent2 让文件格式化方便直接用编辑器查看。JSON 的优点是灵活缺点是量大了以后读写效率低。当数据超过几千条、需要反复查询时我会换成 SQLite它是 Python 自带的嵌入式数据库零配置import sqlite3 conn sqlite3.connect(data.db) conn.execute( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY, title TEXT, price REAL) ) conn.executemany( INSERT OR IGNORE INTO items (title, price) VALUES (?, ?), rows ) conn.commit() conn.close()连接字符串里的 data.db 是数据库文件名第一次运行会自动创建。CREATE TABLE IF NOT EXISTS 保证重复运行不报错executemany 批量插入。INSERT OR IGNORE 配合主键或唯一索引能在源头上去重。这里用参数化?而不是拼字符串是为了防止字段里带引号把 SQL 语句搞坏。4.4 去重与幂等脚本重跑不产生脏数据爬虫脚本几乎都会重跑改了一个字段、断线后重启、每天定时任务数据重复是常态。最朴素的方式是内存 set 去重seen set() deduped [] for row in rows: key (row[title], row[link]) # 选择一个能唯一标识记录的字段组合 if key not in seen: seen.add(key) deduped.append(row)key 的选择决定去重质量。用 title 加 link 的组合比单独用 title 更稳因为不同页面可能出现同名标题。如果后续要入库更彻底的方案是给数据库表加唯一索引然后 INSERT OR IGNORE让数据库层面兜底。内存去重只对本轮有效数据库索引对历史数据也有效两者配合最稳。4.5 二次加工用 pandas 快速检查脏数据爬完数据先别急着交付用 pandas 读一遍做质量检查能发现肉眼看不到的问题import pandas as pd df pd.read_csv(data.csv) print(df.head()) print(df.info()) print(df.isna().sum())head() 看前几行数据长什么样info() 看字段类型和缺失情况isna().sum() 统计每个字段的空值数量。如果 price 字段读出来是 object 而不是 float说明清洗环节有漏网的非数字文本。pandas 不是爬虫的必需组件但它五个函数顶得上肉眼检查半小时当作落地前的质量闸门很合适。5. 反爬拦截实战排查403、动态渲染、限流与空字段5.1 403 与 418 状态码服务器在拒绝你现象浏览器能正常打开页面脚本请求却返回 403 或 418。403 表示服务器理解请求但拒绝处理418 则是部分反爬服务对异常请求的风格化回复本质都是验证没通过。原因最常见的缺失是 User-Agent。有些服务器会检查请求头里是否包含浏览器特征脚本默认的 UA 是python-requests/x.x.x一眼就能认出来。更严格的反爬还会校验 Accept、Accept-Language、Referer 是否成体系只补一个 UA 仍然会拦。解决模拟浏览器的请求头集合headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.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, Referer: https://example.com/, }Referer 表示请求来源页面有些站点会校验这个字段。把这四个头补齐后脚本在服务器眼里才接近一个正常访客。注意 Referer 要填目标站自己的首页填错站点反而会更可疑。5.2 动态渲染页面能看到内容却抓不到现象状态码 200resp.text 也有内容但用选择器提取时一个节点都匹配不到目标字段全是空的。原因页面内容由 JavaScript 在浏览器端动态渲染requests 拿到的是初始 HTML 骨架真正的数据在脚本执行后才插入文档。这种情况在网络请求层面是「看不见的黑匣子」光看 HTML 根本找不到数据。解决先别急着上无头浏览器。打开开发者工具的 Network 面板刷新页面找到返回 JSON 的 XHR 请求那个接口往往就是数据源。直接请求它api_url https://example.com/api/list?page1 resp requests.get(api_url, headersheaders, timeout10) data resp.json() # 如果返回的是 JSON直接转成 Python 对象网页里看到的数据绝大多数来自后端某个接口。找到接口后爬虫从「解析 HTML」变成「请求 JSON」稳定性高一个量级。只有在 Network 面板里找不到数据接口、或者接口加密严重时才考虑 Selenium 之类的无头浏览器那个方案速度慢、资源占用高能不用就不用。5.3 翻到第 N 页突然超时被限流的信号现象前 10 页请求正常第 11 页开始连续超时、连接被重置甚至返回 503。原因请求频率超过了目标站点的限流阈值服务器把当前 IP 暂时拉黑或降速。解决降低频率加随机延时并配上指数退避重试。随机延时比固定延时更难被识别import time import random for page in range(1, 51): time.sleep(random.uniform(1, 3)) # 请求与解析逻辑random.uniform(1, 3) 让每次等待在 1 到 3 秒之间浮动避免出现固定间隔的机械行为。遇到偶发超时重试比直接放弃更合理指数退避是通用做法for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) break except requests.exceptions.RequestException: time.sleep(2 ** attempt) # 依次等待 1、2、4 秒2 ** attempt第一次等 1 秒第二次 2 秒第三次 4 秒给服务器喘息时间。重试超过 3 次仍然失败就打印日志并跳过当前页不要继续死磕。被限流之后最忌讳的是换着花样硬闯那只会让限制更严。5.4 列表项偶发缺字段解析健壮性不足现象脚本跑到某个列表项时突然报错提示 NoneType 没有 text 属性程序中断。检查页面发现这一条确实少了某个字段。原因目标站点数据不完全干净或者页面结构存在局部变化。一个字段缺失导致整个循环崩溃是解析逻辑没做好空值保护。解决所有字段提取统一走「先判断后取值」第 3.4 节的 safe_text 函数就是干这个的。另外把出错时的上下文打出来方便定位是哪一条数据引起的问题try: title safe_text(item, h2, title) except Exception as e: print(解析失败:, e) print(原始片段:, item) continue请求层面的反爬更多发生在网络阶段但解析阶段的「数据反爬」——比如字段随机缺失、顺序打乱——同样能把脚本搞崩。遇到偶发缺字段宁可留空也不要中断全量任务。一个字段缺失记录成空字符串数据分析时能通过 isna() 统计出来而中断任务损失的是所有剩余页面的采集量。6. 从跑通到能扛活断点续爬、验证习惯与边界意识6.1 断点续爬脚本中断后不用从头再来真实抓取经常跑到一半断网、断电、进程被杀。没有断点续爬几十页的进度说没就没重跑还容易触发限流。我会在脚本里维护一个进度文件每完成一页就记录页码import json try: with open(progress.json, r, encodingutf-8) as f: done set(json.load(f)) except (FileNotFoundError, json.JSONDecodeError): done set() for page in range(1, 101): if page in done: continue # 这里放本页的请求与解析逻辑 done.add(page) with open(progress.json, w, encodingutf-8) as f: json.dump(list(done), f)启动时读取已完成页码运行时每处理完一页立即写回。文件写入虽然频繁但对几百页的量级完全可接受。注意进度文件写的是页码集合不是数据本身所以数据存库和进度记录互不干扰。这个习惯持续下去之后几乎所有的批量采集脚本我都会顺手加上成本两行代码收益是整个任务可以不眠不休地跑。6.2 先抓 10 条验证再放全量我的习惯是任何新页面都先跑小批量打印前两行原始结果确认字段对得上再上全量。曾经接手一个模拟项目X脚本全量跑完才发现日期字段错位重跑半小时那之后这个习惯就再没丢过。验证包括三件事状态码是否为 200、字段名是否和预期一致、条目数量是否对得上。全部符合预期再放开循环范围。全量跑起来之后每完成一部分打印一次进度不然脚本挂在那里你甚至不知道它是正常还是卡死。6.3 边界意识哪些情况别硬上爬虫的价值在于自动化公开数据的采集与整理别把它用在需要登录凭据、涉及个人隐私、或明确禁止抓取的资源上。有官方接口就优先用官方接口没有接口再评估静态页面。尊重目标站点的 robots.txt 和访问频率约定控制并发。遇到加密程度很高的站点先算时间成本与其花三天逆向不如直接评估是否值得。这是方向层面的判断也是这行能长期做下去的基础。以前我总想着什么站都能爬后来吃过亏才明白会判断「不该爬」比会写爬虫更重要。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →