Initial Commit
This commit is contained in:
commit
cd67adaae4
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Anything from venv
|
||||
**/bin/*
|
||||
**/include/*
|
||||
**/lib/*
|
||||
**/lib64/*
|
||||
**/lib64/
|
||||
**/lib64
|
||||
**/share/*
|
||||
|
||||
# Anything from MCPServer
|
||||
MCPServer/cache/*
|
||||
MCPServer/lib64/*
|
||||
|
||||
# Anything from Scraper
|
||||
scraper/articles/*
|
||||
|
||||
# Anything from AI Process
|
||||
ai_processor/output/*
|
||||
20
MCPServer/docker-compose.yml
Normal file
20
MCPServer/docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
services:
|
||||
flask-app:
|
||||
build: .
|
||||
container_name: stockdocs-mcp
|
||||
networks:
|
||||
- ainetwork
|
||||
ports:
|
||||
- "5005:5005"
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
deploy:
|
||||
replicas: 1 # You can scale the number of instances (replicas) as needed
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1024M
|
||||
networks:
|
||||
ainetwork:
|
||||
external: true
|
||||
name: ainetwork
|
||||
20
MCPServer/dockerfile
Normal file
20
MCPServer/dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Use an official Python runtime as a base image
|
||||
FROM python:3.12.3-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
# Install Dependencies
|
||||
COPY requirements.txt /app/
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the current directory contents into the container at /app
|
||||
COPY . /app
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5005
|
||||
|
||||
# Run the Flask app
|
||||
CMD ["python", "server.py"]
|
||||
|
||||
74
MCPServer/openapi.json
Normal file
74
MCPServer/openapi.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "StockDocs",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/query": {
|
||||
"post": {
|
||||
"summary": "Query the vector database",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": { "type": "string" }
|
||||
},
|
||||
"required": ["question"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Query results",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"document": { "type": "string" },
|
||||
"score": { "type": "number" },
|
||||
"metadata": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"summary": "Health check",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/info": {
|
||||
"get": {
|
||||
"summary": "Service info",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
MCPServer/pyvenv.cfg
Normal file
5
MCPServer/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /home/user/StockDocs/MCPServer
|
||||
119
MCPServer/requirements.txt
Normal file
119
MCPServer/requirements.txt
Normal file
@ -0,0 +1,119 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
attrs==25.3.0
|
||||
backoff==2.2.1
|
||||
bcrypt==4.3.0
|
||||
blinker==1.9.0
|
||||
build==1.2.2.post1
|
||||
cachetools==5.5.2
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
chromadb==1.0.13
|
||||
click==8.2.1
|
||||
coloredlogs==15.0.1
|
||||
distro==1.9.0
|
||||
durationpy==0.10
|
||||
einops==0.8.1
|
||||
filelock==3.18.0
|
||||
Flask==3.1.1
|
||||
flatbuffers==25.2.10
|
||||
fsspec==2025.5.1
|
||||
google-auth==2.40.3
|
||||
googleapis-common-protos==1.70.0
|
||||
grpcio==1.73.1
|
||||
h11==0.16.0
|
||||
hf-xet==1.1.5
|
||||
httpcore==1.0.9
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
huggingface-hub==0.33.2
|
||||
humanfriendly==10.0
|
||||
idna==3.10
|
||||
importlib_metadata==8.7.0
|
||||
importlib_resources==6.5.2
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
joblib==1.5.1
|
||||
jsonschema==4.24.0
|
||||
jsonschema-specifications==2025.4.1
|
||||
kubernetes==33.1.0
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
mdurl==0.1.2
|
||||
mmh3==5.1.0
|
||||
mpmath==1.3.0
|
||||
networkx==3.5
|
||||
numpy==2.3.1
|
||||
nvidia-cublas-cu12==12.6.4.1
|
||||
nvidia-cuda-cupti-cu12==12.6.80
|
||||
nvidia-cuda-nvrtc-cu12==12.6.77
|
||||
nvidia-cuda-runtime-cu12==12.6.77
|
||||
nvidia-cudnn-cu12==9.5.1.17
|
||||
nvidia-cufft-cu12==11.3.0.4
|
||||
nvidia-cufile-cu12==1.11.1.6
|
||||
nvidia-curand-cu12==10.3.7.77
|
||||
nvidia-cusolver-cu12==11.7.1.2
|
||||
nvidia-cusparse-cu12==12.5.4.2
|
||||
nvidia-cusparselt-cu12==0.6.3
|
||||
nvidia-nccl-cu12==2.26.2
|
||||
nvidia-nvjitlink-cu12==12.6.85
|
||||
nvidia-nvtx-cu12==12.6.77
|
||||
oauthlib==3.3.1
|
||||
onnxruntime==1.22.0
|
||||
opentelemetry-api==1.34.1
|
||||
opentelemetry-exporter-otlp-proto-common==1.34.1
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.34.1
|
||||
opentelemetry-proto==1.34.1
|
||||
opentelemetry-sdk==1.34.1
|
||||
opentelemetry-semantic-conventions==0.55b1
|
||||
orjson==3.10.18
|
||||
overrides==7.7.0
|
||||
packaging==25.0
|
||||
pillow==11.3.0
|
||||
posthog==6.0.1
|
||||
protobuf==5.29.5
|
||||
pyasn1==0.6.1
|
||||
pyasn1_modules==0.4.2
|
||||
pybase64==1.4.1
|
||||
pydantic==2.11.7
|
||||
pydantic_core==2.33.2
|
||||
Pygments==2.19.2
|
||||
PyPika==0.48.9
|
||||
pyproject_hooks==1.2.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.1
|
||||
PyYAML==6.0.2
|
||||
referencing==0.36.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.4
|
||||
requests-oauthlib==2.0.0
|
||||
rich==14.0.0
|
||||
rpds-py==0.26.0
|
||||
rsa==4.9.1
|
||||
safetensors==0.5.3
|
||||
scikit-learn==1.7.0
|
||||
scipy==1.16.0
|
||||
sentence-transformers==5.0.0
|
||||
setuptools==80.9.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sympy==1.14.0
|
||||
tenacity==9.1.2
|
||||
threadpoolctl==3.6.0
|
||||
tokenizers==0.21.2
|
||||
torch==2.7.1
|
||||
tqdm==4.67.1
|
||||
transformers==4.53.0
|
||||
triton==3.3.1
|
||||
typer==0.16.0
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.14.0
|
||||
urllib3==2.5.0
|
||||
uvicorn==0.35.0
|
||||
uvloop==0.21.0
|
||||
watchfiles==1.1.0
|
||||
websocket-client==1.8.0
|
||||
websockets==15.0.1
|
||||
Werkzeug==3.1.3
|
||||
zipp==3.23.0
|
||||
110
MCPServer/server.py
Normal file
110
MCPServer/server.py
Normal file
@ -0,0 +1,110 @@
|
||||
import chromadb
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
|
||||
model = SentenceTransformer(
|
||||
"Snowflake/snowflake-arctic-embed-m-long",
|
||||
device="cpu", # <-- the only line that changes
|
||||
trust_remote_code=True
|
||||
)
|
||||
|
||||
# Test Test Test
|
||||
client = chromadb.HttpClient(host="chromadb", port=8000)
|
||||
|
||||
collection = client.get_or_create_collection("news")
|
||||
|
||||
|
||||
def query(question):
|
||||
|
||||
query_embedding = model.encode(question, normalize_embeddings=True)
|
||||
|
||||
results = collection.query(
|
||||
query_embeddings=query_embedding,
|
||||
n_results=5,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/query", methods=["POST"])
|
||||
def query_endpoint():
|
||||
data = request.get_json()
|
||||
question = data.get("question")
|
||||
if not question:
|
||||
return jsonify({"error": "Missing 'question' in request body"}), 400
|
||||
|
||||
results = query(question)
|
||||
|
||||
# Adapt response to MCP Model standards
|
||||
# Example MCP format:
|
||||
# {
|
||||
# "results": [
|
||||
# {
|
||||
# "document": ...,
|
||||
# "score": ...,
|
||||
# "metadata": {...}
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
mcp_results = []
|
||||
for doc, score, meta in zip(
|
||||
results.get("documents", [[]])[0],
|
||||
results.get("distances", [[]])[0],
|
||||
results.get("metadatas", [[]])[0]):
|
||||
mcp_results.append({
|
||||
"document": doc,
|
||||
"score": float(score),
|
||||
"metadata": meta
|
||||
})
|
||||
|
||||
return jsonify({"results": mcp_results})
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route("/info", methods=["GET"])
|
||||
def info():
|
||||
return jsonify({
|
||||
"provider": "StockDoc",
|
||||
"model": "Snowflake/snowflake-arctic-embed-m-long",
|
||||
"embedding_dim": 1024, # or your actual dimension
|
||||
"collection": "news"
|
||||
})
|
||||
|
||||
@app.route("/tools", methods=["GET"])
|
||||
def tools():
|
||||
return jsonify({
|
||||
"endpoints": [
|
||||
{
|
||||
"path": "/query",
|
||||
"method": "POST",
|
||||
"description": "Query the vector database with a question.",
|
||||
"request_format": {"question": "string"},
|
||||
"response_format": {
|
||||
"results": [
|
||||
{"document": "string", "score": "float", "metadata": "object"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/health",
|
||||
"method": "GET",
|
||||
"description": "Health check endpoint."
|
||||
},
|
||||
{
|
||||
"path": "/info",
|
||||
"method": "GET",
|
||||
"description": "Returns model and service metadata."
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@app.route("/openapi.json", methods=["GET"])
|
||||
def openapi():
|
||||
return send_from_directory('.', 'openapi.json')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5005, threaded=True, debug=True)
|
||||
132
ai_processor/ai_processor.py
Normal file
132
ai_processor/ai_processor.py
Normal file
@ -0,0 +1,132 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
|
||||
LOCAL_AI_SERVICE_URL = "http://192.168.8.124:11434"
|
||||
|
||||
class ArticleFile:
|
||||
def __init__(self, filename, content, source="Unfiltered"):
|
||||
self.filename = filename
|
||||
self.content = content
|
||||
self.source = source
|
||||
|
||||
def __str__(self):
|
||||
return f"ArticleFile(filename={self.filename}, source={self.source}, content_length={len(self.content)})"
|
||||
|
||||
def load_articles_from_folder(folder_path):
|
||||
"""
|
||||
Loads all articles from text files in the specified folder and returns them as a list of ArticleFile objects.
|
||||
"""
|
||||
articles = []
|
||||
for newspaper in os.listdir(folder_path):
|
||||
for filename in os.listdir(os.path.join(folder_path, newspaper)):
|
||||
file_path = os.path.join(folder_path, newspaper, filename)
|
||||
if os.path.isfile(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
first_line = f.readline()
|
||||
if first_line.startswith("SOURCE:"):
|
||||
source = first_line[len("SOURCE:"):].strip()
|
||||
content = f.read()
|
||||
else:
|
||||
source = "Unfiltered"
|
||||
content = first_line + f.read()
|
||||
articles.append(ArticleFile(filename, content, source))
|
||||
|
||||
print(f"Loaded {len(articles)} articles from folder {folder_path}.")
|
||||
return articles
|
||||
|
||||
def call_local_ai_service(article):
|
||||
"""
|
||||
Sends the article to the local Ollama AI server and returns the parsed response.
|
||||
Trims any 'thinking' section from the DeepSeek model response.
|
||||
"""
|
||||
url = LOCAL_AI_SERVICE_URL + "/api/generate"
|
||||
prompt = (
|
||||
"Give a detailed summary of the article and then list out the tickers and their movement in the article. Do not say anything at the end of your response."
|
||||
"Format the response as JSON with keys 'summary' and 'tickersAndMovements', where 'tickersAndMovements' is a list of "
|
||||
"['Ticker', 'Movement direction', 'Percent change']. Here's an example of the expected output:\n"
|
||||
"{'summary': '...', 'tickersAndMovements': [['AAPL', '+', '2.5%'], ['GOOGL', '-', '1.2%']]}\n\n" \
|
||||
"Validate the JSON format and ensure it is parsable before giving the answer. For every entry in "
|
||||
"tickersAndMovements, it must have 3 entries. If you don't know the ticker symbol, don't make one up, just give "
|
||||
"the full name, you must have either +, - or = for the second entry, and you must have a decimal with percent (like 2.4%) or (?%) if you're unsure" \
|
||||
"Do not say anything at the end of your response."
|
||||
)
|
||||
payload = {
|
||||
"model": "llama3.2:latest",
|
||||
"prompt": f"{prompt}\n\nArticle:\n{article}",
|
||||
"stream": False
|
||||
# TODO: In the future, we should force the response to be json and adjust the temperature
|
||||
}
|
||||
|
||||
print(f"Sending request to local AI service ...")
|
||||
|
||||
# Send the request to the local AI service
|
||||
response = requests.post(url, json=payload, timeout=180) # Increased timeout to 300 seconds
|
||||
response.raise_for_status()
|
||||
# The AI should return a JSON string, so parse it
|
||||
raw_response = response.json()["response"]
|
||||
# Remove the 'thinking' section if present
|
||||
think_marker = "</think>"
|
||||
if think_marker in raw_response:
|
||||
raw_response = raw_response.split(think_marker, 1)[1] # Take the second half after the marker
|
||||
|
||||
# Remove any leading code block markers or whitespace (e.g., ```json or ``` or whitespace)
|
||||
raw_response = raw_response.lstrip("` \njson")
|
||||
# Remove any trailing code block marker ```
|
||||
if raw_response.endswith("```"):
|
||||
raw_response = raw_response[:-3].rstrip()
|
||||
|
||||
#print(f"Received response (trimmed): {raw_response}")
|
||||
print(f"Received response from local AI service, processing...")
|
||||
ai_processed_results = json.loads(raw_response)
|
||||
return ai_processed_results
|
||||
|
||||
def process_article(article_file):
|
||||
processed_article = None
|
||||
|
||||
# Check if we already processed this article
|
||||
# TODO : protentially expensive, consider using a database or cache
|
||||
if os.path.exists(f"output/{article_file.filename}.json"):
|
||||
print(f"Article {article_file.filename} already processed, skipping.")
|
||||
|
||||
# open file and return the content
|
||||
with open(f"output/{article_file.filename}.json", 'r', encoding='utf-8') as f:
|
||||
processed_article = json.load(f)
|
||||
return processed_article
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
# Pass the article content to the AI service
|
||||
processed_article = call_local_ai_service(article_file.content)
|
||||
print(f"Processed article: {processed_article['summary'][:100]}...")
|
||||
|
||||
# Add metadata to the processed article
|
||||
processed_article['filename'] = article_file.filename
|
||||
processed_article['original_content'] = article_file.content
|
||||
processed_article['source'] = article_file.source # <-- Add source here
|
||||
break # Success, exit retry loop
|
||||
except Exception as e:
|
||||
print(f"\tError processing article (attempt {attempt+1}/3): {e}")
|
||||
if processed_article is not None:
|
||||
return processed_article
|
||||
else:
|
||||
print(f"\tSkipping article after 3 failed attempts.")
|
||||
|
||||
return processed_article
|
||||
|
||||
# Retrieve the current archive of pulled articles
|
||||
articles_folder = os.path.join(os.path.dirname(__file__), "../scraper/articles")
|
||||
print("Loading articles from folder " + articles_folder + " ...")
|
||||
articles = load_articles_from_folder(articles_folder)
|
||||
|
||||
# Process the articles retrieved
|
||||
results = []
|
||||
for article in articles:
|
||||
try:
|
||||
result = process_article(article)
|
||||
|
||||
# Save the processed result to a JSON file
|
||||
with open("output/" + result['filename'] + ".json", "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error processing or saving article {article.filename}: {e}")
|
||||
5
ai_processor/pyvenv.cfg
Normal file
5
ai_processor/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /home/user/embedding
|
||||
90
embedding/embedder.py
Normal file
90
embedding/embedder.py
Normal file
@ -0,0 +1,90 @@
|
||||
import os
|
||||
import json
|
||||
import chromadb
|
||||
import uuid
|
||||
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
model = SentenceTransformer(
|
||||
"Snowflake/snowflake-arctic-embed-m-long",
|
||||
device="cpu", # <-- the only line that changes
|
||||
trust_remote_code=True
|
||||
)
|
||||
|
||||
|
||||
import math
|
||||
def embed_text(text, specific_context, max_tokens=2048, overlap=256):
|
||||
"""
|
||||
Embeds the given text using the SentenceTransformer model.
|
||||
Splits the text into chunks if it exceeds max_tokens.
|
||||
"""
|
||||
if len(text) + len(specific_context) <= max_tokens:
|
||||
return (text + specific_context, model.encode([text, specific_context], normalize_embeddings=True))
|
||||
|
||||
# Split text into chunks
|
||||
chunks = []
|
||||
start = 0
|
||||
|
||||
chunkNum = math.ceil(len(text) / (max_tokens - len(specific_context)))
|
||||
# 1000 + 20 = 1020
|
||||
# 3000 + 20 = 3020 -> 3000 / (2048 - 20) = 1.47 ~ 2
|
||||
|
||||
for i in range(chunkNum):
|
||||
if start >= len(text):
|
||||
break
|
||||
# Calculate end index for the chunk
|
||||
end = min(start + max_tokens - len(specific_context), len(text))
|
||||
chunk = text[start:end] + specific_context
|
||||
chunks.append(chunk)
|
||||
start += (max_tokens - overlap) # Overlap for next chunk
|
||||
|
||||
return (chunks, model.encode(chunks, normalize_embeddings=True))
|
||||
|
||||
|
||||
client = chromadb.HttpClient(host="localhost", port=8000)
|
||||
|
||||
# client.delete_collection("news") # Replace "news" with your collection name
|
||||
|
||||
collection = client.get_or_create_collection("news")
|
||||
|
||||
# Load all the processed articles from the output folder
|
||||
output_folder = os.path.join(os.path.dirname(__file__), "../ai_processor/output")
|
||||
output_articles = []
|
||||
# When embedding and upserting, use the source from the processed result
|
||||
for filename in os.listdir(output_folder):
|
||||
if filename.endswith(".json"):
|
||||
with open(os.path.join(output_folder, filename), 'r', encoding='utf-8') as f:
|
||||
prev_proc = json.load(f)
|
||||
output_articles.append(prev_proc)
|
||||
|
||||
def makeTickerMovement(ticker, movement, percent_change):
|
||||
"""
|
||||
Helper function to create a ticker movement entry.
|
||||
"""
|
||||
ticker = "" if ticker is None else ticker
|
||||
movement = "" if movement is None else movement
|
||||
percent_change = "" if percent_change is None else percent_change
|
||||
return [ticker, movement, percent_change]
|
||||
|
||||
# Embed the article content
|
||||
try:
|
||||
specific_context = prev_proc['summary'] + ",".join([' '.join(makeTickerMovement(*x)) for x in prev_proc['tickersAndMovements']])
|
||||
chunks, embedded_content = embed_text(prev_proc['original_content'], prev_proc['summary'])
|
||||
|
||||
print(f"Embedded content for {filename}: \n {embedded_content[:10]}...") # Print first 10 values for preview
|
||||
|
||||
collection.upsert(
|
||||
ids = [ str(uuid.uuid4()) for d in chunks ], # Generate unique IDs for each embedded content
|
||||
documents = chunks, # optional but nice for debugging
|
||||
embeddings = embedded_content.tolist(), # Chroma expects List[List[float]]
|
||||
metadatas =[{
|
||||
"source": prev_proc.get('source', 'Unknown'), # Use the source from the processed result
|
||||
"published": prev_proc.get('published', 'Unknown'),
|
||||
"filename": prev_proc.get('filename', 'Unknown') # Add filename for traceability
|
||||
} for d in chunks], # Metadata for each embedded content
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error embedding content for {filename}: {e}")
|
||||
continue
|
||||
|
||||
5
embedding/pyvenv.cfg
Normal file
5
embedding/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /home/user/embedding
|
||||
82
embedding/requirements.txt
Normal file
82
embedding/requirements.txt
Normal file
@ -0,0 +1,82 @@
|
||||
attrs==23.2.0
|
||||
Automat==22.10.0
|
||||
Babel==2.10.3
|
||||
bcc==0.29.1
|
||||
bcrypt==3.2.2
|
||||
blinker==1.7.0
|
||||
boto3==1.34.46
|
||||
botocore==1.34.46
|
||||
certifi==2023.11.17
|
||||
chardet==5.2.0
|
||||
click==8.1.6
|
||||
cloud-init==25.1.2
|
||||
colorama==0.4.6
|
||||
command-not-found==0.3
|
||||
configobj==5.0.8
|
||||
constantly==23.10.4
|
||||
cryptography==41.0.7
|
||||
dbus-python==1.3.2
|
||||
distro==1.9.0
|
||||
distro-info==1.7+build1
|
||||
gpg==1.18.0
|
||||
h11==0.14.0
|
||||
httplib2==0.20.4
|
||||
hyperlink==21.0.0
|
||||
idna==3.6
|
||||
incremental==22.10.0
|
||||
Jinja2==3.1.2
|
||||
jmespath==1.0.1
|
||||
jsonpatch==1.32
|
||||
jsonpointer==2.0
|
||||
jsonschema==4.10.3
|
||||
launchpadlib==1.11.0
|
||||
lazr.restfulclient==0.14.6
|
||||
lazr.uri==1.0.6
|
||||
Markdown==3.5.2
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==2.1.5
|
||||
mdurl==0.1.2
|
||||
netaddr==0.8.0
|
||||
netifaces==0.11.0
|
||||
oauthlib==3.2.2
|
||||
packaging==24.0
|
||||
pexpect==4.9.0
|
||||
ptyprocess==0.7.0
|
||||
pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
Pygments==2.17.2
|
||||
PyGObject==3.48.2
|
||||
PyHamcrest==2.1.0
|
||||
PyJWT==2.7.0
|
||||
pyOpenSSL==23.2.0
|
||||
pyparsing==3.1.1
|
||||
pyrsistent==0.20.0
|
||||
pyserial==3.5
|
||||
python-apt==2.7.7+ubuntu4
|
||||
python-dateutil==2.8.2
|
||||
python-debian==0.1.49+ubuntu2
|
||||
python-magic==0.4.27
|
||||
pytz==2024.1
|
||||
PyYAML==6.0.1
|
||||
requests==2.31.0
|
||||
rich==13.7.1
|
||||
s3transfer==0.10.1
|
||||
service-identity==24.1.0
|
||||
setuptools==68.1.2
|
||||
six==1.16.0
|
||||
sos==4.7.2
|
||||
ssh-import-id==5.11
|
||||
systemd-python==235
|
||||
Twisted==24.3.0
|
||||
ubuntu-drivers-common==0.0.0
|
||||
ubuntu-pro-client==8001
|
||||
ufw==0.36.2
|
||||
unattended-upgrades==0.1
|
||||
urllib3==2.0.7
|
||||
uvicorn==0.27.1
|
||||
uvloop==0.19.0
|
||||
wadllib==1.3.6
|
||||
wheel==0.42.0
|
||||
wsproto==1.2.0
|
||||
xkit==0.0.0
|
||||
zope.interface==6.1
|
||||
5
scraper/pyvenv.cfg
Normal file
5
scraper/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /home/user/scraper
|
||||
46
scraper/requirements.txt
Normal file
46
scraper/requirements.txt
Normal file
@ -0,0 +1,46 @@
|
||||
attrs==25.3.0
|
||||
beautifulsoup4==4.13.4
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
click==8.2.1
|
||||
dnspython==2.7.0
|
||||
feedparser==6.0.11
|
||||
filelock==3.18.0
|
||||
gnews==0.4.1
|
||||
greenlet==3.2.3
|
||||
h11==0.16.0
|
||||
idna==3.10
|
||||
joblib==1.5.1
|
||||
lxml==5.4.0
|
||||
lxml-html-clean==0.4.2
|
||||
newspaper4k==0.9.3.1
|
||||
nltk==3.9.1
|
||||
numpy==2.3.0
|
||||
outcome==1.3.0.post0
|
||||
pandas==2.3.0
|
||||
pillow==11.2.1
|
||||
playwright==1.52.0
|
||||
pyee==13.0.0
|
||||
pysocks==1.7.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2025.2
|
||||
pyyaml==6.0.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.4
|
||||
requests-file==2.1.0
|
||||
selenium==4.33.0
|
||||
sgmllib3k==1.0.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.7
|
||||
tldextract==5.3.0
|
||||
tqdm==4.67.1
|
||||
trio==0.30.0
|
||||
trio-websocket==0.12.2
|
||||
typing-extensions==4.14.0
|
||||
tzdata==2025.2
|
||||
urllib3==2.5.0
|
||||
websocket-client==1.8.0
|
||||
wsproto==1.2.0
|
||||
|
||||
205
scraper/rss_feeds.json
Normal file
205
scraper/rss_feeds.json
Normal file
@ -0,0 +1,205 @@
|
||||
{
|
||||
"rss_feeds": {
|
||||
"Reuters – Business News": {
|
||||
"source_website": "reuters.com",
|
||||
"rss_url": "https://news.google.com/rss/search?q=site:reuters.com+business&hl=en-US&gl=US&ceid=US:en"
|
||||
},
|
||||
"Associated Press – Business": {
|
||||
"source_website": "apnews.com",
|
||||
"rss_url": "https://news.google.com/rss/search?q=site:apnews.com&hl=en-US&gl=US&ceid=US:en"
|
||||
},
|
||||
"Financial Times": {
|
||||
"source_website": "ft.com",
|
||||
"rss_url": "https://www.ft.com/rss/home"
|
||||
},
|
||||
"Fortune – Top Stories": {
|
||||
"source_website": "fortune.com",
|
||||
"rss_url": "https://fortune.com/feed/fortune-feeds/?id=3230629"
|
||||
},
|
||||
"Seeking Alpha – Market News": {
|
||||
"source_website": "seekingalpha.com",
|
||||
"rss_url": "https://seekingalpha.com/feed.xml"
|
||||
},
|
||||
"The Motley Fool – Stock News & Analysis": {
|
||||
"source_website": "fool.com",
|
||||
"rss_url": "https://www.fool.com/a/feeds/partner/googlechromefollow?apikey=5e092c1f-c5f9-4428-9219-908a47d2e2de"
|
||||
},
|
||||
"Nasdaq – Markets News": {
|
||||
"source_website": "nasdaq.com",
|
||||
"rss_url": "https://www.nasdaq.com/feed/rssoutbound?category=Nasdaq"
|
||||
},
|
||||
"Business Standard – Latest News": {
|
||||
"source_website": "business-standard.com",
|
||||
"rss_url": "https://www.business-standard.com/rss/latest.rss"
|
||||
},
|
||||
"TheStreet – Full Articles": {
|
||||
"source_website": "thestreet.com",
|
||||
"rss_url": "https://www.thestreet.com/.rss/full"
|
||||
},
|
||||
"Benzinga – Financial News": {
|
||||
"source_website": "benzinga.com",
|
||||
"rss_url": "https://feeds.benzinga.com/benzinga"
|
||||
},
|
||||
"MarketBeat – Market News": {
|
||||
"source_website": "marketbeat.com",
|
||||
"rss_url": "https://www.marketbeat.com/feed/"
|
||||
},
|
||||
"Money (Time) – Personal Finance": {
|
||||
"source_website": "money.com",
|
||||
"rss_url": "https://money.com/money/feed/"
|
||||
},
|
||||
"Global Finance Magazine": {
|
||||
"source_website": "gfmag.com",
|
||||
"rss_url": "https://www.gfmag.com/feed"
|
||||
},
|
||||
"Financial Samurai": {
|
||||
"source_website": "financialsamurai.com",
|
||||
"rss_url": "https://www.financialsamurai.com/feed/"
|
||||
},
|
||||
"MoneyWeek": {
|
||||
"source_website": "moneyweek.com",
|
||||
"rss_url": "https://moneyweek.com/feed/all"
|
||||
},
|
||||
"Finance Monthly": {
|
||||
"source_website": "finance-monthly.com",
|
||||
"rss_url": "https://www.finance-monthly.com/feed/"
|
||||
},
|
||||
"European Financial Review": {
|
||||
"source_website": "europeanfinancialreview.com",
|
||||
"rss_url": "https://www.europeanfinancialreview.com/feed"
|
||||
},
|
||||
"Money Morning": {
|
||||
"source_website": "moneymorning.com",
|
||||
"rss_url": "https://moneymorning.com/feed"
|
||||
},
|
||||
"Dealbreaker": {
|
||||
"source_website": "dealbreaker.com",
|
||||
"rss_url": "https://dealbreaker.com/.rss/full"
|
||||
},
|
||||
"World Finance": {
|
||||
"source_website": "worldfinance.com",
|
||||
"rss_url": "https://www.worldfinance.com/feed"
|
||||
},
|
||||
"Fox Business – Headlines": {
|
||||
"source_website": "foxbusiness.com",
|
||||
"rss_url": "https://moxie.foxbusiness.com/google-publisher/latest.xml"
|
||||
},
|
||||
"FinanceAsia": {
|
||||
"source_website": "financeasia.com",
|
||||
"rss_url": "https://www.financeasia.com/rss/latest"
|
||||
},
|
||||
"CNBC – Business": {
|
||||
"source_website": "cnbc.com",
|
||||
"rss_url": "https://www.cnbc.com/id/100003114/device/rss/rss.html"
|
||||
},
|
||||
"CNN Money": {
|
||||
"source_website": "cnn.com",
|
||||
"rss_url": "http://rss.cnn.com/rss/money_topstories.rss"
|
||||
},
|
||||
"Markets Insider": {
|
||||
"source_website": "markets.businessinsider.com",
|
||||
"rss_url": "https://markets.businessinsider.com/rss/news"
|
||||
},
|
||||
"U.S. News – Money": {
|
||||
"source_website": "usnews.com",
|
||||
"rss_url": "https://www.usnews.com/rss/money"
|
||||
},
|
||||
"The Economist – Business & Finance": {
|
||||
"source_website": "economist.com",
|
||||
"rss_url": "https://www.economist.com/business/rss.xml"
|
||||
},
|
||||
"Barchart News": {
|
||||
"source_website": "barchart.com",
|
||||
"rss_url": "http://feeds.feedburner.com/BarchartNews"
|
||||
},
|
||||
"The Guardian – Business": {
|
||||
"source_website": "theguardian.com",
|
||||
"rss_url": "http://feeds.theguardian.com/theguardian/uk/business/rss"
|
||||
},
|
||||
"Economy Watch": {
|
||||
"source_website": "economywatch.com",
|
||||
"rss_url": "https://www.economywatch.com/feed"
|
||||
},
|
||||
"CFI.co": {
|
||||
"source_website": "cfi.co",
|
||||
"rss_url": "https://cfi.co/feed"
|
||||
},
|
||||
"BBC News – Business": {
|
||||
"source_website": "bbc.co.uk",
|
||||
"rss_url": "http://feeds.bbci.co.uk/news/business/rss.xml"
|
||||
},
|
||||
"Investor’s Business Daily": {
|
||||
"source_website": "investors.com",
|
||||
"rss_url": "https://www.investors.com/feed/"
|
||||
},
|
||||
"Forbes – Real-Time": {
|
||||
"source_website": "forbes.com",
|
||||
"rss_url": "https://www.forbes.com/real-time/feed2/"
|
||||
},
|
||||
"The Financial Express": {
|
||||
"source_website": "financialexpress.com",
|
||||
"rss_url": "https://www.financialexpress.com/feed/"
|
||||
},
|
||||
"MarketWatch – Top Stories": {
|
||||
"source_website": "marketwatch.com",
|
||||
"rss_url": "http://feeds.marketwatch.com/marketwatch/topstories/"
|
||||
},
|
||||
"Wall Street Journal – U.S. Business": {
|
||||
"source_website": "wsj.com",
|
||||
"rss_url": "https://feeds.a.dj.com/rss/WSJcomUSBusiness.xml"
|
||||
},
|
||||
"Sky News – Business": {
|
||||
"source_website": "news.sky.com",
|
||||
"rss_url": "http://news.sky.com/feeds/rss/business.xml"
|
||||
},
|
||||
"Bloomberg – Surveillance Podcast": {
|
||||
"source_website": "bloomberg.com",
|
||||
"rss_url": "https://www.bloomberg.com/feed/podcast/bloomberg-surveillance.xml"
|
||||
},
|
||||
"Barron’s – Markets": {
|
||||
"source_website": "barrons.com",
|
||||
"rss_url": "https://www.barrons.com/xml/rss/markets.xml"
|
||||
},
|
||||
"Yahoo Finance": {
|
||||
"source_website": "finance.yahoo.com",
|
||||
"rss_url": "https://www.yahoo.com/news/rss/finance"
|
||||
},
|
||||
"Investing.com – News": {
|
||||
"source_website": "investing.com",
|
||||
"rss_url": "https://www.investing.com/rss/news.rss"
|
||||
},
|
||||
"Investopedia – Headlines": {
|
||||
"source_website": "investopedia.com",
|
||||
"rss_url": "https://www.investopedia.com/feedbuilder/feed/getfeed/?feedName=rss_headline"
|
||||
},
|
||||
"NerdWallet – Finance": {
|
||||
"source_website": "nerdwallet.com",
|
||||
"rss_url": "https://www.nerdwallet.com/news/finance/feed"
|
||||
},
|
||||
"Newsmax Finance": {
|
||||
"source_website": "newsmax.com",
|
||||
"rss_url": "https://www.newsmax.com/rss/finance"
|
||||
},
|
||||
"Bankrate – News": {
|
||||
"source_website": "bankrate.com",
|
||||
"rss_url": "https://www.bankrate.com/rss/"
|
||||
},
|
||||
"Morningstar – Articles": {
|
||||
"source_website": "morningstar.com",
|
||||
"rss_url": "https://www.morningstar.com/articles.rss"
|
||||
},
|
||||
"Kiplinger": {
|
||||
"source_website": "kiplinger.com",
|
||||
"rss_url": "https://www.kiplinger.com/kiplinger.rss"
|
||||
},
|
||||
"International Business Times": {
|
||||
"source_website": "ibtimes.com",
|
||||
"rss_url": "https://www.ibtimes.com/rss"
|
||||
},
|
||||
"Policygenius – News": {
|
||||
"source_website": "policygenius.com",
|
||||
"rss_url": "https://www.policygenius.com/news/feed/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
scraper/rss_short_feed.json
Normal file
13
scraper/rss_short_feed.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"rss_feeds": {
|
||||
"Reuters – Business News": {
|
||||
"source_website": "reuters.com",
|
||||
"rss_url": "https://news.google.com/rss/search?q=site:reuters.com+business&hl=en-US&gl=US&ceid=US:en"
|
||||
},
|
||||
"Associated Press – Business": {
|
||||
"source_website": "apnews.com",
|
||||
"rss_url": "https://news.google.com/rss/search?q=site:apnews.com&hl=en-US&gl=US&ceid=US:en"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
scraper/scraper.py
Normal file
219
scraper/scraper.py
Normal file
@ -0,0 +1,219 @@
|
||||
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
|
||||
|
||||
# Ensure necessary NLTK resources are downloaded / Needed for selenium + newspaper4k article parsing
|
||||
nltk.download('punkt_tab')
|
||||
|
||||
articles = []
|
||||
|
||||
def load_rss_feed_sources():
|
||||
"""
|
||||
Loads the RSS feed sources from a JSON file.
|
||||
"""
|
||||
print("Loading RSS feed sources from rss_feeds.json...")
|
||||
|
||||
try:
|
||||
with open("rss_short_feed.json", "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print("rss_feeds.json not found, returning empty list.")
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
print("Error decoding rss_feeds.json, 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.
|
||||
"""
|
||||
os.makedirs("articles", exist_ok=True)
|
||||
|
||||
outputDir = "articles/"+source if source else "articles"
|
||||
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)
|
||||
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()
|
||||
'''
|
||||
# Check for iframes and use the first one's content if present
|
||||
frames = page.frames
|
||||
main_frame = page.main_frame
|
||||
for frame in frames:
|
||||
if frame != main_frame:
|
||||
try:
|
||||
frame.wait_for_load_state("domcontentloaded", timeout=5000)
|
||||
html = frame.content()
|
||||
print("Extracted content from iframe.")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Could not extract iframe content: {e}")
|
||||
'''
|
||||
|
||||
browser.close()
|
||||
|
||||
# Parse with Newspaper4k
|
||||
article = newspaper.article(url, input_html=html, language='en')
|
||||
article.nlp()
|
||||
return article.text
|
||||
|
||||
|
||||
def pull_articles(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", safe_filename)):
|
||||
print(f"Article already cached: {filename}")
|
||||
with open(os.path.join("articles", 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:
|
||||
# --- Google News Source handling temporarily disabled ---
|
||||
# if "news.google.com" in link:
|
||||
# gn = GoogleNewsSource(link)
|
||||
# gn.build()
|
||||
# # Pull the first article from the Google News cluster
|
||||
# if gn.articles:
|
||||
# article = gn.articles[0]
|
||||
# article.download()
|
||||
# article.parse()
|
||||
# text = article.text
|
||||
# if not text or len(text) < 200:
|
||||
# raise ValueError("\tGoogle News article text too short, falling back to Selenium.")
|
||||
# print(f"\tSuccessfully pulled Google News article from {link}")
|
||||
# else:
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
# Pull the latest article from a variety of news sources
|
||||
#[pull_articles(link, source, title) for source, title, link in 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:
|
||||
results = list(executor.map(pull_articles, link_list, source_list, title_list))
|
||||
print(f"Pulled {len(results)} articles in parallel.")
|
||||
print("All articles pulled successfully.")
|
||||
Loading…
x
Reference in New Issue
Block a user