Facility: 003661

Century Square Self Storage

Stale Data Warning: This facility has not been successfully scraped in 26 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
003661
Name
Century Square Self Storage
URL
http://www.soundselfstorage.com/century-square-self-storage/
Address
1120 South 324th St, Federal Way, WA 98003, USA, Federal Way, Washington 98003
Platform
custom_facility_003661
Parser File
src/parsers/custom/facility_003661_parser.py
Last Scraped
2026-03-27 13:56:56.572329
Created
2026-03-14 16:21:53.706708
Updated
2026-03-27 13:56:56.602940
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_003661_parser.py)
"""Parser for KJ Storage (storewithkjstorage.com)."""

from __future__ import annotations

import json

from bs4 import BeautifulSoup

from src.parsers.base import BaseParser, ParseResult, UnitResult


class Facility003661Parser(BaseParser):
    """Extract storage units from KJ Storage via embedded JSON in <pre> tags."""

    platform = "custom_facility_003661"

    def parse(self, html: str, url: str = "") -> ParseResult:
        soup = BeautifulSoup(html, "lxml")
        result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)

        # Unit data is embedded as JSON inside <pre> tags within tables.
        # Each <pre> contains a JSON object with dcWidth, dcLength, dcPreferredRate, sTypeName.
        # The first <pre> is a summary record (fields are dicts with min/max); skip those.
        for pre in soup.find_all("pre"):
            text = pre.get_text(strip=True)
            if not text.startswith("{"):
                continue
            try:
                data = json.loads(text)
            except (json.JSONDecodeError, ValueError):
                continue

            width = data.get("dcWidth")
            length = data.get("dcLength")
            rate = data.get("dcPreferredRate")
            type_name = data.get("sTypeName", "")

            # Skip summary records where fields are dicts (not scalar values)
            if isinstance(width, dict) or isinstance(length, dict):
                continue
            if not (width and length):
                continue

            size_str = f"{int(width)}x{int(length)}"
            size = self.normalize_size(size_str)
            price = self.normalize_price(str(rate)) if rate is not None else None
            climate = data.get("bClimate", False)
            type_name_str = type_name if isinstance(type_name, str) else ""
            desc_parts = [type_name_str] if type_name_str else []
            if climate:
                desc_parts.append("Climate Controlled")
            if "parking" in type_name_str.lower() or "vehicle" in type_name_str.lower():
                desc_parts.append("Parking")

            vacant = data.get("iTotalVacant", 1)
            scarcity = None if (vacant and int(vacant) > 0) else "sold out"

            result.units.append(
                UnitResult(
                    size=size,
                    price=price,
                    description=" | ".join(desc_parts) if desc_parts else None,
                    scarcity=scarcity,
                )
            )

        if not result.units:
            result.warnings.append("No units found")
        return result

Scrape Runs (5)

Run #291 Details

Status
exported
Parser Used
Facility003661Parser
Platform Detected
storageunitsoftware
Units Found
9
Stage Reached
exported
Timestamp
2026-03-14 16:31:11.324797
Timing
Stage Duration
Fetch4344ms
Detect63ms
Parse36ms
Export17ms

Snapshot: 003661_20260314T163115Z.html · Show Snapshot · Open in New Tab

Parsed Units (9)

3x3

No price

5x5

No price

5x10

No price

5x15

No price

10x10

No price

10x15

No price

10x20

No price

10x25

No price

10x30

No price

← Back to dashboard