BEASTZZZ commited on
Commit
df3ba0f
Β·
verified Β·
1 Parent(s): 135a99c

Update pdf_ingest.py

Browse files
Files changed (1) hide show
  1. pdf_ingest.py +72 -7
pdf_ingest.py CHANGED
@@ -8,7 +8,7 @@ time, over the small set of chunks actually retrieved for a given question.
8
  import io
9
  import logging
10
  from typing import List, Optional
11
- from urllib.parse import urljoin
12
 
13
  import requests
14
  from bs4 import BeautifulSoup
@@ -16,7 +16,43 @@ from pypdf import PdfReader
16
 
17
  logger = logging.getLogger("janmat.pdf_ingest")
18
 
19
- USER_AGENT = "JanMatBot/0.2 (+non-commercial civic-tech RAG chatbot)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  def fetch_bytes(url: str, timeout: int = 30) -> bytes:
@@ -49,6 +85,12 @@ def extract_html_article(html: str, base_url: str) -> dict:
49
  Pulls every substantive paragraph/list item β€” no truncation, no
50
  "first N paragraphs" shortcut β€” plus any linked PDF hrefs found on the
51
  page, so a bill's legislative-brief PDF gets queued for ingestion too.
 
 
 
 
 
 
52
  """
53
  soup = BeautifulSoup(html, "lxml")
54
 
@@ -63,10 +105,23 @@ def extract_html_article(html: str, base_url: str) -> dict:
63
  full_text = "\n\n".join(paragraphs)
64
 
65
  pdf_links = []
 
66
  for a in soup.find_all("a", href=True):
67
  href = a["href"]
68
- if href.lower().endswith(".pdf"):
69
- pdf_links.append(urljoin(base_url, href))
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  return {"title": title, "full_text": full_text, "pdf_links": sorted(set(pdf_links))}
72
 
@@ -84,8 +139,18 @@ def ingest_source_document(url: str) -> dict:
84
  "full_text": str, # for HTML: the whole extracted text
85
  "pdf_links": [str, ...], # for HTML: any PDFs linked from the page
86
  }
 
 
87
  """
88
- head_resp = requests.head(url, headers={"User-Agent": USER_AGENT}, timeout=15, allow_redirects=True)
 
 
 
 
 
 
 
 
89
  content_type = head_resp.headers.get("Content-Type", "")
90
 
91
  if is_pdf_url(url, content_type):
@@ -94,7 +159,7 @@ def ingest_source_document(url: str) -> dict:
94
  title = url.rsplit("/", 1)[-1]
95
  return {"title": title, "kind_hint": "pdf", "pages": pages, "full_text": "\n\n".join(pages), "pdf_links": []}
96
 
97
- resp = requests.get(url, headers={"User-Agent": USER_AGENT}, timeout=30)
98
  resp.raise_for_status()
99
  parsed = extract_html_article(resp.text, url)
100
  return {
@@ -103,4 +168,4 @@ def ingest_source_document(url: str) -> dict:
103
  "pages": [],
104
  "full_text": parsed["full_text"],
105
  "pdf_links": parsed["pdf_links"],
106
- }
 
8
  import io
9
  import logging
10
  from typing import List, Optional
11
+ from urllib.parse import urljoin, urlparse
12
 
13
  import requests
14
  from bs4 import BeautifulSoup
 
16
 
17
  logger = logging.getLogger("janmat.pdf_ingest")
18
 
19
+ # A browser-like User-Agent. Several government sites (indiacode.nic.in,
20
+ # mha.gov.in, labour.gov.in) return 403 Forbidden to requests that identify
21
+ # as a bot, but serve the same public documents fine to a normal browser.
22
+ # These are public legislative documents, and this crawler is polite (rate
23
+ # limited, robots-respecting in spirit, non-commercial) β€” the UA change is
24
+ # to avoid blanket bot-blocking, not to evade a deliberate access policy.
25
+ USER_AGENT = (
26
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
27
+ "Chrome/124.0.0.0 Safari/537.36 JanMatBot/0.3 (+non-commercial civic-tech)"
28
+ )
29
+
30
+ # Hosts that fail for reasons a User-Agent can't fix. Skipping them up front
31
+ # saves ~15s per link in connection timeouts and keeps `documents` free of
32
+ # permanently-failed rows. Revisit occasionally β€” these may come back.
33
+ BLOCKED_PDF_HOSTS = {
34
+ # Connection times out from Hugging Face's network (site down or
35
+ # geo/network-restricted β€” not a bot block).
36
+ "bombayhighcourt.nic.in",
37
+ "fcraonline.nic.in",
38
+ # DNS doesn't resolve at all β€” the hostname appears to be wrong/retired
39
+ # on PRS's side (note: api.sci.gov.in DOES work and is NOT blocked).
40
+ "sapi.sci.gov.in",
41
+ # Incomplete SSL certificate chain; fetching would require disabling
42
+ # certificate verification, which isn't worth the MITM risk.
43
+ "egazette.gov.in",
44
+ }
45
+
46
+
47
+ class BlockedHostError(Exception):
48
+ """Raised when a URL's host is on BLOCKED_PDF_HOSTS β€” signals 'skip
49
+ quietly', not 'something went wrong'."""
50
+
51
+
52
+ def is_blocked_host(url: str) -> bool:
53
+ host = urlparse(url).netloc.lower()
54
+ host = host[4:] if host.startswith("www.") else host
55
+ return host in BLOCKED_PDF_HOSTS
56
 
57
 
58
  def fetch_bytes(url: str, timeout: int = 30) -> bytes:
 
85
  Pulls every substantive paragraph/list item β€” no truncation, no
86
  "first N paragraphs" shortcut β€” plus any linked PDF hrefs found on the
87
  page, so a bill's legislative-brief PDF gets queued for ingestion too.
88
+
89
+ PDF links to hosts in BLOCKED_PDF_HOSTS are dropped here rather than
90
+ attempted and failed later β€” see that constant for why each is listed.
91
+ Everything else is followed: PRS bill pages link out to genuinely
92
+ valuable material (Supreme Court judgments, NCRB statistics tables,
93
+ government report PDFs) that belongs in the knowledge base.
94
  """
95
  soup = BeautifulSoup(html, "lxml")
96
 
 
105
  full_text = "\n\n".join(paragraphs)
106
 
107
  pdf_links = []
108
+ skipped_blocked = 0
109
  for a in soup.find_all("a", href=True):
110
  href = a["href"]
111
+ if not href.lower().endswith(".pdf"):
112
+ continue
113
+ absolute = urljoin(base_url, href)
114
+ if is_blocked_host(absolute):
115
+ skipped_blocked += 1
116
+ continue
117
+ pdf_links.append(absolute)
118
+
119
+ if skipped_blocked:
120
+ logger.info(
121
+ "Skipped %s PDF link(s) on %s from known-unreachable hosts",
122
+ skipped_blocked,
123
+ base_url,
124
+ )
125
 
126
  return {"title": title, "full_text": full_text, "pdf_links": sorted(set(pdf_links))}
127
 
 
139
  "full_text": str, # for HTML: the whole extracted text
140
  "pdf_links": [str, ...], # for HTML: any PDFs linked from the page
141
  }
142
+ Raises:
143
+ BlockedHostError if the URL is on a known-unreachable host.
144
  """
145
+ if is_blocked_host(url):
146
+ raise BlockedHostError(f"Skipped known-unreachable host: {url}")
147
+
148
+ # (connect timeout, read timeout) β€” a short connect timeout means a dead
149
+ # host fails in 5s instead of 15s, which matters when a run touches
150
+ # dozens of links.
151
+ head_resp = requests.head(
152
+ url, headers={"User-Agent": USER_AGENT}, timeout=(5, 15), allow_redirects=True
153
+ )
154
  content_type = head_resp.headers.get("Content-Type", "")
155
 
156
  if is_pdf_url(url, content_type):
 
159
  title = url.rsplit("/", 1)[-1]
160
  return {"title": title, "kind_hint": "pdf", "pages": pages, "full_text": "\n\n".join(pages), "pdf_links": []}
161
 
162
+ resp = requests.get(url, headers={"User-Agent": USER_AGENT}, timeout=(5, 30))
163
  resp.raise_for_status()
164
  parsed = extract_html_article(resp.text, url)
165
  return {
 
168
  "pages": [],
169
  "full_text": parsed["full_text"],
170
  "pdf_links": parsed["pdf_links"],
171
+ }