Skip to main content

Overview

FKApi scrapes football kit data from footballkitarchive.com using a robust, ethical scraping system built with Python’s requests and BeautifulSoup4 libraries. The system includes retry logic, proxy support, rate limiting, and comprehensive error handling. All scraping logic is in fkapi/core/scrapers.py (~1400 lines).

Architecture

Core Scraping Functions

scrape_kit()

Scrapes a single kit using its slug and optional kit_id.
str
required
The kit’s base slug from the URL (e.g., “barcelona-2024-25-home-kit”)
str
Kit ID for new URL format (e.g., “402174”)
bool
default:"False"
Whether to use a proxy for the request
int
ID of existing kit to update (instead of creating new)
Returns: Kit | None - Created/updated Kit object or None if failed Features:
  • Automatic retry with exponential backoff (max 3 retries)
  • Handles URL format changes (old vs new format)
  • 403 detection triggers automatic proxy use
  • 404 handling with new URL format fallback
  • Validates page existence before parsing

scrape_club_details()

Scrapes club information including name, logo, and country.
str
required
The club’s slug (e.g., “barcelona”)
bool
default:"False"
Whether to use a proxy
Returns: Club | None Extracted Data:
  • Club name (from page title)
  • Logo URL (both light and dark mode)
  • Creates or updates Club record

scrape_whole_club()

Scrapes all kits for a given club across all seasons. Always uses proxy to prevent IP bans.
Club
required
The Club model instance to scrape
Returns: Club | None Process:
  1. Fetches club’s kit archive page
  2. Iterates through each season container
  3. Extracts brand information
  4. Processes each kit in the season
  5. Uses atomic transactions for data integrity
This function always uses a proxy since it makes many requests in sequence. Expect longer execution time.

scrape_latest()

Scrapes the “Latest Kits” page to find newly added kits.
int
default:"1"
Page number to scrape
bool
default:"False"
Whether to use a proxy
Returns: tuple[bool, bool] - (success, all_kits_exist) Behavior:
  • Checks if kits already exist in database
  • Only queues new kits for scraping
  • Returns early if all kits on page exist
  • Dispatches scraping to Celery tasks for parallel processing

scrape_latest_pages()

Scrapes multiple pages from the latest kits section.
int
default:"1"
First page to scrape
int
default:"1"
Last page to scrape
bool
default:"False"
Whether to use a proxy
int
default:"2"
Delay in seconds between pages
callable
Optional callback function for progress reporting
bool
default:"False"
Process pages in reverse order (newest to oldest)
Returns: tuple[int, int] - (success_count, failure_count)

HTTP Layer

http_get()

Centralized HTTP request function with retry logic and proxy support (defined in core/http.py). Features:
  • Automatic retries on failure
  • Rotating proxy support
  • Custom headers (User-Agent, Accept, etc.)
  • Connection pooling
  • Timeout handling

HTML Parsing

BeautifulSoup4

All HTML parsing uses BeautifulSoup4 with the lxml parser:

Common Extraction Patterns


Error Handling

Exception Hierarchy

Custom exceptions defined in core/exceptions.py:

ScrapingError

Base exception for scraping errors

KitNotFoundError

Kit page not found (404)

ClubNotFoundError

Club page not found (404)

RateLimitExceededError

Rate limit exceeded (403)

InvalidSeasonError

Invalid season format

Retry Logic

1

Initial Request

Make HTTP request to target URL
2

Check Response

  • 200: Success, proceed to parse
  • 403: Rate limited, retry with proxy
  • 404: Page not found, try URL format fallback
  • Other errors: Retry with backoff
3

Retry with Backoff

  • Max retries: 3 (from MAX_RETRIES constant)
  • Retry delay: 2 seconds (from RETRY_DELAY constant)
  • Each retry increments counter
4

Final Attempt

If all retries exhausted, return None or raise exception

Rate Limiting & Proxies

Ethical Scraping Practices

Always respect the source website’s rate limits and terms of service.
Built-in Protections:
  1. Delay Between Requests
    • scrape_latest_pages() enforces configurable delay (default 2 seconds)
    • scrape_user_collection_api() uses 0.5 second delay between pages
  2. Automatic Proxy Use
    • 403 responses trigger automatic proxy retry
    • Bulk operations (scrape_whole_club) always use proxy
    • Proxy rotation prevents IP bans
  3. Request Throttling
    • Celery task queue prevents overwhelming the server
    • Tasks process kits sequentially or in controlled parallelism

Proxy Configuration

Proxy settings are configured in environment variables (specifics in deployment config).

URL Format Handling

Old vs New Format

FootballKitArchive.com uses two URL formats:
Example: /barcelona-2024-25-home-kit402174The trailing digits were part of the slug.

Automatic Conversion

The _try_new_url_format() function automatically detects and converts:

URL Building


Season Parsing

Season Format Variations

The scraper handles multiple season input formats:

get_season()

Parses season from kit slugs:

Year Validation

  • Years must be between 1800-2100
  • Prevents obvious input errors
  • Historical and future seasons supported
  • Modern seasons (1960+): Max 2-year span
  • Historical seasons (pre-1960): Max 20-year span
  • Prevents incorrect season parsing
  • second_year must be ≥ first_year
  • Catches reversed or invalid input

API Scraping

In addition to HTML scraping, FKApi can scrape from FootballKitArchive’s internal APIs.

scrape_user_collection_api()

Scrapes a user’s kit collection using the collection-feed API:
int
required
User ID from FootballKitArchive
Returns: dict with:
  • success: bool
  • entries: list of kit entries
  • total_entries: int
  • pages_scraped: int
  • user: user info dict (if available)
Features:
  • Automatic pagination (fetches all pages)
  • Filters out custom entries (custom_team, custom_type, etc.)
  • Cleans unwanted fields from response
  • 0.5 second delay between pages
  • Returns enriched data with metadata

scrape_user_info_api()

Scrapes user profile information:
int
required
User ID from FootballKitArchive
Returns: dict | None with user data:
  • id: User ID
  • name: Username
  • image: Profile image URL
  • Additional profile fields

Data Processing Flow

ScrapingService.process_kit_data()

The central processing function for scraped kit data:

Transaction Management

All scraping operations use atomic transactions:

Constants

Scraping configuration constants (defined in core/constants.py):

Celery Integration

For parallel processing and scheduled scraping:

Task Definitions

Scheduled Scraping

Configured in settings.py:

Logging

Scraping operations use Python’s logging module:
Log Levels:
  • DEBUG: Detailed scraping progress
  • INFO: Successful operations
  • WARNING: Recoverable errors (rate limits, retries)
  • ERROR: Failed operations

Best Practices

Use Proxies for Bulk

Always enable proxies when scraping multiple pages or entire clubs

Respect Rate Limits

Use appropriate delays between requests (2+ seconds recommended)

Handle Errors Gracefully

Use try-except blocks and return None on failure

Use Transactions

Wrap database operations in atomic transactions

Log Everything

Comprehensive logging helps debug scraping issues

Validate Data

Always validate scraped data before saving
Important: Always check robots.txt and respect the source website’s terms of service. FKApi is designed for educational and archival purposes.

Troubleshooting

Cause: Rate limiting by the source websiteSolution:
  • Enable proxy: use_proxy=True
  • Increase delay between requests
  • Check if IP is banned
  • Use Celery for distributed scraping
Cause: Page moved or URL format changedSolution:
  • Scraper automatically tries new URL format
  • Verify slug is correct
  • Check if kit exists on website
  • Use kit_id parameter if available
Cause: HTML structure changed on source websiteSolution:
  • Update CSS selectors in core/parsers.py
  • Check KIT_CLASS, SECTION_DETAILS_CLASS constants
  • Verify BeautifulSoup selectors
Cause: Unusual season formatSolution:
  • Check get_season() logic
  • Add new format handler in _parse_*_year() functions
  • Validate year ranges (1800-2100)
Cause: Missing foreign key relationshipsSolution:
  • Ensure clubs, brands, seasons exist before creating kits
  • Use scrape_club_details() to create missing clubs
  • Use get_season() to auto-create seasons
  • Wrap operations in transaction.atomic()

Architecture

Understand overall system architecture

Data Models

Learn about database models