Facility: 003628
Affordable Mini Storage
- Facility ID
- 003628
- Name
- Affordable Mini Storage
- URL
- http://www.affordableministoragemtvernon.com/storage-units
- Address
- 112 Pittsburgh Ave, MT Vernon, OH 43050, USA, Mt Vernon, Ohio 43050
- Platform
- custom_facility_003628
- Parser File
- src/parsers/custom/facility_003628_parser.py
- Last Scraped
- 2026-03-27 13:56:42.138265
- Created
- 2026-03-14 16:21:53.706708
- Updated
- 2026-03-27 13:56:42.165425
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_003628_parser.py)
"""Parser for Affordable Mini Storage (Hibu/DudaMobile restaurant-menu widget)."""
from __future__ import annotations
import base64
import json
import re
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
class Facility003628Parser(BaseParser):
"""Extract storage units from Affordable Mini Storage.
The site uses a Hibu "restaurant menu" widget that stores unit data in a
base64-encoded JSON blob in the ``menu_data`` attribute. As a fallback the
parser also scrapes the rendered ``.menuItemBox`` DOM elements.
Note: This facility does not publish dollar prices on its website. Units
are extracted with sizes only; the price field will be ``None`` for most
entries.
"""
platform = "custom_facility_003628"
# Matches dimension strings like 5' x 10', 10'x20', etc.
_SIZE_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__)
# --- Strategy 1: Parse the base64 menu_data JSON blob ---
units_from_json = self._parse_menu_data_json(soup)
if units_from_json:
result.units = units_from_json
else:
# --- Strategy 2: Parse rendered DOM elements ---
result.units = self._parse_menu_dom(soup)
if not result.units:
result.warnings.append("No units found on page")
return result
# ------------------------------------------------------------------
# Strategy 1 – base64 JSON blob
# ------------------------------------------------------------------
def _parse_menu_data_json(self, soup: BeautifulSoup) -> list[UnitResult]:
units: list[UnitResult] = []
menu_el = soup.find(attrs={"menu_data": True})
if not menu_el:
return units
raw = menu_el.get("menu_data", "")
try:
decoded = base64.b64decode(raw)
sections = json.loads(decoded)
except Exception:
return units
currency = menu_el.get("currency", "$")
for section in sections:
for entry in section.get("entries", []):
title = (entry.get("title") or "").strip()
if not title:
continue
unit = UnitResult()
# Try to extract a structured size
m = self._SIZE_RE.search(title)
if m:
unit.size = f"{m.group(1)}' x {m.group(2)}'"
w, ln, sq = self.normalize_size(unit.size)
if w is not None:
unit.metadata = {"width": w, "length": ln, "sqft": sq}
else:
# Non-dimensional entry (e.g. "Outside camper and boat storage")
unit.size = title
# Extract price if present
prices = entry.get("prices", [])
if prices:
price_str = (prices[0].get("price") or "").strip()
if price_str and price_str.replace(currency, "").strip():
parsed_price = self.normalize_price(price_str)
if parsed_price is not None:
unit.price = parsed_price
else:
# Non-numeric price like "Please call"
unit.description = price_str
desc = (entry.get("desc") or "").strip()
if desc and not unit.description:
unit.description = desc
units.append(unit)
return units
# ------------------------------------------------------------------
# Strategy 2 – rendered DOM
# ------------------------------------------------------------------
def _parse_menu_dom(self, soup: BeautifulSoup) -> list[UnitResult]:
units: list[UnitResult] = []
for box in soup.select(".menuItemBox"):
name_el = box.select_one(".menuItemName")
price_el = box.select_one(".menuItemPrice")
name = name_el.get_text(strip=True) if name_el else ""
if not name:
continue
unit = UnitResult()
m = self._SIZE_RE.search(name)
if m:
unit.size = f"{m.group(1)}' x {m.group(2)}'"
w, ln, sq = self.normalize_size(unit.size)
if w is not None:
unit.metadata = {"width": w, "length": ln, "sqft": sq}
else:
unit.size = name
if price_el:
price_text = price_el.get_text(strip=True)
if price_text:
parsed_price = self.normalize_price(price_text)
if parsed_price is not None:
unit.price = parsed_price
else:
unit.description = price_text
units.append(unit)
return units
Scrape Runs (5)
-
exported Run #19412026-03-27 13:56:37.574624 | 4 units | Facility003628Parser | View Data →
-
exported Run #19402026-03-27 13:56:37.199872 | 4 units | Facility003628Parser | View Data →
-
exported Run #12332026-03-23 02:57:57.740022 | 4 units | Facility003628Parser | View Data →
-
exported Run #7402026-03-21 18:49:36.868938 | 4 units | Facility003628Parser | View Data →
-
exported Run #2892026-03-14 16:31:02.296043 | 4 units | Facility003628Parser | View Data →
Run #289 Details
- Status
- exported
- Parser Used
- Facility003628Parser
- Platform Detected
- table_layout
- Units Found
- 4
- Stage Reached
- exported
- Timestamp
- 2026-03-14 16:31:02.296043
Timing
| Stage | Duration |
|---|---|
| Fetch | 5182ms |
| Detect | 15ms |
| Parse | 7ms |
| Export | 14ms |
Snapshot: 003628_20260314T163107Z.html · Show Snapshot · Open in New Tab
Parsed Units (4)
5' x 10'
No price
10' x 10'
No price
10' x 20'
No price
Outside camper and boat storage
No price