switch from scraping to API queries
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user