ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

爬虫三类Payload全解析:表单、JSON与混合格式的构造与排坑

爬虫三类Payload全解析:表单、JSON与混合格式的构造与排坑 做爬虫的人十个里有八个被payload坑过。尤其是刚入门的阶段明明URL、headers、cookie全都核对过了请求就是返回400或者参数错误最后排查半天才发现问题出在请求体body里的payload上。今天这篇就把我在实际抓取中反复处理的三种payload信息统一捋一遍表单类payload、JSON类payload、还有混合复杂类payload。每一类我会讲清楚它长什么样、怎么构造、怎么排查以及哪些地方最容易翻车。“payload”这个词在爬虫语境里指的就是HTTP请求中真正传给服务器的数据体。你可以把它理解成你去食堂打饭时递过去的菜单——URL是你要去的窗口headers是你的饭卡payload才是你具体点了什么菜、要几分辣。服务器拿到请求后真正解析执行的就是这部分内容。所以payload一旦搞错整个请求基本就废了。这篇文章适合正在学爬虫Python入门的人、被某个接口卡住一两天的开发者、以及想系统理解requests库中data、json、files参数到底有什么区别的读者。我会尽量用实际案例来讲不做教科书式的名词堆砌。1. 先搞清楚payload到底是个什么角色1.1 从HTTP请求的角度理解payload很多人刚学爬虫时习惯把注意力放在URL和headers上因为这两个东西在浏览器开发者工具里一眼就能看到而payload藏在请求体里点开“Payload”或“Request Body”标签才能看到。但恰恰是这个不起眼的区域决定了你是在模拟一个真实请求还是在发一个被服务器一眼识破的空壳请求。HTTP请求按携带数据的位置可以分成三块URL参数query string、请求头headers、请求体body。其中URL参数和请求体里的内容合在一起就是广义上的payload。不过在日常爬虫开发中大家说的payload大多数时候指请求体内容也就是POST请求里的body部分。GET请求因为自身特点一般不携带请求体参数全拼在URL后面这部分处理起来相对简单只要注意urlencode编码就行。POST请求的payload才是重头戏。服务器如何解析你的请求体完全取决于Content-Type请求头的值。如果Content-Type是application/x-www-form-urlencoded服务器就按keyvaluekey2value2的方式解析如果是application/json服务器就按JSON格式解析如果是multipart/form-data服务器则会按照带boundary分隔线的格式去处理。你payload里的数据格式必须和Content-Type匹配否则服务器解析出来就是一堆乱码或者直接返回错误。我举个例子你就明白了。假设你要登录一个网站在浏览器里填写用户名和密码表单提交时浏览器自动把数据编码成usernameadminpassword123456这种格式然后在请求头里加上Content-Type: application/x-www-form-urlencoded。如果你用Python的requests库发请求时只写了data{username: admin, password: 123456}requests会自动帮你编码并设置Content-Type这没问题。但如果你手动把data换成json.dumps({username: admin})同时没有改Content-Type那服务器按表单格式解析这串JSON文本拿到的key就变成了{username: 这种奇怪的字符串登录必然失败。1.2 三种典型payload格式的常见场景根据我这些年抓取不同网站的经验爬虫中真正高频出现的payload其实就三大类每一类都有其典型的应用场景第一类是表单格式application/x-www-form-urlencoded常见于各类传统网站的登录接口、搜索接口、分页加载接口。这类接口后端通常用Java的request.getParameter、PHP的$_POST、Python Flask的request.form来取值格式简单直接参数都是扁平的键值对。第二类是JSON格式application/json常见于前后端分离的现代Web应用、移动端App接口、小程序接口。现在前端框架流行很多后端接口直接接收JSON对象参数可以嵌套、可以传数组、可以传对象结构比表单复杂但可读性好。第三类是混合复杂类型包括multipart/form-data文件上传、GraphQL的查询体、JSON-RPC的调用结构、以及一些加密后的自定义格式。这类payload处理起来最麻烦因为格式不标准或者参数经过了额外处理需要特殊对待。搞清楚了这三类再遇到任何接口你就知道该用什么姿势去处理了。下面我分别细说。2. 表单类payload的解析与构造2.1 表单payload的特征和抓取方法表单类payload是最古老、也最容易处理的一类但容易处理不代表不会踩坑。这类payload的特征非常明显请求头里Content-Type是application/x-www-form-urlencoded正文是一串用连接的键值对比如usernametestuserpasswordabc123remember1值里面如果有特殊字符会被URL编码比如中文会被编码成%E4%BD%A0%E5%A5%BD这种形式空格会被编码成号。这一点很关键很多人手工复制浏览器里的payload时容易忽略编码问题导致请求发出去后服务器收到的是乱码。抓取这类payload最简单的办法是打开浏览器开发者工具切到Network面板刷新页面并触发目标请求找到对应的接口后在Headers里往下拉能看到Form Data区域里面就是浏览器实际发送的表单内容。有些浏览器比如Chrome还会直接给你展示“view source”选项点开能看到原始的编码后字符串。如果你用抓包工具Fiddler、Charles或Burp Suite步骤也类似拦截请求后在Inspectors的WebForms标签页里能看到解析后的表单参数在Raw标签页里能看到原始报文。我个人习惯先用浏览器开发者工具看一遍结构再用抓包工具确认原始字节内容因为有时候浏览器会隐藏一些自动添加的字段比如某些前端框架自动加的csrf token这部分在原始报文里才看得到。2.2 实际构造演示与常见坑用Python的requests库构造表单payload非常简单直接传data参数即可import requests url https://example.com/api/login payload { username: testuser, password: abc123, remember: 1 } response requests.post(url, datapayload) print(response.text)这段代码里requests会做三件事把payload字典urlencode成usernametestuserpasswordabc123remember1自动设置Content-Type为application/x-www-form-urlencoded然后发送请求。整个过程看起来没问题但实际使用中有几个坑我踩过不止一次第一某些服务器会严格校验Content-Type如果requests自动设置的Content-Type里带了charset参数比如application/x-www-form-urlencoded; charsetutf-8部分老旧的服务器可能不认识。解决办法是手动指定headers中的Content-Type去掉charset部分。第二如果表单中某个参数的值含有中文字符要特别注意编码方式。requests默认会用utf-8进行urlencode但某些老系统要求gbk编码。这时候需要手动编码payload { keyword: 爬虫.encode(gbk) }这样requests在urlencode时就会按gbk处理。第三表单参数的顺序在某些极端情况下是有意义的。正常来说服务器解析键值对不关心顺序但有些后端框架会把所有参数按字典序排序后再做签名校验。如果你发现请求参数一样但总是签名失败可以对比一下浏览器实际发送的参数顺序必要时用有序字典collections.OrderedDict或自己拼接原始字符串来构造payload。第四注意复选框和多值参数。HTML表单里同名复选框可以勾选多个提交后变成nameanameb这样的格式。在requests里对应的方法是传入列表payload { hobby: [reading, coding, gaming] }requests会自动展开成hobbyreadinghobbycodinghobbygaming。这块如果你不小心传成字符串[reading, coding]服务器收到的就是一段Python列表的文本表示取值时自然对不上。3. JSON payload的解析与处理3.1 JSON payload的特征和动态参数随着前后端分离架构的普及JSON payload已经成为现在爬虫遇到最多的格式。它的特征很直观Content-Type为application/json请求体是一段符合JSON格式的文本比如{ page: 1, pageSize: 20, filters: { category: electronics, priceRange: [100, 500] }, sort: sales_desc }相比表单格式JSON结构可以表达复杂的嵌套关系——列表、字典、多层嵌套都支持。这使得它在处理搜索条件、筛选条件、批量操作等复杂业务时非常有优势。但同时也意味着如果你构造JSON时某个字段的层级错了、类型错了比如整数写成了字符串接口就会返回参数校验失败。用requests构造JSON payload有两种方式import requests url https://example.com/api/search payload { page: 1, pageSize: 20, filters: { category: electronics } } # 方式一直接用json参数 response requests.post(url, jsonpayload) # 方式二手动序列化并指定Content-Type import json headers {Content-Type: application/json} response requests.post(url, datajson.dumps(payload), headersheaders)两种方式本质上一样第一种更简洁。但第二种方式在一些需要自定义JSON序列化逻辑的场景下更有用比如遇到datetime对象时需要自定义default处理。JSON payload处理起来真正的难点不在格式本身而在于动态参数。很多网站为了防止自动化采集会在JSON body里附加一些随时间或会话变化的字段最常见的是timestamp和sign。timestamp就是当前时间戳sign则是对请求参数进行某种算法运算后得到的签名值。服务器收到请求后会用同样的算法重新计算判断两个值是否一致不一致就拒绝请求。3.2 应对动态签名与埋点参数处理带签名的JSON payload核心思路就一句话找到签名生成逻辑在本地复现它。我拿一个实际例子来说。之前我采集过一个电商平台的商品列表接口它的JSON payload长这样{ page: 1, pageSize: 20, keyword: 手机, timestamp: 1712345678901, sign: a1b2c3d4e5f6... }初看以为sign是随便生成的但经过对比多个请求发现sign的算法是把所有业务参数page、pageSize、keyword、timestamp按照key的字母序排序拼成字符串再加上一个固定的salt值最后做MD5。比如import hashlib import time def generate_sign(params, salts3cr3t): sorted_keys sorted(params.keys()) raw_string for key in sorted_keys: raw_string f{key}{params[key]} raw_string fsalt{salt} return hashlib.md5(raw_string.encode(utf-8)).hexdigest() payload { page: 1, pageSize: 20, keyword: 手机, timestamp: int(time.time() * 1000), } payload[sign] generate_sign(payload)找到规律后本地复现就很简单。难的是那些把签名逻辑放在JavaScript里、经过Webpack打包混淆的站点。这种情况下你有几条路可以走第一硬啃JavaScript代码。把浏览器里加载的JS文件下载下来搜索sign、md5、sha256、encrypt、token等关键词找到加密函数用Python重写一遍。这条路最稳定但费时费力适合JS逻辑不复杂的情况。第二用PyExecJS或Js2Py直接执行JS代码。把提取出来的加密函数原封不动地搬过来用Python调用。这种方式的好处是不需要重写算法坏处是如果JS依赖了浏览器环境比如window对象、document对象在Python里跑不起来。我之前用PyExecJS执行某个网站的登录加密逻辑折腾半天发现它依赖了浏览器的localStorage最后只能改用第三种方式。第三用Selenium或Playwright等浏览器自动化工具在真实浏览器环境里执行JS获取加密结果。这种方式最稳但速度慢、资源消耗大一般只在其他方法搞不定时使用。除了签名之外JSON payload里还常见一些“埋点参数”比如deviceId、fingerprint、uuid等。这些参数通常是在页面加载时生成的存在cookie或localStorage里后续请求会带上。处理思路就是首次访问页面时把这些参数抓下来后续请求时填入。要注意的是有些站点会校验参数的合法性和关联性比如deviceId必须和cookie里的某个值对应这时候就需要保持会话一致用requests.Session来维持cookie和headers。4. 复杂混合型payload与特殊边界情况4.1 multipart和GraphQL等特殊格式前面两类是主流但爬虫真正让人头疼的往往是那些非常规的payload。我挑了三种比较有代表性的情况来说multipart文件上传、GraphQL查询、JSON-RPC调用。multipart/form-data格式常见于上传图片、上传文件的接口比如用户头像上传、商品图片上传、批量导入Excel等。它的请求体不是简单的键值对而是用boundary分隔线把多个字段块分开每个字段块内部包含了Content-Disposition、Content-Type和具体内容。长这样------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; namefile; filenamephoto.jpg Content-Type: image/jpeg [二进制内容] ------WebKitFormBoundary7MA4YWxkTrZu0gW--用requests构造multipart请求非常简单直接传files参数url https://example.com/api/upload files { file: (photo.jpg, open(photo.jpg, rb), image/jpeg) } response requests.post(url, filesfiles)requests会自动生成boundary并设置Content-Type。这里容易踩坑的点是除了文件字段有时接口还需要同时传业务参数比如上传图片时附带说明文字。这时候data参数和files参数可以同时传requests会把data里的普通字段和files里的文件字段合并到同一个multipart body里。GraphQL接口现在也越来越多。它的特点是单端点通常只有一个URL所有查询都通过POST请求发送请求体里是GraphQL查询语句和变量。payload结构固定为{ query: query($id: ID!) { user(id: $id) { name email } }, variables: { id: 123 } }处理GraphQL接口时最需要关注的是query字段的内容。有的站点把查询语句写得很复杂嵌套很深你必须完全复现它的查询结构少一个字段都可能返回报错。最好的办法是在开发者工具里直接复制浏览器发出的GraphQL查询语句然后在Python里原样填入只修改variables部分。JSON-RPC常见于一些内部系统或者去中心化应用的接口。它的请求体结构固定为{ jsonrpc: 2.0, method: getBlockByNumber, params: [0x10, true], id: 1 }处理这类接口时注意两点method必须和文档一致params可以是数组也可以是对象id字段一般用自增数字就行但某些严格实现会校验id的唯一性所以建议每个请求生成不同的id。4.2 payload中的编码、混淆与防御识别比格式更麻烦的是编码和混淆。有些接口的payload肉眼看着是JSON但内容完全不可读比如{ data: 8f8f8f8f8f8f8f8f8f8f }这种十六进制字符串一看就是加密后的数据。这种情况下你需要先弄清楚加密发生在哪个环节。常见的情况有几种一是前端先对业务数据做AES或RSA加密再放到JSON里传输二是整个payload外层套了一层加密本质上只有解密后才能看到真正的业务字段。面对这种情况我的处理思路是分步走。先用浏览器调试工具在JS代码里打断点找到加密函数和密钥然后在Python端用对应的算法复现加密过程最后把加密后的结果填入payload。比如某个接口用AES-128-CBC加密密钥和IV都硬编码在JS文件里那你用pycryptodome库就能轻松实现。另外要留意的是有些站点会在payload里塞一些“蜜罐”字段——这些字段对业务没有任何影响但爬虫程序如果漏掉了就会被判定为异常请求。比如一个列表接口payload里有个字段叫fakeField值为固定的please_ignore_me看起来无用但必须带上。这种字段在浏览器里不会显示只有在完整报文里才能发现。所以我的习惯是每次抓包时都看Raw报文不要只看浏览器格式化后的参数视图。还有一类比较恶心的处理是参数编码嵌套。举个例子某个接口的payload从外表看是表单格式但其中一个参数的值本身是URL编码后的JSON字符串data%7B%22page%22%3A1%2C%22keyword%22%3A%22test%22%7D服务器收到后先URL解码再按JSON解析这个参数值。处理这种嵌套时需要先在内层用json.dumps构造JSON字符串再做URL编码最后传入外层表单数据import json from urllib.parse import quote inner_data json.dumps({page: 1, keyword: test}) payload { data: quote(inner_data) # 注意这里的编码 }很多时候你发现接口总是报参数错误排查到最后才发现是这种嵌套编码的问题。5. 常见问题排查与调试技巧5.1 payload出错时该怎么定位我见过太多人在payload上翻车而且翻车的姿势五花八门。为了帮你快速定位问题我把这些年的排查经验总结成一张速查表现象可能原因解决办法服务器返回400 Bad RequestContent-Type与payload格式不匹配核对请求头Content-Type确认是form还是json返回403 Forbidden缺少动态签名或签名计算错误检查sign、token等字段的生成逻辑返回200但数据为空payload字段名与服务器期望不一致对比浏览器实际发送的字段名和值中文乱码urlencode编码不是服务器期望的字符集尝试gbk或其他字符集编码某些参数总是被忽略参数嵌套层级错误检查JSON结构确认字段层级和类型偶发失败payload里有时间戳或随机数参数确认这些参数是否过期动态更新同一个payload用Postman可以但爬虫不行headers中cookie或user-agent不一致复制浏览器完整headers到requests中服务器返回参数校验失败类型不对如整数传成字符串对照抓包内容检查字段类型排查payload问题我的建议是遵循“对比法”先用浏览器正常操作抓包记录一个真实请求然后用Python原样复现这个请求确保返回结果一致最后再逐步修改参数改成动态化。如果一开始就自己凭空构造payload等于把“环境差异”和“参数错误”混在一起排查起来非常费劲。5.2 个人积累的实用工具与调试方法我在处理payload时有几个工具和习惯帮了大忙分享给你。第一个是Fiddler或Charles之类的抓包工具。虽然浏览器开发者工具足够应付大部分场景但抓包工具能让你直接看到原始字节流、修改请求后重放、对比不同请求的差异。尤其是修改重放这个功能我在调试签名参数时经常用先发一个浏览器正常请求保存在抓包工具里然后复制到Python中如果Python请求的响应和浏览器不一致我再回到抓包工具里逐步修改参数来定位是哪个字段导致的。第二个是Postman或Apifox。当Python代码调不通一个接口时我会先用Postman手动构造一遍确认接口本身是否正常排除是不是自己的代码写错了。Postman里还能生成Python代码片段哪怕不全依赖它参考生成的代码也能帮你发现requests库的一些细节用法。第三个是我自己写的一个小工具脚本用Flask在本机开一个HTTP服务器打印所有收到的请求细节。有时候某个接口回调我们自己的服务器比如支付回调、webhook想查看回调payload长什么样就把它指向本地服务器payload的所有细节一目了然。from flask import Flask, request app Flask(__name__) app.route(/callback, methods[POST, GET]) def callback(): print(Headers:, dict(request.headers)) print(Body:, request.get_data(as_textTrue)) return ok if __name__ __main__: app.run(host0.0.0.0, port8080)当你的爬虫程序同时服务于多个目标站点时这种本地调试服务器几乎成了标配。还有一个经验是在爬虫代码里打印完整的请求信息用于后续排查。requests库本身没有直接打印请求报文的功能但你可以用一个简单的钩子函数来实现import requests from requests.structures import CaseInsensitiveDict def log_request(request): print(fURL: {request.url}) print(fMethod: {request.method}) print(fHeaders: {dict(request.headers)}) print(fBody: {request.body}) # 通过事件钩子查看请求 def on_request(response, *args, **kwargs): log_request(response.request) return response session requests.Session() session.hooks[response] [on_request]这样每次请求发送前实际是收到响应后回调都能看到完整的请求信息不会再出现“代码跑了一百遍也不知道发出去的是什么”的情况。说到排查问题这里多提醒一句遇到payload相关的问题先确认是不是编码问题再确认是不是签名问题最后再怀疑是不是反爬策略。因为编码问题最隐蔽但最好解决签名问题复杂度居中反爬策略排查成本最高。按这个优先级排查效率会高很多。还有个小技巧用curl命令快速验证。把浏览器里的请求复制成curl命令在开发者工具Network面板里右键Copy as cURL在终端里执行一次如果curl能正常返回说明请求本身没问题问题出在Python代码上如果curl也返回异常那就是请求构造本身不对。这个“浏览器-curl-Python”三步法能帮你快速缩小问题范围。6. 写在最后的几点实操体会做爬虫这几年payload这块我吃了不少亏也积累了一些判断经验。这里分享几个我个人的习惯不一定适合所有人但至少能帮你少走弯路。第一个习惯是拿到一个新接口我从不跳过抓包这一步直接写代码。哪怕接口看起来再简单我都会先完整看一遍浏览器实际发送的报文——包括URL、所有headers、完整的body。因为浏览器会帮你处理很多隐含的内容比如自动添加的cookie、token、Content-Type等只看表面的Form Data很容易漏掉关键信息。第二个习惯是能用session就用session。requests.Session会自动管理cookie、维持连接池还能方便地设置默认headers。很多payload里的动态参数比如csrf token其实是服务器通过cookie下发的用session能保证cookie的一致性减少很多麻烦。第三个习惯是注意payload里的空值。有些字段在特定场景下不传或传空字符串在另一些场景下传null或0。这个细微差异往往决定接口是正常返回还是报错。我碰到过一个个例列表接口的分页参数pageSize正常请求传20但如果传0服务器会返回一个特殊的错误提示如果完全不传这个字段服务器则默认返回全量数据。最后我要特别强调一下合规问题。爬虫技术本身是中性的但使用它时要遵守目标网站的robots协议、服务条款和相关法律法规。本文中所有示例都基于虚构或授权场景实际开发中请务必确保你的爬虫行为合法合规不要对目标服务器造成过大压力。技术能力越强越需要自律。
返回列表