MatteoScript commited on
Commit
8db827b
·
verified ·
1 Parent(s): 2af4211

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -327
app.py CHANGED
@@ -1,326 +1,146 @@
1
- from io import BytesIO
2
- from bs4 import BeautifulSoup
3
- from collections import namedtuple
4
  import requests
 
 
5
  import re
6
  import pandas as pd
 
 
7
  import numpy as np
8
- import time
9
  import streamlit as st
10
 
11
- prezzo_al_mq = 0
12
-
13
- class Immobiliare:
14
-
15
- def __init__(self, url, *,
16
- verbose=True,
17
- min_house_cost=10_000,
18
- browse_all_pages=True,
19
- area_not_found=0,
20
- price_not_found=np.nan,
21
- floor_not_found=0,
22
- car_not_found=0,
23
- energy_not_found="n/a",
24
- invalid_price_per_area=0,
25
- wait=60):
26
-
27
- self.url = url
28
- self.verbose = verbose
29
- self.min_house_cost = min_house_cost
30
- self.browse_all_pages = browse_all_pages
31
- self.wait = wait / 1000
32
-
33
- self.area_not_found = area_not_found
34
- self.price_not_found = price_not_found
35
- self.floor_not_found = floor_not_found
36
- self.car_not_found = car_not_found
37
- self.energy_not_found = energy_not_found
38
- self.invalid_price_per_area = invalid_price_per_area
39
-
40
- def _say(self, *args, **kwargs):
41
- if self.verbose:
42
- print(*args, **kwargs)
43
-
44
- def get_all_urls(self):
45
- pattern = re.compile(r"\d+\/$")
46
- urls_ = []
47
-
48
- # first page
49
- self._say("Processing page 1")
50
- page = self._get_page(self.url)
51
-
52
- page.seek(0)
53
- soup = BeautifulSoup(page, "html.parser")
54
-
55
- for link in soup.find_all("a"):
56
- time.sleep(self.wait)
57
- l = link.get("href")
58
-
59
- if l is None:
60
- continue
61
-
62
- if "https" in l and "annunci" in l:
63
- if pattern.search(l):
64
- urls_.append(l)
65
-
66
- if self.browse_all_pages:
67
- for i in range(2, 10_000):
68
- self._say(f"Processing page {i}")
69
- curr_url = self.url + f"&pag={i}"
70
-
71
- t = self._get_text(curr_url).lower()
72
-
73
- if "404 not found" in t or "non è presente" in t:
74
- self.urls_ = urls_
75
- break
76
-
77
- else:
78
- page = self._get_page(curr_url)
79
- page.seek(0)
80
- soup = BeautifulSoup(page, "html.parser")
81
-
82
- for link in soup.find_all("a"):
83
- l = link.get("href")
84
-
85
- if l is None:
86
- continue
87
-
88
- if "https" in l and "annunci" in l:
89
- if pattern.search(l):
90
- urls_.append(l)
91
-
92
- self.urls_ = urls_
93
- self._say("All retrieved urls in attribute 'urls_'")
94
- self._say(f"Found {len(urls_)} houses matching criteria.")
95
-
96
- @staticmethod
97
- def _get_page(url):
98
- req = requests.get(url, allow_redirects=False)
99
- page = BytesIO()
100
- page.write(req.content)
101
- return page
102
-
103
- @staticmethod
104
- def _get_text(sub_url):
105
- req = requests.get(sub_url, allow_redirects=False)
106
- page = BytesIO()
107
- page.write(req.content)
108
- page.seek(0)
109
- soup = BeautifulSoup(page, "html.parser")
110
- text = soup.get_text()
111
- t = text.replace("\n", "")
112
- for _ in range(50):
113
- t = t.replace(" ", " ")
114
- return t
115
 
116
- def _get_data(self, sub_url):
117
- t = self._get_text(sub_url).lower()
118
-
119
- # costo appartamento
120
- cost_patterns = (
121
- r"€ (\d+\.\d+\.\d+)", #if that's more than 1M €
122
- r"€ (\d+\.\d+)",
123
- )
124
-
125
- cost = None
126
- locali = None
127
- for pattern in cost_patterns:
128
- cost_pattern = re.compile(pattern)
129
- try:
130
- cost = cost_pattern.search(t)
131
- locali = str(cost.group(1).replace(".", ""))[-1]
132
- cost = str(cost.group(1).replace(".", ""))[:-1]
133
- #cost = cost.group(1).replace(".", "")
134
- break
135
- except AttributeError:
136
- continue
137
-
138
- if cost is None:
139
- if "prezzo su richiesta" in t:
140
- self._say(f"Price available upon request for {sub_url}")
141
- cost = self.price_not_found
142
- else:
143
- self._say(f"Can't get price for {sub_url}")
144
- cost = self.price_not_found
145
-
146
- if cost is not None and cost is not self.price_not_found:
147
- if int(cost) < self.min_house_cost:
148
- if "prezzo su richiesta" in t:
149
- self._say(f"Price available upon request for {sub_url}")
150
- cost = self.price_not_found
151
- else:
152
- self._say(f"Too low house price: {int(cost)}? for {sub_url}")
153
- cost = self.price_not_found
154
-
155
- # piano
156
- floor_patterns = (
157
- r"piano (\d{1,2})",
158
- r"(\d{1,2}) piano",
159
- r"(\d{1,2}) piani",
160
- )
161
-
162
- floor = None
163
- for pattern in floor_patterns:
164
- floor_pattern = re.compile(pattern)
165
- floor = floor_pattern.search(t)
166
- if floor is not None:
167
- floor = floor.group(1)
168
- break
169
-
170
- if "piano terra" in t:
171
- floor = 1
172
-
173
- ultimo = "ultimo" in t
174
-
175
- # metri quadri
176
-
177
- area_pattern = re.compile(r"(\d+) m²")
178
- try:
179
- area = area_pattern.search(t)
180
- area = area.group(1)
181
- except AttributeError:
182
- area = self.area_not_found
183
- if "asta" in t:
184
- self._say(f"Auction house: no area info {sub_url}")
185
- else:
186
- self._say(f"Can't get area info from url {sub_url}")
187
-
188
- # classe energetica
189
- energy_patterns = (
190
- r"energetica (\D{1,2}) ",
191
- r"energetica(\S{1,2})",
192
- )
193
-
194
- def energy_acceptable(stringlike):
195
- if not stringlike.startswith(("A", "B", "C", "D", "E", "F", "G")):
196
- return False
197
- else:
198
- if len(stringlike) == 1:
199
- return True
200
- else:
201
- if not stringlike.endswith(
202
- ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+")
203
- ):
204
- return False
205
- else:
206
- return True
207
-
208
- energy = None
209
- for i, pattern in enumerate(energy_patterns):
210
- energy_pattern = re.compile(pattern)
211
- energy = energy_pattern.search(t)
212
- if energy is not None:
213
- energy = energy.group(1).upper()
214
- if energy_acceptable(energy):
215
- break
216
-
217
- if energy is None or not energy_acceptable(energy):
218
- if "in attesa di certificazione" in t:
219
- self._say(f"Energy efficiency still pending for {sub_url} ")
220
- energy = self.energy_not_found
221
- else:
222
- self._say(f"Can't get energy efficiency from {sub_url}")
223
- energy = self.energy_not_found
224
-
225
- # posto auto
226
- car_patterns = (
227
- r"post\S auto (\d{1,2})",
228
- )
229
-
230
- car = None
231
- for pattern in car_patterns:
232
- car_pattern = re.compile(pattern)
233
- car = car_pattern.search(t)
234
- if car is not None:
235
- car = car.group(1)
236
  break
237
-
238
- if car is None:
239
- available_upon_request = re.compile(r"possibilit\S.{0,10}auto")
240
- if available_upon_request.search(t) is not None:
241
- self._say(f"Car spot/box available upon request for {sub_url}")
242
- car = 0
243
- else:
244
- car = self.car_not_found
245
-
246
- # €/m²
247
- try:
248
- price_per_area = round(int(cost) / int(area), 1)
249
- differenza = prezzo_al_mq - price_per_area
250
- vantaggio = (differenza / prezzo_al_mq) * 120
251
- vantaggio = max(0, vantaggio)
252
- vantaggio = int(vantaggio)
253
- except:
254
- price_per_area = self.energy_not_found
255
- vantaggio = 0
256
-
257
-
258
- # packing the results
259
- House = namedtuple(
260
- "House", [
261
- "Vantaggio",
262
- "Prezzo_Mq",
263
- "Prezzo",
264
- "Superficie",
265
- "Locali",
266
- "Piano",
267
- #"ultimo",
268
- "Url"
269
- #"energy",
270
- #"posto_auto"
271
- ]
272
- )
273
-
274
- res = House(
275
- vantaggio,
276
- price_per_area,
277
- cost,
278
- area,
279
- #ultimo,
280
- locali,
281
- floor,
282
- sub_url
283
- #energy,
284
- #car
285
- )
286
-
287
- return res
288
-
289
- def find_all_houses(self):
290
- if not hasattr(self, "urls_"):
291
- self.get_all_urls()
292
-
293
- all_results = []
294
- for url in self.urls_:
295
- try:
296
- all_results.append(self._get_data(url))
297
- except:
298
- print(f"offending_url='{url}'")
299
- raise
300
-
301
- self.df_ = pd.DataFrame(all_results)
302
- self._say("Results stored in attribute 'df_'")
303
-
304
- # Funzione di styling per evidenziare in rosso i valori inferiori alla variabile
305
- def evidenzia_in_rosso(valore, soglia):
306
- if valore < soglia:
307
- return 'background-color: red; color: white'
308
- return ''
309
 
310
  st.set_page_config(layout="wide")
311
- # Streamlit interface
312
 
313
  st.title('🏠 Immobiliare A.I. ')
314
  st.write("##### Il tuo assistente di intelligenza artificiale per la ricerca di occasioni immobiliari")
315
  with st.expander("Informazioni"):
316
  st.write("Immobiliare A.I. è la webapp che semplifica la ricerca di immobili, grazie a algoritmi avanzati che calcolano il vantaggio di ogni offerta. Trova le migliori occasioni sul mercato con analisi precise e personalizzate. Scopri l’immobile giusto per te con facilità e sicurezza!")
317
-
318
  cerca_premuto = False
319
- # Input field for 'comune'
 
320
  with st.sidebar:
 
321
  st.title("Filtri")
322
- comune_input = st.text_input("Comune", 'lonato del garda')
323
- prezzo_al_mq = st.number_input("Prezzo Medio al Mq", 2500)
 
 
 
324
  prezzo_minimo = st.sidebar.slider("Prezzo Minimo", min_value=0, max_value=1000, value=200)
325
  prezzo_massimo = st.sidebar.slider("Prezzo Massimo", min_value=0, max_value=1000, value=230)
326
 
@@ -339,35 +159,65 @@ with st.sidebar:
339
  prezzo_massimo = prezzo_massimo*1000
340
  cerca_premuto = st.button("Cerca", use_container_width=True, type='primary')
341
 
342
- if cerca_premuto:
343
- if comune_input:
344
- comune = comune_input.replace(" ", "-")
345
-
346
-
347
- url = f"https://www.immobiliare.it/vendita-case/{comune}/?prezzoMinimo={prezzo_minimo}&prezzoMassimo={prezzo_massimo}&localiMinimo={locali_minimo}&localiMassimo={locali_massimo}&random=123456"
348
- #st.write(f"Seraching: {url}")
349
- with st.spinner("Ricerca immobiliare in corso..."):
350
- case = Immobiliare(url)
351
- case.find_all_houses()
352
- df = case.df_
353
- df = df.sort_values(by="Prezzo_Mq", ascending=True)
354
-
355
  st.dataframe(df, hide_index=True, use_container_width=True,
356
  column_config ={
 
357
  "Vantaggio": st.column_config.ProgressColumn(
358
- "Vantaggio",
359
  help="Vantaggio in %",
360
  format='%f',
361
  min_value=0,
362
  max_value=100,
363
  ),
364
- "Prezzo_Mq": " €/Mq",
365
- "Prezzo": "Prezzo Totale",
 
 
 
 
 
 
 
366
  "Superficie": "Superficie",
367
  "Locali": "Locali",
368
- "Piano": "Piano",
369
- "Url": st.column_config.LinkColumn("App URL")
370
- })
371
- st.success("Elaborazione Completata")
372
- else:
373
- st.error("Per favore, inserisci il nome di un comune.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import requests
2
+ from bs4 import BeautifulSoup
3
+ import json
4
  import re
5
  import pandas as pd
6
+ from io import BytesIO
7
+ from collections import namedtuple
8
  import numpy as np
 
9
  import streamlit as st
10
 
11
+ def clean_text(text):
12
+ return re.sub(r'\s+', ' ', text).strip()
13
+
14
+ def formatta_numero(stringa):
15
+ stringa = stringa.split(',')[0]
16
+ stringa = stringa.replace(".", "").replace("da", "").replace("€", "").replace("%","").replace("m²", "").replace("locali", "").strip()
17
+ stringa = stringa.split(' ')[0]
18
+ return stringa
19
+
20
+ # Estrae le informazioni dagli ANNUNCI
21
+ def extract_info(provincia, comune, prezzo_medio_mq, listing):
22
+ info = {}
23
+ superficie = ""
24
+ locali = ""
25
+ info['Provincia'] = provincia
26
+ info['Comune'] = comune
27
+ price_elem = listing.find('div', class_='in-listingCardPrice')
28
+ prezzo = clean_text(price_elem.text) if price_elem else ""
29
+ link_elem = listing.find('a', class_='in-listingCardTitle')
30
+ if link_elem:
31
+ link = link_elem['href']
32
+ titolo = link_elem.text.strip()
33
+ feature_list = listing.find('div', class_='in-listingCardFeatureList')
34
+ if feature_list:
35
+ for item in feature_list.find_all('div', class_='in-listingCardFeatureList__item'):
36
+ use_elem = item.find('use', class_='nd-icon__use')
37
+ if use_elem:
38
+ if use_elem.get('xlink:href') == '#planimetry':
39
+ locali = item.find('span').text.strip()
40
+ elif use_elem.get('xlink:href') == '#size':
41
+ superficie = item.find('span').text.strip()
42
+ image_url = ""
43
+ img = listing.find('figure', class_='nd-figure nd-ratio in-photo')
44
+ if img:
45
+ image_url = img.find('img')['src']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ superficie = formatta_numero(superficie)
48
+ locali = formatta_numero(locali)
49
+ prezzo = formatta_numero(prezzo)
50
+ info['Immagine'] = image_url
51
+ info['Titolo'] = titolo
52
+ info['Prezzo'] = int(prezzo)
53
+ info['Superficie'] = int(superficie)
54
+ try:
55
+ prezzo_numerico = int(prezzo)
56
+ superficie_numerica = int(superficie)
57
+ info['PrezzoMq'] = prezzo_numerico // superficie_numerica
58
+ prezzo_medio_mq = formatta_numero(prezzo_medio_mq)
59
+ prezzo_medio_mq_numerico = int(prezzo_medio_mq)
60
+ differenza = prezzo_medio_mq_numerico - info['PrezzoMq']
61
+ vantaggio = (differenza / prezzo_medio_mq_numerico) * 100
62
+ vantaggio = max(0, vantaggio)
63
+ vantaggio = int(vantaggio)
64
+ except (ValueError, ZeroDivisionError):
65
+ info['PrezzoMq'] = 0
66
+ vantaggio = 0
67
+ info['Locali'] = int(locali)
68
+ info['Link'] = link
69
+ info['PrezzoMedioMq'] = int(prezzo_medio_mq)
70
+ info['Vantaggio'] = vantaggio
71
+ if info['PrezzoMq']< int(prezzo_medio_mq) and info['PrezzoMq']>0:
72
+ info['Vantaggioso'] = True
73
+ else:
74
+ info['Vantaggioso'] = False
75
+ return info
76
+
77
+ # Legge gli ANNUNCI (pagina x pagina) sulla base del COMUNE di appartenenza
78
+ def scrape_immobiliare(provincia, comune, prezzo_medio_mq, prezzo_minimo, prezzo_massimo, locali_minimo, locali_massimo):
79
+ print(provincia + " " + comune + " " + prezzo_medio_mq)
80
+ comune_url = comune.replace(" ", "-")
81
+ base_url = f"https://www.immobiliare.it/vendita-case/{comune_url}/?prezzoMinimo={prezzo_minimo}&prezzoMassimo={prezzo_massimo}&localiMinimo={locali_minimo}&localiMassimo={locali_massimo}&random=123456"
82
+ results = []
83
+ page = 1
84
+ url = base_url
85
+ while True:
86
+ print(f'Elaborazione pagina {page}')
87
+ response = requests.get(url)
88
+ soup = BeautifulSoup(response.content, 'html.parser')
89
+ listings = soup.find_all('li', class_='nd-list__item in-searchLayoutListItem')
90
+ if not listings:
91
+ break
92
+ for listing in listings:
93
+ results.append(extract_info(provincia, comune, prezzo_medio_mq, listing))
94
+ pagination = soup.find('div', class_='in-pagination__list')
95
+ if pagination:
96
+ next_page = pagination.find('a', class_='in-pagination__item', string=lambda text: text and text.strip().isdigit())
97
+ if not next_page:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  break
99
+ page += 1
100
+ url = base_url + '&pag=' + str(page)
101
+
102
+ return json.dumps(results, ensure_ascii=False, indent=2)
103
+
104
+ # Restituisce l'elenco dei COMUNI di una Provincia e il PREZZO MEDIO
105
+ def get_elenco_comuni(provincia):
106
+ base_url = f"https://www.immobiliare.it/mercato-immobiliare/lombardia/{provincia}-provincia/"
107
+ results = []
108
+ print(f'Lettura Comuni e Prezzo Medio al Mq')
109
+ response = requests.get(base_url)
110
+ soup = BeautifulSoup(response.content, 'html.parser')
111
+ rows = soup.find_all('tr', class_='nd-table__row')
112
+ results = []
113
+ for row in rows:
114
+ cells = row.find_all('td', class_='nd-table__cell')
115
+ if len(cells) >= 2:
116
+ comune = cells[0].get_text(strip=True)
117
+ prezzo_vendita = cells[1].get_text(strip=True)
118
+ results.append({
119
+ 'provincia': provincia,
120
+ 'comune': comune,
121
+ 'prezzo': prezzo_vendita
122
+ })
123
+
124
+ return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  st.set_page_config(layout="wide")
 
127
 
128
  st.title('🏠 Immobiliare A.I. ')
129
  st.write("##### Il tuo assistente di intelligenza artificiale per la ricerca di occasioni immobiliari")
130
  with st.expander("Informazioni"):
131
  st.write("Immobiliare A.I. è la webapp che semplifica la ricerca di immobili, grazie a algoritmi avanzati che calcolano il vantaggio di ogni offerta. Trova le migliori occasioni sul mercato con analisi precise e personalizzate. Scopri l’immobile giusto per te con facilità e sicurezza!")
132
+
133
  cerca_premuto = False
134
+ comuni_provincia = {}
135
+
136
  with st.sidebar:
137
+ comuni_provincia = get_elenco_comuni('Brescia')
138
  st.title("Filtri")
139
+ elenco = [d['comune'] for d in comuni_provincia]
140
+ comune_input = st.multiselect(
141
+ "Comuni",
142
+ elenco
143
+ )
144
  prezzo_minimo = st.sidebar.slider("Prezzo Minimo", min_value=0, max_value=1000, value=200)
145
  prezzo_massimo = st.sidebar.slider("Prezzo Massimo", min_value=0, max_value=1000, value=230)
146
 
 
159
  prezzo_massimo = prezzo_massimo*1000
160
  cerca_premuto = st.button("Cerca", use_container_width=True, type='primary')
161
 
162
+ #
163
+ #if __name__ == "__main__":
164
+ # print(df)
165
+
166
+ def scrivi_dataframe(output):
167
+ if len(output) > 0:
168
+ df = pd.DataFrame(output)
169
+ df = df.sort_values(by=["Vantaggio", "PrezzoMq"], ascending=[False, True])
170
+ columns_to_display = ["Vantaggioso", "Vantaggio", "Immagine", "Comune", "Titolo", "PrezzoMq", "Prezzo", "Superficie", "Locali", "PrezzoMedioMq", "Link"]
171
+ df = df[columns_to_display]
172
+ df = df.style.format(thousands='.')
 
 
173
  st.dataframe(df, hide_index=True, use_container_width=True,
174
  column_config ={
175
+ "Vantaggioso": st.column_config.CheckboxColumn("Vantaggioso"),
176
  "Vantaggio": st.column_config.ProgressColumn(
177
+ "Punteggio",
178
  help="Vantaggio in %",
179
  format='%f',
180
  min_value=0,
181
  max_value=100,
182
  ),
183
+ "Immagine": st.column_config.ImageColumn("Anteprima", help="Anteprima", width="small"),
184
+ "PrezzoMq": "€/Mq",
185
+ "PrezzoMedioMq": "Media €/Mq",
186
+ "Prezzo": st.column_config.NumberColumn(
187
+ "Prezzo Totale",
188
+ help="Il prezzo totale dell'immobile in EURO",
189
+ step=1,
190
+ format="%d €",
191
+ ),
192
  "Superficie": "Superficie",
193
  "Locali": "Locali",
194
+ "Link": st.column_config.LinkColumn("App URL")
195
+ })
196
+
197
+
198
+
199
+ if cerca_premuto:
200
+ comuni_selezionati = comune_input
201
+ comuni_selezionati = [comune.upper() for comune in comuni_selezionati]
202
+ output = []
203
+ output_singolo = []
204
+ for comune_provincia in comuni_provincia:
205
+ if comune_provincia['comune'].upper() in comuni_selezionati:
206
+ with st.spinner(f"Ricerca Immobili Comune: {comune_provincia['comune']}"):
207
+ output_singolo = json.loads(scrape_immobiliare(comune_provincia['provincia'],
208
+ comune_provincia['comune'],
209
+ comune_provincia['prezzo'],
210
+ prezzo_minimo,
211
+ prezzo_massimo,
212
+ locali_minimo,
213
+ locali_massimo))
214
+ st.write(f"### {comune_provincia['comune']}")
215
+ scrivi_dataframe(output_singolo)
216
+ st.divider()
217
+ output += output_singolo
218
+ if len(comuni_selezionati)>1:
219
+ st.write(f"### Comuni Selezionati")
220
+ scrivi_dataframe(output)
221
+ st.success("Elaborazione Completata")
222
+ else:
223
+ st.error("Per favore, inserisci il nome di un comune.")