Every extended Labor Day weekend, 80,000 fans of pop culture descend on Atlanta for Dragon Con. It’s a sprawling choose-your-own adventure of a convention with 38 programming tracks and over 5,000 hours of events. It spans five downtown host hotels, and there is no way to see it all.
Sadly, this year’s con is almost over. Still, I thought I’d share a little script I wrote to help me make sense of it all.
The official mobile app is fine for searching and bookmarking events, speakers, and exhibitors. Nonetheless, it’s not suitable for scanning the whole landscape at once. I wanted a single, scrollable view of every event, before I even packed my cosplay.

The web version of the app gave me exactly what I needed: predictable per-day URLs and semantically marked-up HTML. That meant I can skip the API hunt, skip the manual scrolling, and go straight to scraping.

<div>
blocks.From Chaos to Clarity in 40 lines
We’re about to turn a messy, multi-day, multi-hotel schedule into one clean, scroll-once list. This is the forty-five-line Perl map that gets us there, aided by the Mojolicious web toolkit.
Laying the Groundwork: Tools for the Job
#!/usr/bin/env perl
use v5.40;
use Carp;
use English;
use Mojo::UserAgent;
use Mojo::URL;
use Mojo::DOM;
use Mojo::Collection q(c);
use Time::Piece;
use HTML::HTML5::Entities;
use Memoize;
binmode STDOUT, ':encoding(UTF-8)'
or croak "Couldn't encode STDOUT: $OS_ERROR";
my $ua = Mojo::UserAgent->new();
my $site = Mojo::URL->new('https://app.core-apps.com');
my $path = '/dragoncon25/events/view_by_day';
What’s happening: Load the modules that will do the heavy lifting–HTTP fetches, DOM parsing, date handling, Unicode cleanup. Lock STDOUT to UTF‑8 so characters like curly quotes and em-dashes don’t break the output. Point the script at the base schedule URL.
Remembering the Days Without Re-Parsing
my $date_from_dom = memoize( sub ($dom) {
return content_at( $dom, 'div.section_header[class~="alt"]' );
} );
What’s happening: Create a memoized helper that plucks the date from a day’s HTML and caches it. That way, if we need it again, we skip the DOM re-parse and keep the pipeline fast.
content_at
is a helper function I define later.
Starting Where the App Starts
my $today_dom = Mojo::DOM->new( $ua->get("$site$path")->result->text );
What’s happening: Fetch the “today” view–the same default the app shows. This is so we have a known starting point for building the full timeline.
Collecting the Whole Timeline
my $day_doms = c(
$today_dom,
$today_dom->find(qq(div.filter_box-days > a[href^="$path?day="]))
->map( \&dom_from_anchor )
->to_array->@*,
)->sort( sub { day_epoch($a) <=> day_epoch($b) } );
What’s happening: Grab every day link from the filter bar, fetch each day’s HTML, and sort them chronologically. Now we’ve got the entire con’s schedule in memory, ready to process.
dom_from_anchor
and day_epoch
are two more helper functions explained further down.
Turning HTML into a Human-Readable Schedule
$day_doms->each( sub { # process each day's events
my $date = $date_from_dom->($_);
$_->find('a.bookmark[data-type="events"] + a.object_link')
->each( sub { # output start time + title
my $time = content_at( $_, 'div.line[class~="two"]' );
my $title = content_at( $_, 'div.line[class~="one"]' );
my ($start) = split /\s*\p{Dash_Punctuation}/, $time;
say "$date $start: ", decode_entities($title);
} );
} );
What’s happening: For each day, find every event link and pull out the start time and title. Split the time cleanly on any dash and decode HTML entities so the output reads like a real schedule.
The Little Routines That Make It All Work
sub dom_from_anchor ($dom) { # fetch DOM for a day link
return Mojo::DOM->new(
$ua->get( Mojo::URL->new( $dom->attr('href') )->to_abs($site) )
->result->text );
}
sub day_epoch ($dom) { # parse date into epoch
return Time::Piece->strptime( $date_from_dom->($dom), '%A, %b %e' )
->epoch;
}
# extract and trim text from selector
sub content_at ( $dom, @args ) { return trim $dom->at(@args)->content }
What’s happening:
dom_from_anchor
: fetch and parses a linked days’ HTML.day_epoch
: turn a date string into a sort-able epoch.content_at
: extract and trim text from a DOM fragment, given a CSS selector.
These helpers keep the main flow readable and re-usable.
The Schedule, Unlocked
Run the script and you get a clean, UTF-8-safe list of every event, in chronological order, across all days. No swiping around, no tapping, no “what did I miss?” anxiety. (Ha, who am I kidding? There’s too much going on at Dragon Con to not end up missing something.)

And here’s just a small slice of the 2,500+ lines it produces:
Sunday, Aug 31 11:30 AM: Unmasking Sherlock: Beyond the Many Faces
Sunday, Aug 31 11:30 AM: Weaponization of the FCC and Other Agencies to Chill Speech
Sunday, Aug 31 11:30 AM: Where Physics Gets Weird
. . .
Sunday, Aug 31 11:50 AM: Photo Session: Amelia Tyler
Sunday, Aug 31 11:50 AM: Photo Session: Cissy Jones
Sunday, Aug 31 11:50 AM: Photo Session: Emma Gregory
. . .
Sunday, Aug 31 12:00 PM: Dragon Con Mashups
Sunday, Aug 31 12:00 PM: James J. Butcher and R.R. Virdi signing at The Missing Volume booth# 1300
Sunday, Aug 31 12:00 PM: JoeDan Worley and Eric Dontigney signing at the Shadow Alley Press Booth# 2
. . .
Sunday, Aug 31 12:00 PM: Photo Session: Robert Duncan McNeill
Sunday, Aug 31 12:00 PM: Photo Session: Robert Picardo
Sunday, Aug 31 12:00 PM: Photo Session: Tamara Taylor
Key Techniques
Here’s the fun part–the techniques that make this tidy, scroll-once list possible.
CSS selectors for precision
I used a.bookmark[data-type="events" + a.object_link]
to grab only the event title links, and div.line[class~="two"
/div.line[class~="one"]
for time and title, respectively. This avoids scraping unrelated elements.
Memoization for efficiency
memoize
caches the date string for each day’s DOM so I didn’t end up re-parsing the HTML fragment multiple times.
Unicode-safe splitting
\p{Dash_Punctuation}
matches any dash type (em, en, hyphen-minus, etc.), so I could split times reliably without worrying about which dash the site uses.
Functional chaining
Mojo::Collection’s map
, sort
, and each
methods let me express the scrape→transform→output pipeline in a linear, readable way.
Entity decoding at output
HTML::HTML5::Entities’ decode_entities
is applied right before printing, so HTML entities like &
or "
are human-readable in the final output.
A Pattern You Can Take Anywhere
The same approach that tamed Dragon Con’s chaos works anywhere you’ve got:
- Predictable URLs–so you can iterate without guesswork
- Consistent HTML structure–so your selectors stay stable
- A need to see everything at once–so you can make decisions without paging or filtering
From fan conventions to conference schedules, from local sports fixtures to film festival line‑ups–the same pattern applies. Sometimes the right tool isn’t a sprawling framework or heavyweight API client. It’s a forty‑odd‑line Perl script that does one thing with ruthless clarity.
Because once you’ve tamed a schedule like this, the only lines you’ll stand in are the ones that feel like part of the show.