-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrobots_cache.py
More file actions
77 lines (63 loc) · 2.46 KB
/
Copy pathrobots_cache.py
File metadata and controls
77 lines (63 loc) · 2.46 KB
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
"""
Thread-safe per-domain robots.txt cache.
The first thread that asks about a domain becomes that domain's fetcher.
Other threads for the same domain wait on a per-domain Event instead of
launching duplicate network requests. The actual HTTP request happens outside
the cache lock, so unrelated domains are not blocked by slow I/O.
"""
import threading
from typing import Dict
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser
import requests
class RobotsCache:
USER_AGENT = "CrawlGraph/1.0"
def __init__(self, timeout: float = 5.0) -> None:
self._cache: Dict[str, RobotFileParser] = {}
self._inflight: Dict[str, threading.Event] = {}
self._lock = threading.Lock()
self._timeout = timeout
def _fetch_robots(self, domain: str) -> RobotFileParser:
"""Fetch and parse robots.txt for domain. Called outside the lock."""
rp = RobotFileParser()
try:
for scheme in ("https", "http"):
try:
resp = requests.get(
f"{scheme}://{domain}/robots.txt",
timeout=self._timeout,
headers={"User-Agent": self.USER_AGENT},
)
if resp.status_code == 200:
rp.parse(resp.text.splitlines())
return rp
break
except requests.exceptions.SSLError:
continue
except Exception:
pass
# On any error, allow crawling to avoid false blocks.
rp.allow_all = True
return rp
def can_fetch(self, url: str) -> bool:
domain = urlparse(url).netloc.lower()
with self._lock:
if domain in self._cache:
return self._cache[domain].can_fetch(self.USER_AGENT, url)
if domain in self._inflight:
ready = self._inflight[domain]
fetcher = False
else:
ready = threading.Event()
self._inflight[domain] = ready
fetcher = True
if not fetcher:
ready.wait()
with self._lock:
return self._cache[domain].can_fetch(self.USER_AGENT, url)
rp = self._fetch_robots(domain)
with self._lock:
self._cache[domain] = rp
ready.set()
del self._inflight[domain]
return rp.can_fetch(self.USER_AGENT, url)