2024-09-30
爬虫
00

目录

引言
浏览器
requests
go
requests-go
tls_client
最终尝试
结语

引言

本文主要探讨python环境下的文件上传请求,包含各种情况以及最终的完美方案

在接到图搜需求的时候,首先抓包看一下图搜的请求是什么样的,看到curl是这个样子的:

bash
curl -X POST '****' -H 'User-Agent: ****' -H 'content-type: multipart/form-data; boundary=BlFoMb-m.gnAh.sIVeQqAVu3Z.1abMrT2QIu8kmAqv-9ttGN5dLrZGkw5eTp3x5YjyGUrp' -H '****: ****' -H 'x-csrftoken: RRGBA6ten9P0gfSoQJsJBAB1chLbYcbe' -H 'x-search-entrance: HOME' -H 'x-api-source: rn' -H '****: ****' -H '****: ****' -H '****: ****' -H 'x-search-image-source: gallery_panel' -H '****: ****' -H 'referer: ****' -H 'accept-language: ko-KR,ko,en-US,en,en-GB' -H 'Cookie: ****' -F 'md5=imagesearch_ce651b0886b615bcd0cad0c0cd7754c6' -F 'language=1' -F 'shop_id=undefined' -F 'item_id=undefined' -F 'result_type=0' -F 'offset=0' -F 'limit=20' -F 'athenaCameraParams={}' -F 'file=@"/var/mobile/Containers/Data/Application/4AB0A55A-10A4-4DC0-A188-85042D232D28/Library/Application Support/tmp/91ffcd2d-5fe8-4ea4-b5aa-cb9b628cb86d";filename="91ffcd2d-5fe8-4ea4-b5aa-cb9b628cb86d"

我们可以看到这个请求和平时我们经常遇到的发送json荷载的post请求不同,multipart/form-data,并且请求体是以-F结尾,当时看到这个请求的时候其实还是有点蒙圈的,然后我去看了一下请求的原始数据,看到下面的内容:

image.png 经过查阅一些资料知道这是一个表单请求,web也有类似的请求,是用的form表单发起的post请求,在浏览器上的请求体长这个样子:

image.png 这个时候我们问题分析已经结束了,下面就要考虑如何发送这个请求了。

浏览器

类似的请求在浏览器上发送是很简单的,我们只需创建一个表单对象然后发送这个表单对象即可,请求头的content-type浏览器会自己帮我们加上

javascript
const form = new FormData(); form.append('images', ''); form.append('thumbnail_size', '120'); fetch('****', { method: 'POST', body: form });

这样我们就可以发送请求了,但是这个是面向自己开发服务的情况,那么我们如果要做爬虫开发,就需要用脚本开发,这个时候就需要考虑用请求库。

requests

python的requests作为我们最常用的请求库,自然是支持这样的请求的。

python
import requests files = { 'images': ('图片.png', open('图片.png', 'rb'), 'image/png'), 'thumbnail_size': (None, '120'), } headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' } response = requests.post('****', headers=headers, files=files)

同时也不需要指定请求的content-type,因为requests会自动帮我们生成请求头的content-type

一般的情况我们分析到这里就可以愉快的发送请求了,但是经过测试发现,请求什么参数都是准确的,但是过不了服务器的风控,这里就是requests的一些局限性,他不能过爬虫的tls检测。这个时候我们就需要找一个支持tls和文件上传的请求库。

go

go语言是天生容易修改tls的信息的,作者在这里放几个有名的go第三方请求库 https://req.cool/zh/docs/tutorial/set-body/

https://github.com/bogdanfinn/tls-client

https://github.com/wangluozhe/requests

由于作者的go语言水平一般,这里就不赘述go的请求怎么写了,有兴趣的同学可以自行尝试,本文主要讨论如何用python实现,接下来我们继续探索python的写法。

requests-go

在写go语言的时候,突然发现了这样一个python第三方库

https://github.com/wangluozhe/requests-go

这个第三方库是开源作者基于go版requests以及python版的requests写的一个python工具,可以十分方便的转化tls信息,同时看源码的时候发现作者也保留了files类型的请求,我们这个时候很愉快的开始写脚本

python
import requests_go as requests from requests_go import tls_config url = "****" tc = { # 这个可以见github怎么获取 } files = [ ('language', (None, '1')), ('result_type', (None, '0')), ('offset', (None, '0')), ('limit', (None, '40')), ('athenaCameraParams', (None, 'undefined')), ] headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' } tls_conf = tls_config.to_tls_config(tc) response = requests.post(url, files=files, headers=headers, tls_config=tls_conf, proxies={ "http": "http://localhost:17890", "https": "http://localhost:17890", }) print(response.json())

经过测试发现,requesst-go可以模拟表单请求,但是表单里面只支持发送字符串,不能发送图片的信息,如果添加图片的话会出现异常:

python
import requests_go as requests from requests_go import tls_config url = "****" tc = { # 这个可以见github怎么获取 } files = [ ('file', ('图片.png', open('图片.png', 'rb'), 'image/png')), ] headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' } tls_conf = tls_config.to_tls_config(tc) response = requests.post(url, files=files, headers=headers, tls_config=tls_conf, proxies={ "http": "http://localhost:17890", "https": "http://localhost:17890", }) print(response.json())

image.png

这里应该是请求库对body进行了编码,但是图片的字节无法被编码,不知道是不是我的写法有问题还是开源作者的一个小bug。

tls_client

这个时候我又去寻找了另外一个第三方库,这个请求库在python的热度也是很高的,群友在github上找到一个写法可以进行文件上传:

python
import tls_client, random, string from io import BytesIO from os.path import basename __all__ = ['File', 'FileUploader'] def randSeq(length: int, chars: str, encode: str = 'ascii'): return bytes().join(random.choice(chars).encode(encode) for _ in range(length)) class File: def __init__(self, file: str|BytesIO, *, content_type: str = None, name: str = None): self.name = name self.content_type = content_type if type(file) == str: self.file = open(file, 'rb') if name == None or len(name) == 0: self.name = basename(self.file.name) elif isinstance(file, BytesIO): self.file = file self.name = name else: raise TypeError("'file' object must be string or BytesIO") def extract(self): return self.name, self.file, self.content_type class FileUploader: def __init__(self, sess: tls_client.Session): self.new_session(sess) def new_session(self, sess: tls_client.Session): self.sess = sess self.reset() def reset(self): self.__new_boundary() self.body = b'' def __new_boundary(self): self.boundary = b"----WebKitFormBoundary" + randSeq(16, string.ascii_lowercase + string.ascii_uppercase + string.digits) def __generate_file_header(self, name: str, filename: str = None, content_type: str = None): header = b'Content-Disposition: form-data; name="' + name.encode() + b'"; ' if filename != None: header += b'filename="' + filename.encode() + b'"; ' if content_type != None: header += b'\r\nContent-Type: ' + content_type.encode() return header def addFile(self, name: str, file: File): filename, fp, mime = file.extract() self.body = b'--' + self.boundary + b'\r\n' self.body += self.__generate_file_header(name, filename, mime) self.body += b'\r\n\r\n' self.body += fp.read() self.body += b'\r\n--' + self.boundary + b'--\r\n' def upload(self, url: str, *, files = None, data = None, json = None, **kwargs): headers = kwargs.pop('headers', dict()) headers['Content-Type'] = 'multipart/form-data; boundary=' + self.boundary.decode() return self.sess.post(url, data=self.body, headers=headers, **kwargs)

用法是:

python
session = tls_client.Session(client_identifier="chrome_120", random_tls_extension_order=True) url = "****" headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' } form = FileUploader(session) img = File('图片.png', content_type="image/png") form.addFile("images", img) resp = form.upload(url=url, headers=headers, proxy="http://127.0.0.1:17890") print(resp.text)

这样确实是可以发送表单格式的请求,但是缺点是只能添加一个文件,没发添加多个文件。(也可能是我自己不会写)

最终尝试

经过上面几种尝试方法,似乎文件上传的方法都有缺陷,难道python真的不能做到完美的文件上传请求并且模拟tls指纹吗(不进行大量二次开发的前提,当然有实力的同学可以重写一个requests做到完美tls)

这个时候一个第三方库引起了我的注意: https://github.com/requests/toolbelt

这个库可以帮助我们编写表单信息以及content-type

python
from requests_toolbelt.multipart.encoder import MultipartEncoder import hashlib import tls_client session = tls_client.Session(client_identifier="chrome_120", random_tls_extension_order=True) encoder = MultipartEncoder( fields=[ ('language', (None, '1')), ('file', ('图片.png', open('图片.png', 'rb'), 'image/png')), ] ) headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' 'content-type': encoder.content_type, } response = session.post( "****", headers=headers, data=encoder.to_string(), proxy="http://127.0.0.1:17890" ) print(response.json())

我们只要这样就可以做到文件上传以及模拟tls指纹了

结语

我们使用MultipartEncoder用requests-go发送请求同样也会出现前文的异常,所以目前还是用tls_client发起请求比较好。

这次也学到了很多新的知识,自己也摸索了几天,尝试了各种方法,最后在群友的分享整合出了这个方案,所以也写这一篇博客分享。

附上成功请求数据的图片哈哈哈

image.png

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:回锅炒辣椒

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!