it works now
This commit is contained in:
parent
c3997b0bb7
commit
fd971bafbf
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -9,7 +9,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug executable 'surreal_spider'",
|
"name": "Debug executable 'surreal_spider'",
|
||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "surreal_spider=trace",
|
"RUST_LOG": "surreal_spider=trace,reqwest=trace",
|
||||||
},
|
},
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
|
@ -3,6 +3,8 @@ services:
|
|||||||
image: surrealdb/surrealdb:latest-dev
|
image: surrealdb/surrealdb:latest-dev
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
volumes:
|
||||||
|
- ./.surrealdb/:/mydata
|
||||||
command:
|
command:
|
||||||
- start
|
- start
|
||||||
- --log
|
- --log
|
||||||
@ -11,3 +13,4 @@ services:
|
|||||||
- root
|
- root
|
||||||
- --pass
|
- --pass
|
||||||
- root
|
- root
|
||||||
|
- rocksdb:/mydata/database.db
|
||||||
|
143
src/db.rs
143
src/db.rs
@ -3,89 +3,89 @@ use surrealdb::{
|
|||||||
engine::remote::ws::{Client, Ws},
|
engine::remote::ws::{Client, Ws},
|
||||||
opt::auth::Root,
|
opt::auth::Root,
|
||||||
sql::Thing,
|
sql::Thing,
|
||||||
Surreal,
|
Response, Surreal,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, instrument};
|
use tracing::{debug, error, info, instrument, trace};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Website {
|
pub struct Website {
|
||||||
/// The url that this data is found at
|
/// The url that this data is found at
|
||||||
site: Url,
|
site: Url,
|
||||||
/// The url as defined in the <a> tag
|
|
||||||
href: Url,
|
|
||||||
/// Wether or not this link has been crawled yet
|
/// Wether or not this link has been crawled yet
|
||||||
crawled: bool,
|
crawled: bool,
|
||||||
/// Wether or not the href was doctored
|
#[serde(skip_serializing)]
|
||||||
doctored_href: bool,
|
id: Option<Thing>,
|
||||||
original_href: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Website {
|
impl Website {
|
||||||
/// Creates a blank site (assumes that url param is site's root)
|
/// Creates a blank site (assumes that url param is site's root)
|
||||||
pub fn new(url: &str, href: &str, crawled: bool) -> Self {
|
pub fn new(url: &str, crawled: bool) -> Self {
|
||||||
let mut new = Self::from(url);
|
let site = match Url::parse(url) {
|
||||||
new.crawled = crawled;
|
Ok(a) => a,
|
||||||
new.original_href = Some(href.to_string());
|
Err(_) => todo!(),
|
||||||
new.href =
|
|
||||||
match Url::parse(href) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
|
||||||
match e {
|
|
||||||
url::ParseError::RelativeUrlWithoutBase => {
|
|
||||||
// Try to combine the scheme_host and href to get a useable domain
|
|
||||||
new.doctored_href = true;
|
|
||||||
|
|
||||||
let url = if !url.ends_with('/') && !href.starts_with('/') {
|
|
||||||
format!("{url}/{href}")
|
|
||||||
} else {
|
|
||||||
format!("{url}{href}")
|
|
||||||
};
|
};
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
crawled,
|
||||||
|
site,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// paste the domain onto the begining of the href
|
pub fn set_crawled(&mut self) {
|
||||||
Url::parse(&url).map_or_else(|err| {
|
trace!("Set crawled to true");
|
||||||
debug!("Parsing {url} with {href}");
|
|
||||||
error!("{err} Failed to parse href into url on second try. Aborting");
|
|
||||||
panic!("See error logs for more info.");
|
|
||||||
}, |ok| ok)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("{e}");
|
|
||||||
panic!("See error logs for more info.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
new
|
|
||||||
}
|
|
||||||
pub fn crawled(&mut self) {
|
|
||||||
self.crawled = true
|
self.crawled = true
|
||||||
}
|
}
|
||||||
pub fn href_str(&self) -> &str {
|
|
||||||
self.href.as_str()
|
pub fn crawled(&mut self) -> &mut bool {
|
||||||
|
&mut self.crawled
|
||||||
}
|
}
|
||||||
pub fn site(&self) -> String {
|
|
||||||
self.site.to_string()
|
pub fn mut_url(&mut self) -> &mut Url {
|
||||||
|
&mut self.site
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn domain_str(&self) -> &str {
|
pub fn domain_str(&self) -> &str {
|
||||||
self.site.as_str()
|
self.site.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn links_to(&self, other: &Self, db: &Surreal<Client>) {
|
||||||
|
let from = self.site.to_string();
|
||||||
|
let to = other.site.to_string();
|
||||||
|
trace!("Linking {from} to {to}");
|
||||||
|
|
||||||
|
match db
|
||||||
|
.query("RELATE (SELECT id FROM website WHERE site = $in) -> links_to -> (SELECT id FROM website WHERE site = $out)")
|
||||||
|
.bind(("in", from))
|
||||||
|
.bind(("out", to))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(e) => {
|
||||||
|
let _: Response = e;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e.to_string());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn store(&mut self, db: &Surreal<Client>) {
|
pub async fn store(&mut self, db: &Surreal<Client>) {
|
||||||
// is root record?
|
// check if it's been gone thru before
|
||||||
if self.href.path() == "/" {
|
let mut response = db
|
||||||
// Upsert is create or update
|
.query("SELECT * FROM ONLY website WHERE site = $site LIMIT 1")
|
||||||
// Whereas Update is just update
|
.bind(("site", self.site.to_string()))
|
||||||
let record = ("website", &self.href.to_string());
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let crawled = if let Some(old) = db.select(record).await.unwrap() {
|
if let Some(old) = response.take::<Option<Website>>(0).unwrap() {
|
||||||
let old: Website = old; // infer type
|
// site exists already
|
||||||
old.crawled
|
if let Some(id) = old.id {
|
||||||
} else {false};
|
let mut new = self.clone();
|
||||||
|
new.crawled = old.crawled | new.crawled;
|
||||||
|
|
||||||
if !self.crawled {self.crawled = crawled};
|
match db.upsert((id.tb, id.id.to_string())).content(new).await {
|
||||||
|
|
||||||
match db.upsert(record).content(self.clone()).await {
|
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
if let Some(a) = &e {
|
if let Some(a) = &e {
|
||||||
let _: &Record = a;
|
let _: &Record = a;
|
||||||
@ -93,36 +93,27 @@ impl Website {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let _: Option<Record> = match db.create("website").content(self.clone()).await {
|
// sites hasn't existed yet
|
||||||
|
match db.create("website").content(self.clone()).await {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
|
let _: Option<Record> = e;
|
||||||
if let Some(a) = &e {
|
if let Some(a) = &e {
|
||||||
let _: &Record = a;
|
let _: &Record = a;
|
||||||
}
|
}
|
||||||
e
|
|
||||||
}
|
}
|
||||||
Err(_) => todo!(),
|
Err(a) => error!("{:?}", a),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for Website {
|
impl ToString for Website {
|
||||||
/// site == href, crawled = false
|
fn to_string(&self) -> String {
|
||||||
fn from(value: &str) -> Self {
|
self.site.to_string()
|
||||||
let site = match Url::parse(value) {
|
|
||||||
Ok(a) => a,
|
|
||||||
Err(_) => todo!(),
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
href: site.clone(),
|
|
||||||
crawled: false,
|
|
||||||
site,
|
|
||||||
doctored_href: false,
|
|
||||||
original_href: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +140,7 @@ pub async fn connect() -> surrealdb::Result<Surreal<Client>> {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Select a specific namespace / database
|
// Select a specific namespace / database
|
||||||
db.use_ns("test").use_db("test").await?;
|
db.use_ns("test").use_db("time").await?;
|
||||||
|
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
63
src/main.rs
63
src/main.rs
@ -6,7 +6,7 @@ use db::{connect, Website};
|
|||||||
use html5ever::{parse_document, tendril::TendrilSink, tree_builder::TreeBuilderOpts, ParseOpts};
|
use html5ever::{parse_document, tendril::TendrilSink, tree_builder::TreeBuilderOpts, ParseOpts};
|
||||||
use rcdom::{Node, RcDom};
|
use rcdom::{Node, RcDom};
|
||||||
use surrealdb::{engine::remote::ws::Client, Surreal};
|
use surrealdb::{engine::remote::ws::Client, Surreal};
|
||||||
use tracing::{debug, info, instrument};
|
use tracing::{debug, info, instrument, trace, trace_span, warn};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
@ -29,24 +29,37 @@ async fn main() {
|
|||||||
|
|
||||||
// Kick off the whole machine - This Website object doesn't matter, it's just to allow for
|
// Kick off the whole machine - This Website object doesn't matter, it's just to allow for
|
||||||
// get() to work.
|
// get() to work.
|
||||||
let mut site = Website::from(url);
|
let span = trace_span!("Pre-Loop");
|
||||||
|
let pre_loop_span = span.enter();
|
||||||
|
let mut site = Website::new(&url, false);
|
||||||
let dom = get(&mut site, &db).await.expect("Inital page returned None.");
|
let dom = get(&mut site, &db).await.expect("Inital page returned None.");
|
||||||
crawled += 1;
|
crawled += 1;
|
||||||
walk(&dom, &db, &site).await;
|
walk(&dom, &db, &site).await;
|
||||||
|
drop(pre_loop_span);
|
||||||
|
|
||||||
|
let span = trace_span!("Loop");
|
||||||
|
let span = span.enter();
|
||||||
|
// Can go upto 49 above budget because the reterival function gets 50 no matter what
|
||||||
while crawled < budget {
|
while crawled < budget {
|
||||||
let uncrawled = get_uncrawled_links(&db).await;
|
let uncrawled = get_uncrawled_links(&db, 100).await;
|
||||||
debug!("Crawling {} pages...", uncrawled.len());
|
debug!("Crawling {} pages...", uncrawled.len());
|
||||||
|
|
||||||
|
let span = trace_span!("Crawling");
|
||||||
|
let _ = span.enter();
|
||||||
|
|
||||||
for mut site in uncrawled {
|
for mut site in uncrawled {
|
||||||
if let Some(dom) = get(&mut site, &db).await {
|
if let Some(dom) = get(&mut site, &db).await {
|
||||||
|
trace!("pre-walk checkpoint");
|
||||||
walk(&dom, &db, &site).await;
|
walk(&dom, &db, &site).await;
|
||||||
crawled += 1;
|
crawled += 1;
|
||||||
let percent = format!("{:.2}%", (crawled as f32/budget as f32) * 100f32);
|
let percent = format!("{:.2}%", (crawled as f32/budget as f32) * 100f32);
|
||||||
info!("Crawled {crawled} out of {budget} pages. ({percent})");
|
info!("Crawled {crawled} out of {budget} pages. ({percent})");
|
||||||
|
} else {
|
||||||
|
warn!("Failed to get {}", site.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drop(span);
|
||||||
|
|
||||||
info!("Done");
|
info!("Done");
|
||||||
}
|
}
|
||||||
@ -54,7 +67,8 @@ async fn main() {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
/// A quick helper function for downloading a url
|
/// A quick helper function for downloading a url
|
||||||
async fn get(site: &mut Website, db: &Surreal<Client>) -> Option<Rc<Node>> {
|
async fn get(site: &mut Website, db: &Surreal<Client>) -> Option<Rc<Node>> {
|
||||||
if let Ok(response) = reqwest::get(site.href_str()).await {
|
trace!("Get: {}", site.to_string());
|
||||||
|
if let Ok(response) = reqwest::get(site.to_string()).await {
|
||||||
let data = response.text().await.unwrap();
|
let data = response.text().await.unwrap();
|
||||||
let opts = ParseOpts {
|
let opts = ParseOpts {
|
||||||
tree_builder: TreeBuilderOpts {
|
tree_builder: TreeBuilderOpts {
|
||||||
@ -69,29 +83,42 @@ async fn get(site: &mut Website, db: &Surreal<Client>) -> Option<Rc<Node>> {
|
|||||||
.read_from(&mut data.as_bytes())
|
.read_from(&mut data.as_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
site.crawled();
|
site.set_crawled();
|
||||||
site.store(db).await;
|
site.store(db).await;
|
||||||
|
trace!("Got: {}", site.to_string());
|
||||||
return Some(dom.document);
|
return Some(dom.document);
|
||||||
}
|
}
|
||||||
|
trace!("Failed to get: {}", site.to_string());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walks the givin site, placing it's findings in the database
|
/// Walks the givin site, placing it's findings in the database
|
||||||
async fn walk(node: &rcdom::Handle, db: &Surreal<Client> , site_name: &Website) {
|
async fn walk(node: &rcdom::Handle, db: &Surreal<Client> , site: &Website) {
|
||||||
// Insert Or Update
|
let span = trace_span!("Walk");
|
||||||
// create_root(site_name, db).await;
|
let span = span.enter();
|
||||||
|
|
||||||
match &node.data {
|
match &node.data {
|
||||||
rcdom::NodeData::Element { name, attrs, template_contents, mathml_annotation_xml_integration_point } => {
|
rcdom::NodeData::Element { name, attrs, template_contents, mathml_annotation_xml_integration_point } => {
|
||||||
for attr in attrs.borrow().clone() {
|
for attr in attrs.borrow().clone() {
|
||||||
if name.local.to_string() == "a" {
|
if name.local.to_string() == "a" {
|
||||||
if attr.value.starts_with("mailto") {
|
if attr.value.starts_with("mailto") {
|
||||||
|
trace!("Is mailto");
|
||||||
// mailto link, lol
|
// mailto link, lol
|
||||||
let _created: Option<db::Record> = db.create("email").content(db::Email {
|
let _created: Option<db::Record> = db.create("email").content(db::Email {
|
||||||
email: attr.value.to_string()
|
email: attr.value.to_string()
|
||||||
}).await.unwrap();
|
}).await.unwrap();
|
||||||
} else {
|
} else {
|
||||||
let mut web = Website::new(&site_name.site(), &attr.value, false);
|
let mut web = site.clone();
|
||||||
|
let url = web.mut_url();
|
||||||
|
|
||||||
|
// TODO remove #xyz
|
||||||
|
let joined = url.join(&attr.value).unwrap();
|
||||||
|
*url = joined;
|
||||||
|
|
||||||
|
let crawled = web.crawled();
|
||||||
|
*crawled = false;
|
||||||
|
|
||||||
|
site.links_to(&web, &db).await;
|
||||||
web.store(db).await;
|
web.store(db).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,15 +126,23 @@ async fn walk(node: &rcdom::Handle, db: &Surreal<Client> , site_name: &Website)
|
|||||||
},
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
|
drop(span);
|
||||||
for child in node.children.borrow().iter() {
|
for child in node.children.borrow().iter() {
|
||||||
Box::pin(walk(child, db, site_name)).await;
|
Box::pin(walk(child, db, site)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns 0-50 uncrawled links (LIMIT = 50)
|
/// Returns uncrawled links
|
||||||
async fn get_uncrawled_links(db: &Surreal<Client>) -> Vec<Website> {
|
async fn get_uncrawled_links(db: &Surreal<Client>, mut count: usize) -> Vec<Website> {
|
||||||
let mut response = db.query("SELECT * FROM website WHERE crawled = false LIMIT 50").await.expect("Hard-coded query failed..?");
|
if count > 100 {
|
||||||
|
count = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = db
|
||||||
|
.query("SELECT * FROM website WHERE crawled = false LIMIT $count")
|
||||||
|
.bind(("count", count))
|
||||||
|
.await
|
||||||
|
.expect("Hard-coded query failed..?");
|
||||||
response.take(0).expect("Returned websites couldn't be parsed")
|
response.take(0).expect("Returned websites couldn't be parsed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user