fix(login,download): login direct pe goapp, descarcare doar a documentului nou
- BT a mutat login-ul (09.2026): go.bancatransilvania.ro face 302 spre marketing; formularul e pe goapp.bancatransilvania.ro, acelasi tab, fara popup. - Daca formularul nu apare: eroare explicita cu URL/titlu + screenshot/HTML. - scripts/check_login_page.py: verifica pagina de login fara credentiale. - Download: asteapta documentul nou din "Recente"; export gol -> [SKIP], nu timeout. - Screenshot la eroare facut inainte de oprirea Playwright. - opencode.json: agentii opencode nu pot citi .env. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DxPhZABSUf1RJXRWtwecqC
This commit is contained in:
152
btgo_scraper.py
152
btgo_scraper.py
@@ -428,7 +428,10 @@ class BTGoScraper:
|
||||
logging.info("Start BTGO Scraper")
|
||||
logging.info("=" * 60)
|
||||
|
||||
with sync_playwright() as p:
|
||||
# Nu `with`: iesirea din el opreste Playwright inainte de except,
|
||||
# iar screenshot-ul din _handle_error pica ("Event loop is closed").
|
||||
p = sync_playwright().start()
|
||||
try:
|
||||
# Viewport 1366x768: BT afiseaza un splash "redirect to store"
|
||||
# sub ~1100px latime; 1366 e sigur peste prag. Fata de 1920x1080
|
||||
# randeaza ~50% mai putini pixeli -> CPU mult mai mic la randarea
|
||||
@@ -515,6 +518,11 @@ class BTGoScraper:
|
||||
logging.info("✓ Scraper finalizat cu succes!")
|
||||
logging.info("=" * 60)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._handle_error(e)
|
||||
return False
|
||||
finally:
|
||||
p.stop()
|
||||
|
||||
except Exception as e:
|
||||
self._handle_error(e)
|
||||
@@ -523,13 +531,13 @@ class BTGoScraper:
|
||||
def login(self):
|
||||
"""Autentificare cu username si password"""
|
||||
self._update_progress("Deschid pagina de login...")
|
||||
logging.info("Navigare catre https://go.bancatransilvania.ro/")
|
||||
# 'networkidle' nu se atinge niciodata: pagina face polling continuu pe
|
||||
# Google Analytics / LinkedIn / Facebook pixels. Folosim 'domcontentloaded'
|
||||
# si asteptam apoi explicit link-ul Login.
|
||||
self.page.goto('https://go.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
|
||||
self.page.wait_for_selector("a:has-text('Login')", timeout=15000, state="visible")
|
||||
logging.info("Pagina incarcata")
|
||||
# Din 09.2026 go.bancatransilvania.ro redirecteaza (302) spre pagina de
|
||||
# marketing, fara link Login. Formularul e servit direct de goapp
|
||||
# (/ -> /app/auth/login), in acelasi tab, fara popup.
|
||||
# 'networkidle' nu se atinge niciodata (pixeli de tracking) -> 'domcontentloaded'
|
||||
# + asteptarea pe input#user de mai jos.
|
||||
logging.info("Navigare catre https://goapp.bancatransilvania.ro/")
|
||||
self.page.goto('https://goapp.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
|
||||
|
||||
try:
|
||||
# Cookie consent - asteapta si accepta (GDPR wrapper)
|
||||
@@ -547,15 +555,7 @@ class BTGoScraper:
|
||||
except:
|
||||
pass
|
||||
|
||||
# Click pe butonul LOGIN - deschide tab nou
|
||||
logging.info("Click pe butonul LOGIN...")
|
||||
with self.page.expect_popup() as popup_info:
|
||||
login_link = self.page.get_by_role("link", name="Login")
|
||||
login_link.click()
|
||||
|
||||
# Preia referinta la tab-ul de login
|
||||
self.login_page = popup_info.value
|
||||
logging.info("Popup/tab login deschis")
|
||||
self.login_page = self.page
|
||||
|
||||
# Asteapta ca Angular SPA sa randeze formularul de login.
|
||||
# goapp.bancatransilvania.ro afiseaza intai un splash ("Aplicatia ta
|
||||
@@ -582,10 +582,24 @@ class BTGoScraper:
|
||||
"(SPA blocat pe splash) - reincarc popup-ul..."
|
||||
)
|
||||
self.login_page.reload(wait_until="domcontentloaded")
|
||||
else:
|
||||
raise
|
||||
# la a 2-a incercare nu ridicam aici: iesim din bucla si
|
||||
# raportam explicit mai jos (pagina de login s-a schimbat).
|
||||
if not form_loaded:
|
||||
raise PlaywrightTimeout("input#user nu a aparut nici dupa reload")
|
||||
# BT a mutat formularul de login: salvam dovezi si spunem exact
|
||||
# ce URL/titlu am vazut, in loc de un timeout anonim de selector.
|
||||
self._debug_shot(self.login_page, "login_page_changed")
|
||||
try:
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
html_path = Path(self.config.OUTPUT_DIR) / f"login_page_changed_{ts}.html"
|
||||
html_path.write_text(self.login_page.content(), encoding='utf-8')
|
||||
logging.error(f" [DIAG] HTML salvat: {html_path}")
|
||||
except Exception as dump_err:
|
||||
logging.warning(f" [DIAG] Nu am putut salva HTML-ul: {dump_err}")
|
||||
raise Exception(
|
||||
"Pagina de login BT s-a schimbat: "
|
||||
f"URL final {self.login_page.url}, "
|
||||
f"titlu {self.login_page.title()!r}"
|
||||
)
|
||||
logging.info("[OK] Formular login incarcat")
|
||||
|
||||
# Verifica GDPR cookies si pe popup
|
||||
@@ -850,6 +864,9 @@ class BTGoScraper:
|
||||
time.sleep(1)
|
||||
logging.info(" Format CSV selectat")
|
||||
|
||||
# Marcheaza documentele existente in "Recente" (lista e cumulativa)
|
||||
self._mark_existing_documents()
|
||||
|
||||
# Click pe butonul Genereaza
|
||||
generate_btn = self.page.get_by_role("button", name="Generează")
|
||||
generate_btn.click()
|
||||
@@ -879,9 +896,9 @@ class BTGoScraper:
|
||||
logging.info(" Dropdown conturi deschis")
|
||||
|
||||
# Debug: listeaza toate heading-urile vizibile din dropdown
|
||||
visible_names = []
|
||||
try:
|
||||
headings = self.page.locator("fba-account-details h4, .account-name, h4").all()
|
||||
visible_names = []
|
||||
for h in headings[:10]: # Max 10
|
||||
try:
|
||||
if h.is_visible(timeout=500):
|
||||
@@ -893,6 +910,9 @@ class BTGoScraper:
|
||||
except:
|
||||
pass
|
||||
|
||||
if visible_names and nume_cont not in visible_names:
|
||||
logging.warning(f" [WARN] Contul '{nume_cont}' lipseste din lista dropdown: {visible_names}")
|
||||
|
||||
# Selecteaza contul dupa nume - strategii multiple
|
||||
account_selected = False
|
||||
|
||||
@@ -974,6 +994,9 @@ class BTGoScraper:
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Marcheaza documentele existente in "Recente"
|
||||
self._mark_existing_documents()
|
||||
|
||||
# Click pe butonul Genereaza (CSV deja selectat de la primul cont)
|
||||
generate_btn = self.page.get_by_role("button", name="Generează")
|
||||
generate_btn.click()
|
||||
@@ -987,9 +1010,67 @@ class BTGoScraper:
|
||||
logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}")
|
||||
return None
|
||||
|
||||
# Texte afisate de BT cand exportul nu are continut (nu exista fisier de descarcat)
|
||||
EMPTY_EXPORT_PATTERNS = (
|
||||
"nu exist", "nu s-au gasit", "nu s-au găsit",
|
||||
"fara tranzactii", "fără tranzacții",
|
||||
)
|
||||
|
||||
def _mark_existing_documents(self):
|
||||
"""Marcheaza documentele deja prezente in lista 'Recente'.
|
||||
|
||||
Lista este cumulativa, deci `wait_for_selector("fba-document-item")`
|
||||
se satisfacea instant pe un document vechi. Marcand cele existente,
|
||||
putem astepta exact documentul NOU generat.
|
||||
"""
|
||||
try:
|
||||
n = self.page.eval_on_selector_all(
|
||||
"fba-document-item",
|
||||
"els => { els.forEach(e => e.setAttribute('data-seen', '1')); return els.length; }",
|
||||
)
|
||||
logging.info(f" Documente deja in 'Recente': {n}")
|
||||
except Exception as e:
|
||||
logging.debug(f" Nu am putut marca documentele existente: {e}")
|
||||
|
||||
def _dump_download_debug(self, nume_cont, tag):
|
||||
"""La esec: screenshot + outerHTML al listei de documente si al dropdown-ului."""
|
||||
safe = nume_cont.replace(' ', '_').replace('/', '_').replace('\\', '_')
|
||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
self._debug_shot(self.page, f"download_{tag}_{safe}")
|
||||
|
||||
blocks = {
|
||||
"documente": (
|
||||
"() => { const it=[...document.querySelectorAll('fba-document-item')];"
|
||||
" if(!it.length) return null; let p=it[0].parentElement;"
|
||||
" while(p && !it.every(i=>p.contains(i))) p=p.parentElement;"
|
||||
" return p ? p.outerHTML : null; }"
|
||||
),
|
||||
"dropdown": (
|
||||
"() => { const o=document.querySelector('.cdk-overlay-container');"
|
||||
" return o ? o.outerHTML : null; }"
|
||||
),
|
||||
}
|
||||
for name, js in blocks.items():
|
||||
try:
|
||||
html = self.page.evaluate(js)
|
||||
if not html:
|
||||
logging.error(f" [DIAG] {name}: negasit in DOM")
|
||||
continue
|
||||
path = Path(self.config.OUTPUT_DIR) / f"debug_download_{tag}_{safe}_{name}_{stamp}.html"
|
||||
path.write_text(html, encoding='utf-8')
|
||||
logging.error(f" [DIAG] HTML {name} salvat: {path}")
|
||||
except Exception as e:
|
||||
logging.error(f" [DIAG] Nu am putut salva HTML {name}: {e}")
|
||||
|
||||
try:
|
||||
texts = self.page.locator("fba-document-item").all_inner_texts()
|
||||
logging.error(f" [DIAG] fba-document-item ({len(texts)}): {texts}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _wait_and_download(self, nume_cont, iban, timeout=20000):
|
||||
"""
|
||||
Asteapta generarea fisierului si il descarca.
|
||||
Asteapta generarea fisierului NOU si il descarca.
|
||||
|
||||
Args:
|
||||
nume_cont: Numele contului (pentru filename)
|
||||
@@ -1000,13 +1081,29 @@ class BTGoScraper:
|
||||
Dict cu informatii despre fisierul descarcat sau None
|
||||
"""
|
||||
try:
|
||||
# Asteapta sa apara fba-document-item (indica ca fisierul e gata)
|
||||
self.page.wait_for_selector("fba-document-item", timeout=timeout)
|
||||
logging.info(" Document generat - descarc...")
|
||||
# Asteapta documentul NOU (nu unul vechi ramas in "Recente")
|
||||
new_selector = "fba-document-item:not([data-seen])"
|
||||
try:
|
||||
self.page.wait_for_selector(new_selector, state="visible", timeout=timeout)
|
||||
except PlaywrightTimeout:
|
||||
logging.error(f" [EROARE] Niciun document nou generat pentru {nume_cont} ({timeout}ms)")
|
||||
self._dump_download_debug(nume_cont, "no_new_document")
|
||||
return None
|
||||
|
||||
# Click pe document item pentru a descarca
|
||||
new_item = self.page.locator(new_selector).first
|
||||
item_text = (new_item.inner_text() or "").strip()
|
||||
first_line = item_text.splitlines()[0] if item_text else "(fara text)"
|
||||
logging.info(f" Document nou: {first_line}")
|
||||
|
||||
# BT afiseaza un item de EROARE (fara buton de descarcare) cand
|
||||
# exportul nu are tranzactii in perioada selectata.
|
||||
if any(p in item_text.lower() for p in self.EMPTY_EXPORT_PATTERNS):
|
||||
logging.warning(f" [SKIP] Export fara tranzactii pentru {nume_cont}: {item_text[:160]}")
|
||||
return None
|
||||
|
||||
# Click pe documentul NOU pentru a descarca
|
||||
with self.page.expect_download(timeout=timeout) as download_info:
|
||||
download_btn = self.page.locator("fba-document-item svg, fba-document-item path").first
|
||||
download_btn = new_item.locator("svg, path").first
|
||||
download_btn.click()
|
||||
|
||||
download = download_info.value
|
||||
@@ -1028,6 +1125,7 @@ class BTGoScraper:
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f" [EROARE] Download fisier: {e}")
|
||||
self._dump_download_debug(nume_cont, "download_exceptie")
|
||||
return None
|
||||
|
||||
def read_accounts(self):
|
||||
|
||||
Reference in New Issue
Block a user