scrapy爬虫:scrapy.FormRequest中formdata参数详解


Posted in Python onApril 30, 2020

1. 背景

在网页爬取的时候,有时候会使用scrapy.FormRequest向目标网站提交数据(表单提交)。参照scrapy官方文档的标准写法是:

# header信息
unicornHeader = {
  'Host': 'www.example.com',
  'Referer': 'http://www.example.com/',
}

# 表单需要提交的数据
myFormData = {'name': 'John Doe', 'age': '27'}

# 自定义信息,向下层响应(response)传递下去
customerData = {'key1': 'value1', 'key2': 'value2'}

yield scrapy.FormRequest(url = "http://www.example.com/post/action",
             headers = unicornHeader,
             method = 'POST',       # GET or POST
             formdata = myFormData,    # 表单提交的数据
             meta = customerData,    # 自定义,向response传递数据
             callback = self.after_post,
             errback = self.error_handle,
             # 如果需要多次提交表单,且url一样,那么就必须加此参数dont_filter,防止被当成重复网页过滤掉了
             dont_filter = True   
             )

但是,当表单提交数据myFormData 是形如字典内嵌字典的形式,又该如何写?

2. 案例 — 参数为字典

在做亚马逊网站爬取时,当进入商家店铺,爬取店铺内商品列表时,发现采取的方式是ajax请求,返回的是json数据。

请求信息如下:

scrapy爬虫:scrapy.FormRequest中formdata参数详解

scrapy爬虫:scrapy.FormRequest中formdata参数详解

响应信息如下:

scrapy爬虫:scrapy.FormRequest中formdata参数详解

如上图所示,From Data中的数据包含一个字典:

marketplaceID:ATVPDKIKX0DER
seller:A2FE6D62A4WM6Q
productSearchRequestData:{"marketplace":"ATVPDKIKX0DER","seller":"A2FE6D62A4WM6Q","url":"/sp/ajax/products","pageSize":12,"searchKeyword":"","extraRestrictions":{},"pageNumber":"1"}

# formDate 必须构造如下:
myFormData = {
  'marketplaceID' : 'ATVPDKIKX0DER',
  'seller' : 'A2FE6D62A4WM6Q',
  # 注意下面这一行,内部字典是作为一个字符串的形式
  'productSearchRequestData' :'{"marketplace":"ATVPDKIKX0DER","seller":"A2FE6D62A4WM6Q","url":"/sp/ajax/products","pageSize":12,"searchKeyword":"","extraRestrictions":{},"pageNumber":"1"}'
}

在amazon中实际使用的构造方法如下:

def sendRequestForProducts(response):
  ajaxParam = response.meta
  for pageIdx in range(1, ajaxParam['totalPageNum']+1):
    ajaxParam['isFirstAjax'] = False
    ajaxParam['pageNumber'] = pageIdx

    unicornHeader = {
      'Host': 'www.amazon.com',
      'Origin': 'https://www.amazon.com',
      'Referer': ajaxParam['referUrl'],
    }

    '''
    marketplaceID:ATVPDKIKX0DER
    seller:AYZQAQRQKEXRP
    productSearchRequestData:{"marketplace":"ATVPDKIKX0DER","seller":"AYZQAQRQKEXRP","url":"/sp/ajax/products","pageSize":12,"searchKeyword":"","extraRestrictions":{},"pageNumber":1}
    '''

    productSearchRequestData = '{"marketplace": "ATVPDKIKX0DER", "seller": "' + f'{ajaxParam["sellerID"]}' + '","url": "/sp/ajax/products", "pageSize": 12, "searchKeyword": "","extraRestrictions": {}, "pageNumber": "' + str(pageIdx) + '"}'
    formdataProduct = {
      'marketplaceID': ajaxParam['marketplaceID'],
      'seller': ajaxParam['sellerID'],
      'productSearchRequestData': productSearchRequestData
    }
    productAjaxMeta = ajaxParam
    # 请求店铺商品列表
    yield scrapy.FormRequest(
      url = 'https://www.amazon.com/sp/ajax/products',
      headers = unicornHeader,
      formdata = formdataProduct,
      func = 'POST',
      meta = productAjaxMeta,
      callback = self.solderProductAjax,
      errback = self.error, # 处理http error
      dont_filter = True,  # 需要加此参数的
    )

3. 原理分析

举例来说,目前有如下一笔数据:

formdata = {
      'Field': {"pageIdx":99, "size":"10"},
      'func': 'nextPage',
      }

从网页上,可以看到请求数据如下:

Field=%7B%22pageIdx%22%3A99%2C%22size%22%3A%2210%22%7D&func=nextPage

第一种,按照如下方式发出请求,结果如下(正确):

yield scrapy.FormRequest(
      url = 'https://www.example.com/sp/ajax',
      headers = unicornHeader,
      formdata = {
            'Field': '{"pageIdx":99, "size":"10"}',
            'func': 'nextPage',
            },
      func = 'POST',
      callback = self.handleFunc,
    )
# 请求数据为:Field=%7B%22pageIdx%22%3A99%2C%22size%22%3A%2210%22%7D&func=nextPage

第二种,按照如下方式发出请求,结果如下(错误,无法获取到正确的数据):

yield scrapy.FormRequest(
      url = 'https://www.example.com/sp/ajax',
      headers = unicornHeader,
      formdata = {
            'Field': {"pageIdx":99, "size":"10"},
            'func': 'nextPage',
            },
      func = 'POST',
      callback = self.handleFunc,
    )
# 经过错误的编码之后,发送的请求为:Field=size&Field=pageIdx&func=nextPage

我们跟踪看一下scrapy中的源码:

# E:/Miniconda/Lib/site-packages/scrapy/http/request/form.py

# FormRequest
class FormRequest(Request):

  def __init__(self, *args, **kwargs):
    formdata = kwargs.pop('formdata', None)
    if formdata and kwargs.get('func') is None:
      kwargs['func'] = 'POST'

    super(FormRequest, self).__init__(*args, **kwargs)

    if formdata:
      items = formdata.items() if isinstance(formdata, dict) else formdata
      querystr = _urlencode(items, self.encoding)
      if self.func == 'POST':
        self.headers.setdefault(b'Content-Type', b'application/x-www-form-urlencoded')
        self._set_body(querystr)
      else:
        self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr)

  # 关键函数 _urlencode
  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)

分析过程如下:

# 第一步:items = formdata.items() if isinstance(formdata, dict) else formdata
# 第一步结果:经过items()方法执行后,原始的dict格式变成如下列表形式:
   dict_items([('func', 'nextPage'), ('Field', {'size': '10', 'pageIdx': 99})])

# 第二步:再经过后面的 _urlencode方法将items转换成如下:
  [(b'func', b'nextPage'), (b'Field', b'size'), (b'Field', b'pageIdx')]

# 可以看到就是在调用 _urlencode方法的时候出现了问题,上面的方法执行过后,会使字典形式的数据只保留了keys(value是字典的情况下,只保留了value字典中的key).

解决方案: 就是将字典当成普通的字符串,然后编码(转换成bytes),进行传输,到达服务器端之后,服务器会反过来进行解码,得到这个字典字符串。然后服务器按照Dict进行解析。

拓展:对于其他特殊类型的数据,都按照这种方式打包成字符串进行传递。

4. 补充1 ——参数类型

formdata的 参数值 必须是unicode , str 或者 bytes object,不能是整数。

案例:

yield FormRequest(
  url = 'https://www.amztracker.com/unicorn.php',
  headers = unicornHeader,
  # formdata 的参数必须是字符串
  formdata={'rank': 10, 'category': productDetailInfo['topCategory']},
  method = 'GET',
  meta = {'productDetailInfo': productDetailInfo},
  callback = self.amztrackerSale,
  errback = self.error, # 本项目中这里触发errback占绝大多数
  dont_filter = True, # 按理来说是不需要加此参数的
)

# 提示如下ERROR:
Traceback (most recent call last):
 File "E:\Miniconda\lib\site-packages\scrapy\utils\defer.py", line 102, in iter_errback
  yield next(it)
 File "E:\Miniconda\lib\site-packages\scrapy\spidermiddlewares\offsite.py", line 29, in process_spider_output
  for x in result:
 File "E:\Miniconda\lib\site-packages\scrapy\spidermiddlewares\referer.py", line 339, in <genexpr>
  return (_set_referer(r) for r in result or ())
 File "E:\Miniconda\lib\site-packages\scrapy\spidermiddlewares\urllength.py", line 37, in <genexpr>
  return (r for r in result or () if _filter(r))
 File "E:\Miniconda\lib\site-packages\scrapy\spidermiddlewares\depth.py", line 58, in <genexpr>
  return (r for r in result or () if _filter(r))
 File "E:\PyCharmCode\categorySelectorAmazon1\categorySelectorAmazon1\spiders\categorySelectorAmazon1Clawer.py", line 224, in parseProductDetail
  dont_filter = True,
 File "E:\Miniconda\lib\site-packages\scrapy\http\request\form.py", line 31, in __init__
  querystr = _urlencode(items, self.encoding)
 File "E:\Miniconda\lib\site-packages\scrapy\http\request\form.py", line 66, in _urlencode
  for k, vs in seq
 File "E:\Miniconda\lib\site-packages\scrapy\http\request\form.py", line 67, in <listcomp>
  for v in (vs if is_listlike(vs) else [vs])]
 File "E:\Miniconda\lib\site-packages\scrapy\utils\python.py", line 117, in to_bytes
  'object, got %s' % type(text).__name__)
TypeError: to_bytes must receive a unicode, str or bytes object, got int

# 正确写法:
formdata = {'rank': str(productDetailInfo['topRank']), 'category': productDetailInfo['topCategory']},

原理部分(源代码):

# 第一阶段: 字典分解为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__)

5. 补充2 ——参数为中文

formdata的 参数值 必须是unicode , str 或者 bytes object,不能是整数。

以1688网站搜索产品为案例:

搜索信息如下(搜索关键词为:动漫周边):

scrapy爬虫:scrapy.FormRequest中formdata参数详解

可以看到 动漫周边 == %B6%AF%C2%FE%D6%DC%B1%DF

# scrapy中这个请求的构造如下

# python3 所有的字符串都是unicode
unicornHeaders = {
  ':authority': 's.1688.com',
  'Referer': 'https://www.1688.com/',
}

# python3 所有的字符串都是unicode
# 动漫周边 tobyte为:%B6%AF%C2%FE%D6%DC%B1%DF
formatStr = "动漫周边".encode('gbk')

print(f"formatStr = {formatStr}")
yield FormRequest(
  url = 'https://s.1688.com/selloffer/offer_search.htm',
  headers = unicornHeaders,
  formdata = {'keywords': formatStr, 'n': 'y', 'spm': 'a260k.635.1998096057.d1'},
  method = 'GET',
  meta={},
  callback = self.parseCategoryPage,
  errback = self.error, # 本项目中这里触发errback占绝大多数
  dont_filter = True, # 按理来说是不需要加此参数的
)

# 日志如下:
formatStr = b'\xb6\xaf\xc2\xfe\xd6\xdc\xb1\xdf'
2017-11-16 15:11:02 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://sec.1688.com/query.htm?smApp=searchweb2&smPolicy=searchweb2-selloffer-anti_Spider-seo-html-checklogin&smCharset=GBK&smTag=MTE1LjIxNi4xNjAuNDYsLDU5OWQ1NWIyZTk0NDQ1Y2E5ZDAzODRlOGM1MDI2OTZj&smReturn=https%3A%2F%2Fs.1688.com%2Fselloffer%2Foffer_search.htm%3Fkeywords%3D%25B6%25AF%25C2%25FE%25D6%25DC%25B1%25DF%26n%3Dy%26spm%3Da260k.635.1998096057.d1&smSign=05U0%2BJXfKLQmSbsnce55Yw%3D%3D> from <GET https://s.1688.com/selloffer/offer_search.htm?keywords=%B6%AF%C2%FE%D6%DC%B1%DF&n=y&spm=a260k.635.1998096057.d1>
# https://s.1688.com/selloffer/offer_search.htm?keywords=%B6%AF%C2%FE%D6%DC%B1%DF&n=y&spm=a260k.635.1998096057.d1

以上这篇scrapy爬虫:scrapy.FormRequest中formdata参数详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python抓取网页内容示例分享
Feb 24 Python
python通过zlib实现压缩与解压字符串的方法
Nov 19 Python
Python简单日志处理类分享
Feb 14 Python
python3.4下django集成使用xadmin后台的方法
Aug 15 Python
python检索特定内容的文本文件实例
Jun 05 Python
对Python之gzip文件读写的方法详解
Feb 08 Python
python实现电子书翻页小程序
Jul 23 Python
Python3 列表,数组,矩阵的相互转换的方法示例
Aug 05 Python
基于Python安装pyecharts所遇的问题及解决方法
Aug 12 Python
详解Python 字符串相似性的几种度量方法
Aug 29 Python
Python Selenium参数配置方法解析
Jan 19 Python
详解python程序中的多任务
Sep 16 Python
Python爬虫:Request Payload和Form Data的简单区别说明
Apr 30 #Python
如何配置关联Python 解释器 Anaconda的教程(图解)
Apr 30 #Python
python针对Oracle常见查询操作实例分析
Apr 30 #Python
python实现Oracle查询分组的方法示例
Apr 30 #Python
Pytorch数据拼接与拆分操作实现图解
Apr 30 #Python
如何安装并在pycharm使用selenium的方法
Apr 30 #Python
Python基于进程池实现多进程过程解析
Apr 30 #Python
You might like
我的论坛源代码(十)
2006/10/09 PHP
php若干单维数组遍历方法的比较
2011/09/20 PHP
解析PHP跳出循环的方法以及continue、break、exit的区别介绍
2013/07/01 PHP
PHP编程获取音频文件时长的方法【基于getid3类】
2017/04/20 PHP
PHP中的self关键字详解
2019/06/23 PHP
flash 得到自身url参数的代码
2009/11/15 Javascript
JavaScript之自定义类型
2012/05/04 Javascript
写自已的js类库需要的核心代码
2012/07/16 Javascript
JSONP 跨域共享信息
2012/08/16 Javascript
找出字符串中出现次数最多的字母和出现次数精简版
2012/11/07 Javascript
event.currentTarget与event.target的区别介绍
2012/12/31 Javascript
javascript在子页面中函数无法调试问题解决方法
2014/01/17 Javascript
jquery 选取方法都有哪些
2014/05/18 Javascript
JS实现图片产生波纹一样flash效果的方法
2015/02/27 Javascript
js 打开新页面在屏幕中间的实现方法
2016/11/02 Javascript
JavaSctit 利用FileReader和滤镜上传图片预览功能
2017/09/05 Javascript
JS实现自定义状态栏动画文字效果示例
2017/10/12 Javascript
微信小程序实现的动态设置导航栏标题功能示例
2019/01/31 Javascript
vue.js表单验证插件(vee-validate)的使用教程详解
2019/05/23 Javascript
原生JS实现多条件筛选
2020/08/19 Javascript
微信小程序实现底部弹出模态框
2020/11/18 Javascript
[00:20]DOTA2荣耀之路7:-ah fu-抢盾
2018/05/31 DOTA
查看django执行的sql语句及消耗时间的两种方法
2018/05/29 Python
详解python实现识别手写MNIST数字集的程序
2018/08/03 Python
python hashlib加密实现代码
2019/10/17 Python
python 装饰器功能与用法案例详解
2020/03/06 Python
Python sql注入 过滤字符串的非法字符实例
2020/04/03 Python
用python-webdriver实现自动填表的示例代码
2021/01/13 Python
HTML5的结构和语义(5):内嵌媒体
2008/10/17 HTML / CSS
美津浓美国官网:Mizuno美国
2018/08/07 全球购物
保护野生动物倡议书
2014/05/16 职场文书
国家税务局领导班子对照检查材料思想汇报
2014/10/04 职场文书
个人年终总结结尾
2015/03/06 职场文书
2015年政府采购工作总结
2015/05/21 职场文书
廉洁自律承诺书2016
2016/03/25 职场文书
Windows server 2012 NTP时间同步的实现
2022/06/25 Servers