Fed-Sentiment / src /scraper /utils.py
github-actions[bot]
Deploy from MaciSekander/fed-sentiment-analyzer@f9d4abf19279af4ef78eea9d53844dd022994fbb
d0622ef
Raw
History Blame Contribute Delete
2.21 kB
"""Shared HTTP + text-extraction helpers for the scrapers."""
from __future__ import annotations
import re
import time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
USER_AGENT = (
"fed-sentiment-analyzer/0.1 "
"(research tool; contact via GitHub repo issues)"
)
REQUEST_TIMEOUT = 20
REQUEST_DELAY_SECONDS = 1.0 # be polite to federalreserve.gov
def fetch(url: str, session: requests.Session | None = None) -> str:
"""GET a URL and return the response body as text, raising on error."""
session = session or requests.Session()
headers = {"User-Agent": USER_AGENT}
resp = session.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
time.sleep(REQUEST_DELAY_SECONDS)
return resp.text
def extract_main_text(html: str) -> str:
"""Best-effort extraction of the main article text from a Fed web page.
federalreserve.gov pages don't expose a stable single selector across
all sections/years, so this tries a few known containers before
falling back to concatenating every <p> tag on the page.
"""
soup = BeautifulSoup(html, "lxml")
for selector in ("div#article", "div.col-xs-12.col-sm-8.col-md-8", "main"):
node = soup.select_one(selector)
if node is not None:
text = node.get_text(separator="\n", strip=True)
if len(text) > 200:
return _clean(text)
paragraphs = [p.get_text(strip=True) for p in soup.find_all("p")]
return _clean("\n".join(p for p in paragraphs if p))
def _clean(text: str) -> str:
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"[ \t]{2,}", " ", text)
return text.strip()
def save_text(text: str, out_dir: Path, filename: str) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / filename
path.write_text(text, encoding="utf-8")
return path
def extract_date_from_filename(filename: str) -> str | None:
"""Pull a YYYYMMDD or YYYY-MM-DD date out of a filename, if present."""
match = re.search(r"(\d{4})-?(\d{2})-?(\d{2})", filename)
if not match:
return None
year, month, day = match.groups()
return f"{year}-{month}-{day}"