Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- app (7).py +65 -0
- assets.py.txt +145 -0
- pagination_detector.py.txt +206 -0
- requirements (28).txt +15 -0
- scraper.py.txt +458 -0
app (7).py
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import streamlit as st
|
3 |
+
import os
|
4 |
+
from scraper import fetch_html_selenium, format_data, save_raw_data, save_formatted_data
|
5 |
+
from pagination_detector import detect_pagination_elements
|
6 |
+
from assets import PRICING
|
7 |
+
import google.generativeai as genai
|
8 |
+
|
9 |
+
# Access API keys from Hugging Face Secrets
|
10 |
+
openai_api_key = os.getenv('OPENAI_API_KEY')
|
11 |
+
google_api_key = os.getenv('GOOGLE_API_KEY')
|
12 |
+
groq_api_key = os.getenv('GROQ_API_KEY')
|
13 |
+
|
14 |
+
# Check if the keys are available
|
15 |
+
if not openai_api_key or not google_api_key or not groq_api_key:
|
16 |
+
st.error("API keys are missing! Please add them as secrets in Hugging Face.")
|
17 |
+
|
18 |
+
# Initialize Streamlit app
|
19 |
+
st.set_page_config(page_title="Universal Web Scraper", page_icon="🦑")
|
20 |
+
st.title("Universal Web Scraper 🦑")
|
21 |
+
|
22 |
+
# Initialize session state variables if they don't exist
|
23 |
+
if 'results' not in st.session_state:
|
24 |
+
st.session_state['results'] = None
|
25 |
+
if 'perform_scrape' not in st.session_state:
|
26 |
+
st.session_state['perform_scrape'] = False
|
27 |
+
|
28 |
+
# Sidebar components
|
29 |
+
st.sidebar.title("Web Scraper Settings")
|
30 |
+
model_selection = st.sidebar.selectbox("Select Model", options=list(PRICING.keys()), index=0)
|
31 |
+
url_input = st.sidebar.text_input("Enter URL(s) separated by whitespace")
|
32 |
+
|
33 |
+
# Add toggle to show/hide tags field
|
34 |
+
show_tags = st.sidebar.checkbox("Enable Scraping", value=False)
|
35 |
+
|
36 |
+
# Conditionally show tags input based on the toggle
|
37 |
+
tags = []
|
38 |
+
if show_tags:
|
39 |
+
tags = st.sidebar.text_input("Enter Fields to Extract (comma-separated)").split(",")
|
40 |
+
|
41 |
+
st.sidebar.markdown("---")
|
42 |
+
# Add pagination toggle and input
|
43 |
+
use_pagination = st.sidebar.checkbox("Enable Pagination", value=False)
|
44 |
+
pagination_details = None
|
45 |
+
if use_pagination:
|
46 |
+
pagination_details = st.sidebar.text_input("Enter Pagination Details (optional)", help="Describe how to navigate through pages")
|
47 |
+
|
48 |
+
st.sidebar.markdown("---")
|
49 |
+
|
50 |
+
# Define the scraping function
|
51 |
+
def perform_scrape():
|
52 |
+
raw_html = fetch_html_selenium(url_input)
|
53 |
+
markdown = format_data(raw_html)
|
54 |
+
save_raw_data(markdown, "scraped_data")
|
55 |
+
|
56 |
+
if use_pagination:
|
57 |
+
pagination_data, _, _ = detect_pagination_elements(url_input, pagination_details, model_selection, markdown)
|
58 |
+
return pagination_data
|
59 |
+
|
60 |
+
return markdown
|
61 |
+
|
62 |
+
if st.sidebar.button("Scrape"):
|
63 |
+
with st.spinner("Scraping data..."):
|
64 |
+
result = perform_scrape()
|
65 |
+
st.write(result)
|
assets.py.txt
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
assets.py
|
2 |
+
|
3 |
+
"""
|
4 |
+
This module contains configuration variables and constants
|
5 |
+
that are used across different parts of the application.
|
6 |
+
"""
|
7 |
+
|
8 |
+
# List of user agents to mimic different users
|
9 |
+
USER_AGENTS = [
|
10 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
11 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
12 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
|
13 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
14 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
15 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
|
16 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
|
17 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15",
|
18 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
|
19 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
|
20 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0",
|
21 |
+
"Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
22 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
23 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
|
24 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
25 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36",
|
26 |
+
"Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
|
27 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Mobile/15E148 Safari/604.1",
|
28 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15",
|
29 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
|
30 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
|
31 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0",
|
32 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
|
33 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15",
|
34 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1",
|
35 |
+
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0",
|
36 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
|
37 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
|
38 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15",
|
39 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0",
|
40 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36",
|
41 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
42 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
|
43 |
+
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
|
44 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
|
45 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
46 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
|
47 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36",
|
48 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
|
49 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
50 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
|
51 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
52 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0",
|
53 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15",
|
54 |
+
"Mozilla/5.0 (X11; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0",
|
55 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
56 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1",
|
57 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36",
|
58 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15",
|
59 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
|
60 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36",
|
61 |
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
|
62 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
|
63 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
|
64 |
+
]
|
65 |
+
|
66 |
+
|
67 |
+
# Define the pricing for models without Batch API
|
68 |
+
PRICING = {
|
69 |
+
"gpt-4o-mini": {
|
70 |
+
"input": 0.150 / 1_000_000, # $0.150 per 1M input tokens
|
71 |
+
"output": 0.600 / 1_000_000, # $0.600 per 1M output tokens
|
72 |
+
},
|
73 |
+
"gpt-4o-2024-08-06": {
|
74 |
+
"input": 2.5 / 1_000_000, # $2.5 per 1M input tokens
|
75 |
+
"output": 10 / 1_000_000, # $10 per 1M output tokens
|
76 |
+
},
|
77 |
+
"gemini-1.5-flash": {
|
78 |
+
"input": 0.075 / 1_000_000, # $0.075 per 1M input tokens
|
79 |
+
"output": 0.30 / 1_000_000, # $0.30 per 1M output tokens
|
80 |
+
},
|
81 |
+
"Llama3.1 8B": {
|
82 |
+
"input": 0 , # Free
|
83 |
+
"output": 0 , # Free
|
84 |
+
},
|
85 |
+
"Groq Llama3.1 70b": {
|
86 |
+
"input": 0 , # Free
|
87 |
+
"output": 0 , # Free
|
88 |
+
},
|
89 |
+
# Add other models and their prices here if needed
|
90 |
+
}
|
91 |
+
|
92 |
+
# Timeout settings for web scraping
|
93 |
+
TIMEOUT_SETTINGS = {
|
94 |
+
"page_load": 30,
|
95 |
+
"script": 10
|
96 |
+
}
|
97 |
+
|
98 |
+
# Other reusable constants or configuration settings
|
99 |
+
HEADLESS_OPTIONS = ["--disable-gpu", "--disable-dev-shm-usage","--window-size=1920,1080","--disable-search-engine-choice-screen"]
|
100 |
+
|
101 |
+
#in case you don't need to open the website
|
102 |
+
##HEADLESS_OPTIONS=HEADLESS_OPTIONS+[ "--headless=new"]
|
103 |
+
|
104 |
+
#number of scrolls
|
105 |
+
NUMBER_SCROLL=2
|
106 |
+
|
107 |
+
|
108 |
+
LLAMA_MODEL_FULLNAME="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF"
|
109 |
+
GROQ_LLAMA_MODEL_FULLNAME="llama-3.1-70b-versatile"
|
110 |
+
|
111 |
+
SYSTEM_MESSAGE = """You are an intelligent text extraction and conversion assistant. Your task is to extract structured information
|
112 |
+
from the given text and convert it into a pure JSON format. The JSON should contain only the structured data extracted from the text,
|
113 |
+
with no additional commentary, explanations, or extraneous information.
|
114 |
+
You could encounter cases where you can't find the data of the fields you have to extract or the data will be in a foreign language.
|
115 |
+
Please process the following text and provide the output in pure JSON format with no words before or after the JSON:"""
|
116 |
+
|
117 |
+
USER_MESSAGE = f"Extract the following information from the provided text:\nPage content:\n\n"
|
118 |
+
|
119 |
+
|
120 |
+
|
121 |
+
|
122 |
+
|
123 |
+
PROMPT_PAGINATION = """
|
124 |
+
You are an assistant that extracts pagination elements from markdown content of websites your goal as a universal pagination scrapper of urls from all websites no matter how different they are.
|
125 |
+
|
126 |
+
Please extract the following:
|
127 |
+
|
128 |
+
- The url of the 'Next', 'More', 'See more', 'load more' or any other button indicating how to access the next page, if any, it should be 1 url and no more, if there are multiple urls with the same structure leave this empty.
|
129 |
+
|
130 |
+
- A list of page URLs for pagination it should be a pattern of similar urls with pages that are numbered, if you detect this pattern and the numbers starts from a certain low number until a large number generate the rest of the urls even if they're not included,
|
131 |
+
your goal here is to give as many urls for the user to choose from in order for them to do further scraping, you will have to deal with very different websites that can potientially have so many urls of images and other elements,
|
132 |
+
detect only the urls that are clearly defining a pattern to show data on multiple pages, sometimes there is only a part of these urls and you have to combine it with the initial url, that will be provided for you at the end of this prompt.
|
133 |
+
|
134 |
+
- The user can give you indications on how the pagination works for the specific website at the end of this prompt, if those indications are not empty pay special attention to them as they will directly help you understand the structure and the number of pages to generate.
|
135 |
+
|
136 |
+
Provide the output as a JSON object with the following structure:
|
137 |
+
|
138 |
+
{
|
139 |
+
"page_urls": ["url1", "url2", "url3",...,"urlN"]
|
140 |
+
}
|
141 |
+
|
142 |
+
Do not include any additional text or explanations.
|
143 |
+
"""
|
144 |
+
|
145 |
+
|
pagination_detector.py.txt
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pagination_detector.py
|
2 |
+
|
3 |
+
|
4 |
+
# pagination_detector.py
|
5 |
+
|
6 |
+
import os
|
7 |
+
import json
|
8 |
+
from typing import List, Dict, Tuple, Union
|
9 |
+
from pydantic import BaseModel, Field, ValidationError
|
10 |
+
|
11 |
+
import tiktoken
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
|
14 |
+
from openai import OpenAI
|
15 |
+
import google.generativeai as genai
|
16 |
+
from groq import Groq
|
17 |
+
|
18 |
+
from assets import PROMPT_PAGINATION, PRICING, LLAMA_MODEL_FULLNAME, GROQ_LLAMA_MODEL_FULLNAME
|
19 |
+
|
20 |
+
load_dotenv()
|
21 |
+
import logging
|
22 |
+
|
23 |
+
class PaginationData(BaseModel):
|
24 |
+
page_urls: List[str] = Field(default_factory=list, description="List of pagination URLs, including 'Next' button URL if present")
|
25 |
+
|
26 |
+
def calculate_pagination_price(token_counts: Dict[str, int], model: str) -> float:
|
27 |
+
"""
|
28 |
+
Calculate the price for pagination based on token counts and the selected model.
|
29 |
+
|
30 |
+
Args:
|
31 |
+
token_counts (Dict[str, int]): A dictionary containing 'input_tokens' and 'output_tokens'.
|
32 |
+
model (str): The name of the selected model.
|
33 |
+
|
34 |
+
Returns:
|
35 |
+
float: The total price for the pagination operation.
|
36 |
+
"""
|
37 |
+
input_tokens = token_counts['input_tokens']
|
38 |
+
output_tokens = token_counts['output_tokens']
|
39 |
+
|
40 |
+
input_price = input_tokens * PRICING[model]['input']
|
41 |
+
output_price = output_tokens * PRICING[model]['output']
|
42 |
+
|
43 |
+
return input_price + output_price
|
44 |
+
|
45 |
+
def detect_pagination_elements(url: str, indications: str, selected_model: str, markdown_content: str) -> Tuple[Union[PaginationData, Dict, str], Dict, float]:
|
46 |
+
try:
|
47 |
+
"""
|
48 |
+
Uses AI models to analyze markdown content and extract pagination elements.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
selected_model (str): The name of the OpenAI model to use.
|
52 |
+
markdown_content (str): The markdown content to analyze.
|
53 |
+
|
54 |
+
Returns:
|
55 |
+
Tuple[PaginationData, Dict, float]: Parsed pagination data, token counts, and pagination price.
|
56 |
+
"""
|
57 |
+
prompt_pagination = PROMPT_PAGINATION+"\n The url of the page to extract pagination from "+url+"if the urls that you find are not complete combine them intelligently in a way that fit the pattern **ALWAYS GIVE A FULL URL**"
|
58 |
+
if indications != "":
|
59 |
+
prompt_pagination +=PROMPT_PAGINATION+"\n\n these are the users indications that, pay special attention to them: "+indications+"\n\n below are the markdowns of the website: \n\n"
|
60 |
+
else:
|
61 |
+
prompt_pagination +=PROMPT_PAGINATION+"\n There are no user indications in this case just apply the logic described. \n\n below are the markdowns of the website: \n\n"
|
62 |
+
|
63 |
+
if selected_model in ["gpt-4o-mini", "gpt-4o-2024-08-06"]:
|
64 |
+
# Use OpenAI API
|
65 |
+
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
66 |
+
completion = client.beta.chat.completions.parse(
|
67 |
+
model=selected_model,
|
68 |
+
messages=[
|
69 |
+
{"role": "system", "content": prompt_pagination},
|
70 |
+
{"role": "user", "content": markdown_content},
|
71 |
+
],
|
72 |
+
response_format=PaginationData
|
73 |
+
)
|
74 |
+
|
75 |
+
# Extract the parsed response
|
76 |
+
parsed_response = completion.choices[0].message.parsed
|
77 |
+
|
78 |
+
# Calculate tokens using tiktoken
|
79 |
+
encoder = tiktoken.encoding_for_model(selected_model)
|
80 |
+
input_token_count = len(encoder.encode(markdown_content))
|
81 |
+
output_token_count = len(encoder.encode(json.dumps(parsed_response.dict())))
|
82 |
+
token_counts = {
|
83 |
+
"input_tokens": input_token_count,
|
84 |
+
"output_tokens": output_token_count
|
85 |
+
}
|
86 |
+
|
87 |
+
# Calculate the price
|
88 |
+
pagination_price = calculate_pagination_price(token_counts, selected_model)
|
89 |
+
|
90 |
+
return parsed_response, token_counts, pagination_price
|
91 |
+
|
92 |
+
elif selected_model == "gemini-1.5-flash":
|
93 |
+
# Use Google Gemini API
|
94 |
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
95 |
+
model = genai.GenerativeModel(
|
96 |
+
'gemini-1.5-flash',
|
97 |
+
generation_config={
|
98 |
+
"response_mime_type": "application/json",
|
99 |
+
"response_schema": PaginationData
|
100 |
+
}
|
101 |
+
)
|
102 |
+
prompt = f"{prompt_pagination}\n{markdown_content}"
|
103 |
+
# Count input tokens using Gemini's method
|
104 |
+
input_tokens = model.count_tokens(prompt)
|
105 |
+
completion = model.generate_content(prompt)
|
106 |
+
# Extract token counts from usage_metadata
|
107 |
+
usage_metadata = completion.usage_metadata
|
108 |
+
token_counts = {
|
109 |
+
"input_tokens": usage_metadata.prompt_token_count,
|
110 |
+
"output_tokens": usage_metadata.candidates_token_count
|
111 |
+
}
|
112 |
+
# Get the result
|
113 |
+
response_content = completion.text
|
114 |
+
|
115 |
+
# Log the response content and its type
|
116 |
+
logging.info(f"Gemini Flash response type: {type(response_content)}")
|
117 |
+
logging.info(f"Gemini Flash response content: {response_content}")
|
118 |
+
|
119 |
+
# Try to parse the response as JSON
|
120 |
+
try:
|
121 |
+
parsed_data = json.loads(response_content)
|
122 |
+
if isinstance(parsed_data, dict) and 'page_urls' in parsed_data:
|
123 |
+
pagination_data = PaginationData(**parsed_data)
|
124 |
+
else:
|
125 |
+
pagination_data = PaginationData(page_urls=[])
|
126 |
+
except json.JSONDecodeError:
|
127 |
+
logging.error("Failed to parse Gemini Flash response as JSON")
|
128 |
+
pagination_data = PaginationData(page_urls=[])
|
129 |
+
|
130 |
+
# Calculate the price
|
131 |
+
pagination_price = calculate_pagination_price(token_counts, selected_model)
|
132 |
+
|
133 |
+
return pagination_data, token_counts, pagination_price
|
134 |
+
|
135 |
+
elif selected_model == "Llama3.1 8B":
|
136 |
+
# Use Llama model via OpenAI API pointing to local server
|
137 |
+
openai.api_key = "lm-studio"
|
138 |
+
openai.api_base = "http://localhost:1234/v1"
|
139 |
+
response = openai.ChatCompletion.create(
|
140 |
+
model=LLAMA_MODEL_FULLNAME,
|
141 |
+
messages=[
|
142 |
+
{"role": "system", "content": prompt_pagination},
|
143 |
+
{"role": "user", "content": markdown_content},
|
144 |
+
],
|
145 |
+
temperature=0.7,
|
146 |
+
)
|
147 |
+
response_content = response['choices'][0]['message']['content'].strip()
|
148 |
+
# Try to parse the JSON
|
149 |
+
try:
|
150 |
+
pagination_data = json.loads(response_content)
|
151 |
+
except json.JSONDecodeError:
|
152 |
+
pagination_data = {"next_buttons": [], "page_urls": []}
|
153 |
+
# Token counts
|
154 |
+
token_counts = {
|
155 |
+
"input_tokens": response['usage']['prompt_tokens'],
|
156 |
+
"output_tokens": response['usage']['completion_tokens']
|
157 |
+
}
|
158 |
+
# Calculate the price
|
159 |
+
pagination_price = calculate_pagination_price(token_counts, selected_model)
|
160 |
+
|
161 |
+
return pagination_data, token_counts, pagination_price
|
162 |
+
|
163 |
+
elif selected_model == "Groq Llama3.1 70b":
|
164 |
+
# Use Groq client
|
165 |
+
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
|
166 |
+
response = client.chat.completions.create(
|
167 |
+
model=GROQ_LLAMA_MODEL_FULLNAME,
|
168 |
+
messages=[
|
169 |
+
{"role": "system", "content": prompt_pagination},
|
170 |
+
{"role": "user", "content": markdown_content},
|
171 |
+
],
|
172 |
+
)
|
173 |
+
response_content = response.choices[0].message.content.strip()
|
174 |
+
# Try to parse the JSON
|
175 |
+
try:
|
176 |
+
pagination_data = json.loads(response_content)
|
177 |
+
except json.JSONDecodeError:
|
178 |
+
pagination_data = {"page_urls": []}
|
179 |
+
# Token counts
|
180 |
+
token_counts = {
|
181 |
+
"input_tokens": response.usage.prompt_tokens,
|
182 |
+
"output_tokens": response.usage.completion_tokens
|
183 |
+
}
|
184 |
+
# Calculate the price
|
185 |
+
pagination_price = calculate_pagination_price(token_counts, selected_model)
|
186 |
+
|
187 |
+
# Ensure the pagination_data is a dictionary
|
188 |
+
if isinstance(pagination_data, PaginationData):
|
189 |
+
pagination_data = pagination_data.dict()
|
190 |
+
elif not isinstance(pagination_data, dict):
|
191 |
+
pagination_data = {"page_urls": []}
|
192 |
+
|
193 |
+
return pagination_data, token_counts, pagination_price
|
194 |
+
|
195 |
+
else:
|
196 |
+
raise ValueError(f"Unsupported model: {selected_model}")
|
197 |
+
|
198 |
+
except Exception as e:
|
199 |
+
logging.error(f"An error occurred in detect_pagination_elements: {e}")
|
200 |
+
# Return default values if an error occurs
|
201 |
+
return PaginationData(page_urls=[]), {"input_tokens": 0, "output_tokens": 0}, 0.0
|
202 |
+
|
203 |
+
|
204 |
+
|
205 |
+
|
206 |
+
|
requirements (28).txt
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
openai
|
2 |
+
python-dotenv
|
3 |
+
pandas
|
4 |
+
pydantic
|
5 |
+
requests
|
6 |
+
beautifulsoup4
|
7 |
+
html2text
|
8 |
+
tiktoken
|
9 |
+
selenium
|
10 |
+
readability-lxml
|
11 |
+
streamlit
|
12 |
+
streamlit-tags
|
13 |
+
openpyxl
|
14 |
+
groq
|
15 |
+
google-generativeai
|
scraper.py.txt
ADDED
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
scraper.py
|
2 |
+
|
3 |
+
import os
|
4 |
+
import random
|
5 |
+
import time
|
6 |
+
import re
|
7 |
+
import json
|
8 |
+
from datetime import datetime
|
9 |
+
from typing import List, Dict, Type
|
10 |
+
|
11 |
+
import pandas as pd
|
12 |
+
from bs4 import BeautifulSoup
|
13 |
+
from pydantic import BaseModel, Field, create_model
|
14 |
+
import html2text
|
15 |
+
import tiktoken
|
16 |
+
|
17 |
+
from dotenv import load_dotenv
|
18 |
+
from selenium import webdriver
|
19 |
+
from selenium.webdriver.chrome.service import Service
|
20 |
+
from selenium.webdriver.chrome.options import Options
|
21 |
+
from selenium.webdriver.common.by import By
|
22 |
+
from selenium.webdriver.common.action_chains import ActionChains
|
23 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
24 |
+
from selenium.webdriver.support import expected_conditions as EC
|
25 |
+
|
26 |
+
|
27 |
+
from openai import OpenAI
|
28 |
+
import google.generativeai as genai
|
29 |
+
from groq import Groq
|
30 |
+
|
31 |
+
|
32 |
+
from assets import USER_AGENTS,PRICING,HEADLESS_OPTIONS,SYSTEM_MESSAGE,USER_MESSAGE,LLAMA_MODEL_FULLNAME,GROQ_LLAMA_MODEL_FULLNAME
|
33 |
+
load_dotenv()
|
34 |
+
|
35 |
+
# Set up the Chrome WebDriver options
|
36 |
+
|
37 |
+
def setup_selenium():
|
38 |
+
options = Options()
|
39 |
+
|
40 |
+
# Randomly select a user agent from the imported list
|
41 |
+
user_agent = random.choice(USER_AGENTS)
|
42 |
+
options.add_argument(f"user-agent={user_agent}")
|
43 |
+
|
44 |
+
# Add other options
|
45 |
+
for option in HEADLESS_OPTIONS:
|
46 |
+
options.add_argument(option)
|
47 |
+
|
48 |
+
# Specify the path to the ChromeDriver
|
49 |
+
service = Service(r"./chromedriver-win64/chromedriver.exe")
|
50 |
+
|
51 |
+
# Initialize the WebDriver
|
52 |
+
driver = webdriver.Chrome(service=service, options=options)
|
53 |
+
return driver
|
54 |
+
|
55 |
+
def click_accept_cookies(driver):
|
56 |
+
"""
|
57 |
+
Tries to find and click on a cookie consent button. It looks for several common patterns.
|
58 |
+
"""
|
59 |
+
try:
|
60 |
+
# Wait for cookie popup to load
|
61 |
+
WebDriverWait(driver, 10).until(
|
62 |
+
EC.presence_of_element_located((By.XPATH, "//button | //a | //div"))
|
63 |
+
)
|
64 |
+
|
65 |
+
# Common text variations for cookie buttons
|
66 |
+
accept_text_variations = [
|
67 |
+
"accept", "agree", "allow", "consent", "continue", "ok", "I agree", "got it"
|
68 |
+
]
|
69 |
+
|
70 |
+
# Iterate through different element types and common text variations
|
71 |
+
for tag in ["button", "a", "div"]:
|
72 |
+
for text in accept_text_variations:
|
73 |
+
try:
|
74 |
+
# Create an XPath to find the button by text
|
75 |
+
element = driver.find_element(By.XPATH, f"//{tag}[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{text}')]")
|
76 |
+
if element:
|
77 |
+
element.click()
|
78 |
+
print(f"Clicked the '{text}' button.")
|
79 |
+
return
|
80 |
+
except:
|
81 |
+
continue
|
82 |
+
|
83 |
+
print("No 'Accept Cookies' button found.")
|
84 |
+
|
85 |
+
except Exception as e:
|
86 |
+
print(f"Error finding 'Accept Cookies' button: {e}")
|
87 |
+
|
88 |
+
def fetch_html_selenium(url):
|
89 |
+
driver = setup_selenium()
|
90 |
+
try:
|
91 |
+
driver.get(url)
|
92 |
+
|
93 |
+
# Add random delays to mimic human behavior
|
94 |
+
time.sleep(1) # Adjust this to simulate time for user to read or interact
|
95 |
+
driver.maximize_window()
|
96 |
+
|
97 |
+
|
98 |
+
# Try to find and click the 'Accept Cookies' button
|
99 |
+
# click_accept_cookies(driver)
|
100 |
+
|
101 |
+
# Add more realistic actions like scrolling
|
102 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight/2);")
|
103 |
+
time.sleep(random.uniform(1.1, 1.8)) # Simulate time taken to scroll and read
|
104 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight/1.2);")
|
105 |
+
time.sleep(random.uniform(1.1, 1.8))
|
106 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight/1);")
|
107 |
+
time.sleep(random.uniform(1.1, 2.1))
|
108 |
+
html = driver.page_source
|
109 |
+
return html
|
110 |
+
finally:
|
111 |
+
driver.quit()
|
112 |
+
|
113 |
+
def clean_html(html_content):
|
114 |
+
soup = BeautifulSoup(html_content, 'html.parser')
|
115 |
+
|
116 |
+
# Remove headers and footers based on common HTML tags or classes
|
117 |
+
for element in soup.find_all(['header', 'footer']):
|
118 |
+
element.decompose() # Remove these tags and their content
|
119 |
+
|
120 |
+
return str(soup)
|
121 |
+
|
122 |
+
|
123 |
+
def html_to_markdown_with_readability(html_content):
|
124 |
+
|
125 |
+
|
126 |
+
cleaned_html = clean_html(html_content)
|
127 |
+
|
128 |
+
# Convert to markdown
|
129 |
+
markdown_converter = html2text.HTML2Text()
|
130 |
+
markdown_converter.ignore_links = False
|
131 |
+
markdown_content = markdown_converter.handle(cleaned_html)
|
132 |
+
|
133 |
+
return markdown_content
|
134 |
+
|
135 |
+
|
136 |
+
|
137 |
+
def save_raw_data(raw_data: str, output_folder: str, file_name: str):
|
138 |
+
"""Save raw markdown data to the specified output folder."""
|
139 |
+
os.makedirs(output_folder, exist_ok=True)
|
140 |
+
raw_output_path = os.path.join(output_folder, file_name)
|
141 |
+
with open(raw_output_path, 'w', encoding='utf-8') as f:
|
142 |
+
f.write(raw_data)
|
143 |
+
print(f"Raw data saved to {raw_output_path}")
|
144 |
+
return raw_output_path
|
145 |
+
|
146 |
+
|
147 |
+
def remove_urls_from_file(file_path):
|
148 |
+
# Regex pattern to find URLs
|
149 |
+
url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
|
150 |
+
|
151 |
+
# Construct the new file name
|
152 |
+
base, ext = os.path.splitext(file_path)
|
153 |
+
new_file_path = f"{base}_cleaned{ext}"
|
154 |
+
|
155 |
+
# Read the original markdown content
|
156 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
157 |
+
markdown_content = file.read()
|
158 |
+
|
159 |
+
# Replace all found URLs with an empty string
|
160 |
+
cleaned_content = re.sub(url_pattern, '', markdown_content)
|
161 |
+
|
162 |
+
# Write the cleaned content to a new file
|
163 |
+
with open(new_file_path, 'w', encoding='utf-8') as file:
|
164 |
+
file.write(cleaned_content)
|
165 |
+
print(f"Cleaned file saved as: {new_file_path}")
|
166 |
+
return cleaned_content
|
167 |
+
|
168 |
+
|
169 |
+
def create_dynamic_listing_model(field_names: List[str]) -> Type[BaseModel]:
|
170 |
+
"""
|
171 |
+
Dynamically creates a Pydantic model based on provided fields.
|
172 |
+
field_name is a list of names of the fields to extract from the markdown.
|
173 |
+
"""
|
174 |
+
# Create field definitions using aliases for Field parameters
|
175 |
+
field_definitions = {field: (str, ...) for field in field_names}
|
176 |
+
# Dynamically create the model with all field
|
177 |
+
return create_model('DynamicListingModel', **field_definitions)
|
178 |
+
|
179 |
+
|
180 |
+
def create_listings_container_model(listing_model: Type[BaseModel]) -> Type[BaseModel]:
|
181 |
+
"""
|
182 |
+
Create a container model that holds a list of the given listing model.
|
183 |
+
"""
|
184 |
+
return create_model('DynamicListingsContainer', listings=(List[listing_model], ...))
|
185 |
+
|
186 |
+
|
187 |
+
|
188 |
+
|
189 |
+
def trim_to_token_limit(text, model, max_tokens=120000):
|
190 |
+
encoder = tiktoken.encoding_for_model(model)
|
191 |
+
tokens = encoder.encode(text)
|
192 |
+
if len(tokens) > max_tokens:
|
193 |
+
trimmed_text = encoder.decode(tokens[:max_tokens])
|
194 |
+
return trimmed_text
|
195 |
+
return text
|
196 |
+
|
197 |
+
def generate_system_message(listing_model: BaseModel) -> str:
|
198 |
+
"""
|
199 |
+
Dynamically generate a system message based on the fields in the provided listing model.
|
200 |
+
"""
|
201 |
+
# Use the model_json_schema() method to introspect the Pydantic model
|
202 |
+
schema_info = listing_model.model_json_schema()
|
203 |
+
|
204 |
+
# Extract field descriptions from the schema
|
205 |
+
field_descriptions = []
|
206 |
+
for field_name, field_info in schema_info["properties"].items():
|
207 |
+
# Get the field type from the schema info
|
208 |
+
field_type = field_info["type"]
|
209 |
+
field_descriptions.append(f'"{field_name}": "{field_type}"')
|
210 |
+
|
211 |
+
# Create the JSON schema structure for the listings
|
212 |
+
schema_structure = ",\n".join(field_descriptions)
|
213 |
+
|
214 |
+
# Generate the system message dynamically
|
215 |
+
system_message = f"""
|
216 |
+
You are an intelligent text extraction and conversion assistant. Your task is to extract structured information
|
217 |
+
from the given text and convert it into a pure JSON format. The JSON should contain only the structured data extracted from the text,
|
218 |
+
with no additional commentary, explanations, or extraneous information.
|
219 |
+
You could encounter cases where you can't find the data of the fields you have to extract or the data will be in a foreign language.
|
220 |
+
Please process the following text and provide the output in pure JSON format with no words before or after the JSON:
|
221 |
+
Please ensure the output strictly follows this schema:
|
222 |
+
|
223 |
+
{{
|
224 |
+
"listings": [
|
225 |
+
{{
|
226 |
+
{schema_structure}
|
227 |
+
}}
|
228 |
+
]
|
229 |
+
}} """
|
230 |
+
|
231 |
+
return system_message
|
232 |
+
|
233 |
+
|
234 |
+
|
235 |
+
def format_data(data, DynamicListingsContainer, DynamicListingModel, selected_model):
|
236 |
+
token_counts = {}
|
237 |
+
|
238 |
+
if selected_model in ["gpt-4o-mini", "gpt-4o-2024-08-06"]:
|
239 |
+
# Use OpenAI API
|
240 |
+
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
241 |
+
completion = client.beta.chat.completions.parse(
|
242 |
+
model=selected_model,
|
243 |
+
messages=[
|
244 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
245 |
+
{"role": "user", "content": USER_MESSAGE + data},
|
246 |
+
],
|
247 |
+
response_format=DynamicListingsContainer
|
248 |
+
)
|
249 |
+
# Calculate tokens using tiktoken
|
250 |
+
encoder = tiktoken.encoding_for_model(selected_model)
|
251 |
+
input_token_count = len(encoder.encode(USER_MESSAGE + data))
|
252 |
+
output_token_count = len(encoder.encode(json.dumps(completion.choices[0].message.parsed.dict())))
|
253 |
+
token_counts = {
|
254 |
+
"input_tokens": input_token_count,
|
255 |
+
"output_tokens": output_token_count
|
256 |
+
}
|
257 |
+
return completion.choices[0].message.parsed, token_counts
|
258 |
+
|
259 |
+
elif selected_model == "gemini-1.5-flash":
|
260 |
+
# Use Google Gemini API
|
261 |
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
262 |
+
model = genai.GenerativeModel('gemini-1.5-flash',
|
263 |
+
generation_config={
|
264 |
+
"response_mime_type": "application/json",
|
265 |
+
"response_schema": DynamicListingsContainer
|
266 |
+
})
|
267 |
+
prompt = SYSTEM_MESSAGE + "\n" + USER_MESSAGE + data
|
268 |
+
# Count input tokens using Gemini's method
|
269 |
+
input_tokens = model.count_tokens(prompt)
|
270 |
+
completion = model.generate_content(prompt)
|
271 |
+
# Extract token counts from usage_metadata
|
272 |
+
usage_metadata = completion.usage_metadata
|
273 |
+
token_counts = {
|
274 |
+
"input_tokens": usage_metadata.prompt_token_count,
|
275 |
+
"output_tokens": usage_metadata.candidates_token_count
|
276 |
+
}
|
277 |
+
return completion.text, token_counts
|
278 |
+
|
279 |
+
elif selected_model == "Llama3.1 8B":
|
280 |
+
|
281 |
+
# Dynamically generate the system message based on the schema
|
282 |
+
sys_message = generate_system_message(DynamicListingModel)
|
283 |
+
# print(SYSTEM_MESSAGE)
|
284 |
+
# Point to the local server
|
285 |
+
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
|
286 |
+
|
287 |
+
completion = client.chat.completions.create(
|
288 |
+
model=LLAMA_MODEL_FULLNAME, #change this if needed (use a better model)
|
289 |
+
messages=[
|
290 |
+
{"role": "system", "content": sys_message},
|
291 |
+
{"role": "user", "content": USER_MESSAGE + data}
|
292 |
+
],
|
293 |
+
temperature=0.7,
|
294 |
+
|
295 |
+
)
|
296 |
+
|
297 |
+
# Extract the content from the response
|
298 |
+
response_content = completion.choices[0].message.content
|
299 |
+
print(response_content)
|
300 |
+
# Convert the content from JSON string to a Python dictionary
|
301 |
+
parsed_response = json.loads(response_content)
|
302 |
+
|
303 |
+
# Extract token usage
|
304 |
+
token_counts = {
|
305 |
+
"input_tokens": completion.usage.prompt_tokens,
|
306 |
+
"output_tokens": completion.usage.completion_tokens
|
307 |
+
}
|
308 |
+
|
309 |
+
return parsed_response, token_counts
|
310 |
+
elif selected_model== "Groq Llama3.1 70b":
|
311 |
+
|
312 |
+
# Dynamically generate the system message based on the schema
|
313 |
+
sys_message = generate_system_message(DynamicListingModel)
|
314 |
+
# print(SYSTEM_MESSAGE)
|
315 |
+
# Point to the local server
|
316 |
+
client = Groq(api_key=os.environ.get("GROQ_API_KEY"),)
|
317 |
+
|
318 |
+
completion = client.chat.completions.create(
|
319 |
+
messages=[
|
320 |
+
{"role": "system","content": sys_message},
|
321 |
+
{"role": "user","content": USER_MESSAGE + data}
|
322 |
+
],
|
323 |
+
model=GROQ_LLAMA_MODEL_FULLNAME,
|
324 |
+
)
|
325 |
+
|
326 |
+
# Extract the content from the response
|
327 |
+
response_content = completion.choices[0].message.content
|
328 |
+
|
329 |
+
# Convert the content from JSON string to a Python dictionary
|
330 |
+
parsed_response = json.loads(response_content)
|
331 |
+
|
332 |
+
# completion.usage
|
333 |
+
token_counts = {
|
334 |
+
"input_tokens": completion.usage.prompt_tokens,
|
335 |
+
"output_tokens": completion.usage.completion_tokens
|
336 |
+
}
|
337 |
+
|
338 |
+
return parsed_response, token_counts
|
339 |
+
else:
|
340 |
+
raise ValueError(f"Unsupported model: {selected_model}")
|
341 |
+
|
342 |
+
|
343 |
+
|
344 |
+
def save_formatted_data(formatted_data, output_folder: str, json_file_name: str, excel_file_name: str):
|
345 |
+
"""Save formatted data as JSON and Excel in the specified output folder."""
|
346 |
+
os.makedirs(output_folder, exist_ok=True)
|
347 |
+
|
348 |
+
# Parse the formatted data if it's a JSON string (from Gemini API)
|
349 |
+
if isinstance(formatted_data, str):
|
350 |
+
try:
|
351 |
+
formatted_data_dict = json.loads(formatted_data)
|
352 |
+
except json.JSONDecodeError:
|
353 |
+
raise ValueError("The provided formatted data is a string but not valid JSON.")
|
354 |
+
else:
|
355 |
+
# Handle data from OpenAI or other sources
|
356 |
+
formatted_data_dict = formatted_data.dict() if hasattr(formatted_data, 'dict') else formatted_data
|
357 |
+
|
358 |
+
# Save the formatted data as JSON
|
359 |
+
json_output_path = os.path.join(output_folder, json_file_name)
|
360 |
+
with open(json_output_path, 'w', encoding='utf-8') as f:
|
361 |
+
json.dump(formatted_data_dict, f, indent=4)
|
362 |
+
print(f"Formatted data saved to JSON at {json_output_path}")
|
363 |
+
|
364 |
+
# Prepare data for DataFrame
|
365 |
+
if isinstance(formatted_data_dict, dict):
|
366 |
+
# If the data is a dictionary containing lists, assume these lists are records
|
367 |
+
data_for_df = next(iter(formatted_data_dict.values())) if len(formatted_data_dict) == 1 else formatted_data_dict
|
368 |
+
elif isinstance(formatted_data_dict, list):
|
369 |
+
data_for_df = formatted_data_dict
|
370 |
+
else:
|
371 |
+
raise ValueError("Formatted data is neither a dictionary nor a list, cannot convert to DataFrame")
|
372 |
+
|
373 |
+
# Create DataFrame
|
374 |
+
try:
|
375 |
+
df = pd.DataFrame(data_for_df)
|
376 |
+
print("DataFrame created successfully.")
|
377 |
+
|
378 |
+
# Save the DataFrame to an Excel file
|
379 |
+
excel_output_path = os.path.join(output_folder, excel_file_name)
|
380 |
+
df.to_excel(excel_output_path, index=False)
|
381 |
+
print(f"Formatted data saved to Excel at {excel_output_path}")
|
382 |
+
|
383 |
+
return df
|
384 |
+
except Exception as e:
|
385 |
+
print(f"Error creating DataFrame or saving Excel: {str(e)}")
|
386 |
+
return None
|
387 |
+
|
388 |
+
def calculate_price(token_counts, model):
|
389 |
+
input_token_count = token_counts.get("input_tokens", 0)
|
390 |
+
output_token_count = token_counts.get("output_tokens", 0)
|
391 |
+
|
392 |
+
# Calculate the costs
|
393 |
+
input_cost = input_token_count * PRICING[model]["input"]
|
394 |
+
output_cost = output_token_count * PRICING[model]["output"]
|
395 |
+
total_cost = input_cost + output_cost
|
396 |
+
|
397 |
+
return input_token_count, output_token_count, total_cost
|
398 |
+
|
399 |
+
|
400 |
+
def generate_unique_folder_name(url):
|
401 |
+
timestamp = datetime.now().strftime('%Y_%m_%d__%H_%M_%S')
|
402 |
+
url_name = re.sub(r'\W+', '_', url.split('//')[1].split('/')[0]) # Extract domain name and replace non-alphanumeric characters
|
403 |
+
return f"{url_name}_{timestamp}"
|
404 |
+
|
405 |
+
|
406 |
+
def scrape_multiple_urls(urls, fields, selected_model):
|
407 |
+
output_folder = os.path.join('output', generate_unique_folder_name(urls[0]))
|
408 |
+
os.makedirs(output_folder, exist_ok=True)
|
409 |
+
|
410 |
+
total_input_tokens = 0
|
411 |
+
total_output_tokens = 0
|
412 |
+
total_cost = 0
|
413 |
+
all_data = []
|
414 |
+
markdown = None # We'll store the markdown for the first (or only) URL
|
415 |
+
|
416 |
+
for i, url in enumerate(urls, start=1):
|
417 |
+
raw_html = fetch_html_selenium(url)
|
418 |
+
current_markdown = html_to_markdown_with_readability(raw_html)
|
419 |
+
if i == 1:
|
420 |
+
markdown = current_markdown # Store markdown for the first URL
|
421 |
+
|
422 |
+
input_tokens, output_tokens, cost, formatted_data = scrape_url(url, fields, selected_model, output_folder, i, current_markdown)
|
423 |
+
total_input_tokens += input_tokens
|
424 |
+
total_output_tokens += output_tokens
|
425 |
+
total_cost += cost
|
426 |
+
all_data.append(formatted_data)
|
427 |
+
|
428 |
+
return output_folder, total_input_tokens, total_output_tokens, total_cost, all_data, markdown
|
429 |
+
|
430 |
+
def scrape_url(url: str, fields: List[str], selected_model: str, output_folder: str, file_number: int, markdown: str):
|
431 |
+
"""Scrape a single URL and save the results."""
|
432 |
+
try:
|
433 |
+
# Save raw data
|
434 |
+
save_raw_data(markdown, output_folder, f'rawData_{file_number}.md')
|
435 |
+
|
436 |
+
# Create the dynamic listing model
|
437 |
+
DynamicListingModel = create_dynamic_listing_model(fields)
|
438 |
+
|
439 |
+
# Create the container model that holds a list of the dynamic listing models
|
440 |
+
DynamicListingsContainer = create_listings_container_model(DynamicListingModel)
|
441 |
+
|
442 |
+
# Format data
|
443 |
+
formatted_data, token_counts = format_data(markdown, DynamicListingsContainer, DynamicListingModel, selected_model)
|
444 |
+
|
445 |
+
# Save formatted data
|
446 |
+
save_formatted_data(formatted_data, output_folder, f'sorted_data_{file_number}.json', f'sorted_data_{file_number}.xlsx')
|
447 |
+
|
448 |
+
# Calculate and return token usage and cost
|
449 |
+
input_tokens, output_tokens, total_cost = calculate_price(token_counts, selected_model)
|
450 |
+
return input_tokens, output_tokens, total_cost, formatted_data
|
451 |
+
|
452 |
+
except Exception as e:
|
453 |
+
print(f"An error occurred while processing {url}: {e}")
|
454 |
+
return 0, 0, 0, None
|
455 |
+
|
456 |
+
|
457 |
+
|
458 |
+
|