Facility: 090747

U-Haul Evansville WY (S.)

Stale Data Warning: This facility has not been successfully scraped in 30 days (threshold: 3 days). Data may be outdated.
Facility Information active
Facility ID
090747
Name
U-Haul Evansville WY (S.)
URL
https://www.uhaul.com/Locations/Self-Storage-near-Evansville-WY-82636/792077/
Address
N/A
Platform
custom_facility_090747
Parser File
src/parsers/custom/facility_090747_parser.py
Last Scraped
2026-03-23 03:21:25.802370
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:21:25.810514
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_090747_parser.py)
"""Parser for U-Haul Storage of Evansville (facility 090747).

U-Haul uses a Foundation-based layout where each unit type is rendered as a
"callout condensed" div containing dimensions, price, amenity list, and optional
scarcity text (e.g. "2 Units Left!").
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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

# Matches dimension strings like "5' x 10' x 8'" or "10' x 20'"
_DIM_RE = re.compile(
    r"(\d+(?:\.\d+)?)['\u2019\u2032]\s*x\s*(\d+(?:\.\d+)?)['\u2019\u2032]",
    re.IGNORECASE,
)

# Matches scarcity text like "2 Units Left!" or "1 Unit Left"
_SCARCITY_RE = re.compile(r"\d+\s+Units?\s+Left", re.IGNORECASE)


class Facility090747Parser(BaseParser):
    """Extract storage units from U-Haul Storage of Evansville.

    Unit containers are ``<div class="callout condensed ...">`` elements that
    contain a dimension span, a price ``<b class="text-xl">``, an amenity list,
    and optional scarcity text.
    """

    platform = "custom_facility_090747"

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

        # Each rentable unit is wrapped in a "callout condensed" div.
        # Filter to only those that contain actual dimension data.
        callouts = soup.find_all(
            "div",
            class_=lambda c: c and "callout" in c and "condensed" in c,
        )

        for callout in callouts:
            text = callout.get_text()
            # Skip category header blocks that have no dimensions
            if not _DIM_RE.search(text):
                continue

            unit = UnitResult()

            # --- Size ---
            dim_span = callout.find("span", class_="nowrap")
            if dim_span:
                raw_dim = dim_span.get_text(strip=True)
                # Use only width x length (first two numbers) for the display size
                m = _DIM_RE.search(raw_dim)
                if m:
                    width = float(m.group(1))
                    length = float(m.group(2))
                    unit.size = f"{int(width)}' x {int(length)}'"
                    _, _, sqft = self.normalize_size(f"{int(width)}x{int(length)}")
                    unit.metadata = {"width": width, "length": length, "sqft": sqft}

            # --- Price ---
            price_el = callout.find("b", class_="text-xl")
            if price_el:
                unit.price = self.normalize_price(price_el.get_text(strip=True))

            # --- Amenities (description) ---
            amenity_ul = callout.find("ul", class_="condensed")
            if amenity_ul:
                amenities = [li.get_text(strip=True) for li in amenity_ul.find_all("li")]
                if amenities:
                    unit.description = ", ".join(amenities)
                    # Persist amenity flags in metadata
                    if unit.metadata is None:
                        unit.metadata = {}
                    unit.metadata["amenities"] = amenities
                    unit.metadata["climate_controlled"] = any(
                        "climate" in a.lower() for a in amenities
                    )
                    unit.metadata["drive_up"] = any(
                        "drive" in a.lower() for a in amenities
                    )

            # --- Scarcity ---
            scarcity_match = _SCARCITY_RE.search(text)
            if scarcity_match:
                unit.scarcity = scarcity_match.group(0)

            if unit.size or unit.price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("No unit callout elements found on page")

        return result

Scrape Runs (5)

Run #556 Details

Status
exported
Parser Used
Facility090747Parser
Platform Detected
storageunitsoftware
Units Found
6
Stage Reached
exported
Timestamp
2026-03-14 16:56:14.941630
Timing
Stage Duration
Fetch7077ms
Detect115ms
Parse113ms
Export14ms

Snapshot: 090747_20260314T165622Z.html · Show Snapshot · Open in New Tab

Parsed Units (6)

5' x 10'

$74.95/mo
2 Units Left

10' x 15'

$139.95/mo
1 Unit Left

10' x 10'

$84.95/mo

10' x 15'

$124.95/mo

10' x 20'

$139.95/mo

15' x 30'

$279.95/mo
1 Unit Left

← Back to dashboard