import requests from bs4 import BeautifulSoup import pandas as pd import re from urllib.parse import urljoin, urlparse import time import json import os from typing import List, Optional from dataclasses import dataclass, asdict import openpyxl from concurrent.futures import ThreadPoolExecutor, as_completed import threading from pathlib import Path @dataclass class DesignObject: name: str year: str classification: str dimension: str makers: List[str] image_urls: List[str] country: str price: Optional[str] = None popularity: Optional[str] = None source: Optional[str] = None class DatamathCompleteScraper: def __init__(self): self.base_url = "http://www.datamath.org/" self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) self.calculator_links = [] def collect_links_from_page(self, url, category_name): """Step 1: Collect ALL .htm links from a page""" print(f"\nProcessing: {category_name}") print(f"URL: {url}") print("-" * 50) links_data = [] try: response = self.session.get(url, timeout=10) if response.status_code == 200: soup = BeautifulSoup(response.content, 'html.parser') # Get ALL links all_links = soup.find_all('a', href=True) for link in all_links: href = link['href'] link_text = link.get_text().strip() # Skip anchors, mailto, and external links if href.startswith('#') or href.startswith('mailto:'): continue if href.startswith('http') and 'datamath.org' not in href: continue # Get all .htm and .html files if href.endswith('.htm') or href.endswith('.html'): # Build full URL if href.startswith('http'): full_url = href else: full_url = urljoin(url, href) # Extract filename filename = os.path.basename(urlparse(full_url).path) # Skip index/main pages skip_files = ['index.htm', 'main.htm', 'start.htm', 'album_'] if any(skip in filename.lower() for skip in skip_files): continue links_data.append({ 'category': category_name, 'name': link_text if link_text else filename.replace('.htm', ''), 'filename': filename, 'url': full_url }) print(f" Found: {link_text[:50] if link_text else filename}") print(f" Total links found: {len(links_data)}") else: print(f" Error: Status code {response.status_code}") except Exception as e: print(f" Error fetching page: {e}") return links_data def collect_all_links(self): """Step 1: Collect all calculator links from the 10 Album pages""" print("=" * 70) print(" STEP 1: COLLECTING ALL CALCULATOR LINKS") print("=" * 70) # The 10 main category pages categories = [ {'filename': 'Album_Basic.htm', 'name': 'Basic Calculators'}, {'filename': 'Album_Desktop.htm', 'name': 'Desktop Calculators'}, {'filename': 'Album_Sci.htm', 'name': 'Scientific Calculators'}, {'filename': 'Album_Graph.htm', 'name': 'Graphing Calculators'}, {'filename': 'Album_Edu.htm', 'name': 'Educational Products'}, {'filename': 'Album_Personal.htm', 'name': 'Personal Calculators'}, {'filename': 'Album_Speech.htm', 'name': 'Speech Products'}, {'filename': 'Album_TISTUFF.htm', 'name': 'TI Stuff'}, {'filename': 'Album_Others.htm', 'name': 'Other Brands'}, {'filename': 'Album_Related.htm', 'name': 'Related Products'}, ] all_links = [] # Process each category for category in categories: url = urljoin(self.base_url, category['filename']) links = self.collect_links_from_page(url, category['name']) all_links.extend(links) time.sleep(0.5) # Be polite # Remove duplicates based on URL unique_links = [] seen_urls = set() for link in all_links: if link['url'] not in seen_urls: unique_links.append(link) seen_urls.add(link['url']) print("\n" + "=" * 70) print(f" LINKS COLLECTION SUMMARY") print("=" * 70) print(f"Total links collected: {len(all_links)}") print(f"Unique links: {len(unique_links)}") self.calculator_links = unique_links return unique_links def scrape_calculator_page(self, url): """Step 2: Scrape a single calculator page for DesignObject data""" try: response = self.session.get(url, timeout=10) if response.status_code != 200: return None soup = BeautifulSoup(response.content, 'html.parser') all_text = soup.get_text() # Initialize DesignObject fields name = "" year = "" dimension = "" country = "" image_urls = [] # 1. Extract NAME from title title = soup.find('title') if title: title_text = title.get_text().strip() # Remove "Texas Instruments" or "DATAMATH" prefix name = title_text.replace('Texas Instruments', '').replace('DATAMATH', '').strip() # Clean up common patterns name = re.sub(r'^\s*-\s*', '', name) # Remove leading dash name = name.strip() # If no name from title, try to get from URL if not name: filename = os.path.basename(urlparse(url).path) name = filename.replace('.htm', '').replace('_', ' ').replace('-', ' ') # 2. Extract YEAR (full Date of manufacture text) # Look for "Date of manufacture" first manufacture_pattern = r'Date of manufacture:\s*([^\n|]+)' match = re.search(manufacture_pattern, all_text, re.IGNORECASE) if match: year = match.group(1).strip() # Clean up year = re.sub(r'\s+', ' ', year) # Remove extra spaces # If no manufacture date, try introduction date as fallback if not year: intro_pattern = r'Date of introduction:\s*([^\n|]+)' match = re.search(intro_pattern, all_text, re.IGNORECASE) if match: year = match.group(1).strip() year = re.sub(r'\s+', ' ', year) # 3. Extract DIMENSION (Physical Size, not Display size) # Make sure we get "Size:" not "Display size:" # Use negative lookbehind to exclude "Display size:" size_pattern = r'(? 0: break else: print("Please enter a positive number") except ValueError: print("Please enter a valid number") # Step 1: Collect all links links = scraper.collect_all_links() if not links: print("\nNo links found! Exiting.") return # Step 2: Scrape calculator data based on choice if choice == '1': print(f"\nThis will scrape the first 10 calculators using {max_workers} threads.") design_objects = scraper.scrape_all_calculators(limit=10, max_workers=max_workers) elif choice == '2': print(f"\nThis will scrape the first 50 calculators using {max_workers} threads.") design_objects = scraper.scrape_all_calculators(limit=50, max_workers=max_workers) else: print(f"\nThis will scrape {len(links)} calculators using {max_workers} threads.") print("This may take some time but will be much faster than sequential processing!") confirm = input("Continue? (yes/no): ").strip().lower() if confirm in ['yes', 'y']: design_objects = scraper.scrape_all_calculators(max_workers=max_workers) else: print("Cancelled.") return # Step 3: Save results to Excel if design_objects: scraper.save_to_xlsx(design_objects) print("\n" + "=" * 70) print(" ALL DONE!") print("=" * 70) print("\nOutput file:") print("- ../../data/metadata/datamath_calculators.xlsx (all calculator data)") if __name__ == "__main__": main()