Facility: 003245

Alan Heated Mini 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
003245
Name
Alan Heated Mini Storage
URL
https://www.alanheatedministorage.com/unit-sizes
Address
1106 Sicklesteel Ln, Mount Vernon, WA 98274, USA, Mount Vernon, Washington 98274
Platform
custom_facility_003245
Parser File
src/parsers/custom/facility_003245_parser.py
Last Scraped
2026-03-27 13:55:45.732691
Created
2026-03-14 16:21:53.706708
Updated
2026-03-27 13:55:45.760380
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_003245_parser.py)
"""Parser for Alan Heated Mini Storage."""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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


class Facility003245Parser(BaseParser):
    """Extract storage units from Alan Heated Mini Storage."""

    platform = "custom_facility_003245"

    _UNIT_RE = re.compile(
        r"(\d+\s*[\'\'\u2032]?\s*[xX\u00d7]\s*\d+\s*[\'\'\u2032]?)"
        r"[^\$]{0,120}"
        r"\$(\d[\d,.]*)",
        re.DOTALL,
    )

    _PRICE_SIZE_RE = re.compile(
        r"\$(\d[\d,.]*)"
        r".{0,120}"
        r"(\d+\s*[\'\'\u2032]?\s*[xX\u00d7]\s*\d+\s*[\'\'\u2032]?)",
        re.DOTALL,
    )

    _SIZE_ONLY_RE = re.compile(
        r"(\d+\s*[\'\'\u2032]?\s*[xX\u00d7]\s*\d+\s*[\'\'\u2032]?)"
    )

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

        for tag in soup.find_all(["script", "style"]):
            tag.decompose()

        body_text = soup.get_text(separator="\n")

        seen: set[tuple[str, str]] = set()

        # Try size-then-price pattern
        for m in self._UNIT_RE.finditer(body_text):
            size_text = m.group(1).strip()
            price_text = m.group(2).strip()
            key = (size_text, price_text)
            if key in seen:
                continue
            seen.add(key)

            unit = UnitResult()
            unit.size = size_text
            w, ln, sq = self.normalize_size(size_text)
            if w is not None:
                unit.metadata = {"width": w, "length": ln, "sqft": sq}
            unit.price = self.normalize_price(price_text)
            unit.description = m.group(0).strip()[:200]
            if unit.size or unit.price:
                result.units.append(unit)

        # Try price-then-size pattern if no results
        if not result.units:
            for m in self._PRICE_SIZE_RE.finditer(body_text):
                price_text = m.group(1).strip()
                size_text = m.group(2).strip()
                key = (size_text, price_text)
                if key in seen:
                    continue
                seen.add(key)

                unit = UnitResult()
                unit.size = size_text
                w, ln, sq = self.normalize_size(size_text)
                if w is not None:
                    unit.metadata = {"width": w, "length": ln, "sqft": sq}
                unit.price = self.normalize_price(price_text)
                unit.description = m.group(0).strip()[:200]
                if unit.size or unit.price:
                    result.units.append(unit)

        # Fallback: extract sizes without prices
        if not result.units:
            seen_sizes: set[str] = set()
            for m in self._SIZE_ONLY_RE.finditer(body_text):
                size_text = m.group(1).strip()
                if size_text in seen_sizes:
                    continue
                w, ln, sq = self.normalize_size(size_text)
                if w is None or w < 3 or ln < 3:
                    continue
                seen_sizes.add(size_text)
                unit = UnitResult()
                unit.size = size_text
                unit.metadata = {"width": w, "length": ln, "sqft": sq}
                result.units.append(unit)

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

        return result

Scrape Runs (5)

Run #275 Details

Status
exported
Parser Used
Facility003245Parser
Platform Detected
table_layout
Units Found
16
Stage Reached
exported
Timestamp
2026-03-14 16:30:09.123522
Timing
Stage Duration
Fetch5482ms
Detect16ms
Parse10ms
Export17ms

Snapshot: 003245_20260314T163014Z.html · Show Snapshot · Open in New Tab

Parsed Units (16)

5x5

No price

6x5

No price

4x5

No price

4x6

No price

3x10

No price

5x10

No price

5x7

No price

5x8

No price

6x10

No price

8x10

No price

9x10

No price

10x10

No price

10x12

No price

10x13

No price

10x15

No price

10x20

No price

← Back to dashboard