mobenta commited on
Commit
f68f2ab
·
verified ·
1 Parent(s): 1530946

Upload 5 files

Browse files
Files changed (5) hide show
  1. app (7).py +65 -0
  2. assets.py.txt +145 -0
  3. pagination_detector.py.txt +206 -0
  4. requirements (28).txt +15 -0
  5. 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
+