{"id":679,"date":"2022-11-11T01:46:05","date_gmt":"2022-11-11T01:46:05","guid":{"rendered":"https:\/\/chrisfaulkner.org\/?p=679"},"modified":"2022-11-11T01:51:09","modified_gmt":"2022-11-11T01:51:09","slug":"scraping-election-data-with-scrapy","status":"publish","type":"post","link":"https:\/\/chrisfaulkner.org\/index.php\/2022\/11\/11\/scraping-election-data-with-scrapy\/","title":{"rendered":"Scraping Election data with Scrapy"},"content":{"rendered":"\n<p>For our office election watch party last night, we wanted to watch results on as many simultaneous races as we could but we were particularly interested in the locals.<\/p>\n\n\n\n<p>That&#8217;s because we knew the national races would be covered heavily and we had a projector set up for TV coverage, but the local races would be &nbsp;more difficult to follow without a lot of channel flipping. &nbsp;At the same time, we also knew we weren&#8217;t going to come up with some sort of hologram-Wolf-Blitzer-projected-on-a-touch-screen-ice-rink level visualization in just a couple of hours. &nbsp;We&nbsp;<em>could<\/em>&nbsp;build something fun, responsive, and autonomous though.<\/p>\n\n\n\n<p>To get it done quickly I&#8217;d need to channel my past in the newspaper industry and remember that doing it &#8220;right&#8221; wasn&#8217;t worth much of anything if it wouldn&#8217;t be done until it was two days too late.<\/p>\n\n\n\n<p>The game plan was as follows:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Scrape local\/state election data from the internet, on an interval<\/li>\n\n\n\n<li>Load election data into a prebuilt dashboarding tool, on an interval<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Identify Sources<\/h2>\n\n\n\n<p>I was interested in two sets of races: &nbsp;The Arkansas state&nbsp;referendum, and the major City of Fayetteville races (4 city council alderman races and one mayoral race).<\/p>\n\n\n\n<p>I reached out to a contact at the local newspaper to ask if they&#8217;d have live data coming in across the AP wire or elsewhere. &nbsp;It turned out an election results webpage was already live for me to start inspecting. &nbsp;Score. &nbsp;I checked out the local races page and found the markup looked like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;div class=\"elections-race card card-block\"&gt;\n  &lt;h4 class=\"card-title\"&gt;ALDERMAN &lt;\/h4&gt;\n  &lt;h4 class=\"card-title\"&gt;WARD 1, POSITION 2  &lt;\/h4&gt;\n  &lt;span class=\"elections-county-reporting\"&gt;&lt;\/span&gt;\n  &lt;div class=\"elections-candidates clearfix\" id=\"race-9053\"&gt;\n    &lt;div class=\"candidate-tr clearfix candidate\"&gt;\n      &lt;div class=\"col-xs-4 col-lg-3 candidate-td candidate-name\"&gt;\n        &lt;span&gt; &lt;span&gt;*&lt;\/span&gt;                                                                      Sarah Marsh\n        &lt;\/span&gt;\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-4 col-lg-2 candidate-td candidate-vote-count\"&gt;\n        0\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-4 col-lg-1 candidate-td vandidate-vote-percent\"&gt;\n        0&lt;span&gt;%&lt;\/span&gt;\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-12 col-lg-6 candidate-td candidate-progress\"&gt;\n        &lt;progress class=\"progress progress-striped progress-success\" value=\"0\" max=\"0\"&gt;\n          &lt;div class=\"progress progress-success\"&gt;\n            &lt;span class=\"progress-bar\" style=\"width: 0%;\"&gt;&lt;\/span&gt;\n          &lt;\/div&gt;\n        &lt;\/progress&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"candidate-tr clearfix candidate\"&gt;\n      &lt;div class=\"col-xs-4 col-lg-3 candidate-td candidate-name\"&gt;\n        &lt;span&gt;                                                                    Paul Phaneuf\n        &lt;\/span&gt;\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-4 col-lg-2 candidate-td candidate-vote-count\"&gt;\n        0\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-4 col-lg-1 candidate-td vandidate-vote-percent\"&gt;\n        0&lt;span&gt;%&lt;\/span&gt;\n      &lt;\/div&gt;\n      &lt;div class=\"col-xs-12 col-lg-6 candidate-td candidate-progress\"&gt;\n        &lt;progress class=\"progress progress-striped\" value=\"0\" max=\"0\"&gt;\n          &lt;div class=\"progress\"&gt;\n            &lt;span class=\"progress-bar\" style=\"width: 0%;\"&gt;&lt;\/span&gt;\n          &lt;\/div&gt;\n        &lt;\/progress&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n    &lt;span class=\"elections-meta-time\"&gt;\n    &lt;\/span&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">Not bad. Bootstrap definitely gives us a lot of CSS selectors to work with. There's a lot of whitespace around the candidate names, but that can be dealt with. The race title, at least in some cases, is split between two&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">&amp;lt;h4 class=&quot;card-title&quot;&amp;gt;<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;tags which will also need to be reconciled.<\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">Not shown in this markup is the way individual counties and cities are labeled in this data, but they were&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">h3<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;and&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">h4<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;tags, respectively, that were siblings to these&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">&amp;lt;div class=&quot;elections-race&quot;&amp;gt;<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;divs. That would be harder to sort out--we'd have to walk back through the nearest preceding sibling elements to find the relevant municipality.<\/pre>\n\n\n\n<p>I then turned my attention to the state ballot issues page and found a lot of this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;script language=\"JavaScript\" src=\"http:\/\/hosted.ap.org\/elections\/2016\/general\/by_race\/AR_6486.js?SITE=ARLIDELN&amp;amp;SECTION=POLITICS\"&gt;&lt;\/script&gt;\n<\/pre>\n\n\n\n<p>After having a newspaper flashback (these hosted Associated Press scripts are full of&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">document.write()<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n) I decided I didn&#8217;t want to deal with parsing&nbsp;<em>or<\/em>&nbsp;rendering javascript, so I looked elsewhere for the state data which should be easier to find&#8211;and it was.<\/p>\n\n\n\n<p>Politico had the state referendums, and they looked like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&lt;div class=\"results-detail\" data-racenum=\"6485\" data-stateabb=\"AR\"&gt;\n  &lt;div class=\"macro\"&gt;\n    &lt;div class=\"results-about\"&gt;\n      &lt;h6&gt;Amend Term Limits and Rules&lt;\/h6&gt;\n      &lt;p class=\"summary\"&gt;Concerns term limits for members of the Arkansas General Assembly. &lt;\/p&gt;\n      &lt;p class=\"reporting\"&gt;0% Reporting&lt;\/p&gt;\n      &lt;p class=\"share\"&gt;&lt;a href=\"https:\/\/twitter.com\/intent\/tweet?text=%23AR amend term limits and rules ballot measure passes%3B 0%25 reporting.&amp;amp;url=http:\/\/politi.co\/2epgNZ5&amp;amp;hashtags=election2016\" class=\"twitter\" target=\"_blank\"&gt;&lt;b aria-hidden=\"true\" class=\"icon icon-twitter\"&gt;&lt;\/b&gt;&lt;\/a&gt;&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n  &lt;div class=\"micro\"&gt;\n    &lt;table class=\"results-table\"&gt;\n      &lt;tbody&gt;\n        &lt;tr class=\"type-ballot\"&gt;\n          &lt;th scope=\"row\" class=\"results-name\"&gt;&lt;span class=\"name-combo\"&gt;&lt;span class=\"token token-winner\"&gt;&lt;b aria-hidden=\"true\" class=\"icon icon-check\"&gt;&lt;\/b&gt;&lt;span class=\"icon-text\"&gt;Winner&lt;\/span&gt;&lt;\/span&gt; For&lt;\/span&gt;&lt;\/th&gt;\n          &lt;td class=\"results-percentage\"&gt;&lt;span class=\"percentage-combo\"&gt;&lt;span class=\"number\"&gt;0%&lt;\/span&gt;&lt;span class=\"graph\"&gt;&lt;span class=\"bar\"&gt;&lt;span class=\"index\" style=\"width:0%;\"&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/td&gt;\n          &lt;td class=\"results-popular\"&gt;0&lt;\/td&gt;\n        &lt;\/tr&gt;\n        &lt;tr class=\"type-ballot\"&gt;\n          &lt;th scope=\"row\" class=\"results-name\"&gt;&lt;span class=\"name-combo\"&gt; Against&lt;\/span&gt;&lt;\/th&gt;\n          &lt;td class=\"results-percentage\"&gt;&lt;span class=\"percentage-combo\"&gt;&lt;span class=\"number\"&gt;0%&lt;\/span&gt;&lt;span class=\"graph\"&gt;&lt;span class=\"bar\"&gt;&lt;span class=\"index\" style=\"width:0;\"&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/span&gt;&lt;\/td&gt;\n          &lt;td class=\"results-popular\"&gt;0&lt;\/td&gt;\n        &lt;\/tr&gt;\n      &lt;\/tbody&gt;\n    &lt;\/table&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n\n\n\n<p>This markup was a bit more structured, so there wouldn&#8217;t be much trouble ingesting it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Enter Scrapy<\/h2>\n\n\n\n<p><a href=\"https:\/\/web.archive.org\/web\/20211028101334\/https:\/\/scrapy.org\/\">Scrapy is a handy Python framework for building web crawlers<\/a>&nbsp;and parsing HTML content.&nbsp;<\/p>\n\n\n\n<p>Now, Scrapy was probably overkill for what we needed since we were just parsing a couple of single page targets. &nbsp;As a framework, Scrapy is geared towards building spiders: &nbsp;bots that crawl webpages across links and build very robust pipelines of data. &nbsp; That said, it&#8217;s a breeze to install and comes with enough syntactic bells and whistles that it was worth using, even if we only used about 1% what it is capable of.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mkvirtualenv elx_scrape\npip install scrapy\nscrape startproject elx .\n<\/pre>\n\n\n\n<p>This starts a script project in the current directory called &#8220;elx&#8221;. &nbsp; &nbsp;<\/p>\n\n\n\n<p>I didn&#8217;t end up doing much more with scrapy than is outlined in the&nbsp;<a href=\"https:\/\/web.archive.org\/web\/20211028101334\/https:\/\/doc.scrapy.org\/en\/1.2\/intro\/tutorial.html\">tutorial section of their documentation<\/a>, which is nicely detailed. &nbsp;I jumped in and created two crawlers, one for each data source:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">#elx\/spiders\/base.py\n\nclass LocalSpider(scrapy.Spider):\n    name = 'local'\n    start_urls = [\n        'http:\/\/elections.wehco.com\/elections\/general2016\/contested\/vote\/results',\n    ]\n\n    def parse(self, response):\n        return []\n\nclass StateSpider(scrapy.Spider):\n    name = 'state'\n    start_urls = [\n        'http:\/\/www.politico.com\/2016-election\/results\/map\/ballot-measures\/arkansas\/',\n    ]\n\n    def parse(self, response):\n        return []\n<\/pre>\n\n\n\n<p>These of course don&#8217;t do anything but return an empty list, but each one pulls down the appropriate page and is ready for parsing.<\/p>\n\n\n\n<p>The next step was to write the CSS\/XPath selectors that would locate the data I sought. &nbsp;Possibly the coolest tool in the scrapy toolbox is its interactive shell that lets you quickly play around with a scraped page&#8217;s data to design your parse methods:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/web.archive.org\/web\/20211028101334im_\/http%3A\/\/hirelofty.com\/documents\/4\/9JxFtG1DeP.gif?w=640&#038;ssl=1\" alt=\"scrapy demo\"\/><\/figure>\n\n\n\n<p>After a lot of fiddling I ended up with parse functions for each crawler that looked something like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    def parse(self, response):\n        for race in response.css('.elections-race'):\n            candidates = race.css('.candidate-tr')\n\n            try:\n                municipality = race.xpath('.\/preceding-sibling::h5\/text()').extract()[-1]\n            except IndexError:\n                municipality = \"\"\n\n            yield {\n                'race_title': \" \".join(race.css('.card-title::text').extract()),\n                'municipality': municipality,\n                'candidates':  [\"\".join(c.css('.candidate-name &gt; span::text').extract()).strip() for c in candidates],\n                'votes': [float(\"\".join(c.css('.candidate-vote-count::text').extract()).strip().replace(',', '')) for c in candidates],\n            }\n<\/pre>\n\n\n\n<p>Which allowed me to run&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">scrapy crawl local -o results.json<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;and get something like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">[\n  {\n    \"municipality\": \"Dewitt\",\n    \"race_title\": \"ALDERMAN  WARD 1, POSITION 1  \",\n    \"candidates\": [\n      \"Ada Clark\",\n      \"Brent London\"\n    ],\n    \"votes\": [\n      460,\n      407\n    ]\n  },\n\/\/...\n]\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Creating a Rube Goldberg Device<\/h2>\n\n\n\n<p>We decided on&nbsp;<a href=\"https:\/\/web.archive.org\/web\/20211028101334\/https:\/\/www.geckoboard.com\/\">Geckoboard<\/a>&nbsp;as the best option for quickly visualizing our data. &nbsp;It could pull sources over the web, we already had an account there, and it looks pretty decent.<\/p>\n\n\n\n<p>We immediately found some problems in our data formatting though. &nbsp;The prebuilt widgets we thought would work best required:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>CSV data<\/li>\n\n\n\n<li>One CSV file per chart<\/li>\n<\/ul>\n\n\n\n<p>That didn&#8217;t quite fit what we had scraped, but we did what any programmer with minutes left on a deadline would do. &nbsp;We hacked and repeated a bunch of code!<\/p>\n\n\n\n<p>Each Spider above was turned into a Pythonic base class with some sort of filtering in the&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">parse<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;method (only yield results if the race matches a certain title, etc) and created individual spiders for each race.<\/p>\n\n\n\n<p>Luckily, switching scrapy&#8217;s output to CSV was painless&#8211;it can be done through a project setting, or by simply specifying a&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">.csv<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;extension in your output file.<\/p>\n\n\n\n<p>That machinery got wrapped by a shamefully barbaric shell script which refreshed our data every five minutes:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">while true;\ndo\nrm output\/alderman1.csv\nscrapy crawl alderman1 -o output\/alderman1.csv\nrm output\/alderman2.csv\nscrapy crawl alderman2 -o output\/alderman2.csv\nrm output\/alderman3.csv\nscrapy crawl alderman3 -o output\/alderman3.csv\n\n# ...\n\nsleep 300;\ndone;\n<\/pre>\n\n\n\n<p>Now we had data being pulled to my laptop every 5 minutes&#8211;we just needed to get it web accessible for GeckoBoard to ingest it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">ngrok and SimpleHTTPServer<\/h2>\n\n\n\n<p><a href=\"https:\/\/web.archive.org\/web\/20211028101334\/https:\/\/ngrok.com\/\">Ngrok<\/a>&nbsp;is a neat tool that sets up a dynamic proxy to a local port. You receive a dynamic DNS name at&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">*.ngrok.com<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n&nbsp;which can make local device temporarily web accessible.<\/p>\n\n\n\n<p>We ngrok all the time to demo local web applications to our team and clients before a product is in the cloud. Today though, I just needed to serve some static files to Geckoboard over HTTP.<\/p>\n\n\n\n<p>Equally handy, Python ships with a lightweight (albeit not terribly efficient) web server module. We&#8217;ll let Python serve the files and ask ngrok to proxy the local web server so Geckoboard can see it.<\/p>\n\n\n\n<p>We turned on the Python webserver in our project&#8217;s output dir:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">cd output\npython -m SimpleHTTPServer 8002\n<\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/web.archive.org\/web\/20211028101334im_\/https%3A\/\/hirelofty-prod.s3.amazonaws.com\/media\/images\/Screen_Shot_2016-11-09_at_1.35.58_PM.width-800.png?w=640&#038;ssl=1\" alt=\"Python server\"\/><\/figure>\n\n\n\n<p>And instructed ngrok to proxy port&nbsp;\n\n<div class=\"codecolorer-container text blackboard\" style=\"overflow:auto;white-space:nowrap;width:800px;\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td class=\"line-numbers\"><div>1<br \/><\/div><\/td><td><div class=\"text codecolorer\">8002<\/div><\/td><\/tr><\/tbody><\/table><\/div>\n\n:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ngrok http 8002\n<\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/web.archive.org\/web\/20211028101334im_\/https%3A\/\/hirelofty-prod.s3.amazonaws.com\/media\/images\/Screen_Shot_2016-11-09_at_1.36.37_PM.width-800.png?w=640&#038;ssl=1\" alt=\"ngrok\"\/><\/figure>\n\n\n\n<p>And that&#8217;s everything we needed to serve the data up for Geckoboard. &nbsp;We were pretty happy with how it turned out and it was nice to get some fancy local results alongside the national coverage during our watch party (even if things didn&#8217;t exactly go the way we had hoped,&nbsp;<em>ahem).<\/em><\/p>\n\n\n\n<p>Of course, there are many other ways to accomplish the same thing&#8211;some far more robust and stable. &nbsp;Perhaps closer to real time if you have access to the correct data sources. &nbsp;<\/p>\n\n\n\n<p>In all, though, we were pretty happy with what could be accomplished in just a couple of hours of relatively low effort.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/web.archive.org\/web\/20211028101334im_\/https%3A\/\/hirelofty-prod.s3.amazonaws.com\/media\/images\/Screen_Shot_2016-11-09_at_11.49.31_AM.width-800.png?w=640&#038;ssl=1\" alt=\"elx dashboard\"\/><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>For our office election watch party last night, we wanted to watch results on as many simultaneous races as we could but we were particularly interested in the locals. That&#8217;s because we knew the national races would be covered heavily and we had a projector set up for TV coverage, but the local races would &hellip; <\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/chrisfaulkner.org\/index.php\/2022\/11\/11\/scraping-election-data-with-scrapy\/\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-679","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p46pud-aX","jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/posts\/679","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/comments?post=679"}],"version-history":[{"count":4,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/posts\/679\/revisions"}],"predecessor-version":[{"id":684,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/posts\/679\/revisions\/684"}],"wp:attachment":[{"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/media?parent=679"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/categories?post=679"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/chrisfaulkner.org\/index.php\/wp-json\/wp\/v2\/tags?post=679"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}