ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python爬虫必学:接口、JSON与分页实战全解析

Python爬虫必学:接口、JSON与分页实战全解析 很多零基础学Python爬虫的人真正卡住的地方往往不是requests用不熟而是这样一个瞬间网页上明明能看到自己想要的数据可把抓下来的HTML源码翻个底朝天就是搜不到目标文本。我第一次遇到这个情况硬是折腾了一下午最后才弄明白——网页上的内容根本不是藏在HTML里而是浏览器悄悄去请求了一个接口接口返回数据后再由JavaScript渲染到页面上。也是从那一刻起我确认了爬虫入门除了要玩转requests和解析库之外还绕不开三样东西接口、JSON、分页。这篇文章就用最直白的方式把这三件事拆开讲清楚你只要有一点点Python基础跟着读下来问题不大。1. 页面上的数据不在HTML里接口才是真正的数据老家1.1 用开发者工具亲眼看一次数据请求我当时抓的是一个资讯列表页浏览器里能看到文章标题、浏览量、发布时间但requests抓回来的源码里搜文章标题干干净净搜不到。后来按F12打开开发者工具切到Network面板勾选XHR再按F5刷新页面才发现了端倪——页面上每出现一批数据Network里就会多出几个请求名字一般长这样/api/article/list、/api/feed、/query?page1。点开其中一个切到Preview或Response选项卡页面上的文章标题、浏览量全都躺在里面。这就解释清楚了现在的主流网站尤其资讯、电商、社交类页面里的数据大多是前端通过JavaScript向后台接口发起请求拿到JSON之后再动态渲染出来的。requests默认抓到的只是最外层的HTML壳子壳子里没有数据当然什么都搜不到。所以爬虫真正要盯的不是那个给人看的页面URL而是页面背后返回JSON数据的接口URL。1.2 接口的基本构成URL、方法、参数、响应接口这个词翻译成大白话就是两个程序之间约定好的通话方式。你可以把服务器想象成一家餐厅接口就是点餐台。URL是菜单上的菜名编号你想吃什么就对着哪个编号下单请求参数是口味备注比如不要辣打包服务器处理后返回给你的结果就是一份做好的菜而这盘菜在接口世界里通常是JSON格式。从技术角度拆一个HTTP接口由四部分组成URL告诉服务器你要访问哪个资源比如/api/article/list。请求方法最常用的两个是GET和POST。GET一般用来获取数据参数拼在URL问号后面POST一般用来提交数据参数放在请求体里。请求头给服务器看的身份和浏览器环境信息比如User-Agent、Cookie。响应服务器返回的状态码、响应头和响应体。响应体如果是JSONContent-Type通常是application/json。零基础阶段你接触的绝大多数分页列表接口都是GET所以先专注于GET就够了。判断一个URL是网页还是接口最直接的办法就是看响应头import requests resp requests.get(https://jsonplaceholder.typicode.com/posts/1, timeout10) print(resp.headers.get(Content-Type)) print(resp.text)如果输出application/json; charsetutf-8说明这确实是接口返回的是JSON而不是HTML网页。把这段地址直接粘贴到浏览器地址栏里看到的也是一段结构化文本而不是花花绿绿的页面这就是接口URL和网页URL最直观的区别。2. JSON 是接口返回数据的通用语言2.1 为什么接口普遍用JSON而不是别的格式很多人第一次接触JSON都会犯怵觉得这是个新语言。其实JSON全称是JavaScript Object Notation翻译过来叫JavaScript对象表示法它本质上就是一种数据交换格式不是编程语言也不需要你有前端基础才能学。早年接口返回数据用过XML结构严谨但写起来啰嗦一个字段几行标签传输起来又占带宽。后来大家逐渐转向JSON因为它足够轻量结构清晰而且几乎所有编程语言都有现成的解析库。可以这样理解JSON就是搬家用的纸箱箱子外面贴的标签是键箱子里装的东西是值小箱子套大箱子就是嵌套结构。服务器把数据装进JSON这口统一的箱子里客户端负责开箱取货双方没有歧义。2.2 JSON的四种值类型和两种容器结构JSON的全部家当其实就这两类容器、四个基础值对象用花括号{}表示里面是一组一组键值对键必须用双引号包起来比如name: 张三。数组用方括号[]表示里面按顺序放一堆值比如[1, 2, 3]、[a, b]。基础值字符串你好、数字18、3.14、布尔值true/false、空值null。下面是一段典型的接口返回JSON我加了些注释你一眼就能看明白结构{ code: 0, message: success, data: { total: 1499, page_num: 1, page_size: 20, has_more: true, items: [ { id: 101, title: 文章标题, view_count: 234, is_top: false, tag_list: [python, 爬虫] } ] } }最外层是一个对象里面有code、message、data三个键。data的值又是一个对象里面除了几个分页相关字段还有一个items数组数组里每个元素都是一篇文章对象。文章对象里又有title、view_count这些基础值以及tag_list这样一个字符串数组。学会一层层剥开这种嵌套结构就是JSON解析的核心。2.3 JSON语法里最容易犯的错用Python写惯了字典回头写JSON容易踩三个坑。第一键必须用双引号不能用单引号很多新手把Python字典那一套直接搬过来解析就报错第二JSON里不允许写注释也不能有尾逗号最后一个键值对后面多了个逗号就报错第三布尔值必须是true/false小写而不是Python的True/False。遇到解析错误别慌把报错信息里的行号和列号对应到字符串里基本都能定位。临时校验最方便的办法是把JSON粘贴到任意在线JSON校验工具或者直接用Python的json.loads跑一下它会告诉你错在哪一行。写解析代码之前我习惯先把一段真实的返回数据复制到本地格式化好看清楚层级关系再动手这个习惯帮我少踩了很多坑。3. Python 与 JSON 打交道的三种姿势3.1 json.loads 和 json.dumps最基础的转换函数Python内置的json模块是处理JSON的主力零基础先记住两个函数就够了。json.loads()负责把JSON字符串转成Python对象json.dumps()负责把Python对象转成JSON字符串。名字里带s可以理解成string也就是处理的是字符串。import json text {name: 小明, age: 18} data json.loads(text) print(type(data)) # class dict print(data[name]) # 小明 back_to_text json.dumps(data) print(back_to_text)这里有个细节容易被忽略json.dumps()默认会把中文转成\uXXXX这种Unicode转义序列打印出来人看不懂。解决办法是加上ensure_asciiFalse参数back_to_text json.dumps(data, ensure_asciiFalse) print(back_to_text) # {name: 小明, age: 18}如果你解析的是JSON文件而不是字符串那就用json.load()和json.dump()这对函数注意少了个s一个接收文件对象一个写入文件对象。3.2 requests 的 .json() 方法比手动转换更方便用requests发请求接着手动json.loads(resp.text)当然可以但requests自带一个.json()方法它会在内部完成响应内容的解码和转换resp requests.get(https://jsonplaceholder.typicode.com/posts/1, timeout10) data resp.json() print(data[title])一个值得注意的点是.json()只有在响应体确实是合法JSON时才会正常工作如果服务器返回的是HTML错误页或者一段普通文本调用.json()会直接抛异常。建议在解析前先判断状态码resp requests.get(url, timeout10) if resp.status_code 200: data resp.json() else: print(请求失败, resp.status_code)3.3 多层嵌套取值学会之后再考虑用jsonpath刚入门时取嵌套数据最朴素的方式就是一层层索引比如要拿上面示例里第一篇文章的标题代码就是first_title data[data][items][0][title] print(first_title)这段代码就是标准的剥洋葱先取data再取data里的data再取items数组然后取数组第一个元素最后拿title。等嵌套越来越深这个写法会变得很长而且任何一个环节字段名写错就取不到值。这时候可以用jsonpath这个库它像XPath一样用路径表达式定位数据from jsonpath import jsonpath titles jsonpath(data, $.data.items[*].title) print(titles) # [文章标题, ...]$.data.items[*].title的意思是从根节点开始取data下的items数组里的所有元素各自的title。不过我不建议零基础一上来就依赖jsonpath最好先手写几层索引、把Python字典和列表的操作练熟。等哪天真被多层嵌套烦到了再上这个库理解成本会低很多。4. 分页服务器为什么总是一次只给一页4.1 一次性把全量数据塞给你会发生什么假设一个接口背后有100万条数据服务器如果一次性全部返回响应体可能达到几百MB网络传输慢、客户端解析卡、服务器内存和带宽双双告急。即便数据量没这么大很多产品设计也不希望调用方一次性拿到全量数据这样做既难控制风险也不利于统计调用频次。于是分页成了接口设计的默认做法每次只返回一页数据通过参数告诉服务器我要第几页、每页多少条。爬虫新手容易有一个误区拼命想把所有数据一次性拿回来。真实的爬虫恰恰相反要尊重接口的分页规则一页一页拿中间留出合适的间隔这才既稳定又不容易触发限流。4.2 分页参数三种主流形式分页参数没有统一的国际标准但最常见的就三种我用表格给你列清楚分页形式请求参数示例特点常见场景页码分页page2pageSize20直观第2页每页20条返回通常会带total或has_more资讯列表、商品列表、文章列表偏移分页offset40limit20从第40条开始取20条老接口比较多老版本API、后台管理系统游标分页cursorMTU4MjAw服务器返回一个不透明游标下次请求带着它继续社交动态、即时消息、高频更新数据页码分页最好理解适合数据相对稳定的场景。偏移分页有个明显的痛点如果在翻页过程中有人新增或删除了数据后面页的数据会重复或者漏掉因为offset是固定偏移量。游标分页对高频更新最友好服务器用游标记住你上次看到哪了但实现起来略复杂新手先看得懂即可。4.3 返回结果里的分页信息到底怎么看服务器不光返回本页数据通常还会附带分页元信息常见字段有total总记录数比如1499条。page或page_num当前页码。page_size每页条数。has_more是否还有下一页返回true说明继续翻。next_cursor游标分页专用表示下一次请求要带的游标值。也有少数接口不走寻常路不把总数放在响应体里而是放在响应头里比如X-Total-Count。第一次接触某个接口时先花两分钟把返回体完整打出来看看确认到底有哪些分页字段这比闷头写循环重要得多。5. 实战写一个通用分页爬虫把数据全部拉回来5.1 用公开测试接口练手不碰任何有争议的数据学习阶段建议用明确开放、无版权争议的测试接口练手。我这里用的是JSONPlaceholder一个专门给开发者做练习的免费假数据接口地址是https://jsonplaceholder.typicode.com/posts。它支持的参数是_page和_limit分别表示页码和每页条数返回的是文章数据。这个接口稳定、公开用来学分页再合适不过。动手之前先做个三步分析第一步用浏览器打开接口地址观察返回的JSON结构第二步试着在地址栏加上?_page1_limit2看返回条数是不是变成2条第三步关注响应头你会发现里面有个X-Total-Count: 100这就是总记录数。顺便提一句有些接口的总数不在响应体里而在响应头里这是一线开发里很常见的隐蔽坑。5.2 循环翻页直到拿完所有数据先写一个最基础的版本不断把页码加1直到返回的列表为空。为什么用while而不是for因为你一开始不知道总共有多少页而while天然适合翻到没数据为止这种不确定次数的循环。import requests base_url https://jsonplaceholder.typicode.com/posts page 1 limit 20 all_posts [] while True: resp requests.get( base_url, params{_page: page, _limit: limit}, timeout10 ) resp.raise_for_status() data resp.json() if not data: break all_posts.extend(data) total int(resp.headers.get(X-Total-Count, 0)) print(f已抓取第 {page} 页累计 {len(all_posts)} 条) if len(all_posts) total: break page 1 print(f全部完成共 {len(all_posts)} 条数据)代码里有两个终止条件第一个是当前页返回空列表说明服务器那边没有更多数据了第二个是累计条数达到响应头里的X-Total-Count提前结束循环避免多打一次空页。两个条件都写上是稳妥做法因为有些接口在请求超出范围时会返回200加空列表有些则会直接返回非200状态码只依赖一个条件容易漏。5.3 把数据落盘顺手做一次去重抓回来的数据存内存里程序一结束就没了实际使用中要落盘。最省事的方式是直接存成JSON文件import json with open(posts.json, w, encodingutf-8) as f: json.dump(all_posts, f, ensure_asciiFalse, indent2)落盘前顺手做个去重。分页过程中如果有新数据插入同一篇文章可能出现在不同页导致重复。最简单的去重方式是按照id字段建一个集合seen_ids set() unique_posts [] for post in all_posts: if post[id] in seen_ids: continue seen_ids.add(post[id]) unique_posts.append(post)这一步看着简单但真实项目里因为没去重导致数据统计翻倍的例子太多了不值得再踩一遍。6. 分页爬虫最容易翻车的三个细节6.1 接口返回空列表不代表你要的数据已经拿完这是我第一次写分页循环时踩过的坑程序某一次请求返回了200状态码但data是空我当时直接break了后来发现漏了很多数据。排查到最后发现是因为请求频率太高服务器在这一页悄悄返回了空数据但状态码依然是200。正常结束和限流返回空之间光靠状态码判断不出来。正确的做法是如果当前页为空但累计数量还没达到总数或者has_more还是true不要急着结束先降低频率、换个时间再重试一到两次。同时打印进度信息看到累计条数突然不再增长立刻停止排查而不是盲目继续跑。6.2 请求太快接口会直接给你颜色看爬虫最忌讳的就是一股脑高频率请求。一次抓几千页的任务每页间隔0.5到1秒实测下来已经比较稳妥如果你用的是免费公共接口请求间隔要更长别让对方服务因为你的脚本出现压力。代码里加个随机延时很简单import time import random time.sleep(random.uniform(0.3, 0.8))同样的请求头最好通过requests.Session()来发它能复用底层的连接效率更高也不容易被服务器当成异常流量。Session的用法很简单就是创建一个session对象然后把requests.get换成session.get。6.3 爬取任何数据前先想清楚合规边界最后这条压轴。学爬虫练技术和实际去抓一个网站的数据完全是两回事。练习阶段优先使用明确开放的公开接口和官方API如果某个接口没有公开文档也不建议为了获取数据去绕过对方的限制。网上常有爬虫被判的新闻绝大多数问题不是爬虫这个技术本身而是被抓取的对象、获取的数据、使用的方式踩了红线。个人学习阶段我建议坚持三条底线只抓公开可访问的数据尊重网站的robots协议和服务条款拿到的数据只用来自学和分析不传播、不商用。养成这个习惯后面用爬虫做任何事你都不会因为数据来源问题睡不着觉。最后分享一个我自己的习惯凡是官网文档里能查到的API接口我都优先用官方API原因很简单——稳定、省得自己维护解析逻辑、也不会被对方限制。学爬虫真正的核心能力是读懂客户端和服务器之间的通信方式把JSON结构分析和分页逻辑这两个基本功练扎实以后遇到再复杂的接口你只需要几分钟就能摸清它的脾气。
返回列表