1. 简介
最近有做APP相关的爬虫,在使用scrapy框架调取post请求时遇到不少意外情况,现在做一下总结。
2.问题总结
在HTTP协议中,post提交的数据必须放在消息主体中,但协议中并没有规定必须使用什么编码方式,从而导致提交方式的不同,服务端根据请求头中的Content-Type字段来获取请求中的消息主体是用何种方式进行编码,从而对消息主体进行解析。
具体的编码方式包括如下:
- application/x-www-form-urlencoded # 以form表单形式提交数据,最常见也是大家最熟悉的
- application/json # 以json串提交数据
- multipart/form-data # 上传文件
下面针对不同的POST请求分别用requests和scrapy做发送
1.提交Form表单
Form表单的post请求一般用于网站登录,用来提交用户名和密码,以Form表单形式发送post请求,只需要将请求的参数构造成一个字典,然后传给相应的参数就可以了。
requests模块请求
url = ‘http://baidu/login'
data = {'name': 'zhangsan', 'password': '123456'}
res = requests.post(url, data=data}
print(res.text)
使用scrapy模块请求
Form表单的post请求在scrapy框架中使用scrapy.FormRequest向目标网站提交数据。
# header信息
headers = {
'Host': 'www.baidu.com',
'Referer': 'http://www.baidu.com/',
'User-Agent': 'okhttp/3.12.0',
}
# 表单需要提交的数据
form_data = {'name': 'John Doe', 'age': '27'}
# 自定义信息,向下层响应(response)传递下去
customer_data = {'key1': 'value1', 'key2': 'value2'}
# scrapy post提交代码
yield scrapy.FormRequest(url = "http://www.baidu.com/post/action",
headers = headers,
method = 'POST',
formdata = form_data, # 表单提交的数据
meta = customer_data, # 自定义,向response传递数据
callback = self.parse,
errback = self.error_handle,
# 如果需要多次提交表单,且url一样,那么就必须加此参数dont_filter,防止被当成重复网页过滤掉了
dont_filter = True
)
上面是正常的post提交,今天遇到的问题是表单提交数据内嵌了字典的形式:
内嵌字典的形式刚开始的写法是这样的:
这样写把头部信息写进去,请求的是一个400的信息和一个400的页面,废了好长时间,最终找到了问题所在。
首先在请求头信息里有一个Content-Length的字段,这个字段的长度本来以为是固定不变的,但看返回信息中如果带着这个Content-Length的字段,请求头里就会有两个Content-Length的字段,这样就表明了这个字段是不需要我们填进去的,Content-Length的字段是根据Form表单里的信息会自动生成在请求头里。而且会发现两个Content-Length的字段的长度不一致,继续研究就会发现我们在传From表单数据时,表单信息必须不能自动分行,且在内嵌字典里不能有空格存在。后来传递的头部信息是这样的:
传递的表单信息是这样的:
而且我们在传的时候应该讲内嵌字典转换成普通的字符串,这是因为在scrapy源码中有一个对字典的value进行to_bytes 编码的过程,详细如下:
# 第一阶段: 字典分解为items
if formdata:
items = formdata.items() if isinstance(formdata, dict) else formdata
querystr = _urlencode(items, self.encoding)
# 第二阶段: 对value,调用 to_bytes 编码
def _urlencode(seq, enc):
values = [(to_bytes(k, enc), to_bytes(v, enc))
for k, vs in seq
for v in (vs if is_listlike(vs) else [vs])]
return urlencode(values, doseq=1)
# 第三阶段: 执行 to_bytes ,参数要求是bytes, str
def to_bytes(text, encoding=None, errors='strict'):
"""Return the binary representation of `text`. If `text`
is already a bytes object, return it as-is."""
if isinstance(text, bytes):
return text
if not isinstance(text, six.string_types):
raise TypeError('to_bytes must receive a unicode, str or bytes '
'object, got %s' % type(text).__name__)
formdata的参数值必须是Unicode、str、bytes object 不能是整数。
2.提交Json串
对于提交json串,主要是发送ajax请求,动态加载数据。
requests模块请求
错误写法
import requests
url = "http://jinbao.pinduoduo.com/network/api/common/goodsList"
data ={"pageSize":60,"pageNumber":1,"withCoupon":0,"sortType":0}
headers = {
'Content-Type':'application/json; charset=UTF-8',
'Host':'jinbao.pinduoduo.com',
'Origin':'http://jinbao.pinduoduo.com',
'Referer':'http://jinbao.pinduoduo.com/',
'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
}
r = requests.post(url=url,data =data,headers=headers)
print(r.text)
打印的内容如下:
{"success":false,"errorCode":4000000,"errorMsg":"System Error","result":null}
即使写上了 ‘Content-Type’:‘application/json; charset=UTF-8’ ,返回依然出错了,原因就在于 你的请求实体的格式错了,服务端无法解码。
正确写法1:
正确代码是把data进行json编码,再发送。代码如下:
r = requests.post(url=url,data=json.dumps(data),headers=headers) # 利用 json 对 字典序列化
正确写法2:
requests 提供了一个json参数,自动使用json方式发送,而且在请求头中也不用显示声明’Content-Type’:‘application/json; charset=UTF-8’ ,代码如下:
import requests
url = "http://jinbao.pinduoduo.com/network/api/common/goodsList"
data ={"pageSize":60,"pageNumber":1,"withCoupon":0,"sortType":0}
headers = {
'Host':'jinbao.pinduoduo.com',
'Origin':'http://jinbao.pinduoduo.com',
'Referer':'http://jinbao.pinduoduo.com/',
'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36',
}
r = requests.post(url=url,json =data,headers=headers) # 直接把字典传给 requests.post() 的 json 参数
print(r.text)
使用scrapy模块请求
3.上传文件
上传文件在爬虫中使用的不多,Content-Type类型为multipart/form-data,以multipart形式发送post请求,只需将一文件传给 requests.post() 的 files参数 即可。还是以 http://httpbin.org/post 为例,代码如下:
url = 'http://httpbin.org/post'
files = {'file': open('upload.txt', 'rb')}
r = requests.post(url, files=files # 文件传给 requests.post() 的 files 参数
print(r.text)