news 2026/10/15 7:42:21

Python网络爬虫实战指南:从请求设计到工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python网络爬虫实战指南:从请求设计到工程化落地

简介:基于Python的网络爬虫设计与实现论文,面向爬虫初学者、在校学生以及需要课程设计或毕业设计参考的开发者。内容以股票信息抓取为案例,从初始链接到数据存储完整展示爬虫设计全过程,涵盖系统结构、爬行流程、页面采集、页面解析、连接处理、数据存储等模块;利用Requests、BeautifulSoup和Scrapy等工具进行多线程URL处理与定向抓取,最终将股票数据存入PostgreSQL数据库并做可视化展示。资源仅含1个PDF文档,大小约1.2MB,为期刊正式论文,包含系统功能模块划分、软硬件环境要求、技术经济操作可行性分析、关键代码示例以及开发环境配置建议,便于读者借鉴环境配置与框架选型,也可作为毕业设计或技术报告的写作模板。目前已有483人浏览学习,适合想系统掌握爬虫架构、快速构建爬虫项目或撰写相关技术文档的学习者。

1. 基于Python的网络爬虫:先弄清楚它在真实项目里解决什么

基于Python的网络爬虫,本质上是在做一件反直觉的事:把浏览器里的一次点击,变成程序里的上万次循环。一个爬虫项目的核心价值不在于“能访问多少个网址”,而在于它能不能持续、稳定、可维护地把分散在网页里的数据变成结构化记录。我在实际项目里见过太多跑两天就报废的爬虫脚本,也见过只用了不到两百行代码就能稳定运行几个月的采集模块。两者的差距不在框架,而在设计:请求层是否健壮、解析层是否可靠、数据落盘是否可续。这篇文章就是围绕这三个层次展开的,同时会把最容易让人翻车的边界条件单独拎出来讲。

如果你是需要定期采集公开数据做分析的从业者,或者在做课程设计、毕业设计时被“设计一个网络爬虫”这个题目卡住,又或者想从单脚本走向规模化采集,这篇笔记都能对应上你的进度。这里不堆架构图,只讲动手能复现的东西。

2. 请求层设计:从urllib到requests,把“拿到页面”这一步做稳

2.1 urllib与requests:为什么多数落地项目选requests

先说一个常见的选择题:标准库里已经有urllib,为什么还要装requests?我的答案是,urllib适合没有第三方依赖的环境,但日常落地项目里requests提供的API设计更符合人类直觉。requests把连接池、Cookie持久化、编码猜测、重定向这些细节封装好了,这让开发者把精力省下来放到解析和存储上,而不是沉在HTTP协议细节里。

从工程角度看,urllib还有一个让人难受的点:它对响应内容的编码处理比较粗糙,经常需要手动从响应头或页面meta里找charset,否则中文内容容易乱码。而requests会结合响应头做一次编码判断,大部分情况下直接访问.text就能拿到正确字符串。当然,编解码也有它自己的坑,这一点我会在避坑章节专门展开。

如果项目里不限制第三方库,我几乎总是先装requests。即使以后要迁移到异步框架,requests写出来的逻辑也能很自然地平移过去。urllib则保留在“必须零依赖”的场合,比如某些受限的CI环境或离线部署机器上。简单说,requests是默认选项,urllib是兜底方案。

2.2 最小请求模块:超时、重试、会话一次配齐

一个能持续运行的爬虫,请求层至少要回答四个问题:超时多久算失败、失败后要不要重试、重试几次、要不要复用TCP连接。我见过太多爬虫死于“没有设置超时”,目标站一卡,整个脚本就挂在那里不动。下面这段代码是我常用的最小请求模块,它把会话、请求头和连接池一次性配置好。

import requests from requests.adapters import HTTPAdapter session = requests.Session() session.headers.update({ "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/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", }) adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=3) session.mount("https://", adapter) session.mount("http://", adapter) def get_page(url, timeout=(5, 10)): resp = session.get(url, timeout=timeout) resp.raise_for_status() return resp.text

这段代码里有几个参数值得拆开说。timeout=(5, 10)传入的是一个元组,前一个值是“连接超时”,后一个是“读取超时”。连接超时解决的是目标服务器不响应TCP握手的问题,读取超时解决的是数据传了一半卡住的问题。如果只传一个数字,requests会把它同时用在两个阶段,但连接和读取的耗时特征完全不同,拆开设置更合理。

HTTPAdapter的pool_connections控制的是不同主机之间的连接池数量,pool_maxsize控制的是同一主机最多复用多少连接。对单机爬虫来说,这两个值不需要调得很大,10就够用。max_retries=3的作用是在请求遇到连接错误时自动重试3次,但它不会对HTTP 404或500做重试,这是一个容易误解的点。如果需要按状态码做重试,得在业务代码里自行判断。

2.3 会话保持与Cookie:登录态爬取的正确姿势

很多目标网站的数据需要登录后才能看到,这就引出第二个请求层问题:怎么维持登录态。requests.Session的机制决定了它会自动保存服务端下发的Cookie,并在后续请求中自动携带。这意味着只要登录一次,后续请求都带着会话身份,不需要每次手动塞Cookie头。

登录过程通常分为两步:先请求登录页拿到表单里的令牌,再用POST提交账号密码。很多站点会在登录表单里埋一个csrf_token,这个值通常藏在HTML里,先用正则取出来。下面的例子演示了带登录态的采集流程。

import re login_url = "https://example.com/login" resp = session.get(login_url, timeout=(5, 10)) token = re.search(r'name="csrf_token" value="([^"]+)"', resp.text).group(1) session.post(login_url, data={ "username": "your_account", "password": "your_password", "csrf_token": token, }, timeout=(5, 10)) data_url = "https://example.com/orders?page=1" page = session.get(data_url, timeout=(5, 10)) if "请登录" in page.text or "login" in page.url: print("登录态已失效") else: print("登录态保持成功")

这里的关键是判断登录态是否失效的逻辑。不要在拿到200状态码后就默认数据有效,很多站点在Cookie失效后返回的仍然是200页面,只不过内容变成了登录页。一个实用的做法是,在登录成功的响应里记住某个标识字符串,之后每次请求检查页面文本里是否还存在这个标识。如果不存在,就说明会话已经断了,需要重新登录。

另外,requests默认会跟随重定向,allow_redirects默认是True。在采集过程中如果发现resp.url和请求的URL不一致,通常意味着服务端做了跳转,这时要留意跳转后的页面是否还是目标业务页面。我一般会在调试阶段把resp.url打印出来,这比只看状态码更能定位问题。

3. 数据解析层:正则、XPath、CSS选择器怎么分工

3.1 正则只做末端字段提取,别拿它解析整个页面

拿到HTML之后,下一步是提取数据。很多人第一反应是用正则硬啃,因为正则看起来最直接,但实际上它只适合做末端字段的提取,不适合做整页结构的解析。HTML是嵌套结构,正则擅长的是“在一个字符串里按模式找片段”,它记不住层级关系,面对不规范的标签很容易写出又长又脆的匹配式。

常见的做法是,用正则去处理那些“寄生”在脚本或JSON字符串里的数据。比如页面里有一段var listData = [...],整块是JSON格式,直接用正则提取listData等号后面的内容,再用json.loads解析。这种场景正则很合适,因为目标明确、格式统一。

import json import re text = "<script>var listData = [{\"title\": \"Python工程师\", \"salary\": \"20K\"}];</script>" match = re.search(r"var listData = (\[.*?\]);", text) if match: data = json.loads(match.group(1)) for item in data: print(item["title"], item["salary"])

这段代码的思路是:先用正则把JSON片段从脚本里剥出来,再用JSON解析器做后续处理。注意.*?用了非贪婪匹配,防止截断到第一个];就停下来。正则在这里扮演的是“定位器”角色,而不是“解析器”角色,这是它与XPath的最本质区别。

3.2 XPath与lxml:从混乱HTML里批量抽数据的核心写法

如果目标页面是规范的HTML列表结构,XPath是抽数据的主力工具。lxml的etree.HTML会自动补全缺失的标签,这个容错处理尤其适合真实网页里大量存在的“标签没闭合”“属性值带引号”等情况。

from lxml import etree html = """ <ul class="job-list"> <li class="item">from parsel import Selector selector = Selector(text=html) titles = selector.css("li.item a::text").getall() hrefs = selector.css("li.item a::attr(href)").getall() for title, href in zip(titles, hrefs): print(title, href)

::text和::attr(href)是parsel对CSS选择器做的扩展,标准CSS没有这种语法。用getall()拿全部匹配结果,用get()只拿第一个,且在没有匹配时返回None而不是抛异常。相比lxml的[0]写法,parsel在处理“某些条目可能缺少某个字段”时更从容,不用每次判断列表是否为空。如果你的项目后续要上Scrapy,解析代码几乎可以原封不动搬过去。就这一点而言,parsel是我推荐的解析层首选。

4. 爬虫工程化:限速、重试与增量断点,把脚本变成系统

4.1 请求头与UA:在源头减少被拒的概率

爬虫写完之后,最常见的失败不是解析出错,而是请求被目标站点拒绝。很多站点反爬的第一道关卡就是请求头校验,它不需要多复杂的机器学习模型,只要检测到请求头里的浏览器标识缺失或异常,就直接返回403。这就是为什么请求头设计要“完整得像一个真实浏览器”,而不只是塞一个User-Agent。

一个最小完整的请求头至少要包含User-Agent、Accept、Accept-Language。Referer在某些场景下也很关键,比如访问列表页时带上上一级页面的URL。还有一点很多人会忽略:Accept-Encoding不要随意设置,如果设置了gzip,就要正确处理压缩后的响应,否则页面内容会变成乱码字节。requests默认会帮我们处理这部分,手动覆盖时要谨慎。

如果需要在多个请求间轮换身份标识,常见做法是准备一个UA池,每次请求随机取一个再更新到Session里。但要注意,UA池轮换不等于伪装成功,它只能应对最基础的检查。更关键的是请求频率,这一点下面单独讲。

4.2 限速与指数退避重试:给目标站点留余地

很多爬虫翻车的直接原因是请求频率太快。单线程跑没问题,一旦并发提上去,几分钟内就被封IP。设计爬虫的时候,限速不应该是事后补救,而应该是实现之初就写进采集循环里的基础约束。一个基本的限速策略是每次请求之间sleep一个随机时间,让节奏不规律且平均频率可控。

import random import time def fetch_one(session, url): time.sleep(random.uniform(0.5, 1.2)) try: resp = session.get(url, timeout=(5, 10)) except requests.RequestException: return None if resp.status_code != 200: return None return resp.text

random.uniform(0.5, 1.2)的含义是每次请求前随机等待0.5到1.2秒,这样平均每秒大约发起1个请求。这个速率对大多数信息公开站点都是能接受的。比起固定的sleep(1),随机延迟的好处是节奏更像人工浏览,也比固定频率更难被规则检测命中。

关于重试,我的习惯是只在“网络异常”和“5xx状态码”时重试,4xx几乎不重试,因为4xx通常意味着客户端的问题,重试多少次都没用。如果要用指数退避,推荐手写一段轻量逻辑,规则就是每次重试等待时间翻倍再叠加随机抖动。重试上限一般设置在3到5次,超过上限就放弃当前请求,把URL记录下来,供后续补采。

4.3 增量去重与数据落盘:重启不丢进度

一个采集任务往往不是跑一次就结束,而是要按天或按小时持续执行。如果脚本中途崩了,或者需要停掉重启,能不能从断点继续跑,就是工程化程度的分水岭。最朴素也最有效的方案是:数据库里已存在的记录标识,启动时加载到内存集合,采集前先查一次集合。

import sqlite3 conn = sqlite3.connect("jobs.db") conn.execute("CREATE TABLE IF NOT EXISTS job (id TEXT PRIMARY KEY, title TEXT, url TEXT)") visited = set(row[0] for row in conn.execute("SELECT id FROM job")) def collect_page(page_id, html): title = extract_title(html) # 假设已有解析函数 url = f"https://example.com/list/{page_id}" conn.execute( "INSERT OR IGNORE INTO job (id, title, url) VALUES (?, ?, ?)", (str(page_id), title, url) ) conn.commit() visited.add(str(page_id)) for page_id in range(1, 100): if str(page_id) in visited: continue html = fetch_one(session, f"https://example.com/list/{page_id}") if html is None: continue collect_page(page_id, html)

这段代码有三个设计点值得说。第一,visited集合启动时从数据库里恢复,而不是从空集合开始,这样就实现了重启续跑。第二,INSERT OR IGNORE利用主键唯一约束天然防重,即使两个逻辑环节之间出了并发问题,数据库层面也能兜底。第三,collect_page里先解析再提交,提交成功后立刻加入visited,保证“处理成功”和“标记已访问”保持同步。

SQLite对单机爬虫足够用了,多线程场景下写入会触发锁冲突,这时可以给写操作加一个简单的线程锁,或者退一步用CSV加追加写。但无论如何,不要把去重只放在内存里,否则一次重启就是一场血泪教训。

5. 避坑:五个让爬虫翻车的现场,每条都有解法

5.1 页面内容全是乱码:编码探测不是靠猜

现象:请求返回的HTML打开后全是``或者问号,中文字符全部乱掉。原因:requests在解析响应时优先使用HTTP响应头里的charset,而这个值和页面实际编码可能不一致。很多站点的响应头不声明编码,或者声明为ISO-8859-1,但页面实际是UTF-8。解决:先打印resp.encoding和resp.apparent_encoding,如果两者不一致,手动指定正确的编码值再取.text。

resp = session.get(url, timeout=(5, 10)) if resp.encoding != resp.apparent_encoding: resp.encoding = resp.apparent_encoding html = resp.text

这样处理比每次都从<meta charset>里正则匹配快得多。注意,如果页面是GBK编码,这种方式也能正确识别,但前提是页面的字节流里有足够的编码特征。如果实在拿不准,就直接用resp.content先拿到原始字节,再用bytes.decode("gbk", errors="ignore")逐种编码尝试。

5.2 页面明明有数据,解析结果却为空

现象:在浏览器里打开目标页面,数据清清楚楚,但爬虫解析出来的列表是空的。原因大概率不是XPath写错,而是这些数据是动态加载的,根本不在初始HTML里。解决:打开浏览器的开发者工具,切到Network面板,刷新页面,找到返回JSON数据的XHR请求,直接采集这个接口而不是采集HTML页面。接口返回的JSON用resp.json()一行解析,比在HTML里找数据省事得多。

判断动态加载还有一个快速方法:在浏览器里右键查看网页源代码,搜索你在页面上看到的关键词,如果搜不到,说明数据是通过脚本后来填充的。这种情况下花时间调XPath是浪费,直接转接口采集才是正路。

5.3 请求被403/418拦截

现象:刚开始还能正常采集,过了几十条请求后突然大量返回403或418状态码。原因:服务端检测到了异常请求特征,可能是单一IP请求过快,也可能是请求头指纹与真实浏览器差异过大。418的出现通常意味着服务端明确识别出了“这不是浏览器”。解决:先把请求头补完整,UA、Accept、Accept-Language、Referer缺一不可;然后进一步降低请求频率;如果还是被拦,考虑为采集任务增加代理源,并把代理与请求频率的配合做灰度验证。

这一步里最玄学的部分在TLS指纹识别。有些站点不看你带了什么头,而是直接探测客户端TLS握手特征,普通的requests库在这种检查下基本伪装不了。这是requests本身的限制,不是靠改参数能解决的。遇到这种情况,常见的出路是换用能模拟浏览器完整协议指纹的客户端库,同时把采集频率压到最低。但归根结底,是否值得为了某个目标站投入这种成本,要在项目启动前就评估清楚。

5.4 多线程一开,IP就被封

现象:单线程采集一切正常,把代码改成ThreadPoolExecutor之后,十分钟内所有请求都被拒绝。原因非常简单直接:请求频率翻了几倍,超过了目标站容忍阈值。解决:在引入多线程之前,先想清楚单位时间内的总请求数是否可控。

一个可行的做法是把限速器抽成全局唯一实例,所有线程共用一个令牌或睡眠锁,让总请求速率维持在线程数不变时的水平。多数人在这里犯的错误是只在线程函数里写了sleep,但线程数量翻倍意味着实际每秒请求数翻倍。先用单线程把限速逻辑调整到位,再逐步增加线程数,每次加一倍并观察封禁情况,这是最稳妥的路径。

5.5 登录态突然失效,采集数据中断

现象:采集任务跑了几十分钟,突然返回的数据都变成“请先登录”或登录页HTML。原因:会话Cookie过期,服务端session超时,或者目标站在检测到异常频率时主动失效了当前会话。解决:在采集主循环里加入登录态校验逻辑,检测到失效时先停止采集,走重新登录流程,成功后从断点续采。

重新登录的逻辑要写在请求模块里,而不是写在业务解析逻辑里。也就是说,get_page内部发现页面跳转到登录页时,自动完成一次登录再重试原请求。这样采集主循环完全感知不到登录态变化,也不需要为“会话失效”写额外状态机。这套逻辑看着不起眼,却是让爬虫长稳运行的关键设计。

6. 进阶:上分布式之前,先把去重、队列和采集边界想清楚

6.1 从单机到分布式:任务队列与URL去重的抽象

当你手里的目标站点从一两个变成几十个,单机脚本的逻辑开始爆炸时,才需要考虑分布式。但分布式爬虫最容易犯的错是过早引入重型组件。我的建议是,上分布式之前先把三个抽象做对:任务队列、URL去重、数据分层。把“下一个要采集的URL”从函数参数变成一条消息,把内存里的visited集合换成可共享的存储,这一步做好了,分布式只是换个消息队列的事。

URL去重在分布式场景下的标准做法是使用Redis的Set,这里给一段最简实现。

import redis r = redis.Redis(host="localhost", port=6379, db=0) def is_visited(url): return bool(r.sismember("visited_urls", url)) def mark_visited(url): r.sadd("visited_urls", url)

当URL数量过亿,Set的内存占用会变得可观,这时才值得引入BloomFilter。但很多项目其实到不了这个量级,提前上布隆过滤器只会增加不必要的心智负担。真实的分布式难点从来不是“谁去重”或者“谁拿任务”,而是数据怎么汇总、任务怎么平衡、失败怎么重试。我的经验是,先把这三件事在单机单进程里用清晰接口抽象出来,迁移到分布式时替换实现就好。

还有采集边界的问题。爬虫能做不等于什么都该采集,工程上要遵守目标站点的访问协议与频率限制,只采集公开数据,并考虑目标服务器的承载压力。这不是套话,而是延续一个采集项目的基本前提。我曾经为了赶数据进度,把一个简单页面的并发开到50,结果不仅IP被封了一周,还给对方服务器添了不必要的麻烦。那之后我养成了一个习惯:先写限速器,再写采集逻辑。数据采集是长跑,不是一锤子买卖。

希望这些踩坑经验能帮你在做爬虫设计时少走弯路,也欢迎把你自己的翻车现场拿来对照,很多问题的解法其实早就有迹可循。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 7:42:16

大规模电动汽车充放电优化:MATLAB局部优化实战解析

我最近刚把一个跑了两个多月的电动汽车充放电优化项目收尾&#xff0c;趁着记忆还热乎&#xff0c;把整套思路和MATLAB实现细节整理出来。这个项目解决的核心问题是&#xff1a;当一个充电站聚合商手里握着几百上千辆电动汽车的充放电权限时&#xff0c;怎么在电价波动、用户出…

作者头像 李华
网站建设 2026/10/15 7:42:14

SpringBoot+Vue图书管理系统:前后端分离项目实战与部署全解析

前后端分离的图书管理系统&#xff0c;SpringBootVueMyBatisMySQL这套组合应该算是Java Web开发里最经典的就业级配置了。我前阵子刚好把一个带完整源码和部署教程的项目从头到尾跑通了一遍&#xff0c;今天就把整个思路、踩坑记录、配置细节全部摊开聊一聊&#xff0c;给正在做…

作者头像 李华
网站建设 2026/10/15 7:42:02

C++内联与编译器优化:从代码膨胀到LTO的工程实践指南

1. 项目概述1.1 核心需求解析提到"C编程中的内联与编译器优化"&#xff0c;很多刚入门的朋友会把它简单理解成"给函数加上inline关键字就完事了"。但实际工程里&#xff0c;内联远不止一个关键字那么简单&#xff0c;它背后牵扯到编译器的优化策略、代码膨…

作者头像 李华
网站建设 2026/10/15 7:41:47

存算分离架构落地:网络、元数据、缓存与成本四大瓶颈深度解析

说实话&#xff0c;这几年不管是在技术社区还是公司内部的架构评审会上&#xff0c;“存算分离”都快被说成大数据平台的标准答案了。但是真正动手迁移过的人心里都清楚&#xff0c;一个看起来只是“把计算和存储拆开”的架构&#xff0c;落地之后会冒出一堆在设计文档里根本看…

作者头像 李华
网站建设 2026/10/15 7:41:47

RK3229 BSP开发实战:芯片指南解读与避坑要点

简介&#xff1a;RK3229软件开发指南V1.03是瑞芯微官方发布的技术文档&#xff0c;面向嵌入式软件开发与技术支持工程师&#xff0c;帮助他们围绕RK3229芯片在Linux 3.10内核及Android 6.0.1/7.1.2平台下完成开发、编译与调试。压缩包内为1个PDF文件&#xff0c;大小1.61MB&…

作者头像 李华
网站建设 2026/10/15 7:41:34

智能会议门牌提醒与超时释放选型指南

每到工作日上午十点或下午三点&#xff0c;行政前台总能看到几组抱着笔记本的同事在走廊徘徊&#xff0c;盯着紧闭的会议室门犹豫不决。这种场景在很多企业并不陌生&#xff1a;上一场会议明明预约时间已结束&#xff0c;里面却讨论正酣毫无散场迹象&#xff0c;而下一场急需场…

作者头像 李华