bigywater.tech
Scrapy · 33

FormRequest.from_response:表单字段是怎么拼出来的

登录、搜索这类表单怎么用 Scrapy 提交?from_response 会带哪些字段、点哪个按钮?disabled 字段呢?GET 和 POST 有什么不同?文件上传行不行?这篇在一个带各种控件的表单上逐项实测。

disabled 也带
from_response 对 disabled 字段的处理(locked=x 出现在请求体里)
nr = 12
clickdata 的 nr 指 form.inputs 里的序号,12 才是 Register 按钮
0 个文件
enctype=multipart 的表单上传文件的数量(空值的 f= 而已)

状态

实测 · 本机执行输出
  警告: ScrapyDeprecationWarning: FormRequest.from_response() is deprecated. Use the form2request library instead.
  FormRequest(url, formdata=…) 直接构造的警告数: 0

2.19 里 FormRequest.from_response() 被标记为弃用,建议用 form2request 库;FormRequest(url, formdata=…) 直接构造没有警告。

收集规则

实测 · 本机执行输出
  formid="login",formdata 填 user、pass
      POST http://site.test/do/login?next=1
      body: [('csrf', 'tok123'), ('remember', 'yes'), ('plan', 'pro'), ('lang', 'zh'), ('multi', 'a'), ('multi', 'b'), ('bio', 'hello\nworld'), ('locked', 'x'), ('act', 'Login'), ('user', 'alice'), ('pass', 'pw')]
      Content-Type: application/x-www-form-urlencoded

默认收集隐藏字段、文本框默认值、选中的 checkbox/radio/select、textarea,并点第一个提交按钮;没选中的 checkbox 不带,disabled 字段仍会带上。

选表单

实测 · 本机执行输出
  (不指定)默认第一个                                → POST http://site.test/do/login?next=1
  formnumber=1                              → GET http://site.test/dir/search?q=python
  formname="loginform"                      → POST http://site.test/do/login?next=1
  formid="search"                           → GET http://site.test/dir/search?q=python
  formxpath="//form[@action='search']"      → GET http://site.test/dir/search?q=python
  formcss="form#search"                     → GET http://site.test/dir/search?q=python
  formnumber=5:抛 IndexError Form number 5 not found in <200 http://site.test/dir/page.html>
  页面里没有表单:抛 ValueError No <form> element found in <200 http://site.test/dir/page.html>

用 formid / formname / formnumber / formxpath / formcss 选表单,不指定取第一个。

选按钮

实测 · 本机执行输出
  默认(第一个提交按钮)                                         → 按钮相关字段 [('act', 'Login')]
  clickdata={"value": "Register"}                     → 按钮相关字段 [('act', 'Register')]
  clickdata={"name": "btn"}                           → 按钮相关字段 [('btn', 'b1')]
  clickdata={"nr": 0}(form.inputs 的第 0 个是隐藏字段 csrf)   → 按钮相关字段 []
  clickdata={"nr": 12}(第 13 个控件)                      → 按钮相关字段 [('act', 'Register')]
  clickdata={"value": "Nope"}(没有匹配)                   → 抛 ValueError: No clickable element matching clickdata: {'value': 'Nope'}
  dont_click=True                                     → 按钮相关字段 []

clickdata 选按钮(nr 按 form.inputs 序号,不是第几个按钮),dont_click 不点。

formdata

实测 · 本机执行输出
  formdata={remember: None, plan: enterprise, extra: [x, y], user: 中文}
      POST http://site.test/do/login?next=1
      body: [('csrf', 'tok123'), ('pass', ''), ('lang', 'zh'), ('multi', 'a'), ('multi', 'b'), ('bio', 'hello\nworld'), ('locked', 'x'), ('plan', 'enterprise'), ('extra', 'x'), ('extra', 'y'), ('user', '中文')]
      Content-Type: application/x-www-form-urlencoded

覆盖、追加、用 None 删除字段;列表产生多个同名参数。

action 与 method

实测 · 本机执行输出
  POST 表单 action=/do/login?next=1
      POST http://site.test/do/login?next=1
      body: [('csrf', 'tok123'), ('user', ''), ('pass', ''), ('remember', 'yes'), ('plan', 'pro'), ('lang', 'zh'), ('multi', 'a'), ('multi', 'b'), ('bio', 'hello\nworld'), ('locked', 'x')]
      Content-Type: application/x-www-form-urlencoded
  GET 表单 action=search(相对路径)
      GET http://site.test/dir/search?q=python
      Content-Type: None
  GET 表单,参数 method="POST" 覆盖
      POST http://site.test/dir/search
      body: [('q', 'python')]
      Content-Type: application/x-www-form-urlencoded
  <base href> 下的相对 action
      GET http://other.test/base/x?a=1
      Content-Type: None
  表单没有 action
      GET http://site.test/here?a=1
      Content-Type: None
  FormRequest(url, formdata={…}) 直接构造(值都是字符串)
      POST http://site.test/post
      body: [('k', 'v'), ('n', '1'), ('list', 'a'), ('list', 'b')]
      Content-Type: application/x-www-form-urlencoded
  formdata 里放整数 1:抛 TypeError to_bytes must receive a str or bytes object, got int
  FormRequest(url, formdata=…, method="GET")
      GET http://site.test/get?k=v
      Content-Type: None

相对 action 按页面 URL 或 <base> 补全;GET 表单把字段放进查询串;可用 method 参数覆盖。

不支持 multipart

实测 · 本机执行输出
  enctype="multipart/form-data" 的表单
      POST http://site.test/up
      body: [('title', 't'), ('f', '')]
      Content-Type: application/x-www-form-urlencoded
  type=file 的字段是否出现在请求体里: True

表单 enctype 被忽略,总是 urlencoded;文件字段只会变成空值参数。

分步演示

表单
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。表单是手写的 HTML(登录表单含隐藏字段、各种控件、两个提交按钮和一个 button;另有一个 GET 搜索表单);直接用 HtmlResponse 和 FormRequest.from_response 构造请求,看生成的方法、URL、请求体
    • 没有验证:form2request 库本身;非 UTF-8 页面的表单编码;JavaScript 动态生成的表单; 的坐标参数;formdata 同名字段的多个来源合并顺序;