Last active
March 18, 2026 23:18
-
-
Save adrianmgg/f71e77894f9fc67d61fed68b92f9cb7f to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| import bs4 | |
| import itertools | |
| import re | |
| import json | |
| import argparse | |
| import typing | |
| import csv | |
| def cli(): | |
| parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, epilog=r"""examples: | |
| curl -L -o - 'https://sjsu.edu/classes/schedules/spring-2026.php' | %(prog)s > schedule.json | |
| %(prog)s schedule.html -o schedule.json""") | |
| parser.add_argument('infile', metavar='in', type=argparse.FileType('r'), nargs='?', default='-', help='input file. "-" for stdin (default).') | |
| parser.add_argument('--out', '-o', metavar='OUT', dest='outfile', type=argparse.FileType('w', encoding='utf-8'), default='-', help='output file. "-" for stdout (default).') | |
| return parser.parse_args() | |
| def main(infile: typing.TextIO, outfile: typing.TextIO): | |
| txt = infile.read() | |
| soup = bs4.BeautifulSoup(txt, 'html.parser') | |
| schedule_table = soup.find(id='classSchedule') | |
| assert schedule_table is not None | |
| # some preprocessing | |
| for br in schedule_table.find_all('br'): | |
| br.replace_with('\n') | |
| raw_headers = [th.string for th in schedule_table.thead.tr.find_all('th', recursive=False)] | |
| headers = [re.sub(r"\s+", r"_", str(h)).lower() for h in raw_headers] | |
| class_rows = ( | |
| dict(zip( | |
| headers, | |
| (''.join(s.strip(' \t\xa0') for s in td.strings).strip() for td in tr.find_all('td', recursive=False)), | |
| )) | |
| for tr in schedule_table.tbody.find_all('tr', recursive=False) | |
| ) | |
| outfile.write('[') | |
| for is_first, row in zip(itertools.chain((True,), itertools.repeat(False)), class_rows): | |
| if not is_first: outfile.write(',') | |
| outfile.write('\n\t') | |
| json.dump(row, outfile) | |
| outfile.write('\n]') | |
| if __name__ == '__main__': | |
| main(**vars(cli())) |
This file contains hidden or 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
| usage: scrape_sjsu_schedule.py [-h] [--out OUT] [in] | |
| positional arguments: | |
| in input file. "-" for stdin (default). | |
| options: | |
| -h, --help show this help message and exit | |
| --out, -o OUT output file. "-" for stdout (default). | |
| examples: | |
| curl -L -o - 'https://sjsu.edu/classes/schedules/spring-2026.php' | scrape_sjsu_schedule.py > schedule.json | |
| scrape_sjsu_schedule.py schedule.html -o schedule.json |
This file contains hidden or 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
| """ | |
| find un-occupied classrooms | |
| e.g. | |
| python freespace.py W '03:00PM-06:00PM' '^MH\d+$' | |
| """ | |
| import argparse | |
| import datetime | |
| from pathlib import Path | |
| import json | |
| import collections | |
| from intervaltree import Interval, IntervalTree | |
| import re | |
| import pprint | |
| data = json.loads(Path('schedule.json').read_text()) | |
| relevant_classes = ( | |
| c | |
| for c in data | |
| if c['mode_of_instruction'] == 'In Person' | |
| ) | |
| def parse_time_interval(s: str) -> tuple[datetime.datetime, datetime.datetime]: | |
| a,b = tuple( | |
| datetime.datetime.strptime(t, '%I:%M%p') | |
| for t in s.split('-') | |
| ) | |
| return a,b | |
| def initial_interval(): | |
| a,b = parse_time_interval('12:00AM-11:59PM') | |
| t = IntervalTree() | |
| t[a:b] = False | |
| return t | |
| def extract_times(timestr: str): | |
| return map(parse_time_interval, re.findall(r"\d{2}:\d{2}[AP]M-\d{2}:\d{2}[AP]M", timestr)) | |
| # room -> ( day -> times ) | |
| room_occupations: collections.defaultdict[str, collections.defaultdict[str, IntervalTree[bool]]] = ( | |
| collections.defaultdict( | |
| lambda: collections.defaultdict(initial_interval) )) | |
| for c in relevant_classes: | |
| for daychar in c['days']: | |
| for a,b in extract_times(c['times']): | |
| room_occupations[c['location']][daychar][a:b] = True | |
| def foo(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('daychar') | |
| parser.add_argument('interval', type=parse_time_interval) | |
| parser.add_argument('room_pattern') | |
| args = parser.parse_args() | |
| assert len(args.daychar) == 1 | |
| a,b = args.interval | |
| for room,d in sorted(room_occupations.items(), key=lambda rd: rd[0]): | |
| for day,times in d.items(): | |
| if day != args.daychar: continue | |
| if re.match(args.room_pattern, room) is None: continue | |
| relevant_times = times[a:b] | |
| if len(relevant_times) >= 1 and not any(rt.data for rt in relevant_times): | |
| print(room) | |
| foo() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment