switch from scraping to API queries

This commit is contained in:
Dr. Matthias Ratajczak
2022-12-22 15:42:37 +01:00
parent daed8fee58
commit c84a446713
6 changed files with 47 additions and 725 deletions
-86
View File
@@ -1,86 +0,0 @@
use anyhow::{bail, Context, Result};
use scraper::{ElementRef, Html, Selector};
pub(crate) struct Extractor {
time: Selector,
departure: Selector,
delay: Selector,
}
impl Extractor {
pub(crate) fn new() -> Self {
Self {
time: Self::get_time_selector(),
departure: Self::get_departure_selector(),
delay: Self::get_delay_selector(),
}
}
fn get_time_selector() -> Selector {
Selector::parse("div.col.c2of12.tour p").unwrap()
}
fn get_departure_selector() -> Selector {
Selector::parse("li[data-filter][data-departure-id][data-departure-time]").unwrap()
}
fn get_delay_selector() -> Selector {
Selector::parse("span.late").unwrap()
}
pub(crate) fn get_departures<'a>(&'a self, html: &'a Html) -> impl Iterator<Item = ElementRef> {
let result: Vec<_> = html.select(&self.departure).collect();
// we seem to extract the blocks twice with slightly different layout, so we discard half of them
let half = result.len() / 2;
result.into_iter().take(half)
}
pub(crate) fn get_transport_line(departure_block: scraper::ElementRef) -> Result<&str> {
let val = departure_block.value();
let result = val
.attr("data-filter")
.context("accessing 'data-filter'")?
.trim();
Ok(result)
}
pub(crate) fn extract_delay(
departure_block: scraper::ElementRef,
late_selector: &Selector,
) -> Option<String> {
departure_block
.select(late_selector)
.next()
.map(|content| content.inner_html())
}
pub(crate) fn get_times(&self, departure: scraper::ElementRef) -> Result<String> {
let delay = Self::extract_delay(departure, &self.delay);
for time_block in departure
.select(&self.time)
.next()
.context("extracting time block")?
.children()
{
let value = time_block.value();
if value.is_text() {
let scheduled_time = value
.as_text()
.context("extracting text of scheduled time")?
.trim();
if !scheduled_time.is_empty() {
let mut result = "Abfahrt: ".to_owned();
result.push_str(scheduled_time);
if let Some(late_text) = &delay {
result.push_str(" | Verspätet: ");
result.push_str(late_text);
}
return Ok(result);
}
}
}
bail!("could not get the times")
}
}
+3 -13
View File
@@ -1,22 +1,12 @@
mod extractor;
mod query;
mod response;
use anyhow::Result;
use scraper::Html;
use extractor::Extractor;
fn main() -> Result<()> {
let message = query::query_server()?;
let html = Html::parse_document(&message);
let extractor = Extractor::new();
for departure in extractor.get_departures(&html) {
println!("{}", Extractor::get_transport_line(departure)?);
println!("{}", extractor.get_times(departure)?);
println!();
let response = query::query_server()?;
for departure in response.departures {
println!("{departure}");
}
Ok(())
}
+13 -8
View File
@@ -1,11 +1,16 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
const URL_DEPARTURES: &str =
"https://www.vvo-online.de/de/fahrplan/aktuelle-abfahrten-ankuenfte/abfahrten?stopid=33006765";
use crate::response::Response;
pub(crate) fn query_server() -> Result<String> {
match ureq::get(URL_DEPARTURES).call() {
Ok(response) => Ok(response.into_string().map_err(|e| anyhow!(e))?),
Err(e) => Err(anyhow!(e)),
}
const DEPARTURE_MONITOR: &str = "https://webapi.vvo-online.de/dm";
pub(crate) fn query_server() -> Result<Response> {
let json_string = ureq::post(DEPARTURE_MONITOR)
.set("Content-Type", "application/json")
.set("charset", "UTF-8")
.send_string("{\"stopid\":\"33006765\",\"limit\":5}")?
.into_string()?;
let result = serde_json::from_str(&json_string)?;
Ok(result)
}
+22 -4
View File
@@ -1,6 +1,6 @@
#![allow(dead_code)]
use std::fmt::Debug;
use std::fmt::{Debug, Display};
use anyhow::Result;
use chrono::{DateTime, Local, TimeZone, Utc};
@@ -8,7 +8,7 @@ use serde::{de, Deserialize, Deserializer};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct Departure {
pub struct Departure {
id: String,
dl_id: String,
line_name: String,
@@ -54,12 +54,12 @@ struct CancelReason {
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct Response {
pub struct Response {
name: String,
status: Status,
place: String,
expiration_time: String,
departures: Vec<Departure>,
pub departures: Vec<Departure>,
}
#[derive(Deserialize, Debug)]
@@ -97,3 +97,21 @@ where
Err(_) => Ok(None),
}
}
impl Display for Departure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{} {}\n", &self.line_name, &self.direction))?;
let scheduled_time = self.scheduled_time.format("%H:%M");
f.write_str(&format!("departure: {}", &scheduled_time))?;
if let Some(real_time) = self.real_time {
let actual_time = real_time.format("%H:%M");
if real_time != self.scheduled_time {
f.write_str(&format!(" | delayed: {actual_time} "))?;
}
}
f.write_str("\n")?;
Ok(())
}
}