Created
February 22, 2022 01:31
-
-
Save sfuller/b412bee26784a8c084cf5c62bed6b141 to your computer and use it in GitHub Desktop.
Golden gate picnic table reservation site availability checker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# pip install requests beautifulsoup4 | |
import argparse | |
import re | |
import datetime | |
import time | |
import pickle | |
from typing import Iterable, List, Dict, Tuple | |
import requests | |
from bs4 import BeautifulSoup | |
BOOK_ID_PATTERN = re.compile(r'btnBook(\d\d\d\d)(\d\d)(\d\d)') | |
def make_argparser() -> argparse.ArgumentParser: | |
argparser = argparse.ArgumentParser() | |
argparser.add_argument('--cached', action='store_true', help='Use cached results instead of scraping.') | |
argparser.add_argument('--urls', action='store_true', help='Print booking urls in the summary.') | |
argparser.add_argument('--weekdays', help='Comma separated list of weekdays to filter to. Mon-Sun, 0-6.') | |
argparser.add_argument('--months', default=1, type=int, help='Number of months in the future to scrape.') | |
return argparser | |
class Facility(object): | |
def __init__(self, id_, name: str): | |
self.id = id_ | |
self.name = name | |
FACILTIES: Iterable[Facility] = ( | |
Facility(1063, 'Esprit Park Picnic Area - Lawn'), | |
Facility(1065, 'Esprit Park Picnic Area - North East 2 Tables'), | |
Facility(1068, 'Esprit Park Picnic Area - South East 1 Table'), | |
Facility(1202, 'GGP - 14th Avenue East Picnic Area'), | |
Facility(78, 'GGP - Bunny Meadow Picnic Area'), | |
Facility(80, 'GGP - Dahlia Dell Picnic Area'), | |
Facility(87, 'GGP - Doughboy Meadow Picnic Area'), | |
Facility(90, 'GGP - Elk Glen Picnic Area'), | |
Facility(92, 'GGP - George Washington Grove Picnic Area'), | |
Facility(94, 'GGP - Hellman Hollow Picnic Area - Table 1 & 2'), | |
Facility(103, 'GGP - Hellman Hollow Picnic Area - Table 3 & 4'), | |
Facility(96, 'GGP - Hellman Hollow Picnic Area - Table 5 & 6'), | |
Facility(97, 'GGP - Hellman Hollow Picnic Area - Table 7, 8, 9'), | |
Facility(98, 'GGP - Hellman Hollow Picnic Area - Table 10, 11, 12'), | |
Facility(99, 'GGP - Hellman Hollow Picnic Area - Table 13 & 14'), | |
Facility(100, 'GGP - Hellman Hollow Picnic Area - Table 15 & 16'), | |
Facility(101, 'GGP - Hellman Hollow Picnic Area - Table 17 & 18'), | |
Facility(102, 'GGP - Hellman Hollow Picnic Area - Table 19 & 20'), | |
Facility(105, 'GGP - Hoover Redwood Grove Picnic Area'), | |
Facility(111, 'GGP - Lindley Meadow Picnic Area - Table 1'), | |
Facility(118, 'GGP - Lindley Meadow Picnic Area - Table 2'), | |
Facility(106, 'GGP - Lindley Meadow Picnic Area - Table 3 & 4'), | |
Facility(119, 'GGP - Lindley Meadow Picnic Area - Table 5'), | |
Facility(107, 'GGP - Lindley Meadow Picnic Area - Table 6 & 7'), | |
Facility(108, 'GGP - Lindley Meadow Picnic Area - Table 8 & 9'), | |
Facility(109, 'GGP - Lindley Meadow Picnic Area - Table 10 & 11'), | |
Facility(110, 'GGP - Lindley Meadow Picnic Area - Table 12 & 13'), | |
Facility(113, 'GGP - Lindley Meadow Picnic Area - Table 14'), | |
Facility(114, 'GGP - Lindley Meadow Picnic Area - Table 15'), | |
Facility(115, 'GGP - Lindley Meadow Picnic Area - Table 16'), | |
Facility(116, 'GGP - Lindley Meadow Picnic Area - Table 17'), | |
Facility(120, 'GGP - Marx Meadow Picnic Area - Table 1 & 2'), | |
Facility(123, 'GGP - Marx Meadow Picnic Area - Table 3'), | |
Facility(124, 'GGP - Marx Meadow Picnic Area - Table 4'), | |
Facility(125, 'GGP - Marx Meadow Picnic Area - Table 5'), | |
Facility(126, 'GGP - Marx Meadow Picnic Area - Table 6'), | |
Facility(122, 'GGP - Marx Meadow Picnic Area - Table 7 & 8'), | |
Facility(131, 'GGP - Old Speedway Meadow Picnic Area'), | |
Facility(133, 'GGP - Peacock Meadow Picnic Area'), | |
Facility(134, 'GGP - Pioneer East Meadow Picnic Area'), | |
Facility(135, 'GGP - Pioneer Log Cabin Picnic Area'), | |
Facility(139, 'GGP - Stow Lake Boathouse Picnic Area'), | |
Facility(2317, 'GGP - Stow Lake South Side Lawn'), | |
Facility(141, 'GGP – Strawberry Hill Near Chinese Pavilion'), | |
Facility(142, 'GGP – Strawberry Hill Top Picnic Area'), | |
Facility(1555, 'John McLaren Park John F. Shelley Drive Picnic Area - Picnic Area A, West 7 tables + counter'), | |
Facility(1562, 'John McLaren Park John F. Shelley Drive Picnic Area - Picnic Area B, East 5 tables + counter'), | |
Facility(1778, 'Mountain Lake Park Picnic Area - 8th Ave 2 Tables'), | |
Facility(1773, 'Mountain Lake Park Picnic Area - 9th Ave Shelter'), | |
Facility(1776, 'Mountain Lake Park Picnic Area - 10th Ave Meadow'), | |
Facility(1777, 'Mountain Lake Park Picnic Area - 11th Ave Meadow'), | |
Facility(1779, 'Mountain Lake Park Picnic Area - 12th Ave 1 Table'), | |
) | |
FACILTIES_BY_ID = {facility.id: facility for facility in FACILTIES} | |
WEEKDAY_NAMES = { | |
0: 'Monday', | |
1: 'Tuesday', | |
2: 'Wednesday', | |
3: 'Thursday', | |
4: 'Friday', | |
5: 'Saturday', | |
6: 'Sunday' | |
} | |
def scrape_month(facility_id, year, month) -> Dict[int, str]: | |
startyear = year | |
endyear = year | |
response = requests.get(f'https://apm.activecommunities.com/sfrecpark/facility_search?IsCalendar=true' | |
f'&facility_id={facility_id}&year={year}&month={month}' | |
f'&startyear={startyear}&endyear={endyear}') | |
response.raise_for_status() | |
soup = BeautifulSoup(response.text, 'html.parser') | |
table = soup.html.body\ | |
.find('div', id='an-background')\ | |
.find('div', id='an-wrapper')\ | |
.find('div', id='an-contentarea')\ | |
.find('form', id='IronPointForm_1')\ | |
.find('div', id='ctl06_ctlFacilityCalendar_divCalendar')\ | |
.find('div', id='ctl06_ctlFacilityCalendar_pnlCalendarAll')\ | |
.find('div', id='ctl06_ctlFacilityCalendar_pnlCalendarRefresh') \ | |
.find('div', id='ctl06_ctlFacilityCalendar_pnlCalendar') \ | |
.find('table', id='ctl06_ctlFacilityCalendar_tblMain') | |
available_days: Dict[int, str] = {} | |
all_anchors = table.find_all('a') | |
for anchor in all_anchors: | |
anchor_id = anchor.get('id') | |
if not anchor_id: | |
continue | |
match = BOOK_ID_PATTERN.match(anchor_id) | |
if match: | |
day = match.group(3) | |
try: | |
day_num = int(day) | |
except ValueError: | |
continue | |
available_days[day_num] = anchor.get('href', '') | |
return available_days | |
def get_dates(num: int) -> List[datetime.date]: | |
today = datetime.date.today() | |
dates = [today] | |
current_month = today.month | |
current_year = today.year | |
for i in range(num): | |
current_month += 1 | |
if current_month > 12: | |
current_month = 1 | |
current_year += 1 | |
dates.append(datetime.date(year=current_year, month=current_month, day=1)) | |
return dates | |
def scrape(args): | |
dates = get_dates(args.months) | |
available_days_by_facility: Dict[int, List[Tuple[datetime.date, str]]] = {} | |
for facility in FACILTIES: | |
for date in dates: | |
# Be considerate, wait between requests | |
time.sleep(1.0) | |
print(f'GET: Month: {date.month}, Facility: "{facility.name}"') | |
available_days = scrape_month(facility.id, date.year, date.month) | |
print(f'\tFound {len(available_days)} spot(s).') | |
availabilities_list = available_days_by_facility.setdefault(facility.id, []) | |
for day, href in available_days.items(): | |
availabilities_list.append((datetime.date(date.year, date.month, day), href)) | |
with open('results.bin', 'wb') as f: | |
pickle.dump(available_days_by_facility, f) | |
print_summary(args, available_days_by_facility) | |
def display_cached(args): | |
with open('results.bin', 'rb') as f: | |
data = pickle.load(f) | |
print_summary(args, data) | |
def datekey(date: datetime.date) -> int: | |
return date.year * 10000 + date.month * 100 + date.day | |
def print_summary(args, available_days_by_facility: Dict[int, List[Tuple[datetime.date, str]]]): | |
weekdays = None | |
if args.weekdays: | |
weekdays = {int(day) for day in args.weekdays.split(',')} | |
print('--- SUMMARY ---') | |
for facility in FACILTIES: | |
facility_results = available_days_by_facility.get(facility.id) | |
if not facility_results: | |
continue | |
if weekdays: | |
facility_results = [t for t in facility_results if t[0].weekday() in weekdays] | |
if len(facility_results) == 0: | |
continue | |
print(f'🌳 {facility.name}') | |
for date, href in sorted(facility_results, key=lambda t: datekey(t[0])): | |
entry = f'\t🧺 {WEEKDAY_NAMES[date.weekday()]}\t{date.month}/{date.day} ' | |
if args.urls: | |
print(f'{entry}\t{href}') | |
else: | |
print(entry) | |
if __name__ == '__main__': | |
args = make_argparser().parse_args() | |
if args.cached: | |
display_cached(args) | |
else: | |
scrape(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment