File size: 4,549 Bytes
aaa4ec9
6b62397
 
aaa4ec9
 
e8e3115
1d1b426
aaa4ec9
2cd2e81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8e3115
 
 
2cd2e81
 
aaa4ec9
b55588d
aaa4ec9
 
 
 
 
 
 
 
 
b55588d
aaa4ec9
b55588d
 
aaa4ec9
 
 
 
 
 
 
2cd2e81
 
 
 
 
 
 
 
 
 
 
 
 
 
b55588d
aaa4ec9
 
6b62397
 
 
 
 
 
 
 
 
 
 
aaa4ec9
e8e3115
 
 
 
 
 
 
 
 
aaa4ec9
 
2cd2e81
 
 
 
aaa4ec9
 
 
 
 
 
 
6b62397
 
aaa4ec9
6b62397
aaa4ec9
 
2cd2e81
6b62397
 
 
 
 
 
 
 
 
 
1316781
6b62397
 
 
2cd2e81
6b62397
 
 
 
 
 
 
 
 
 
 
 
 
 
b55588d
 
 
 
2cd2e81
b55588d
aaa4ec9
b55588d
aaa4ec9
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import time
import re
import requests
import trafilatura
from trafilatura.sitemaps import sitemap_search
from trafilatura.spider import focused_crawler
from api.utils import logger, is_valid_url

# 1. PRIORITY KEYWORDS
# Pages containing these words get crawled FIRST.
PRIORITY_KEYWORDS = [
    "about",
    "mission",
    "vision",
    "history",
    "values",
    "team",
    "leadership",
    "board",
    "administration",
    "structure",
    "contact",
    "locations",
    "overview",
    "who-we-are",
    "careers",
    "office-of-the-president",
    "executive",
    "volunteer-service",
    "give-blood",
    "ways-to-donate",
]


def smart_chunk(text: str, chunk_size=1000, overlap=100) -> list[str]:
    if not text:
        return []
    chunks = []
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    current_chunk = ""
    for para in paragraphs:
        if len(current_chunk) + len(para) > chunk_size:
            if current_chunk:
                chunks.append(current_chunk.strip())
                current_chunk = current_chunk[-overlap:] + "\n" + para + "\n"
            else:
                chunks.append(para[:chunk_size])
                current_chunk = ""
        else:
            current_chunk += para + "\n"
    if current_chunk:
        chunks.append(current_chunk.strip())
    return chunks


def get_url_priority(url: str) -> float:
    """Higher score = Crawled sooner"""
    score = 0
    url_lower = url.lower()

    for keyword in PRIORITY_KEYWORDS:
        if keyword in url_lower:
            score += 10

    # Prefer shorter URLs (e.g., /about is better than /news/2023/10/12/title)
    score -= len(url) * 0.05
    return score


def crawl_website(base_url: str, limit: int = 25):
    logger.info(f"Starting crawl for {base_url}")

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    try:
        urls = sitemap_search(base_url)
    except Exception:
        urls = []

    if not urls:
        logger.warning(f"No sitemap found for {base_url}. Engaging Spider...")
        try:
            _, known_links = focused_crawler(base_url, max_seen_urls=1)
            urls = list(known_links)

            urls.append(base_url)
        except Exception as e:
            logger.error(f"Spider failed: {e}")
            urls = [base_url]

    valid_urls = [u for u in urls if is_valid_url(u, base_url)]

    valid_urls.sort(key=get_url_priority, reverse=True)

    logger.info(f"Found {len(urls)} URLs. Top priority: {valid_urls[:3]}")

    count = 0
    for link in valid_urls:
        if count >= limit:
            break

        try:
            time.sleep(1.0)
            response = requests.get(link, headers=headers, timeout=10)

            if response.status_code != 200:
                continue

            # Robust Extraction
            page_title = "Unknown Page"
            raw_text = ""

            try:
                result = trafilatura.bare_extraction(
                    response.text, include_comments=False
                )
                if result and isinstance(result, dict) and result.get("text"):
                    page_title = result.get("title", "Unknown Page")
                    raw_text = result["text"]
            except Exception:
                pass

            if not raw_text:
                # Fallback manual extraction
                raw_text = trafilatura.extract(response.text, include_comments=False)
                if raw_text:
                    title_match = re.search(
                        r"<title>(.*?)</title>",
                        response.text,
                        re.IGNORECASE | re.DOTALL,
                    )
                    if title_match:
                        clean_title = title_match.group(1).strip()
                        if clean_title:
                            page_title = clean_title

            if not raw_text:
                continue

            text_chunks = smart_chunk(raw_text)

            contextualized_chunks = [
                f"Source: {page_title}\nURL: {link}\n\n{chunk}" for chunk in text_chunks
            ]

            yield link, contextualized_chunks
            count += 1

        except Exception as e:
            logger.error(f"Failed to crawl {link}: {e}")
            continue