use crate::spiders::links::LinkExtractor;
use crate::spiders::request::SpiderRequest;
use crate::spiders::response::SpiderResponse;
use crate::spiders::spider::Spider;
use async_trait::async_trait;
use std::collections::HashSet;
use std::sync::Arc;
pub type ProcessRequestFn =
Arc<dyn Fn(SpiderRequest, &SpiderResponse) -> Option<SpiderRequest> + Send + Sync>;
pub type ParseItemFn = Arc<dyn Fn(&SpiderResponse) -> Vec<serde_json::Value> + Send + Sync>;
#[derive(Clone)]
#[must_use = "a CrawlRule does nothing until added to a spider via `.rule()`/`.rules()`"]
pub struct CrawlRule {
pub link_extractor: LinkExtractor,
pub priority: Option<i32>,
pub process_request: Option<ProcessRequestFn>,
}
impl std::fmt::Debug for CrawlRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CrawlRule")
.field("link_extractor", &self.link_extractor)
.field("priority", &self.priority)
.finish_non_exhaustive()
}
}
impl CrawlRule {
pub fn new(link_extractor: LinkExtractor) -> Self {
Self {
link_extractor,
priority: None,
process_request: None,
}
}
pub fn priority(mut self, priority: i32) -> Self {
self.priority = Some(priority);
self
}
pub fn process_request(mut self, f: ProcessRequestFn) -> Self {
self.process_request = Some(f);
self
}
pub fn build_request(&self, url: &str, response: &SpiderResponse) -> Option<SpiderRequest> {
let mut builder = SpiderRequest::builder(url);
if let Some(p) = self.priority {
builder = builder.priority(p);
}
let req = builder.build();
match &self.process_request {
Some(f) => f(req, response),
None => Some(req),
}
}
pub fn requests(&self, response: &SpiderResponse) -> Vec<SpiderRequest> {
self.link_extractor
.extract(response)
.iter()
.filter_map(|url| self.build_request(url, response))
.collect()
}
}
pub fn follow_rules(rules: &[CrawlRule], response: &SpiderResponse) -> Vec<SpiderRequest> {
rules.iter().flat_map(|r| r.requests(response)).collect()
}
pub struct CrawlSpider {
name: String,
start_urls: Vec<String>,
rules: Vec<CrawlRule>,
parse_item: Option<ParseItemFn>,
allowed_domains: HashSet<String>,
concurrent_requests: u32,
development_mode: bool,
robots_txt_obey: bool,
}
impl CrawlSpider {
pub fn builder(name: &str) -> CrawlSpiderBuilder {
CrawlSpiderBuilder::new(name)
}
pub fn rules(&self) -> &[CrawlRule] {
&self.rules
}
}
#[async_trait]
impl Spider for CrawlSpider {
fn name(&self) -> &str {
&self.name
}
fn start_urls(&self) -> Vec<String> {
self.start_urls.clone()
}
fn allowed_domains(&self) -> HashSet<String> {
self.allowed_domains.clone()
}
fn concurrent_requests(&self) -> u32 {
self.concurrent_requests
}
fn development_mode(&self) -> bool {
self.development_mode
}
fn robots_txt_obey(&self) -> bool {
self.robots_txt_obey
}
async fn parse(
&self,
response: SpiderResponse,
) -> (Vec<serde_json::Value>, Vec<SpiderRequest>) {
let items = self
.parse_item
.as_ref()
.map(|f| f(&response))
.unwrap_or_default();
let requests = follow_rules(&self.rules, &response);
(items, requests)
}
}
#[must_use = "builders do nothing until `.build()` is called"]
pub struct CrawlSpiderBuilder {
spider: CrawlSpider,
}
impl CrawlSpiderBuilder {
fn new(name: &str) -> Self {
Self {
spider: CrawlSpider {
name: name.to_string(),
start_urls: Vec::new(),
rules: Vec::new(),
parse_item: None,
allowed_domains: HashSet::new(),
concurrent_requests: 4,
development_mode: false,
robots_txt_obey: false,
},
}
}
pub fn start_urls<I, S>(mut self, urls: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.spider
.start_urls
.extend(urls.into_iter().map(|u| u.as_ref().to_string()));
self
}
pub fn rule(mut self, rule: CrawlRule) -> Self {
self.spider.rules.push(rule);
self
}
pub fn rules<I>(mut self, rules: I) -> Self
where
I: IntoIterator<Item = CrawlRule>,
{
self.spider.rules.extend(rules);
self
}
pub fn parse_item(mut self, f: ParseItemFn) -> Self {
self.spider.parse_item = Some(f);
self
}
pub fn allowed_domains<I, S>(mut self, domains: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.spider
.allowed_domains
.extend(domains.into_iter().map(|d| d.as_ref().to_string()));
self
}
pub fn concurrent_requests(mut self, n: u32) -> Self {
self.spider.concurrent_requests = n;
self
}
pub fn development_mode(mut self, on: bool) -> Self {
self.spider.development_mode = on;
self
}
pub fn robots_txt_obey(mut self, on: bool) -> Self {
self.spider.robots_txt_obey = on;
self
}
#[must_use]
pub fn build(self) -> CrawlSpider {
self.spider
}
}