aramej

crawl4ai_documentation_downloader

Mar 8th, 2025
189
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.05 KB | None | 0 0
  1. import asyncio
  2. import os
  3. import logging
  4. import xml.etree.ElementTree as ET
  5. import requests
  6. from typing import Dict, List, Optional
  7. from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
  8. from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
  9.  
  10. # Configure logging
  11. logging.basicConfig(
  12. level=logging.INFO,
  13. format='%(asctime)s - %(levelname)s - %(message)s',
  14. handlers=[
  15. logging.FileHandler("crawl_log.txt"),
  16. logging.StreamHandler()
  17. ]
  18. )
  19. logger = logging.getLogger(__name__)
  20.  
  21. # Define URL to filename mapping
  22. URL_TO_FILENAME_MAP = {
  23. "https://docs.crawl4ai.com/": "00_Home_00_Home.md",
  24. "https://docs.crawl4ai.com/basic/installation/": "01_Setup & Installation_01_Installation.md",
  25. "https://docs.crawl4ai.com/core/docker-deployment/": "01_Setup & Installation_02_Docker Deployment.md",
  26. "https://docs.crawl4ai.com/core/quickstart/": "02_Quick Start_01_Quick Start.md",
  27. "https://docs.crawl4ai.com/blog/": "03_Blog & Changelog_01_Blog Home.md",
  28. "https://docs.crawl4ai.com/blog/releases/0.4.0/": "03_Blog & Changelog_02_Changelog v0.4.0.md",
  29. "https://docs.crawl4ai.com/blog/releases/0.4.1/": "03_Blog & Changelog_03_Changelog v0.4.1.md",
  30. "https://docs.crawl4ai.com/blog/releases/0.4.2/": "03_Blog & Changelog_04_Changelog v0.4.2.md",
  31. "https://docs.crawl4ai.com/blog/releases/0.5.0/": "03_Blog & Changelog_05_Changelog v0.5.0.md",
  32. "https://docs.crawl4ai.com/blog/releases/v0.4.3b1/": "03_Blog & Changelog_06_Changelog v0.4.3b1.md",
  33. "https://docs.crawl4ai.com/core/cli/": "04_Core_01_Command Line Interface.md",
  34. "https://docs.crawl4ai.com/core/deep-crawling/": "04_Core_02_Deep Crawling.md",
  35. "https://docs.crawl4ai.com/core/simple-crawling/": "04_Core_03_Simple Crawling.md",
  36. "https://docs.crawl4ai.com/core/crawler-result/": "04_Core_04_Crawler Result.md",
  37. "https://docs.crawl4ai.com/core/browser-crawler-config/": "04_Core_05_Browser & Crawler Config.md",
  38. "https://docs.crawl4ai.com/core/markdown-generation/": "04_Core_06_Markdown Generation.md",
  39. "https://docs.crawl4ai.com/core/fit-markdown/": "04_Core_07_Fit Markdown.md",
  40. "https://docs.crawl4ai.com/core/page-interaction/": "04_Core_08_Page Interaction.md",
  41. "https://docs.crawl4ai.com/core/content-selection/": "04_Core_09_Content Selection.md",
  42. "https://docs.crawl4ai.com/core/cache-modes/": "04_Core_10_Cache Modes.md",
  43. "https://docs.crawl4ai.com/core/local-files/": "04_Core_11_Local Files.md",
  44. "https://docs.crawl4ai.com/core/link-media/": "04_Core_12_Link & Media.md",
  45. "https://docs.crawl4ai.com/advanced/advanced-features/": "05_Advanced_01_Overview.md",
  46. "https://docs.crawl4ai.com/advanced/file-downloading/": "05_Advanced_02_File Downloading.md",
  47. "https://docs.crawl4ai.com/advanced/lazy-loading/": "05_Advanced_03_Lazy Loading.md",
  48. "https://docs.crawl4ai.com/advanced/hooks-auth/": "05_Advanced_04_Hooks & Auth.md",
  49. "https://docs.crawl4ai.com/advanced/proxy-security/": "05_Advanced_05_Proxy & Security.md",
  50. "https://docs.crawl4ai.com/advanced/session-management/": "05_Advanced_06_Session Management.md",
  51. "https://docs.crawl4ai.com/advanced/multi-url-crawling/": "05_Advanced_07_Multi-URL Crawling.md",
  52. "https://docs.crawl4ai.com/advanced/crawl-dispatcher/": "05_Advanced_08_Crawl Dispatcher.md",
  53. "https://docs.crawl4ai.com/advanced/identity-based-crawling/": "05_Advanced_09_Identity Based Crawling.md",
  54. "https://docs.crawl4ai.com/advanced/ssl-certificate/": "05_Advanced_10_SSL Certificate.md",
  55. "https://docs.crawl4ai.com/extraction/no-llm-strategies/": "06_Extraction_01_No LLM Strategies.md",
  56. "https://docs.crawl4ai.com/extraction/llm-strategies/": "06_Extraction_02_LLM Strategies.md",
  57. "https://docs.crawl4ai.com/extraction/clustring-strategies/": "06_Extraction_03_Clustering Strategies.md",
  58. "https://docs.crawl4ai.com/extraction/chunking/": "06_Extraction_04_Chunking.md",
  59. "https://docs.crawl4ai.com/api/async-webcrawler/": "07_API Reference_01_AsyncWebCrawler.md",
  60. "https://docs.crawl4ai.com/api/arun/": "07_API Reference_02_arun.md",
  61. "https://docs.crawl4ai.com/api/arun_many/": "07_API Reference_03_arun_many.md",
  62. "https://docs.crawl4ai.com/api/parameters/": "07_API Reference_04_Parameters.md",
  63. "https://docs.crawl4ai.com/api/crawl-result/": "07_API Reference_05_CrawlResult.md",
  64. "https://docs.crawl4ai.com/api/strategies/": "07_API Reference_06_Strategies.md",
  65. }
  66.  
  67. def get_markdown_content(result) -> Optional[str]:
  68. """Extract markdown content from the crawl result, handling different versions/formats."""
  69. # Try different approaches to get markdown content
  70.  
  71. # 1. Direct markdown string
  72. if hasattr(result, 'markdown') and isinstance(result.markdown, str):
  73. return result.markdown
  74.  
  75. # 2. MarkdownGenerationResult in result.markdown
  76. if hasattr(result, 'markdown') and hasattr(result.markdown, 'raw_markdown'):
  77. return result.markdown.raw_markdown
  78.  
  79. # 3. MarkdownGenerationResult in result.markdown_v2
  80. if hasattr(result, 'markdown_v2') and hasattr(result.markdown_v2, 'raw_markdown'):
  81. return result.markdown_v2.raw_markdown
  82.  
  83. # 4. Try fit_markdown if raw_markdown is not available
  84. if hasattr(result, 'markdown_v2') and hasattr(result.markdown_v2, 'fit_markdown'):
  85. return result.markdown_v2.fit_markdown
  86.  
  87. # 5. Last resort: use the HTML content if no markdown was generated
  88. if result.html:
  89. logger.warning(f"Using raw HTML for {result.url} as markdown was not available")
  90. return f"# Content from {result.url}\n\n(Raw HTML was converted to plain text)\n\n{result.cleaned_html or result.html}"
  91.  
  92. # No content found
  93. return None
  94.  
  95. async def download_sitemap(sitemap_url: str) -> List[str]:
  96. """Download the sitemap.xml and extract all URLs."""
  97. try:
  98. # Request with appropriate headers
  99. headers = {
  100. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
  101. }
  102.  
  103. logger.info(f"Downloading sitemap from {sitemap_url}")
  104. response = requests.get(sitemap_url, headers=headers, timeout=30)
  105. response.raise_for_status()
  106.  
  107. # Parse the XML
  108. root = ET.fromstring(response.content)
  109.  
  110. # Extract URLs (handle namespaces in XML)
  111. urls = []
  112.  
  113. # Try different namespace approaches (sitemap XML can vary)
  114. namespaces = {
  115. 'default': 'http://www.sitemaps.org/schemas/sitemap/0.9',
  116. 'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'
  117. }
  118.  
  119. for prefix, uri in namespaces.items():
  120. if prefix == 'default':
  121. xpath = './/loc'
  122. else:
  123. xpath = './/{' + uri + '}loc'
  124.  
  125. locations = root.findall(xpath)
  126. if locations:
  127. urls = [loc.text for loc in locations]
  128. break
  129.  
  130. if not urls:
  131. # Fallback: try simple parsing without namespaces
  132. urls = [element.text for element in root.findall(".//loc")]
  133.  
  134. logger.info(f"Successfully extracted {len(urls)} URLs from sitemap")
  135. return urls
  136. except requests.RequestException as e:
  137. logger.error(f"Failed to download sitemap: {e}")
  138. raise
  139. except ET.ParseError as e:
  140. logger.error(f"Failed to parse sitemap XML: {e}")
  141. raise
  142.  
  143. def get_filename_for_url(url: str) -> Optional[str]:
  144. """Get the filename for a given URL based on the mapping."""
  145. # Remove trailing slash for consistency
  146. url_normalized = url.rstrip('/')
  147.  
  148. # Direct lookup from the mapping
  149. for map_url, filename in URL_TO_FILENAME_MAP.items():
  150. map_url_normalized = map_url.rstrip('/')
  151. if url_normalized == map_url_normalized:
  152. return filename
  153.  
  154. logger.warning(f"No filename mapping found for URL: {url}")
  155. return None
  156.  
  157. async def crawl_and_save_page(url: str, output_dir: str, crawler: AsyncWebCrawler, max_retries: int = 3) -> None:
  158. """Crawl a single page and save it as markdown."""
  159. filename = get_filename_for_url(url)
  160. if not filename:
  161. logger.warning(f"Skipping URL with no filename mapping: {url}")
  162. return
  163.  
  164. output_path = os.path.join(output_dir, filename)
  165.  
  166. for attempt in range(max_retries):
  167. try:
  168. logger.info(f"Crawling: {url} (attempt {attempt+1}/{max_retries})")
  169.  
  170. # Configure markdown generator
  171. md_generator = DefaultMarkdownGenerator(
  172. options={"ignore_links": False, "body_width": 0}
  173. )
  174.  
  175. config = CrawlerRunConfig(
  176. cache_mode=CacheMode.BYPASS, # Always get fresh content
  177. page_timeout=60000, # 60 seconds timeout
  178. word_count_threshold=5, # Include most content blocks
  179. excluded_tags=['script', 'style', 'svg'], # Exclude non-content tags
  180. markdown_generator=md_generator
  181. )
  182.  
  183. result = await crawler.arun(url=url, config=config)
  184.  
  185. if not result.success:
  186. logger.error(f"Failed to crawl {url}: {result.error_message}")
  187. if attempt < max_retries - 1:
  188. logger.info(f"Retrying {url} in 3 seconds...")
  189. await asyncio.sleep(3) # Wait before retry
  190. continue
  191. return
  192.  
  193. # Get markdown content using our helper function
  194. markdown_content = get_markdown_content(result)
  195.  
  196. if markdown_content:
  197. with open(output_path, 'w', encoding='utf-8') as f:
  198. f.write(markdown_content)
  199. logger.info(f"Saved: {output_path}")
  200. break # Success, exit retry loop
  201. else:
  202. logger.warning(f"No content found for {url}")
  203. if attempt < max_retries - 1:
  204. logger.info(f"Retrying {url} in 3 seconds...")
  205. await asyncio.sleep(3)
  206. continue
  207. else:
  208. # Last attempt failed, create a placeholder file
  209. with open(output_path, 'w', encoding='utf-8') as f:
  210. f.write(f"# Error retrieving content for {url}\n\nThe content could not be retrieved after {max_retries} attempts.")
  211. logger.error(f"Created placeholder file for {url} after {max_retries} failed attempts")
  212.  
  213. except Exception as e:
  214. logger.error(f"Error processing {url}: {str(e)}")
  215. if attempt < max_retries - 1:
  216. logger.info(f"Retrying {url} in 3 seconds...")
  217. await asyncio.sleep(3)
  218. else:
  219. logger.error(f"Failed to process {url} after {max_retries} attempts")
  220. # Create a placeholder file on complete failure
  221. with open(output_path, 'w', encoding='utf-8') as f:
  222. f.write(f"# Error processing {url}\n\nAn error occurred: {str(e)}")
  223. logger.error(f"Created error placeholder file for {url}")
  224.  
  225. async def process_urls_with_semaphore(urls: List[str], output_dir: str, crawler: AsyncWebCrawler, max_concurrent: int = 5):
  226. """Process URLs with a semaphore to limit concurrency."""
  227. semaphore = asyncio.Semaphore(max_concurrent)
  228.  
  229. async def process_with_semaphore(url):
  230. async with semaphore:
  231. await crawl_and_save_page(url, output_dir, crawler)
  232.  
  233. # Create tasks
  234. tasks = [process_with_semaphore(url) for url in urls]
  235.  
  236. # Process URLs and handle exceptions
  237. for task in asyncio.as_completed(tasks):
  238. try:
  239. await task
  240. except Exception as e:
  241. logger.error(f"Task error: {str(e)}")
  242.  
  243. async def main():
  244. # Create output directory
  245. output_dir = "Crawl4AI_Documentation"
  246. os.makedirs(output_dir, exist_ok=True)
  247.  
  248. sitemap_url = "https://docs.crawl4ai.com/sitemap.xml"
  249.  
  250. try:
  251. # Option 1: Process URLs from sitemap
  252. urls = await download_sitemap(sitemap_url)
  253.  
  254. # Option 2: If sitemap fails, fallback to predefined URLs
  255. if not urls:
  256. logger.warning("No URLs found in sitemap, using predefined mapping instead")
  257. urls = list(URL_TO_FILENAME_MAP.keys())
  258.  
  259. logger.info(f"Processing {len(urls)} URLs")
  260.  
  261. # Configure browser and crawler
  262. browser_config = BrowserConfig(
  263. headless=True, # Run in headless mode
  264. verbose=True, # Enable verbose logging
  265. user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
  266. )
  267.  
  268. # Create crawler and process URLs
  269. async with AsyncWebCrawler(config_browser=browser_config) as crawler:
  270. await process_urls_with_semaphore(urls, output_dir, crawler, max_concurrent=3)
  271.  
  272. # Log completion status
  273. total_files = len(os.listdir(output_dir))
  274. logger.info(f"Crawling complete. {total_files} files saved to {output_dir}")
  275.  
  276. # Verify all files were created
  277. missing_files = []
  278. for url, filename in URL_TO_FILENAME_MAP.items():
  279. if not os.path.exists(os.path.join(output_dir, filename)):
  280. missing_files.append(f"{filename} ({url})")
  281.  
  282. if missing_files:
  283. logger.warning(f"Some files are missing: {len(missing_files)} files")
  284. for missing in missing_files:
  285. logger.warning(f"Missing: {missing}")
  286. else:
  287. logger.info("All expected files were successfully created")
  288.  
  289. except Exception as e:
  290. logger.error(f"An error occurred during execution: {str(e)}")
  291. import traceback
  292. logger.error(traceback.format_exc())
  293.  
  294. if __name__ == "__main__":
  295. asyncio.run(main())
Advertisement
Add Comment
Please, Sign In to add comment