use std::rc::Rc;
use log::info;
use reqwest::Client;
use html_parser::{Dom, Node as DomNode};
use qualname::{NcName, QName};
use xrust::item::{Item, Node};
use xrust::parser::xpath::parse;
use xrust::transform::Transform;
use xrust::transform::context::{ContextBuilder, StaticContextBuilder};
use xrust::trees::smite::RNode;
use xrust::value::Value;
use xrust::xdmerror::{Error, ErrorKind};
const CLIENT_TIMEOUT: u64 = 10;
struct Site {
url: String,
xpaths: Vec<(String, Transform<RNode>)>,
}
fn make_doc(html: String) -> RNode {
let d = RNode::new_document();
Dom::parse(&html)
.expect("unable to parse HTML")
.children
.iter()
.for_each(|c| add_node(d.clone(), c.clone()));
d
}
fn add_node(s: RNode, n: DomNode) {
let mut r = s.clone();
match n {
DomNode::Element(e) => {
let t = r
.new_element(QName::from_local_name(
NcName::try_from(e.name.as_str()).expect("not a valid element name"),
))
.expect("unable to create element");
e.attributes.iter().for_each(|(k, v)| {
t.add_attribute(
r.new_attribute(
QName::from_local_name(
NcName::try_from(k.to_string().as_str())
.expect("not a valid atttribute name"),
),
Rc::new(Value::from(v.clone().unwrap_or(String::new()))),
)
.expect("unable to create attribute"),
)
.expect("unable to add attribute")
});
r.push(t.clone()).expect("unable to add element node");
e.children
.iter()
.for_each(|c| add_node(t.clone(), c.clone()))
}
DomNode::Text(s) => r
.push(
r.new_text(Rc::new(Value::from(s)))
.expect("unable to create text node"),
)
.expect("unable to add text node"),
_ => {}
}
}
async fn parse_site(client: &Client, site: Site) -> Result<(), reqwest::Error> {
let response = client.get(&site.url).send().await?;
if response.status().is_success() {
let body = response.text().await?;
let document = make_doc(body);
for (xpath, cons) in site.xpaths {
let mut stctxt = StaticContextBuilder::new()
.message(|m| {
eprintln!("{}", m);
Ok(())
})
.parser(|_| Err(Error::new(ErrorKind::NotImplemented, "not implemented")))
.fetcher(|_| Err(Error::new(ErrorKind::NotImplemented, "not implemented")))
.build();
let rd = RNode::new_document();
if let Ok(nodes) = ContextBuilder::new()
.context(vec![Item::Node(document.clone())])
.result_document(rd)
.build()
.dispatch(&mut stctxt, &cons)
{
println!(
"{} elements found by {} on {}",
nodes.len(),
xpath,
site.url
);
for node in nodes {
let text = node.to_string();
println!("{}", text);
}
} else {
println!("Invalid xpath expression: {}", xpath);
}
}
} else {
println!("Couldn't get the page: {}", site.url);
}
Ok(())
}
async fn run(sites: Vec<Site>) -> Result<(), reqwest::Error> {
info!("Starting the crawler");
let client = Client::builder()
.timeout(std::time::Duration::from_secs(CLIENT_TIMEOUT))
.build()
.unwrap();
let tasks = sites.into_iter().map(|site| parse_site(&client, site));
futures::future::join_all(tasks).await;
info!("Finishing the crawler");
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
env_logger::init();
let sites = vec![
Site {
url: "https://www.rust-lang.org/learn".to_string(),
xpaths: vec![
(
"/descendant::title".to_string(),
parse("/descendant::title", None, None)
.expect("unable to parse XPath expression"),
),
(
"/descendant::h2".to_string(),
parse("/descendant::h2", None, None).expect("unable to parse XPath expression"),
),
],
},
Site {
url: "https://www.python.org/about/".to_string(),
xpaths: vec![
(
"/descendant::title".to_string(),
parse("/descendant::title", None, None)
.expect("unable to parse XPath expression"),
),
(
"/descendant::h1".to_string(),
parse("/descendant::h1", None, None).expect("unable to parse XPath expression"),
),
],
},
Site {
url: "https://www.haskell.org/".to_string(),
xpaths: vec![
(
"/descendant::title".to_string(),
parse("/descendant::title", None, None)
.expect("unable to parse XPath expression"),
),
(
"/descendant::a[attribute::class eq 'readmore']".to_string(),
parse("/descendant::a[attribute::class eq 'readmore']", None, None)
.expect("unable to parse XPath expression"),
),
],
},
];
run(sites).await?;
Ok(())
}