本文主要探讨python环境下的文件上传请求,包含各种情况以及最终的完美方案
在接到图搜需求的时候,我们首先抓包看一下图搜的请求是什么样的,看到curl是这个样子的:
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结尾,当时看到这个请求的时候其实还是有点蒙圈的,然后我去看了一下请求的原始数据,看到下面的内容:
经过查阅一些资料知道这是一个表单请求,web也有类似的请求,是用的form表单发起的post请求,在浏览器上的请求体长这个样子:
这个时候我们问题分析已经结束了,下面就要考虑如何发送这个请求了。
浏览器
类似的请求在浏览器上发送是很简单的,我们只需创建一个表单对象然后发送这个表单对象即可,请求头的content-type浏览器会自己帮我们加上
const form = new FormData();
form.append('images', '');
form.append('thumbnail_size', '120');
fetch('****', {
method: 'POST',
body: form
});
这样我们就可以发送请求了,但是这个是面向自己开发服务的情况,那么我们如果要做爬虫开发,就需要用脚本开发,这个时候就需要考虑用请求库。
requests
python的requests作为我们最常用的请求库,自然是支持这样的请求的。
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类型的请求,我们这个时候很愉快的开始写脚本
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可以模拟表单请求,但是表单里面只支持发送字符串,不能发送图片的信息,如果添加图片的话会出现异常:
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())
这里应该是请求库对body进行了编码,但是图片的字节无法被编码,不知道是不是我的写法有问题还是开源作者的一个小bug。
tls_client
这个时候我又去寻找了另外一个第三方库,这个请求库在python的热度也是很高的,群友在github上找到一个写法可以进行文件上传:
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)
用法是:
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
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发起请求比较好。
这次也学到了很多新的知识,自己也摸索了几天,尝试了各种方法,最后在群友的分享整合出了这个方案,所以也写这一篇博客分享。
附上成功请求数据的图片哈哈哈
更多优质文章见个人博客:https://xsblog.site/