Facility: 051413
U-Haul Casper WY (82609)
- Facility ID
- 051413
- Name
- U-Haul Casper WY (82609)
- URL
- https://www.uhaul.com/Locations/Self-Storage-near-Casper-WY-82609/792073/
- Address
- N/A
- Platform
- custom_facility_051413
- Parser File
- src/parsers/custom/facility_051413_parser.py
- Last Scraped
- 2026-03-23 03:20:39.048830
- Created
- 2026-03-06 23:45:35.865957
- Updated
- 2026-03-23 03:20:39.062483
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_051413_parser.py)
"""Parser for U-Haul Moving & Storage of East Casper (facility 051413).
U-Haul uses a custom layout with three category lists (small/medium/larger),
each containing <li class="divider"> elements — one per available unit type.
Each element holds: dimensions in an <h4>, amenities in a <ul>, and price in a
<b class="text-2x"> element.
"""
from __future__ import annotations
import re
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
# Matches dimension strings like "5' x 5' x 10'" or "5' x 8' x 7.5'"
_DIM_RE = re.compile(
r"(\d+(?:\.\d+)?)['\u2019\u2032]\s*[xX\u00d7]\s*(\d+(?:\.\d+)?)['\u2019\u2032]"
r"(?:\s*[xX\u00d7]\s*(\d+(?:\.\d+)?)['\u2019\u2032])?",
)
class Facility051413Parser(BaseParser):
"""Extract storage units from U-Haul Moving & Storage of East Casper.
The page has three <ul class="uhjs-unit-list"> sections (small, medium,
larger units). Every direct <li class="divider"> child represents one
unit type. Within each li:
- Dimensions: <h4> containing a <span class="nowrap"> or raw text.
- Amenities: <ul style="list-style-type:disc;"> with one <li> per feature.
- Price: <b class="text-2x"> (the "show for medium" rate cell).
"""
platform = "custom_facility_051413"
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
unit_lists = soup.find_all(
"ul", class_=lambda c: c and "uhjs-unit-list" in c
)
for unit_list in unit_lists:
items = unit_list.find_all("li", recursive=False)
for item in items:
# All direct li children in these lists are unit rows
unit = self._parse_item(item, url)
if unit is not None:
result.units.append(unit)
if not result.units:
result.warnings.append("No units found — U-Haul page structure may have changed")
return result
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _parse_item(self, item: BeautifulSoup, url: str) -> UnitResult | None:
"""Parse a single <li> unit row and return a UnitResult, or None."""
# --- Size / dimensions ---
size_str = self._extract_size(item)
if not size_str:
return None
# Normalize the display size to "W' x L'" (drop height dimension)
display_size, width, length = self._format_size(size_str)
# --- Price ---
price_b = item.find("b", class_="text-2x")
price = self.normalize_price(price_b.get_text(strip=True)) if price_b else None
# --- Description ---
desc_span = None
desc_p = item.find("p", class_="medium-collapse")
if desc_p:
desc_span = desc_p.find("span")
description = desc_span.get_text(strip=True) if desc_span else None
# --- Amenities ---
amenity_ul = item.find("ul", style=True)
amenities: list[str] = []
if amenity_ul:
amenities = [li.get_text(strip=True) for li in amenity_ul.find_all("li")]
climate_controlled = "Climate" in amenities or "Heated" in amenities
drive_up = "Drive Up" in amenities
interior = "Interior" in amenities
ada = "ADA" in amenities
portable = "Portable Container" in amenities
# --- Promotion ---
promo_link = item.find("a", class_=lambda c: c and "tag" in c)
promotion = promo_link.get_text(strip=True) if promo_link else None
# Build metadata
metadata: dict = {
"amenities": amenities,
"climate_controlled": climate_controlled,
"drive_up": drive_up,
"interior": interior,
}
if ada:
metadata["ada"] = True
if portable:
metadata["portable"] = True
if width is not None:
metadata["width"] = width
metadata["length"] = length
metadata["sqft"] = round(width * length, 2)
return UnitResult(
size=display_size,
description=description,
price=price,
promotion=promotion,
url=url,
metadata=metadata,
)
def _extract_size(self, item: BeautifulSoup) -> str | None:
"""Return raw dimension string from the item's <h4>."""
h4 = item.find("h4")
if not h4:
return None
# Prefer <span class="nowrap"> inside the h4
nowrap = h4.find("span", class_="nowrap")
if nowrap:
return nowrap.get_text(strip=True)
# Fallback: extract dimension substring from full h4 text
h4_text = h4.get_text(separator=" ", strip=True)
m = _DIM_RE.search(h4_text)
return m.group(0) if m else None
def _format_size(self, size_str: str) -> tuple[str, float | None, float | None]:
"""Return (display_size, width, length) from a raw dimension string."""
m = _DIM_RE.search(size_str)
if not m:
return size_str, None, None
width = float(m.group(1))
length = float(m.group(2))
# Format as "W' x L'" — drop the height dimension for display
display = f"{int(width) if width == int(width) else width}' x {int(length) if length == int(length) else length}'"
return display, width, length
Scrape Runs (5)
-
exported Run #14902026-03-23 03:20:31.264444 | 6 units | Facility051413Parser | View Data →
-
exported Run #9972026-03-21 19:13:44.684564 | 7 units | Facility051413Parser | View Data →
-
exported Run #5502026-03-14 16:55:24.032771 | 11 units | Facility051413Parser | View Data →
-
exported Run #1542026-03-14 04:59:21.450706 | 11 units | Facility051413Parser | View Data →
-
exported Run #772026-03-14 01:00:47.706199 | 11 units | Facility051413Parser | View Data →
Run #154 Details
- Status
- exported
- Parser Used
- Facility051413Parser
- Platform Detected
- storageunitsoftware
- Units Found
- 11
- Stage Reached
- exported
- Timestamp
- 2026-03-14 04:59:21.450706
Timing
| Stage | Duration |
|---|---|
| Fetch | 6552ms |
| Detect | 286ms |
| Parse | 216ms |
| Export | 6ms |
Snapshot: 051413_20260314T045928Z.html · Show Snapshot · Open in New Tab
Parsed Units (11)
5' x 5'
$54.95/mo
5' x 5'
$54.95/mo
5' x 8'
$59.95/mo
5' x 10'
$64.95/mo
5' x 10'
$69.95/mo
10' x 10'
$124.95/mo
10' x 10'
$134.95/mo
10' x 15'
$189.95/mo
10' x 20'
$244.95/mo
10' x 10'
$119.95/mo
10' x 15'
$169.95/mo