Facility: 095827

U-Haul Casper WY (82601)

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
095827
Name
U-Haul Casper WY (82601)
URL
https://www.uhaul.com/Locations/Self-Storage-near-Casper-WY-82601/792072/
Address
N/A
Platform
custom_facility_095827
Parser File
src/parsers/custom/facility_095827_parser.py
Last Scraped
2026-03-23 03:20:30.525933
Created
2026-03-06 23:45:35.865957
Updated
2026-03-23 03:20:30.532453
Parser & Healing Diagnosis working
Parser Status
✓ Working
Status Reason
N/A
Last Healing Attempt
Not attempted
Parser Source (src/parsers/custom/facility_095827_parser.py)
"""Parser for U-Haul Moving & Storage of Casper, WY (facility 095827).

U-Haul uses a custom layout where each storage unit type is rendered as a
``<li class="divider">`` card. The size is in an ``<h4>`` element, the
monthly price appears in ``<b class="text-2x">`` (or ``<b class="text-xl">``
for portable container units), and available unit count is in a hidden input
named ``VacantUnitsCount``. Features/amenities are listed in a ``<ul>``
with ``class="collapse condensed"``.
"""

from __future__ import annotations

import re

from bs4 import BeautifulSoup

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


class Facility095827Parser(BaseParser):
    """Extract storage units from U-Haul Moving & Storage of Casper (WY)."""

    platform = "custom_facility_095827"

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

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

        # Each unit type is a <li class="divider"> card
        cards = soup.find_all("li", class_="divider")

        for card in cards:
            unit = UnitResult(url=url)

            # --- Size ---
            h4 = card.find("h4")
            if not h4:
                continue

            # The size is either in a <span class="nowrap"> child or directly
            # as text after the label <br> in h4.
            nowrap = h4.find("span", class_="nowrap")
            if nowrap:
                raw_size = nowrap.get_text(strip=True)
            else:
                # Fallback: strip the label word (Small/Medium/Large) and grab
                # the remaining text via get_text with the <br> as separator.
                h4_text = h4.get_text(separator="\n", strip=True)
                lines = [ln.strip() for ln in h4_text.splitlines() if ln.strip()]
                # Lines are typically ["Medium", "10' x 10' x 10'"]
                raw_size = lines[-1] if len(lines) >= 2 else h4_text

            unit.size = raw_size

            # Parse first two dimension values for width/length/sqft
            m = self._DIM_RE.search(raw_size)
            if m:
                width = float(m.group(1))
                length = float(m.group(2))
                unit.metadata = {"width": width, "length": length, "sqft": round(width * length, 2)}

            # --- Size label (Small / Medium / Large) ---
            h4_first_line = h4.get_text(separator="\n", strip=True).splitlines()[0].strip()
            if unit.metadata is not None:
                unit.metadata["size_label"] = h4_first_line
            else:
                unit.metadata = {"size_label": h4_first_line}

            # --- Price ---
            # Prefer the larger display price (text-2x); fall back to text-xl
            price_tag = card.find("b", class_="text-2x") or card.find("b", class_="text-xl")
            if price_tag:
                unit.price = self.normalize_price(price_tag.get_text(strip=True))

            # --- Vacancy / scarcity ---
            vacant_inp = card.find(
                "input",
                id=lambda i: i and "VacantUnitsCount" in i and "RentNow" in i,
            )
            if vacant_inp:
                vacant_val = vacant_inp.get("value", "").strip()
                if vacant_val:
                    unit.scarcity = f"{vacant_val} available"
                    unit.metadata["vacant_count"] = int(vacant_val)

            # --- Features / amenities ---
            features_ul = card.find("ul", class_=lambda c: c and "condensed" in c)
            if features_ul:
                features = [li.get_text(strip=True) for li in features_ul.find_all("li")]
                if features:
                    unit.metadata["features"] = features
                    unit.description = ", ".join(features)

            # Only add if we have at minimum a size or price
            if unit.size or unit.price:
                result.units.append(unit)

        if not result.units:
            result.warnings.append("No unit cards (li.divider) found on page")

        return result

Scrape Runs (5)

Run #549 Details

Status
exported
Parser Used
Facility095827Parser
Platform Detected
storageunitsoftware
Units Found
6
Stage Reached
exported
Timestamp
2026-03-14 16:55:17.035224
Timing
Stage Duration
Fetch6023ms
Detect118ms
Parse63ms
Export13ms

Snapshot: 095827_20260314T165523Z.html · Show Snapshot · Open in New Tab

Parsed Units (6)

10' x 10' x 10'

$109.95/mo
7 available

10' x 15' x 10'

$149.95/mo
29 available

10' x 30' x 10'

$289.95/mo
3 available

7.3' x 16.8' x 6.5'

$109.95/mo
9 available

7.3' x 23.5' x 6.8'

$114.95/mo
24 available

5' x 8' x 7.5'

$59.95/mo

← Back to dashboard