Source code for operators.bedrock_scraper.website_scraper_operator

from typing import Literal
from airflow.utils.decorators import apply_defaults
from operators.bedrock_scraper.website_converter.website_converter import (
    WebsiteToHTML,
    WebsiteToPDf,
)
from operators.bedrock_scraper.website_reader.sitemap_reader import GeneralSiteMapReader
from operators.bedrock_scraper.website_reader.govtrack_reader import GovtrackReader
from operators.update_knowledge_base import UpdateKnowledgeBaseOperator
from util import Util
import logging
from airflow.exceptions import AirflowSkipException


[docs] class WebsiteScraperOperator(UpdateKnowledgeBaseOperator): """ Airflow operator for scraping websites using sitemaps and updating knowledge bases. This operator performs the following tasks: 1. Reads and parses a sitemap from a given URL 2. Converts web pages to PDF or HTML format 3. Stores the converted files in S3 4. Updates the specified knowledge base with the new content Attributes: ui_color (str): Color code for the operator in Airflow UI sitemap_url (str): URL of the sitemap to process s3_prefix (str): Prefix for S3 storage path s3_bucket_name (str): Name of the S3 bucket for storage data_source_id (str): Identifier for the data source sitemap_class_type (str): Type of sitemap reader to use ("GeneralSiteMapReader" or "GovtrackReader") knowledge_base_id (str): ID of the knowledge base to update kendra_index_id (str): ID of the Kendra index export_format (str): Format to export pages as ("pdf" or "html") """ ui_color = "#A6E6A6" logger = logging.getLogger(__name__) @apply_defaults def __init__( self, sitemap_url, s3_prefix, s3_bucket_name, data_source_id, sitemap_class_type: Literal[ "GeneralSiteMapReader", "GovtrackReader" ] = "GeneralSiteMapReader", knowledge_base_id=None, kendra_index_id=None, export_format="pdf", *args, **kwargs, ): super(WebsiteScraperOperator, self).__init__( data_source_id=data_source_id, knowledge_base_id=knowledge_base_id, kendra_index_id=kendra_index_id, *args, **kwargs, ) self.sitemap_url = sitemap_url self.s3_prefix = s3_prefix self.s3_bucket_name = s3_bucket_name self.data_source_id = data_source_id self.knowledge_base_id = knowledge_base_id self.map_name = None self.export_format = export_format self.sitemap_class_type = sitemap_class_type
[docs] def execute(self, context): """ Handles the website scraping logic: - Initializes the appropriate sitemap reader - Parses the sitemap to extract URLs - Processes each URL by converting it to PDF or HTML and saving to S3 - Handles checking for existing files to avoid duplicate processing """ self.log.info( f"Starting website scraping operation - Type: {self.sitemap_class_type}" ) self.log.info( f"Configuration: Sitemap URL: {self.sitemap_url}, Export Format: {self.export_format}" ) # Initialize sitemap reader self.log.info(f"Initializing {self.sitemap_class_type}...") if self.sitemap_class_type == "GeneralSiteMapReader": self.sitemap_class = GeneralSiteMapReader(self.sitemap_url) elif self.sitemap_class_type == "GovtrackReader": self.sitemap_class = GovtrackReader(self.sitemap_url) self.log.info("Parsing sitemap...") try: self.sitemap_class.parse_sitemap() self.log.info( f"Sitemap parsed successfully - Found {len(self.sitemap_class.urls)} URLs" ) except Exception as e: self.log.error(f"Failed to parse sitemap: {str(e)}") raise AirflowSkipException(f"Sitemap parsing failed: {str(e)}") # Update context with domain information context["params"]["map_name"] = self.sitemap_class.domain context["map_index_template"] = "{{ params.map_name }}" self.log.info(f"Set map name in context: {self.sitemap_class.domain}") # Get existing S3 files s3_keys = Util.get_existing_s3_files( self.s3_bucket_name, self.s3_prefix, full_key=True, file_type=f".{self.export_format}", ) self.log.info( f"Found {len(s3_keys)} existing files in S3 bucket: {self.s3_bucket_name}" ) processed_count = 0 skipped_count = 0 # Process URLs total_urls = len(self.sitemap_class.urls) for url in self.sitemap_class.urls: try: if self.export_format == "pdf": site = WebsiteToPDf(url, self.s3_prefix, self.s3_bucket_name) elif self.export_format == "html": site = WebsiteToHTML(url, self.s3_prefix, self.s3_bucket_name) else: raise ValueError(f"Unsupported export format: {self.export_format}") key = f"{site.get_key()}.{self.export_format}" if key in s3_keys: self.log.debug( f"Skipping existing file: s3://{self.s3_bucket_name}/{key}" ) skipped_count += 1 continue self.log.info( f"Processing URL [{processed_count + 1}/{total_urls}]: {url}" ) site.save() processed_count += 1 except Exception as e: self.log.error(f"Failed to process URL {url}: {str(e)}") raise self.log.info( f"Website scraping complete - Processed: {processed_count}, Skipped: {skipped_count}" ) self.log.info( "Starting the knowledge base update via parent class UpdateKnowledgeBaseOperator..." ) super().execute(context) self.log.info("Knowledge base update completed successfully")