mobenta commited on
Commit
1a979d0
·
verified ·
1 Parent(s): 3d49f2a

Upload folder using huggingface_hub

Browse files
Textdokument neu.txt ADDED
File without changes
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
+
instruction.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 1. Create a new folder and Install & activate your virtual environnement
2
+
3
+ bash (create virtual environment)
4
+
5
+ python -m venv .venv
6
+
7
+ 2. Create a file requirements.txt and copy the following libraries
8
+
9
+ requirements.txt
10
+
11
+ openai
12
+ python-dotenv
13
+ pandas
14
+ pydantic
15
+ requests
16
+ beautifulsoup4
17
+ html2text
18
+ tiktoken
19
+ selenium
20
+ readability-lxml
21
+ streamlit
22
+ streamlit-tags
23
+ openpyxl
24
+ groq
25
+ google-generativeai
26
+
27
+ 3. pip install requirements
28
+
29
+ bash
30
+
31
+ pip install -r requirements.txt
32
+
33
+ 4. Create .env file and add your api keys
34
+
35
+ .env
36
+
37
+ OPENAI_API_KEY=sk-xxxxxxxx(place your own key)
38
+ GOOGLE_API_KEY=AIzaSyxxxxxxx
39
+ GROQ_API_KEY=gskxxxxxxxxx
40
+
41
+ 5. Downlaod ChromeDriver from https://googlechromelabs.github.io/chrome-for-testing/
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.py.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ requirements.txt
2
+
3
+ openai
4
+ python-dotenv
5
+ pandas
6
+ pydantic
7
+ requests
8
+ beautifulsoup4
9
+ html2text
10
+ tiktoken
11
+ selenium
12
+ readability-lxml
13
+ streamlit
14
+ streamlit-tags
15
+ openpyxl
16
+ groq
17
+ 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
+
streamlit_app.py.txt ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit_app.py
2
+
3
+
4
+ import streamlit as st
5
+ from streamlit_tags import st_tags_sidebar
6
+ import pandas as pd
7
+ import json
8
+ from datetime import datetime
9
+ from scraper import fetch_html_selenium, save_raw_data, format_data, save_formatted_data, calculate_price, html_to_markdown_with_readability, create_dynamic_listing_model, create_listings_container_model, scrape_url
10
+ from pagination_detector import detect_pagination_elements, PaginationData
11
+ import re
12
+ from urllib.parse import urlparse
13
+ from assets import PRICING
14
+ import os
15
+ from pydantic import BaseModel
16
+
17
+
18
+ def serialize_pydantic(obj):
19
+ if isinstance(obj, BaseModel):
20
+ return obj.dict()
21
+ raise TypeError(f'Object of type {obj.__class__.__name__} is not JSON serializable')
22
+
23
+ # Initialize Streamlit app
24
+ st.set_page_config(page_title="Universal Web Scraper", page_icon="🦑")
25
+ st.title("Universal Web Scraper 🦑")
26
+
27
+ # Initialize session state variables if they don't exist
28
+ if 'results' not in st.session_state:
29
+ st.session_state['results'] = None
30
+ if 'perform_scrape' not in st.session_state:
31
+ st.session_state['perform_scrape'] = False
32
+
33
+ # Sidebar components
34
+ st.sidebar.title("Web Scraper Settings")
35
+ model_selection = st.sidebar.selectbox("Select Model", options=list(PRICING.keys()), index=0)
36
+ url_input = st.sidebar.text_input("Enter URL(s) separated by whitespace")
37
+
38
+ # Add toggle to show/hide tags field
39
+ show_tags = st.sidebar.toggle("Enable Scraping")
40
+
41
+ # Conditionally show tags input based on the toggle
42
+ tags = []
43
+ if show_tags:
44
+ tags = st_tags_sidebar(
45
+ label='Enter Fields to Extract:',
46
+ text='Press enter to add a tag',
47
+ value=[],
48
+ suggestions=[],
49
+ maxtags=-1,
50
+ key='tags_input'
51
+ )
52
+
53
+ st.sidebar.markdown("---")
54
+ # Add pagination toggle and input
55
+ use_pagination = st.sidebar.toggle("Enable Pagination")
56
+ pagination_details = None
57
+ if use_pagination:
58
+ pagination_details = st.sidebar.text_input("Enter Pagination Details (optional)",
59
+ help="Describe how to navigate through pages (e.g., 'Next' button class, URL pattern)")
60
+
61
+ st.sidebar.markdown("---")
62
+
63
+
64
+ def generate_unique_folder_name(url):
65
+ timestamp = datetime.now().strftime('%Y_%m_%d__%H_%M_%S')
66
+
67
+ # Parse the URL
68
+ parsed_url = urlparse(url)
69
+
70
+ # Extract the domain name
71
+ domain = parsed_url.netloc or parsed_url.path.split('/')[0]
72
+
73
+ # Remove 'www.' if present
74
+ domain = re.sub(r'^www\.', '', domain)
75
+
76
+ # Remove any non-alphanumeric characters and replace with underscores
77
+ clean_domain = re.sub(r'\W+', '_', domain)
78
+
79
+ return f"{clean_domain}_{timestamp}"
80
+
81
+ def scrape_multiple_urls(urls, fields, selected_model):
82
+ output_folder = os.path.join('output', generate_unique_folder_name(urls[0]))
83
+ os.makedirs(output_folder, exist_ok=True)
84
+
85
+ total_input_tokens = 0
86
+ total_output_tokens = 0
87
+ total_cost = 0
88
+ all_data = []
89
+ first_url_markdown = None
90
+
91
+ for i, url in enumerate(urls, start=1):
92
+ raw_html = fetch_html_selenium(url)
93
+ markdown = html_to_markdown_with_readability(raw_html)
94
+ if i == 1:
95
+ first_url_markdown = markdown
96
+
97
+ input_tokens, output_tokens, cost, formatted_data = scrape_url(url, fields, selected_model, output_folder, i, markdown)
98
+ total_input_tokens += input_tokens
99
+ total_output_tokens += output_tokens
100
+ total_cost += cost
101
+ all_data.append(formatted_data)
102
+
103
+ return output_folder, total_input_tokens, total_output_tokens, total_cost, all_data, first_url_markdown
104
+
105
+ # Define the scraping function
106
+ def perform_scrape():
107
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
108
+ raw_html = fetch_html_selenium(url_input)
109
+ markdown = html_to_markdown_with_readability(raw_html)
110
+ save_raw_data(markdown, timestamp)
111
+
112
+ # Detect pagination if enabled
113
+ pagination_info = None
114
+ if use_pagination:
115
+ pagination_data, token_counts, pagination_price = detect_pagination_elements(
116
+ url_input, pagination_details, model_selection, markdown
117
+ )
118
+ pagination_info = {
119
+ "page_urls": pagination_data.page_urls,
120
+ "token_counts": token_counts,
121
+ "price": pagination_price
122
+ }
123
+
124
+ # Initialize token and cost variables with default values
125
+ input_tokens = 0
126
+ output_tokens = 0
127
+ total_cost = 0
128
+
129
+ if show_tags:
130
+ DynamicListingModel = create_dynamic_listing_model(tags)
131
+ DynamicListingsContainer = create_listings_container_model(DynamicListingModel)
132
+ formatted_data, tokens_count = format_data(
133
+ markdown, DynamicListingsContainer, DynamicListingModel, model_selection
134
+ )
135
+ input_tokens, output_tokens, total_cost = calculate_price(tokens_count, model=model_selection)
136
+ df = save_formatted_data(formatted_data, timestamp)
137
+ else:
138
+ formatted_data = None
139
+ df = None
140
+
141
+ return df, formatted_data, markdown, input_tokens, output_tokens, total_cost, timestamp, pagination_info
142
+
143
+ if st.sidebar.button("Scrape"):
144
+ with st.spinner('Please wait... Data is being scraped.'):
145
+ urls = url_input.split()
146
+ field_list = tags
147
+ output_folder, total_input_tokens, total_output_tokens, total_cost, all_data, first_url_markdown = scrape_multiple_urls(urls, field_list, model_selection)
148
+
149
+ # Perform pagination if enabled and only one URL is provided
150
+ pagination_info = None
151
+ if use_pagination and len(urls) == 1:
152
+ try:
153
+ pagination_result = detect_pagination_elements(
154
+ urls[0], pagination_details, model_selection, first_url_markdown
155
+ )
156
+
157
+ if pagination_result is not None:
158
+ pagination_data, token_counts, pagination_price = pagination_result
159
+
160
+ # Handle both PaginationData objects and dictionaries
161
+ if isinstance(pagination_data, PaginationData):
162
+ page_urls = pagination_data.page_urls
163
+ elif isinstance(pagination_data, dict):
164
+ page_urls = pagination_data.get("page_urls", [])
165
+ else:
166
+ page_urls = []
167
+
168
+ pagination_info = {
169
+ "page_urls": page_urls,
170
+ "token_counts": token_counts,
171
+ "price": pagination_price
172
+ }
173
+ else:
174
+ st.warning("Pagination detection returned None. No pagination information available.")
175
+ except Exception as e:
176
+ st.error(f"An error occurred during pagination detection: {e}")
177
+ pagination_info = {
178
+ "page_urls": [],
179
+ "token_counts": {"input_tokens": 0, "output_tokens": 0},
180
+ "price": 0.0
181
+ }
182
+
183
+ st.session_state['results'] = (all_data, None, first_url_markdown, total_input_tokens, total_output_tokens, total_cost, output_folder, pagination_info)
184
+ st.session_state['perform_scrape'] = True
185
+
186
+ # Display results if they exist in session state
187
+ if st.session_state['results']:
188
+ all_data, _, _, input_tokens, output_tokens, total_cost, output_folder, pagination_info = st.session_state['results']
189
+
190
+ # Display scraping details in sidebar only if scraping was performed and the toggle is on
191
+ if all_data and show_tags:
192
+ st.sidebar.markdown("---")
193
+ st.sidebar.markdown("### Scraping Details")
194
+ st.sidebar.markdown("#### Token Usage")
195
+ st.sidebar.markdown(f"*Input Tokens:* {input_tokens}")
196
+ st.sidebar.markdown(f"*Output Tokens:* {output_tokens}")
197
+ st.sidebar.markdown(f"**Total Cost:** :green-background[**${total_cost:.4f}**]")
198
+
199
+ # Display scraped data in main area
200
+ st.subheader("Scraped/Parsed Data")
201
+ for i, data in enumerate(all_data, start=1):
202
+ st.write(f"Data from URL {i}:")
203
+
204
+ # Handle string data (convert to dict if it's JSON)
205
+ if isinstance(data, str):
206
+ try:
207
+ data = json.loads(data)
208
+ except json.JSONDecodeError:
209
+ st.error(f"Failed to parse data as JSON for URL {i}")
210
+ continue
211
+
212
+ if isinstance(data, dict):
213
+ if 'listings' in data and isinstance(data['listings'], list):
214
+ df = pd.DataFrame(data['listings'])
215
+ else:
216
+ # If 'listings' is not in the dict or not a list, use the entire dict
217
+ df = pd.DataFrame([data])
218
+ elif hasattr(data, 'listings') and isinstance(data.listings, list):
219
+ # Handle the case where data is a Pydantic model
220
+ listings = [item.dict() for item in data.listings]
221
+ df = pd.DataFrame(listings)
222
+ else:
223
+ st.error(f"Unexpected data format for URL {i}")
224
+ continue
225
+
226
+ # Display the dataframe
227
+ st.dataframe(df, use_container_width=True)
228
+
229
+ # Download options
230
+ st.subheader("Download Options")
231
+ col1, col2 = st.columns(2)
232
+ with col1:
233
+ json_data = json.dumps(all_data, default=lambda o: o.dict() if hasattr(o, 'dict') else str(o), indent=4)
234
+ st.download_button(
235
+ "Download JSON",
236
+ data=json_data,
237
+ file_name="scraped_data.json"
238
+ )
239
+ with col2:
240
+ # Convert all data to a single DataFrame
241
+ all_listings = []
242
+ for data in all_data:
243
+ if isinstance(data, str):
244
+ try:
245
+ data = json.loads(data)
246
+ except json.JSONDecodeError:
247
+ continue
248
+ if isinstance(data, dict) and 'listings' in data:
249
+ all_listings.extend(data['listings'])
250
+ elif hasattr(data, 'listings'):
251
+ all_listings.extend([item.dict() for item in data.listings])
252
+ else:
253
+ all_listings.append(data)
254
+
255
+ combined_df = pd.DataFrame(all_listings)
256
+ st.download_button(
257
+ "Download CSV",
258
+ data=combined_df.to_csv(index=False),
259
+ file_name="scraped_data.csv"
260
+ )
261
+
262
+ st.success(f"Scraping completed. Results saved in {output_folder}")
263
+
264
+ # Add pagination details to sidebar
265
+ if pagination_info and use_pagination:
266
+ st.sidebar.markdown("---")
267
+ st.sidebar.markdown("### Pagination Details")
268
+ st.sidebar.markdown(f"**Number of Page URLs:** {len(pagination_info['page_urls'])}")
269
+ st.sidebar.markdown("#### Pagination Token Usage")
270
+ st.sidebar.markdown(f"*Input Tokens:* {pagination_info['token_counts']['input_tokens']}")
271
+ st.sidebar.markdown(f"*Output Tokens:* {pagination_info['token_counts']['output_tokens']}")
272
+ st.sidebar.markdown(f"**Pagination Cost:** :red-background[**${pagination_info['price']:.4f}**]")
273
+
274
+ st.markdown("---")
275
+ st.subheader("Pagination Information")
276
+ pagination_df = pd.DataFrame(pagination_info["page_urls"], columns=["Page URLs"])
277
+
278
+ st.dataframe(
279
+ pagination_df,
280
+ column_config={
281
+ "Page URLs": st.column_config.LinkColumn("Page URLs")
282
+ },use_container_width=True
283
+ )
284
+
285
+ # Create columns for download buttons
286
+ col1, col2 = st.columns(2)
287
+ with col1:
288
+ st.download_button(
289
+ "Download Pagination JSON",
290
+ data=json.dumps(pagination_info["page_urls"], indent=4),
291
+ file_name=f"pagination_urls.json"
292
+ )
293
+ with col2:
294
+ st.download_button(
295
+ "Download Pagination CSV",
296
+ data=pagination_df.to_csv(index=False),
297
+ file_name=f"pagination_urls.csv"
298
+ )
299
+
300
+ # Display combined totals only if both scraping and pagination were performed and both toggles are on
301
+ if all_data and pagination_info and show_tags and use_pagination:
302
+ st.markdown("---")
303
+ total_input_tokens = input_tokens + pagination_info['token_counts']['input_tokens']
304
+ total_output_tokens = output_tokens + pagination_info['token_counts']['output_tokens']
305
+ total_combined_cost = total_cost + pagination_info['price']
306
+ st.markdown("### Total Counts and Cost (Including Pagination)")
307
+ st.markdown(f"**Total Input Tokens:** {total_input_tokens}")
308
+ st.markdown(f"**Total Output Tokens:** {total_output_tokens}")
309
+ st.markdown(f"**Total Combined Cost:** :green[**${total_combined_cost:.4f}**]")
310
+
311
+ # Add a clear results button
312
+ if st.sidebar.button("Clear Results"):
313
+ st.session_state['results'] = None
314
+ st.session_state['perform_scrape'] = False
315
+ st.rerun()
316
+
317
+