Facility: 110830
Pro Self Storage
- Facility ID
- 110830
- Name
- Pro Self Storage
- URL
- https://proself-storage.com/self-storage/hammond-in-46324
- Address
- 249 Kenwood St, Hammond, IN 46324, USA, Hammond, Indiana 46324
- Platform
- custom_facility_110830
- Parser File
- src/parsers/custom/facility_110830_parser.py
- Last Scraped
- 2026-03-27 13:44:30.915724
- Created
- 2026-03-20 23:32:48.933261
- Updated
- 2026-03-27 13:44:30.956171
- Parser Status
- ✓ Working
- Status Reason
- N/A
- Last Healing Attempt
- Not attempted
Parser Source (src/parsers/custom/facility_110830_parser.py)
"""Parser for Pro Self Storage - Hammond, IN (storedge platform)."""
from __future__ import annotations
import json
import re
from collections import Counter
from bs4 import BeautifulSoup
from src.parsers.base import BaseParser, ParseResult, UnitResult
class Facility110830Parser(BaseParser):
"""Extract storage units from Pro Self Storage - Hammond, IN.
The page embeds a Schema.org JSON-LD block with @type:Product entries
covering multiple Pro Self Storage locations. Each product description
encodes the unit size and facility ID like "15x10 - $114.00 - 243491 - ...".
We identify the Hammond, IN facility by picking the most-represented
facility ID (which corresponds to the primary location on this page).
"""
platform = "custom_facility_110830"
# Parses "WxL - $price - facilityId - ..."
_DESC_RE = re.compile(r"^(\d+)[xX×](\d+)\s*-\s*\$?([\d,.]+)\s*-\s*(\d{5,8})")
def parse(self, html: str, url: str = "") -> ParseResult:
soup = BeautifulSoup(html, "lxml")
result = ParseResult(platform=self.platform, parser_name=self.__class__.__name__)
all_products: list[dict] = []
for script in soup.find_all("script", type="application/ld+json"):
if not script.string:
continue
try:
data = json.loads(script.string)
except (json.JSONDecodeError, ValueError):
continue
graph = data.get("@graph", []) if isinstance(data, dict) else []
for item in graph:
if not isinstance(item, dict):
continue
if item.get("@type") != "Product":
continue
desc = item.get("description", "")
m = self._DESC_RE.match(desc)
if not m:
continue
offers = item.get("offers", {})
all_products.append(
{
"width": int(m.group(1)),
"length": int(m.group(2)),
"price_raw": m.group(3),
"facility_id": m.group(4),
"category": item.get("category", ""),
"price_offer": offers.get("price") if isinstance(offers, dict) else None,
}
)
if not all_products:
result.warnings.append("No Product JSON-LD found")
return result
# Identify the primary facility (most products = the target location)
id_counts = Counter(p["facility_id"] for p in all_products)
primary_id = id_counts.most_common(1)[0][0]
for p in all_products:
if p["facility_id"] != primary_id:
continue
width, length = p["width"], p["length"]
size_str = f"{width}x{length}"
# Skip warehouse-scale entries
if width * length > 2000:
continue
price_val = p["price_offer"] if p["price_offer"] is not None else p["price_raw"]
price = self.normalize_price(str(price_val))
if price is None:
continue
unit = UnitResult()
unit.size = size_str
unit.price = price
unit.description = p["category"] or size_str
w, ln, sq = self.normalize_size(size_str)
if w is not None:
unit.metadata = {"width": w, "length": ln, "sqft": sq, "facility_id": primary_id}
result.units.append(unit)
if not result.units:
result.warnings.append("No units found for primary facility ID")
return result
Scrape Runs (4)
-
exported Run #16512026-03-27 13:44:24.037039 | 122 units | Facility110830Parser | View Data →
-
exported Run #16502026-03-27 13:44:23.862527 | 122 units | Facility110830Parser | View Data →
-
exported Run #10872026-03-23 02:44:45.008082 | 119 units | Facility110830Parser | View Data →
-
exported Run #5922026-03-21 18:35:05.720465 | 119 units | Facility110830Parser | View Data →
Run #1650 Details
- Status
- exported
- Parser Used
- Facility110830Parser
- Platform Detected
- storageunitsoftware
- Units Found
- 122
- Stage Reached
- exported
- Timestamp
- 2026-03-27 13:44:23.862527
Timing
| Stage | Duration |
|---|---|
| Fetch | 6844ms |
| Detect | 100ms |
| Parse | 40ms |
| Export | 32ms |
Snapshot: 110830_20260327T134430Z.html · Show Snapshot · Open in New Tab
Parsed Units (122)
10x15
$89.00/mo
8x10
$95.00/mo
8x23
$155.00/mo
10x20
$140.00/mo
10x15
$149.00/mo
14x11
$135.00/mo
5x5
$15.00/mo
10x12
$55.00/mo
15x20
$185.00/mo
10x20
$149.00/mo
15x10
$114.00/mo
10x10
$125.00/mo
20x30
$155.00/mo
5x6
$19.00/mo
10x10
$50.00/mo
8x10
$75.00/mo
8x20
$140.00/mo
8x10
$89.00/mo
10x12
$100.00/mo
10x10
$65.00/mo
5x10
$25.00/mo
10x20
$280.00/mo
4x8
$19.00/mo
10x10
$175.00/mo
10x15
$180.00/mo
7x16
$125.00/mo
10x20
$110.00/mo
4x8
$44.00/mo
4x8
$30.00/mo
10x20
$260.00/mo
5x12
$75.00/mo
10x10
$59.00/mo
5x6
$20.00/mo
5x8
$60.00/mo
4x8
$60.00/mo
15x12
$114.00/mo
5x15
$64.00/mo
7x12
$90.00/mo
4x8
$15.00/mo
10x17
$75.00/mo
4x8
$44.00/mo
15x20
$160.00/mo
9x18
$145.00/mo
11x6
$75.00/mo
10x13
$104.00/mo
4x8
$5.00/mo
10x20
$175.00/mo
5x10
$19.00/mo
12x15
$150.00/mo
14x20
$200.00/mo
8x20
$101.00/mo
10x10
$129.00/mo
10x10
$90.00/mo
7x16
$115.00/mo
10x15
$60.00/mo
10x12
$80.00/mo
6x9
$50.00/mo
4x20
$95.00/mo
5x10
$73.00/mo
5x14
$85.00/mo
5x10
$69.00/mo
6x10
$35.00/mo
10x20
$115.00/mo
10x15
$119.00/mo
8x10
$95.00/mo
16x20
$185.00/mo
5x10
$50.00/mo
5x5
$35.00/mo
10x20
$160.00/mo
10x5
$47.00/mo
10x20
$160.00/mo
5x5
$25.00/mo
10x8
$90.00/mo
10x6
$33.00/mo
8x23
$130.00/mo
5x10
$50.00/mo
10x15
$99.00/mo
15x11
$63.00/mo
10x15
$100.00/mo
15x22
$185.00/mo
5x10
$55.00/mo
12x16
$77.50/mo
10x20
$189.00/mo
5x6
$40.00/mo
10x10
$59.00/mo
10x11
$75.00/mo
5x15
$65.00/mo
4x8
$20.00/mo
4x8
$15.00/mo
5x5
$20.00/mo
10x10
$60.00/mo
11x12
$90.00/mo
26x22
$300.00/mo
6x9
$50.00/mo
8x20
$130.00/mo
8x16
$125.00/mo
16x20
$185.00/mo
10x20
$140.00/mo
10x20
$200.00/mo
6x10
$44.00/mo
7x12
$95.00/mo
10x10
$119.00/mo
10x10
$60.00/mo
5x12
$80.00/mo
21x22
$265.00/mo
10x5
$75.00/mo
10x15
$124.00/mo
10x20
$120.00/mo
10x15
$124.00/mo
10x20
$270.00/mo
8x13
$75.00/mo
10x10
$170.00/mo
6x10
$19.00/mo
10x10
$109.00/mo
8x20
$118.75/mo
5x10
$10.00/mo
10x6
$30.00/mo
12x20
$210.00/mo
10x11
$124.00/mo
5x8
$40.00/mo
10x6
$49.00/mo
10x10
$65.00/mo