Requests, forms and JSON APIs

Scrapy · Practical · 12 min

Logins, search forms and the JSON endpoints behind modern pages are where most valuable data hides.

Beyond response.follow, you can construct requests explicitly: scrapy.Request(url, method="POST", headers={...}, body=..., callback=..., cb_kwargs=..., meta=...). To post form-encoded data you already know, FormRequest(url, formdata={...}) url-encodes it for you.

HTML forms: logins and search pages usually carry hidden fields (CSRF tokens, view state) you must send back. Recent Scrapy versions recommend the form2request library for this (pip install form2request): form2request(response.css("form#login"), {"username": "u", "password": "p"}) reads the form, keeps its hidden inputs, applies your values, and resolves the action URL and method; .to_scrapy(callback=...) turns the result into a Scrapy request. Older code uses FormRequest.from_response(response, formdata=...), which does the same but is deprecated in current releases.

Cookies are handled automatically by the cookies middleware, so after logging in, later requests in the same crawl stay logged in.

JSON APIs: many sites load data from JSON endpoints visible in the browser's network tab. Call them directly:

  • response.json() parses a JSON response body.
  • scrapy.http.JsonRequest(url, data={...}) sends a JSON body with the right headers (POST by default when data is given).
  • Paginate by computing the next page from the JSON (page + 1, a cursor field) and yielding a new request until it's empty.

APIs are usually faster, more stable and lighter on the server than HTML pages. Still respect the site's terms, authentication and rate limits.

Example

import scrapy
from form2request import form2request

class AccountSpider(scrapy.Spider):
    name = "account"
    start_urls = ["https://quotes.toscrape.com/login"]

    def parse(self, response):
        # keeps hidden fields like csrf_token, fills in ours
        req = form2request(response.css("form"), {"username": "demo", "password": "demo"})
        yield req.to_scrapy(callback=self.after_login)

    def after_login(self, response):
        if "Logout" not in response.text:
            self.logger.error("login failed"); return
        yield response.follow("/api/quotes?page=1", callback=self.parse_api)

    def parse_api(self, response):
        data = response.json()
        for q in data["quotes"]:
            yield {"author": q["author"]["name"], "text": q["text"]}
        if data["has_next"]:
            yield response.follow(f"/api/quotes?page={data['page'] + 1}",
                                  callback=self.parse_api)

Remember

  • scrapy.Request takes method, headers, body, callback, cb_kwargs
  • form2request builds a submission from a real form, keeping hidden fields
  • Cookies persist across requests automatically
  • response.json() and JsonRequest for JSON APIs

Watch out for

  • Posting login data with a plain FormRequest and missing the page's CSRF token.

Check yourself

  1. Why build a login request from the page's <form> instead of posting the fields yourself?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scrapy.org/en/latest/topics/request-response.html