hvega-0.7.0.0: tools/GetBetelgeuse.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-
Grab Betelgeuse data from the AAVSO site. This is based on
code in https://github.com/hippke/betelbot which is
used to generate the tweets from https://twitter.com/betelbot
The idea is to download a bunch of Vis-band observations
and grab the data from the HTML, convert to a very-simple
JSON format and write to the screen. There is limited
error checking.
-}
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.Text as T
import qualified Network.HTTP.Conduit as NHC
import Data.Aeson (ToJSON(..), encode, defaultOptions, genericToEncoding)
import Data.List (sortBy)
import Data.Function (on)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text.Encoding (decodeUtf8)
import GHC.Generics
import Text.HTML.TagSoup (Tag(..)
, isTagOpenName
, parseTags
, partitions)
import Text.Read (readMaybe)
import Numeric.Natural (Natural)
-- | The given page of results from AAVSO for Betelgeuse
--
-- When ran there were ~3900 results, so at 200 per page that's
-- 20 pages
--
pageURL :: Natural -> String
pageURL p = "https://www.aavso.org/apps/webobs/results/?start=2017-06-01&num_results=200&obs_types=vis+ccd&star=betelgeuse&page=" <> show p
nPages :: Natural
nPages = 20
-- | Return the given page.
--
getPage :: Natural -> IO T.Text
getPage p = decodeUtf8 . L.toStrict <$> NHC.simpleHttp (pageURL p)
data Row = Row { jd :: Double
, date :: T.Text
, magnitude :: Double
, magErr :: Maybe Double
, filterName :: T.Text
, observer :: T.Text
} deriving (Generic, Show)
instance ToJSON Row where
toEncoding = genericToEncoding defaultOptions
-- Extract rows
--
-- The data structure is regular, but complicated. I am going
-- to ignore the "extra" data for each observation.
--
extractRows :: [Tag T.Text] -> [Row]
extractRows tgs =
let rows = partitions (isTagOpenName "tr") tgs
in mapMaybe extractRow rows
isObs :: [(T.Text, T.Text)] -> Bool
isObs xs =
let hasObs cls = "obs" `elem` T.splitOn " " cls
act = hasObs <$> lookup "class" xs
in fromMaybe False act
-- Is this a row we care about? The choice is made by
-- only looking for rows with a class name of 'obs'.
--
extractRow :: [Tag T.Text] -> Maybe Row
extractRow (TagOpen "tr" attrs : ts) | isObs attrs = toRow ts
extractRow _ = Nothing
-- extract the text contents of the td element; highly specialised
-- to this use case
getText :: [Tag T.Text] -> T.Text
getText (TagText txt:_) = txt
getText (_:ts) = getText ts
getText [] = error "unexpected data"
-- probably not enough data checks
toRow :: [Tag T.Text] -> Maybe Row
toRow tgs =
let tds = partitions (isTagOpenName "td") tgs
jdTag = getText (tds !! 2)
dateTag = getText (tds !! 3)
magTag = getText (tds !! 4)
magETag = getText (tds !! 5)
filterTag = getText (tds !! 6)
obsTag = getText (tds !! 7)
magE = readMaybe (T.unpack magETag)
in if length tds /= 9
then Nothing
else Row <$> readMaybe (T.unpack jdTag)
<*> pure dateTag
<*> readMaybe (T.unpack magTag)
<*> pure magE
<*> pure filterTag
<*> pure obsTag
processPage :: T.Text -> [Row]
processPage = extractRows . parseTags
main :: IO ()
main = do
rs <- mapM (fmap processPage . getPage) [1..nPages]
-- assuming no chance of getting repeated rows from this query
let rows = concat rs
srows = sortBy (compare `on` jd) rows
js = encode srows
L8.putStrLn js