This fully implements the Stock Docs Project with full Docker Containerization support. This is a working prototype that is actively running on the Media Server. There's a few issues noted, including the following: - Support for some sites could be improved. Reuters has many articles behind an adblock and some websites present banners that don't need to be processed by our AI engine - Some caching could be smarter. As the size of files grows, it will get expensive to search through all files to be sure we've not scraped it, ai proccessed it or embedded it. - Logging could be improved to be much better than just print statements and telemetry could be sent for dashboard monitoring if this were ever to become a full service where we cared about reliability. - MCP server has been noted to return some poorly matching results. Would be better if it returned nothing at all. And should never really return banners or ads as that provides awful input for the model. Perhaps the model could be told to not care about this, but it's better to just never show irrelevant info to the model I think this is an overall really good jumping off point, and we've already gotten to see the max capabilities of our system so far. It's a major win to have the Scraper for instance running at all times getting articles from across the web. I look forward to expending this scraper in the near future for projects like scraping all local news websites in the US or general scraping and monitoring of websites. Co-authored-by: Jarian Cottingham <jariancottingham@dev-machine.local> Co-authored-by: jarianc <user@example.com> Reviewed-on: http://git.example.com/jarianc/StockDocs/pulls/2
230 lines
8.1 KiB
Python
230 lines
8.1 KiB
Python
import newspaper
|
|
import json
|
|
import feedparser
|
|
import time
|
|
import os
|
|
from selenium import webdriver
|
|
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
|
from newspaper.google_news import GoogleNewsSource
|
|
from concurrent.futures import ProcessPoolExecutor
|
|
import nltk
|
|
from nltk.downloader import Downloader
|
|
|
|
FEED_FILE = os.getenv("FEED_FILE")
|
|
|
|
# Ensure necessary NLTK resources are downloaded / Needed for selenium + newspaper4k article parsing
|
|
d = Downloader()
|
|
if not d.is_installed('punkt_tab'):
|
|
nltk.download('punkt_tab')
|
|
|
|
articles = []
|
|
|
|
def load_rss_feed_sources(feed_file=FEED_FILE):
|
|
"""
|
|
Loads the RSS feed sources from a JSON file.
|
|
"""
|
|
print("Loading RSS feed sources from rss_feeds.json...")
|
|
|
|
try:
|
|
with open(FEED_FILE, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except FileNotFoundError:
|
|
print(FEED_FILE + " not found, returning empty list.")
|
|
return []
|
|
except json.JSONDecodeError:
|
|
print("Error decoding " + FEED_FILE + " , returning empty list.")
|
|
return []
|
|
|
|
def mine_all_articles(rss_feed_sources, limit=None):
|
|
"""
|
|
Mines all articles from the given RSS feed sources.
|
|
Returns a list of (site, title, link) tuples.
|
|
"""
|
|
all_links = []
|
|
sources = rss_feed_sources['rss_feeds']
|
|
|
|
for site, data in sources.items():
|
|
print(f"Parsing RSS feed: {data['rss_url']}")
|
|
try:
|
|
feed = feedparser.parse(data["rss_url"])
|
|
feed_entries = feed.entries[:limit] if limit else feed.entries
|
|
|
|
for entry in feed_entries:
|
|
if "link" in entry and "title" in entry:
|
|
all_links.append((site, entry.title, entry.link))
|
|
except Exception as e:
|
|
print(f"Error parsing RSS feed: {site} Error: {str(e)}")
|
|
return all_links
|
|
|
|
def generate_filename_from_url(url):
|
|
"""
|
|
Generates a filename from the given URL by replacing slashes with underscores.
|
|
"""
|
|
# Use only the last part of the URL or replace slashes
|
|
return url.replace("https://", "").replace("http://", "").replace("/", "_")
|
|
|
|
def generate_safe_filename(name):
|
|
# Remove/replace characters not allowed in filenames
|
|
import re
|
|
safe = re.sub(r'[\\/*?:"<>|]', "_", name)
|
|
return safe
|
|
|
|
def save_article_to_file(article, filename, source="Unfiltered"):
|
|
"""
|
|
Saves the given article text to a file with the specified filename.
|
|
"""
|
|
# articles dir should already be there
|
|
# os.makedirs("articles", exist_ok=True)
|
|
|
|
outputDir = "articles/"+source
|
|
os.makedirs(outputDir, exist_ok=True) if source else None
|
|
|
|
# Sanitize filename: use only the last part of the URL or replace slashes
|
|
safe_filename = generate_filename_from_url(filename)
|
|
file_path = os.path.join(outputDir, safe_filename)
|
|
# Save the source as the first line in the file for later retrieval
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
f.write(f"SOURCE:{source}\n")
|
|
f.write(article)
|
|
print(f"Article saved to {file_path}")
|
|
|
|
def get_article_with_selenium(url):
|
|
options = FirefoxOptions()
|
|
options.add_argument("--headless")
|
|
driver = webdriver.Firefox(options=options)
|
|
driver.set_page_load_timeout(30) # 30 seconds timeout
|
|
|
|
|
|
#driver = webdriver.Remote(
|
|
#command_executor='http://localhost:4444/wd/hub',
|
|
#options=options)
|
|
try:
|
|
driver.get(url)
|
|
time.sleep(5) # Wait for JS to load
|
|
html = driver.page_source
|
|
|
|
# Parse with Newspaper4k
|
|
article = newspaper.article(url, input_html=html, language='en')
|
|
article.nlp()
|
|
return article.text
|
|
finally:
|
|
driver.quit()
|
|
|
|
def get_article_with_playwright(url):
|
|
import asyncio
|
|
from playwright.sync_api import sync_playwright
|
|
from newspaper import Article
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
page = browser.new_page()
|
|
page.goto(url)
|
|
|
|
# Optional: wait for specific content to load
|
|
time.sleep(5) # Adjust as needed for the page to load completely
|
|
|
|
html = page.content()
|
|
|
|
browser.close()
|
|
|
|
# Parse with Newspaper4k
|
|
article = newspaper.article(url, input_html=html, language='en')
|
|
article.nlp()
|
|
return article.text
|
|
|
|
|
|
def pull_article(link, source, title=None, save_to_file=True):
|
|
filename = title if title else link
|
|
safe_filename = generate_filename_from_url(filename)
|
|
if os.path.exists(os.path.join("articles", source, safe_filename)):
|
|
print(f"Article already cached: {filename}")
|
|
with open(os.path.join("articles", source, safe_filename), "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
text = ""
|
|
|
|
time.sleep(5) # Since we spawn lots of processes, we need to sleep at the start
|
|
|
|
try:
|
|
article = newspaper.article(link)
|
|
article.download()
|
|
article.parse()
|
|
text = article.text
|
|
if not text or len(text) < 200:
|
|
raise ValueError("\tArticle text too short, falling back to Playwright/Selenium.")
|
|
print(f"\tSuccessfully pulled article with newspaper4k from {link}")
|
|
|
|
except Exception as e:
|
|
print(f"\tnewspaper4k extraction failed for {link}: {e}, falling back to Playwright.")
|
|
try:
|
|
text = get_article_with_playwright(link)
|
|
print(f"\t\tSuccessfully pulled article from {link} with Playwright")
|
|
if not text or len(text) < 200:
|
|
print(f"\t\tPlaywright article too short, falling back to Selenium.")
|
|
try:
|
|
text = get_article_with_selenium(link)
|
|
print(f"\t\t\tSuccessfully pulled article from {link} with Selenium")
|
|
except Exception as e:
|
|
print(f"\t\t\tSelenium failed for {link}: {e}")
|
|
return ""
|
|
except Exception as e:
|
|
print(f"\t\tPlaywright failed for {link}: {e}")
|
|
try:
|
|
text = get_article_with_selenium(link)
|
|
print(f"\t\tSuccessfully pulled article from {link} with Selenium")
|
|
except Exception as e:
|
|
print(f"\t\tSelenium failed for {link}: {e}")
|
|
return ""
|
|
|
|
if save_to_file:
|
|
save_article_to_file(text, filename, source)
|
|
return text
|
|
|
|
while True:
|
|
print("=========================================")
|
|
print("Starting new scraping iteration...")
|
|
# Pull the RSS feed sources from the JSON file
|
|
rss_feed_sources = load_rss_feed_sources()
|
|
|
|
# Mine all articles from the RSS feed sources
|
|
rss_feed_links = mine_all_articles(rss_feed_sources)
|
|
|
|
# Randomize the order of the links to help with load balancing
|
|
import random
|
|
random.shuffle(rss_feed_links)
|
|
|
|
link_list = [link for _, title, link in rss_feed_links]
|
|
source_list = [source for source, _, _ in rss_feed_links]
|
|
title_list = [title for _, title, link in rss_feed_links]
|
|
|
|
with ProcessPoolExecutor() as executor:
|
|
futures = [executor.submit(pull_article, link, source, title) for link, source, title in zip(link_list, source_list, title_list)]
|
|
results = []
|
|
errors = []
|
|
for future in futures:
|
|
try:
|
|
results.append(future.result(timeout=60)) # seconds
|
|
except Exception as e:
|
|
print(f"Error in pull_article: {e}")
|
|
errors.append(e)
|
|
print(f"Attempted to Pull {len(results)} articles in parallel.")
|
|
print(f"Encountered {len(errors)} errors during article pulling. " +\
|
|
"Outputting errors to a local file.")
|
|
|
|
# Ouput errors to a local file
|
|
if errors:
|
|
with open("errors.txt", "w", encoding="utf-8") as f:
|
|
for error in errors:
|
|
f.write(str(error) + "\n")
|
|
print(f"Errors logged to errors.txt")
|
|
|
|
# Print all results to a log file
|
|
with open("results.txt", "w", encoding="utf-8") as f:
|
|
for result in results:
|
|
f.write(result + "\n")
|
|
print("All articles pulled successfully.")
|
|
|
|
# Sleep for a while before the next iteration
|
|
print("Sleeping for 15 minutes before the next iteration...")
|
|
time.sleep(15 * 60) # Sleep for 15 minutes
|