Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions scrapers/wv/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
class WVEventScraper(Scraper, LXMLMixin):
verify = False
_tz = pytz.timezone("US/Eastern")
# Matches a time of day such as "1:00 PM", "9 AM" or "10:30a.m."
_time_re = re.compile(
r"\d{1,2}(:\d{2})?\s*[ap]\.?m\.?",
re.IGNORECASE,
)

def scrape(self):
com_urls = [
Expand Down Expand Up @@ -48,8 +53,9 @@ def scrape_committee_page(self, url):
page = self.lxmlize(url)
page.make_links_absolute(url)

# if the page starts w/ a meeting
if page.xpath('//div[@id="wrapleftcol"]/h1'):
# if the page starts w/ a meeting. The h1 is always the committee title;
# a meeting's date/time lives in the h2, so use that to detect a meeting.
if page.xpath('//div[@id="wrapleftcol"]/h2'):
yield from self.scrape_meeting_page(url)

for row in page.xpath('//td/a[contains(@href, "agendas.cfm")]'):
Expand All @@ -59,7 +65,7 @@ def scrape_committee_page(self, url):
# meeting pages show events going back years
# so just grab this cal year and later
when = row.xpath("text()")[0].strip()
when = when.split("-")[0]
when = self.strip_date_range(when)
when = self.clean_date(when)

# manual fix for un-yeared leap year meeting
Expand All @@ -79,12 +85,14 @@ def scrape_meeting_page(self, url):
if page.xpath('//div[text()="Error"]'):
return

if not page.xpath('//div[@id="wrapleftcol"]/h3'):
if not page.xpath('//div[@id="wrapleftcol"]/h2'):
return

com = page.xpath('//div[@id="wrapleftcol"]/h3[1]/text()')[0].strip()
com = re.sub(r"[\s\-]+Agenda", "", com)
when = page.xpath('//div[@id="wrapleftcol"]/h1[1]/text()')[0].strip()
# The committee name is in the h1 (e.g. "Senate Finance Committee - Agenda")
# and the meeting date/time is in the h2 (e.g. "March 12, 2026, 3:00 PM").
com = page.xpath('//div[@id="wrapleftcol"]/h1[1]/text()')[0].strip()
com = re.sub(r"\s*-\s*Agenda\s*$", "", com).strip()
when = page.xpath('//div[@id="wrapleftcol"]/h2[1]/text()')[0].strip()

if when == "test, test" or when == ",":
# Ignore test page
Expand All @@ -106,7 +114,7 @@ def scrape_meeting_page(self, url):
)
when = re.sub(r",?\s+After Floor", "", when, flags=re.IGNORECASE)

when = when.split("-")[0]
when = self.strip_date_range(when)
when = self.clean_date(when)
when = dateutil.parser.parse(when)
when = self._tz.localize(when)
Expand Down Expand Up @@ -171,11 +179,41 @@ def scrape_meeting_page(self, url):

yield event

def strip_date_range(self, when):
"""
Strip a trailing date range and keep only the start date.

Agenda dates are occasionally expressed as a range using a hyphen
surrounded by whitespace (e.g. "March 3, 2023 - March 4, 2023"), in
which case we only want the first date.

We must NOT split on a bare hyphen because numeric dates use hyphens
as separators (e.g. "1-14-14", "2-11-20"). Splitting those on "-"
would leave just the month component (e.g. "1"), which dateutil then
parses using today's month/year as defaults, producing wildly wrong
dates. Only treat a hyphen surrounded by whitespace as a range
delimiter.
"""
return re.split(r"\s+-\s+", when)[0].strip()

def clean_date(self, when):
# Remove all text after the third comma to make sure no extra text
# is included in the date. Required to correctly parse text like this:
# "Friday, March 3, 2023, Following wrap up of morning agenda"
when = ",".join(when.split(",")[:3])
"""
Cleans the meeting date string by removing trailing non-date text while
preserving any valid meeting time.

The date occupies the first three comma-separated segments (weekday,
month/day, year). Some agenda pages include additional text such as
"Following wrap up of morning agenda", while others include the meeting
time (e.g. "1:00 PM") after the date. Preserve any segment containing a
valid time and discard other trailing text so the datetime is parsed
correctly.
"""
segments = when.split(",")
kept = segments[:3]
for segment in segments[3:]:
if self._time_re.search(segment):
kept.append(segment)
when = ",".join(kept)

removals = [
r"(\d+|Thirty) (min\.|mins\.|minutes) After (.*)",
Expand Down
Loading