Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -51,16 +51,19 @@ DB_PATH = STATE_DIR / "crawler.db"
|
|
| 51 |
TRANCO_URL = os.getenv("TRANCO_URL", "https://tranco-list.eu/top-1m.csv.zip")
|
| 52 |
SEED_COUNT = int(os.getenv("SEED_COUNT", "100000"))
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
| 57 |
CONNECT_TIMEOUT = float(os.getenv("CONNECT_TIMEOUT", "5"))
|
| 58 |
-
MAX_REDIRECTS = int(os.getenv("MAX_REDIRECTS", "
|
| 59 |
|
| 60 |
MAX_RESPONSE_BYTES = int(os.getenv("MAX_RESPONSE_BYTES", str(4 * 1024 * 1024)))
|
| 61 |
MAX_TEXT_CHARS = int(os.getenv("MAX_TEXT_CHARS", "20000"))
|
| 62 |
MAX_KEYWORDS = int(os.getenv("MAX_KEYWORDS", "30"))
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
|
| 65 |
DB_FLUSH_INTERVAL = float(os.getenv("DB_FLUSH_INTERVAL", "1.0"))
|
| 66 |
HF_UPLOAD_INTERVAL = int(os.getenv("HF_UPLOAD_INTERVAL", str(3 * 60 * 60)))
|
|
@@ -76,14 +79,14 @@ USER_AGENT = os.getenv("USER_AGENT", "NebulixSearchBot/1.0 (+https://nebulixlabs
|
|
| 76 |
# APP & RUNTIME STATE
|
| 77 |
# ============================================================
|
| 78 |
|
| 79 |
-
app = FastAPI(title="Nebulix Search Crawler", version="4.
|
| 80 |
|
| 81 |
db: Optional[aiosqlite.Connection] = None
|
| 82 |
http_session: Optional[aiohttp.ClientSession] = None
|
| 83 |
|
| 84 |
frontier = asyncio.PriorityQueue()
|
| 85 |
-
record_queue = asyncio.Queue(maxsize=
|
| 86 |
-
db_event_queue = asyncio.Queue(maxsize=
|
| 87 |
|
| 88 |
db_lock = asyncio.Lock()
|
| 89 |
host_state_lock = asyncio.Lock()
|
|
@@ -170,6 +173,7 @@ async def init_db():
|
|
| 170 |
await db.execute("PRAGMA journal_mode=WAL")
|
| 171 |
await db.execute("PRAGMA synchronous=NORMAL")
|
| 172 |
await db.execute("PRAGMA temp_store=MEMORY")
|
|
|
|
| 173 |
|
| 174 |
await db.execute("""
|
| 175 |
CREATE TABLE IF NOT EXISTS urls (
|
|
@@ -198,7 +202,7 @@ async def db_writer():
|
|
| 198 |
batch.append(first)
|
| 199 |
except asyncio.TimeoutError: pass
|
| 200 |
|
| 201 |
-
while len(batch) <
|
| 202 |
try: batch.append(db_event_queue.get_nowait())
|
| 203 |
except asyncio.QueueEmpty: break
|
| 204 |
|
|
@@ -238,10 +242,15 @@ async def db_writer():
|
|
| 238 |
|
| 239 |
async def frontier_feeder():
|
| 240 |
while True:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
try:
|
| 242 |
rows = []
|
| 243 |
async with db_lock:
|
| 244 |
-
cursor = await db.execute("SELECT url_hash, url, priority, depth FROM urls WHERE status = 0 ORDER BY priority ASC, discovered_at ASC LIMIT
|
| 245 |
rows = await cursor.fetchall()
|
| 246 |
|
| 247 |
if rows:
|
|
@@ -260,7 +269,7 @@ async def frontier_feeder():
|
|
| 260 |
await asyncio.sleep(1)
|
| 261 |
|
| 262 |
# ============================================================
|
| 263 |
-
# TRANCO SEED
|
| 264 |
# ============================================================
|
| 265 |
|
| 266 |
async def seed_tranco():
|
|
@@ -437,15 +446,12 @@ async def fetch_page(session, url):
|
|
| 437 |
async with semaphore:
|
| 438 |
await respect_host_delay(host, DEFAULT_HOST_DELAY)
|
| 439 |
timeout = ClientTimeout(total=REQUEST_TIMEOUT, connect=CONNECT_TIMEOUT)
|
| 440 |
-
|
|
|
|
|
|
|
| 441 |
try:
|
| 442 |
async with session.get(url, timeout=timeout, allow_redirects=True, max_redirects=MAX_REDIRECTS) as response:
|
| 443 |
status = response.status
|
| 444 |
-
if status in {429, 500, 502, 503, 504}:
|
| 445 |
-
if attempt == 0:
|
| 446 |
-
await asyncio.sleep(0.5)
|
| 447 |
-
continue
|
| 448 |
-
return None
|
| 449 |
if status != 200: return None
|
| 450 |
|
| 451 |
content_type = response.headers.get("Content-Type", "").lower()
|
|
@@ -463,10 +469,7 @@ async def fetch_page(session, url):
|
|
| 463 |
body = b"".join(chunks)
|
| 464 |
async with stats_lock: stats["bytes_downloaded"] += len(body)
|
| 465 |
return {"url": str(response.url), "status": status, "html": body, "headers": {str(k).lower(): str(v) for k, v in response.headers.items()}}
|
| 466 |
-
except
|
| 467 |
-
if attempt == 0:
|
| 468 |
-
await asyncio.sleep(0.25)
|
| 469 |
-
continue
|
| 470 |
return None
|
| 471 |
return None
|
| 472 |
|
|
@@ -529,7 +532,7 @@ async def record_writer():
|
|
| 529 |
batch.append(first)
|
| 530 |
except asyncio.TimeoutError: pass
|
| 531 |
|
| 532 |
-
while len(batch) <
|
| 533 |
try: batch.append(record_queue.get_nowait())
|
| 534 |
except asyncio.QueueEmpty: break
|
| 535 |
|
|
@@ -605,7 +608,7 @@ async def hf_upload_loop():
|
|
| 605 |
except Exception: pass
|
| 606 |
|
| 607 |
# ============================================================
|
| 608 |
-
# CRAWLER WORKER
|
| 609 |
# ============================================================
|
| 610 |
|
| 611 |
async def crawl_worker(worker_id):
|
|
@@ -618,7 +621,8 @@ async def crawl_worker(worker_id):
|
|
| 618 |
response = await fetch_page(http_session, url)
|
| 619 |
|
| 620 |
if response is None:
|
| 621 |
-
|
|
|
|
| 622 |
async with stats_lock: stats["failed"] += 1
|
| 623 |
continue
|
| 624 |
|
|
@@ -627,11 +631,13 @@ async def crawl_worker(worker_id):
|
|
| 627 |
record, discovered_urls, noindex = parsed
|
| 628 |
|
| 629 |
if noindex:
|
| 630 |
-
|
|
|
|
| 631 |
async with stats_lock: stats["noindex"] += 1
|
| 632 |
else:
|
| 633 |
await persist_record(record)
|
| 634 |
-
|
|
|
|
| 635 |
async with stats_lock:
|
| 636 |
stats["successful"] += 1
|
| 637 |
recent_successes.append(time.monotonic())
|
|
@@ -641,20 +647,26 @@ async def crawl_worker(worker_id):
|
|
| 641 |
|
| 642 |
for discovered_url in discovered_urls:
|
| 643 |
target_host = urllib.parse.urlsplit(discovered_url).hostname or ""
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
normalized = normalize_url(discovered_url)
|
| 646 |
if normalized:
|
| 647 |
-
|
| 648 |
-
|
|
|
|
| 649 |
|
| 650 |
except Exception as exc:
|
| 651 |
-
|
|
|
|
| 652 |
async with stats_lock: stats["failed"] += 1
|
| 653 |
finally:
|
| 654 |
frontier.task_done()
|
| 655 |
|
| 656 |
# ============================================================
|
| 657 |
-
# API ENDPOINTS
|
| 658 |
# ============================================================
|
| 659 |
|
| 660 |
async def get_db_counts():
|
|
@@ -740,16 +752,15 @@ async def force_save_data():
|
|
| 740 |
async def start_app():
|
| 741 |
global http_session
|
| 742 |
|
| 743 |
-
#
|
| 744 |
loop = asyncio.get_running_loop()
|
| 745 |
-
safe_threads = min(
|
| 746 |
executor = concurrent.futures.ThreadPoolExecutor(max_workers=safe_threads)
|
| 747 |
loop.set_default_executor(executor)
|
| 748 |
|
| 749 |
await init_db()
|
| 750 |
await reset_in_progress()
|
| 751 |
|
| 752 |
-
# Check if DB is empty, then seed Tranco
|
| 753 |
async with db_lock:
|
| 754 |
async with db.execute("SELECT COUNT(*) FROM urls") as cursor:
|
| 755 |
count = (await cursor.fetchone())[0]
|
|
@@ -773,7 +784,7 @@ async def start_app():
|
|
| 773 |
for i in range(GLOBAL_CONCURRENCY):
|
| 774 |
worker_tasks.append(asyncio.create_task(crawl_worker(i)))
|
| 775 |
|
| 776 |
-
print(f"Crawler started with {GLOBAL_CONCURRENCY} workers and {safe_threads} CPU threads.")
|
| 777 |
|
| 778 |
@asynccontextmanager
|
| 779 |
async def lifespan(_app):
|
|
|
|
| 51 |
TRANCO_URL = os.getenv("TRANCO_URL", "https://tranco-list.eu/top-1m.csv.zip")
|
| 52 |
SEED_COUNT = int(os.getenv("SEED_COUNT", "100000"))
|
| 53 |
|
| 54 |
+
# Speed badhane ke liye concurrency 256 kar di gayi hai
|
| 55 |
+
GLOBAL_CONCURRENCY = int(os.getenv("GLOBAL_CONCURRENCY", "256"))
|
| 56 |
+
PER_HOST_CONCURRENCY = int(os.getenv("PER_HOST_CONCURRENCY", "2")) # Alag domains pe force karne ke liye kam kiya
|
| 57 |
+
REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "10")) # 15 se 10 kiya taki slow sites time waste na karein
|
| 58 |
CONNECT_TIMEOUT = float(os.getenv("CONNECT_TIMEOUT", "5"))
|
| 59 |
+
MAX_REDIRECTS = int(os.getenv("MAX_REDIRECTS", "3")) # 5 se 3 kiya
|
| 60 |
|
| 61 |
MAX_RESPONSE_BYTES = int(os.getenv("MAX_RESPONSE_BYTES", str(4 * 1024 * 1024)))
|
| 62 |
MAX_TEXT_CHARS = int(os.getenv("MAX_TEXT_CHARS", "20000"))
|
| 63 |
MAX_KEYWORDS = int(os.getenv("MAX_KEYWORDS", "30"))
|
| 64 |
+
|
| 65 |
+
# Queue block hone se bachane ke liye 500 se 50 links per page kiya
|
| 66 |
+
MAX_LINKS_PER_PAGE = int(os.getenv("MAX_LINKS_PER_PAGE", "50"))
|
| 67 |
|
| 68 |
DB_FLUSH_INTERVAL = float(os.getenv("DB_FLUSH_INTERVAL", "1.0"))
|
| 69 |
HF_UPLOAD_INTERVAL = int(os.getenv("HF_UPLOAD_INTERVAL", str(3 * 60 * 60)))
|
|
|
|
| 79 |
# APP & RUNTIME STATE
|
| 80 |
# ============================================================
|
| 81 |
|
| 82 |
+
app = FastAPI(title="Nebulix Search Crawler", version="4.2.0")
|
| 83 |
|
| 84 |
db: Optional[aiosqlite.Connection] = None
|
| 85 |
http_session: Optional[aiohttp.ClientSession] = None
|
| 86 |
|
| 87 |
frontier = asyncio.PriorityQueue()
|
| 88 |
+
record_queue = asyncio.Queue(maxsize=10000)
|
| 89 |
+
db_event_queue = asyncio.Queue(maxsize=100000) # Queue limit badha di gayi hai
|
| 90 |
|
| 91 |
db_lock = asyncio.Lock()
|
| 92 |
host_state_lock = asyncio.Lock()
|
|
|
|
| 173 |
await db.execute("PRAGMA journal_mode=WAL")
|
| 174 |
await db.execute("PRAGMA synchronous=NORMAL")
|
| 175 |
await db.execute("PRAGMA temp_store=MEMORY")
|
| 176 |
+
await db.execute("PRAGMA mmap_size=3000000000") # Speed up SQLite
|
| 177 |
|
| 178 |
await db.execute("""
|
| 179 |
CREATE TABLE IF NOT EXISTS urls (
|
|
|
|
| 202 |
batch.append(first)
|
| 203 |
except asyncio.TimeoutError: pass
|
| 204 |
|
| 205 |
+
while len(batch) < 10000:
|
| 206 |
try: batch.append(db_event_queue.get_nowait())
|
| 207 |
except asyncio.QueueEmpty: break
|
| 208 |
|
|
|
|
| 242 |
|
| 243 |
async def frontier_feeder():
|
| 244 |
while True:
|
| 245 |
+
# RAM ko free rakhne ke liye memory queue ko 5000 par limit kiya
|
| 246 |
+
if frontier.qsize() > 5000:
|
| 247 |
+
await asyncio.sleep(0.5)
|
| 248 |
+
continue
|
| 249 |
+
|
| 250 |
try:
|
| 251 |
rows = []
|
| 252 |
async with db_lock:
|
| 253 |
+
cursor = await db.execute("SELECT url_hash, url, priority, depth FROM urls WHERE status = 0 ORDER BY priority ASC, discovered_at ASC LIMIT 3000")
|
| 254 |
rows = await cursor.fetchall()
|
| 255 |
|
| 256 |
if rows:
|
|
|
|
| 269 |
await asyncio.sleep(1)
|
| 270 |
|
| 271 |
# ============================================================
|
| 272 |
+
# TRANCO SEED
|
| 273 |
# ============================================================
|
| 274 |
|
| 275 |
async def seed_tranco():
|
|
|
|
| 446 |
async with semaphore:
|
| 447 |
await respect_host_delay(host, DEFAULT_HOST_DELAY)
|
| 448 |
timeout = ClientTimeout(total=REQUEST_TIMEOUT, connect=CONNECT_TIMEOUT)
|
| 449 |
+
|
| 450 |
+
# Dead websites par rukne ke bajaye aage badhne ke liye sirf 1 attempt
|
| 451 |
+
for attempt in range(1):
|
| 452 |
try:
|
| 453 |
async with session.get(url, timeout=timeout, allow_redirects=True, max_redirects=MAX_REDIRECTS) as response:
|
| 454 |
status = response.status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
if status != 200: return None
|
| 456 |
|
| 457 |
content_type = response.headers.get("Content-Type", "").lower()
|
|
|
|
| 469 |
body = b"".join(chunks)
|
| 470 |
async with stats_lock: stats["bytes_downloaded"] += len(body)
|
| 471 |
return {"url": str(response.url), "status": status, "html": body, "headers": {str(k).lower(): str(v) for k, v in response.headers.items()}}
|
| 472 |
+
except Exception:
|
|
|
|
|
|
|
|
|
|
| 473 |
return None
|
| 474 |
return None
|
| 475 |
|
|
|
|
| 532 |
batch.append(first)
|
| 533 |
except asyncio.TimeoutError: pass
|
| 534 |
|
| 535 |
+
while len(batch) < 1000:
|
| 536 |
try: batch.append(record_queue.get_nowait())
|
| 537 |
except asyncio.QueueEmpty: break
|
| 538 |
|
|
|
|
| 608 |
except Exception: pass
|
| 609 |
|
| 610 |
# ============================================================
|
| 611 |
+
# CRAWLER WORKER (FIXED PRIORITY & BLOCKING)
|
| 612 |
# ============================================================
|
| 613 |
|
| 614 |
async def crawl_worker(worker_id):
|
|
|
|
| 621 |
response = await fetch_page(http_session, url)
|
| 622 |
|
| 623 |
if response is None:
|
| 624 |
+
if not db_event_queue.full():
|
| 625 |
+
db_event_queue.put_nowait(("failed", page_hash))
|
| 626 |
async with stats_lock: stats["failed"] += 1
|
| 627 |
continue
|
| 628 |
|
|
|
|
| 631 |
record, discovered_urls, noindex = parsed
|
| 632 |
|
| 633 |
if noindex:
|
| 634 |
+
if not db_event_queue.full():
|
| 635 |
+
db_event_queue.put_nowait(("done", page_hash))
|
| 636 |
async with stats_lock: stats["noindex"] += 1
|
| 637 |
else:
|
| 638 |
await persist_record(record)
|
| 639 |
+
if not db_event_queue.full():
|
| 640 |
+
db_event_queue.put_nowait(("done", page_hash))
|
| 641 |
async with stats_lock:
|
| 642 |
stats["successful"] += 1
|
| 643 |
recent_successes.append(time.monotonic())
|
|
|
|
| 647 |
|
| 648 |
for discovered_url in discovered_urls:
|
| 649 |
target_host = urllib.parse.urlsplit(discovered_url).hostname or ""
|
| 650 |
+
|
| 651 |
+
# FIX: Breadth-First Search. Alag domain ko +10 (High Priority), Same domain ko +100 (Low Priority)
|
| 652 |
+
# Isse workers alag-alag websites pe jayenge aur wait nahi karenge.
|
| 653 |
+
next_priority = priority + 10 if target_host != source_host else priority + 100
|
| 654 |
+
|
| 655 |
normalized = normalize_url(discovered_url)
|
| 656 |
if normalized:
|
| 657 |
+
if not db_event_queue.full():
|
| 658 |
+
db_event_queue.put_nowait(("discover", normalized, next_priority, next_depth))
|
| 659 |
+
async with stats_lock: stats["urls_discovered"] += 1
|
| 660 |
|
| 661 |
except Exception as exc:
|
| 662 |
+
if not db_event_queue.full():
|
| 663 |
+
db_event_queue.put_nowait(("failed", page_hash))
|
| 664 |
async with stats_lock: stats["failed"] += 1
|
| 665 |
finally:
|
| 666 |
frontier.task_done()
|
| 667 |
|
| 668 |
# ============================================================
|
| 669 |
+
# API ENDPOINTS
|
| 670 |
# ============================================================
|
| 671 |
|
| 672 |
async def get_db_counts():
|
|
|
|
| 752 |
async def start_app():
|
| 753 |
global http_session
|
| 754 |
|
| 755 |
+
# CPU ka 70-80% use karne ke liye ThreadPool badha diya gaya hai
|
| 756 |
loop = asyncio.get_running_loop()
|
| 757 |
+
safe_threads = min(48, (os.cpu_count() or 4) * 8)
|
| 758 |
executor = concurrent.futures.ThreadPoolExecutor(max_workers=safe_threads)
|
| 759 |
loop.set_default_executor(executor)
|
| 760 |
|
| 761 |
await init_db()
|
| 762 |
await reset_in_progress()
|
| 763 |
|
|
|
|
| 764 |
async with db_lock:
|
| 765 |
async with db.execute("SELECT COUNT(*) FROM urls") as cursor:
|
| 766 |
count = (await cursor.fetchone())[0]
|
|
|
|
| 784 |
for i in range(GLOBAL_CONCURRENCY):
|
| 785 |
worker_tasks.append(asyncio.create_task(crawl_worker(i)))
|
| 786 |
|
| 787 |
+
print(f"Crawler started with {GLOBAL_CONCURRENCY} workers and {safe_threads} CPU threads for maximum speed.")
|
| 788 |
|
| 789 |
@asynccontextmanager
|
| 790 |
async def lifespan(_app):
|