# scrape-2025.ps1 - Scrapes 2025 Inlander Restaurant Week menus from Wayback Machine # Run from local path (UNC paths block PS execution) $slugs = @( "1898", "24taps", "315cuisine", "ambrosia", "anthonys", "arrowhead", "baba", "backyardpublichouse", "bangkokthai", "bardenay", "barkrescuepub", "beverlys", "blackpearl", "borracho", "burgerdock", "cascadia", "cedars", "centennial", "chaps", "chinook", "chowderhead", "clinkerdagger", "cochinito", "collectivekitchen", "dassteinhaus", "deleons", "deleonstexmex", "dockside", "downriver", "dryfly", "durkins", "east", "emrys", "feastworldkitchen", "flameandcork", "flatstick", "flyinggoat", "fortheloveofgod", "francaise", "ganderryegrass", "gardenparty", "gildedunicorn", "hang10", "heritage", "hogwash", "honey", "hulapot", "indiahouse", "indicana", "inlandpacifickitchen", "irongoat", "ironwoodice", "karma", "kasa", "kismet", "kunisthai", "latahbistro", "lebanon", "legendsoffire", "littledragon", "littlenoodle", "longhornbbq", "loren", "lumberbeard", "macdaddys", "mackenzieriver", "mammamias", "mangotree", "maryhill", "masselowslounge", "max", "meltingpot", "mortys", "northhill", "odohertys", "osprey", "outsider", "palmcourtgrill", "ponderosa", "purenorthwest", "purgatory", "qqsushi", "redtail", "republickitchen", "republicpi", "rut", "safariroom", "saranac", "satay", "sauced", "screamingyak", "seasons", "shawnodonnells", "shelbys", "skewers", "southhillgrill", "southperrylantern", "spencers", "steamplant", "steelhead", "stylus", "sweetlous", "swinglounge", "table13", "tavolata", "terraza", "thaibamboo", "thedambar", "titos", "tomatostreet", "tonysonthelake", "torratea", "truelegends", "twigs", "uprise", "vaqueros", "vicinopizza", "victoryburger", "vieuxcarre", "vineolive", "wileys" ) $areaMap = [ordered]@{ "AIRWAY HEIGHTS" = "Airway Heights" "ATHOL" = "Athol" "COEUR D'ALENE" = "Coeur d'Alene" "POST FALLS" = "Post Falls" "HAYDEN" = "Hayden" "LIBERTY LAKE" = "Liberty Lake" "NORTH SPOKANE" = "North Spokane" "SOUTH SPOKANE" = "South Spokane" "SPOKANE VALLEY" = "Spokane Valley" "WEST SPOKANE" = "West Spokane" "WORLEY" = "Worley" "DOWNTOWN" = "Downtown" } function Get-CleanText($rawHtml) { $t = $rawHtml -replace '<[^>]+>', ' ' $t = $t -replace '&', '&' $t = $t -replace '<', '<' $t = $t -replace '>', '>' $t = $t -replace '"', '"' $t = $t -replace ''', "'" $t = $t -replace ' ', ' ' $t = $t -replace '–', '-' $t = $t -replace '—', '-' $t = $t -replace '\s+', ' ' $t.Trim() } function Extract-Dishes($courseHtml) { $dishes = [System.Collections.ArrayList]@() $opts = [System.Text.RegularExpressions.RegexOptions]::Singleline $pMatches = [regex]::Matches($courseHtml, ']*>(.*?)

', $opts) foreach ($pm in $pMatches) { $pContent = $pm.Groups[1].Value if ($pContent -notmatch '') { continue } # First = dish name $nameM = [regex]::Match($pContent, '(.*?)', $opts) if (-not $nameM.Success) { continue } $name = Get-CleanText $nameM.Groups[1].Value # Skip dietary-only names and very short strings if ($name -match '^(GF|GFA|V\+?|DF|V:|2025)$') { continue } if ($name.Length -lt 3) { continue } if ($name -match '^[A-Z]{1,3}:') { continue } # skip legend lines like "GF:" if ($name.Length -gt 80) { continue } # Description: everything after first
$afterBr = '' if ($pContent -match '(?s)(.*?)$') { $afterBr = $matches[1] } else { $afterStrong = [regex]::Match($pContent, '(?s)
(.*?)$', $opts) if ($afterStrong.Success) { $afterBr = $afterStrong.Groups[1].Value } } $desc = Get-CleanText $afterBr $null = $dishes.Add([PSCustomObject]@{ name = $name; desc = $desc }) } return ,$dishes } function Extract-CourseBlock($html, $courseLabel, $nextLabel) { $opts = [System.Text.RegularExpressions.RegexOptions]::Singleline # Strategy 1: find content in et_pb_text_inner after course label, before next label if ($nextLabel) { $pattern = [regex]::Escape($courseLabel) + '(.+?)(?=' + [regex]::Escape($nextLabel) + ')' $m = [regex]::Match($html, $pattern, $opts) if ($m.Success) { return $m.Groups[1].Value } } # Strategy 2: find the et_pb_text_inner block immediately following the course label $idx = $html.IndexOf($courseLabel) if ($idx -ge 0) { $sub = $html.Substring($idx, [Math]::Min(6000, $html.Length - $idx)) # Skip past the heading block and find the next text_inner content $innerM = [regex]::Match($sub, '(?s)et_pb_text_inner">(?!\s*\s*\s*\s*(.+?) \| Inlander') $name = if ($nameM.Success) { $nameM.Groups[1].Value.Trim() } else { $slug } # --- Price (from $45 in an h1) --- $priceM = [regex]::Match($html, '\$(\d+)') $price = if ($priceM.Success) { [int]$priceM.Groups[1].Value } else { 0 } # --- Cuisine --- $cuisineM = [regex]::Match($html, 'CUISINE:\s*([A-Z][A-Za-z/ ]+?)(?:\s* $name [$price] $fc1count/$fc2count/$fc3count courses" $null = $restaurants.Add([PSCustomObject]@{ name = $name slug = $slug price = $price areas = $areas cuisine = $cuisine url = "https://inlanderrestaurantweek.com/project/$slug/" menu = [PSCustomObject]@{ hours = $hours phone = $phone courses = [PSCustomObject]@{ 'First Course' = @($firstCourse) 'Second Course' = @($secondCourse) 'Third Course' = @($thirdCourse) } } }) } catch { Write-Host " ERROR: $_" $null = $restaurants.Add([PSCustomObject]@{ name = $slug slug = $slug price = 0 areas = @('Downtown') cuisine = '' url = "https://inlanderrestaurantweek.com/project/$slug/" menu = [PSCustomObject]@{ hours = 'FETCH_ERROR' phone = '' courses = [PSCustomObject]@{ 'First Course' = @() 'Second Course' = @() 'Third Course' = @() } } }) } Start-Sleep -Milliseconds 500 } $outPath = 'C:\Users\derekc.CHNSLocal\AppData\Local\Temp\2025-restaurants.json' $json = $restaurants | ConvertTo-Json -Depth 10 [System.IO.File]::WriteAllText($outPath, $json, [System.Text.Encoding]::UTF8) Write-Host "" Write-Host "Done! Saved $($restaurants.Count) restaurants to $outPath"