Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Textdokument neu.txt +0 -0
- assets.py.txt +145 -0
- instruction.txt +41 -0
- pagination_detector.py.txt +206 -0
- requirements.py.txt +17 -0
- scraper.py.txt +458 -0
- streamlit_app.py.txt +317 -0
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 |
+
|