import json import re import time from datetime import datetime from urllib.parse import urljoin, urlparse import requests from bs4 import BeautifulSoup SEED_URLS = [ "https://www.cbp.gov/trade", "https://www.cbp.gov/trade/basic-import-export", "https://www.cbp.gov/trade/basic-import-export/importer-exporter-tips", "https://www.cbp.gov/trade/basic-import-export/e-commerce", "https://www.cbp.gov/newsroom", "https://www.cbp.gov/newsroom/media-releases/trade", "https://www.cbp.gov/newsroom/stats/trade" ] ALLOWED_PREFIXES = [ "https://www.cbp.gov/trade", "https://www.cbp.gov/newsroom/media-releases/trade", "https://www.cbp.gov/newsroom/national-media-release", "https://www.cbp.gov/newsroom/announcements", "https://www.cbp.gov/newsroom/stats/trade" ] HEADERS = { "User-Agent": "TradeBridgeAI-Hackathon/1.0 educational prototype" } def is_allowed_url(url: str) -> bool: if not url.startswith("https://www.cbp.gov/"): return False blocked_extensions = ( ".jpg", ".jpeg", ".png", ".gif", ".svg", ".mp4", ".zip", ".css", ".js" ) if url.lower().endswith(blocked_extensions): return False return any(url.startswith(prefix) for prefix in ALLOWED_PREFIXES) def clean_text(text: str) -> str: text = re.sub(r"\s+", " ", text) text = text.replace("\xa0", " ") return text.strip() def extract_page(url: str) -> dict | None: try: response = requests.get(url, headers=HEADERS, timeout=20) response.raise_for_status() except Exception as e: print(f"Failed: {url} | {e}") return None soup = BeautifulSoup(response.text, "html.parser") for tag in soup(["script", "style", "noscript", "header", "footer", "nav"]): tag.decompose() title = soup.find("h1") if title: title_text = clean_text(title.get_text(" ")) elif soup.title: title_text = clean_text(soup.title.get_text(" ")) else: title_text = url # Try to find release/publication date date_text = None possible_date = soup.find(string=re.compile(r"Release Date|Last Modified|Published", re.I)) if possible_date: date_text = clean_text(possible_date) main = soup.find("main") or soup.find("article") or soup.body if not main: return None text = clean_text(main.get_text(" ")) if len(text) < 200: return None parsed = urlparse(url) page_type = "news" if "/newsroom/" in parsed.path else "guidance" section = "newsroom" if "/newsroom/" in parsed.path else "trade" return { "id": re.sub(r"[^a-zA-Z0-9]+", "_", parsed.path.strip("/")).lower(), "source": "CBP", "section": section, "page_type": page_type, "title": title_text, "url": url, "retrieved_at": datetime.utcnow().isoformat() + "Z", "published_date": date_text, "topics": [], "text": text, "summary": "", "hackathon_use": "", "staleness_risk": "high" if page_type == "news" else "medium" } def discover_links(url: str) -> set[str]: links = set() try: response = requests.get(url, headers=HEADERS, timeout=20) response.raise_for_status() except Exception as e: print(f"Could not discover links from {url}: {e}") return links soup = BeautifulSoup(response.text, "html.parser") for a in soup.find_all("a", href=True): href = a["href"] full_url = urljoin(url, href).split("#")[0] # Remove tracking/query parameters except pagination if needed if "?" in full_url and "page=" not in full_url: full_url = full_url.split("?")[0] if is_allowed_url(full_url): links.add(full_url) return links def crawl(max_pages: int = 200): seen = set() queue = list(SEED_URLS) results = [] while queue and len(results) < max_pages: url = queue.pop(0) if url in seen: continue seen.add(url) if not is_allowed_url(url): continue print(f"Fetching: {url}") page = extract_page(url) if page: results.append(page) new_links = discover_links(url) for link in new_links: if link not in seen and link not in queue: queue.append(link) time.sleep(1.0) # polite delay return results if __name__ == "__main__": pages = crawl(max_pages=200) with open("cbp_pages.jsonl", "w", encoding="utf-8") as f: for page in pages: f.write(json.dumps(page, ensure_ascii=False) + "\n") print(f"Saved {len(pages)} pages to cbp_pages.jsonl")