henriquebastos / itauscraper

Scraper para baixar seus extratos do Itaú com um comando.
GNU Lesser General Public License v3.0
195 stars 31 forks source link

Primeira versão gambiarra para pegar o OFX do Itau com Selenium. #8

Open henriquebastos opened 6 years ago

henriquebastos commented 6 years ago

O Itaú sacaneou a gente removendo o extrato do site mobile. Então eu implementei um primeiro rascunho de como seria obter o OFX.

Só testei no macOS com o ChromeDriver. Seria legal ter feedbacks de mais gente principalmente em outros sistemas operacionais.

O código tá "daquele jeito", mas funcionou de boa aqui. Próximo desafio é implementar o fluxo de obter os dados da fatura atual do cartão de crédito para só então pensar em como organizar essa zona.

Feedbacks são bem-vindos. :)

"""
Primeira versão gambiarra para pegar o OFX do Itau com Selenium.
Funciona na minha máquina. E na sua? :D
"""
from pathlib import Path

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class ItauHome(Page):
    URL = 'https://www.itau.com.br/'  

    def __init__(self, driver):
        self.driver = driver    

    def login(self, agencia, conta):
        input_ag = self.driver.find_element_by_id('campo_agencia')
        input_ag.send_keys(agencia)

        input_cc = self.driver.find_element_by_id('campo_conta')
        input_cc.send_keys(conta)

        btn = self.driver.find_element_by_css_selector("a.btnSubmit")
        btn.click()

    def identify(self, first_name):
        titular = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, first_name))
        )

        href = titular.get_attribute('href')
        cmd = href.partition(':')[-1]
        self.driver.execute_script(cmd)

    def authenticate(self, password):
        submit = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.PARTIAL_LINK_TEXT, 'acessar'))
        )

        WebDriverWait(self.driver, 10).until(
            EC.invisibility_of_element_located((By.CSS_SELECTOR, 'div.blockUI.blockOverlay'))
        )

        for digit in password:
            btn = self.driver.find_element_by_partial_link_text(digit)
            btn.click()

        submit = self.driver.find_element_by_partial_link_text('acessar')
        submit.click()

    def extrato(self, menu_text):           
        menu = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//a[contains(text(),'Saldo e extrato')]"))
        )
        href = menu.get_attribute('href')
        cmd = href.partition(':')[-1]
        self.driver.execute_script(cmd)

    def extrato(self):           
        menu = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//a[contains(text(),'Saldo e extrato')]"))
        )
        href = menu.get_attribute('href')
        cmd = href.partition(':')[-1]
        self.driver.execute_script(cmd)

    def extrato_formatos(self):
        self.driver.switch_to.frame('CORPO')

        menu = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//a[contains(text(),'Salvar em outros formatos')]"))
        )
        href = menu.get_attribute('href')
        cmd = href.partition(':')[-1]
        self.driver.execute_script(cmd)

        self.driver.switch_to.default_content()

    def salvar_ofx(self, dia, mes, ano):
        self.driver.switch_to.frame('CORPO')

        input_dia = self.driver.find_element_by_id('Dia')
        input_dia.send_keys(f'{dia:02}{mes:02}{ano}')

        input_ofx = self.driver.find_element_by_css_selector("input[value='OFX']")
        input_ofx.click()

        btn = self.driver.find_element_by_xpath("//a[img[@alt='Continuar']]")
        btn.click()

        self.driver.switch_to.default_content()

    def wait_downloads(self):
        cur = self.driver.current_window_handle

        self.driver.execute_script('window.open("about:blank", "_blank");')

        self.driver.switch_to_window(self.driver.window_handles[-1])

        self.driver.get("chrome://downloads/")

        def every_downloads_chrome(driver):
            return driver.execute_script("""
                var items = downloads.Manager.get().items_;
                if (items.every(e => e.state === "COMPLETE"))
                    return items.map(e => e.file_path);
                """)

        # waits for all the files to be completed and returns the paths
        paths = WebDriverWait(self.driver, 120, 1).until(every_downloads_chrome)

        self.driver.close()

        self.driver.switch_to_window(cur)

        return paths

if __name__ == '__main__':
    options = webdriver.ChromeOptions()
    options.add_argument('--disable-web-security')
    options.add_experimental_option(
        "prefs", {
            'download.default_directory' : str(Path().absolute()),
        }
    )

    browser = webdriver.Chrome(chrome_options=options)

    itau = ItauHome(browser)
    itau.get()
    itau.login('1234', '123456')
    itau.identify('MEUNOME')
    itau.authenticate('000000')
    itau.extrato()
    itau.extrato_formatos()
    itau.salvar_ofx(1, 1, 2018)
    print(itau.wait_downloads())
gmonnerat commented 6 years ago

Adicionei "from itauscraper.pages import Page" no topo e rodei

(tools3) gabriel@localhost:/tmp$ xvfb-run python test.py
['chromedriver', '--port=53297']
Traceback (most recent call last):
  File "test.py", line 131, in <module>
    itau.get()
AttributeError: 'ItauHome' object has no attribute 'get'

(tools3) gabriel@localhost:/tmp$ pip freeze | grep itau
itauscraper==1.0

EDIT:

Adicionei:

    def get(self):
        self.driver.get(self.URL)
huogerac commented 6 years ago

Funcionou no Ubuntu com:

1 - pip install nos pacotes

itauscraper==1.0
selenium==3.13.0

2 - download do chrome driver https://sites.google.com/a/chromium.org/chromedriver/getting-started copiei o executável para o mesmo diretório do codigo

3 - Precisei alterar o código: i) adicionei o método get no ItauHome

    def get(self):
        self.driver.get(self.URL)

ii) import do Page: from itauscraper.pages import Page iii) Informar o path do executável do chrome na inicialização do browser: browser = webdriver.Chrome('/home/roger/itau/chromedriver', chrome_options=options) iv) Comentado a linha de indentificação do nome # itau.identify('MEUNOME')

Valeu

henriquebastos commented 6 years ago

Legal, @gmonnerat! Esse rascunho é pra ser rodado isolado com o selenium.

@huogerac Achei curiosa a necessidade de especificar o path do chromedriver. Qual seu OS?

Não resisti e refiz o rascunho pra dar um leve tapa na organização:

from pathlib import Path
from contextlib import contextmanager

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class Chrome(webdriver.Chrome):
    def __init__(self, *args, **kwargs):        
        options = webdriver.ChromeOptions()
        options.add_argument('--disable-web-security')
        options.add_experimental_option(
            "prefs", {
                'download.default_directory' : str(Path().absolute()),
            }
        )

        return super().__init__(chrome_options=options)

    def wait_until(self, method, timeout=10, interval=1):
        return WebDriverWait(self, timeout, interval).until(method)

    def element(self, locator):
        return self.find_element(*locator)

    @contextmanager
    def frame(self, name):
        self.switch_to.frame(name)
        yield
        self.switch_to.default_content()

    @contextmanager
    def tab(self):
        current = self.current_window_handle
        # open new tab
        self.execute_script('window.open("about:blank", "_blank");')
        # switch to new tab
        self.switch_to_window(self.window_handles[-1])

        yield

        # close tab
        self.close()
        # switch back to previous tab
        self.switch_to_window(current)

    def wait_downloads(self):
        self.get("chrome://downloads/")

        cmd = """
            var items = downloads.Manager.get().items_;
            if (items.every(e => e.state === "COMPLETE"))
                return items.map(e => e.file_path);     
        """

        # waits for all the files to be completed and returns the paths
        return self.wait_until(lambda d: d.execute_script(cmd), timeout=120)

def ID(value):
    return By.ID, value

def css(value):
    return By.CSS_SELECTOR, value

def xpath(value):
    return By.XPATH, value

def link_contains(value):
    return By.PARTIAL_LINK_TEXT, value

present = EC.presence_of_element_located
invisible = EC.invisibility_of_element_located

def js_href(el):
    return el.get_attribute('href').partition(':')[-1]

class ItauHome:
    URL = 'https://www.itau.com.br/'  

    def __init__(self, driver):
        self.driver = driver    

    def login(self, agencia, conta, nome, password):
        # 1. Acessa o site.
        self.driver.get(self.URL)

        # 2. Preenche agência e conta.
        el = self.driver.element
        el(ID('campo_agencia')).send_keys(agencia)
        el(ID('campo_conta')).send_keys(conta)
        el(css('a.btnSubmit')).click()

        # 3. Identifica o titular.
        titular = self.driver.wait_until(present(link_contains(nome)))
        self.driver.execute_script(js_href(titular))

        # 4. Clica a senha no teclado virtual.
        submit = self.driver.wait_until(present(link_contains('acessar')))
        self.driver.wait_until(invisible(css('div.blockUI.blockOverlay')))

        for digit in password:
            self.driver.element(link_contains(digit)).click()

        submit.click()

        # 5. Aguarda carregar o dashboard.
        self.driver.wait_until(present(xpath("//a[contains(text(),'Saldo e extrato')]")))

    def go_to_extrato(self):
        menu = self.driver.wait_until(present(xpath("//a[contains(text(),'Saldo e extrato')]")))
        self.driver.execute_script(js_href(menu))

    def go_to_ofx(self):
        self.go_to_extrato()

        with self.driver.frame('CORPO'):
            menu = self.driver.wait_until(present(xpath("//a[contains(text(),'Salvar em outros formatos')]")))
            self.driver.execute_script(js_href(menu))

    def salvar_ofx(self, dia, mes, ano):
        with self.driver.frame('CORPO'):
            el = self.driver.element
            el(ID('Dia')).send_keys(f'{dia:02}{mes:02}{ano}')
            el(css("input[value='OFX']")).click()
            el(xpath("//a[img[@alt='Continuar']]")).click()

        with self.driver.tab():
            paths = self.driver.wait_downloads()

        return paths

if __name__ == '__main__':
    itau = ItauHome(Chrome())
    itau.login('1234', '123456', 'MEUNOME', '000000')
    itau.go_to_ofx()
    print(itau.salvar_ofx(1, 1, 2018))
gmonnerat commented 6 years ago

@henriquebastos, não sei se o caso do @huogerac é mesmo caso que o meu, mas no Debian eu fiz o download e copiei o binario para /usr/bin. Sem isso ele não encontra no meu $PATH e vai acontecer esse erro:

(tools3) gabriel@localhost:~$ xvfb-run python test.py 
['chromedriver', '--port=48779']
Traceback (most recent call last):
  File "/home/gabriel/.pyenv/versions/tools3/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 77, in start
    stdin=PIPE)
  File "/home/gabriel/.pyenv/versions/3.6.3/lib/python3.6/subprocess.py", line 709, in __init__
    restore_signals, start_new_session)
  File "/home/gabriel/.pyenv/versions/3.6.3/lib/python3.6/subprocess.py", line 1344, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'chromedriver': 'chromedriver'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 130, in <module>
    browser = webdriver.Chrome(chrome_options=options)
  File "/home/gabriel/.pyenv/versions/tools3/lib/python3.6/site-packages/selenium/webdriver/chrome/webdriver.py", line 68, in __init__
    self.service.start()
  File "/home/gabriel/.pyenv/versions/tools3/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 84, in start
    os.path.basename(self.path), self.start_error_message)
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

Talvez definindo o path do chromedriver no $PATH funcione também, tipo:

PATH=$PATH:/home/gabriel/itau/chromedriver python test.py
gmonnerat commented 6 years ago

@henriquebastos, valeu pelo refactoring no rascunho!

No meu caso, minha não é conta conjunta, então eu não preciso selecionar o titular, aparece apenas Ola Gabriel.

Alterei o código para suportar os dois e por padrão, selecionar o titular.

--- test2.py.bak        2018-07-26 14:21:27.436270557 +0000
+++ test2.py    2018-07-26 14:23:42.748867087 +0000
@@ -86,7 +86,7 @@
     def __init__(self, driver):
         self.driver = driver

-    def login(self, agencia, conta, nome, password):
+    def login(self, agencia, conta, nome, password, conta_conjunta=True):
         # 1. Acessa o site.
         self.driver.get(self.URL)

@@ -96,9 +96,16 @@
         el(ID('campo_conta')).send_keys(conta)
         el(css('a.btnSubmit')).click()

-        # 3. Identifica o titular.
-        titular = self.driver.wait_until(present(link_contains(nome)))
-        self.driver.execute_script(js_href(titular))
+        if conta_conjunta:
+          # 3. Identifica o titular
+          titular = self.driver.wait_until(present(link_contains(nome)))
+          self.driver.execute_script(js_href(toitular))
+        else:
+          # 3. O titular é selecionado automaticamente e
+          # a página é redirecionada para o teclado virtual
+          self.driver.wait_until(present(
+            xpath('//strong[text()="{}"]'.format(nome))
+          ))

         # 4. Clica a senha no teclado virtual.
         submit = self.driver.wait_until(present(link_contains('acessar')))
@@ -138,6 +145,6 @@

 if __name__ == '__main__':
     itau = ItauHome(Chrome())
-    itau.login('1234', '123456', 'MEUNOME', '000000')
+    itau.login('1234', '123456', 'MEUNOME', '000000', False)
     itau.go_to_ofx()
     print(itau.salvar_ofx(1, 1, 2018))

Estou rodando em um servidor Debian sem X, então estou usando Xvfb e esta rodando perfeitamente.

Para rodar instalei o Xvfb e Chrome:

apt-get install xvfb google-chrome

Depois fiz o download do ChromeDriver aqui https://sites.google.com/a/chromium.org/chromedriver/downloads e movi para /usr/bin para não te estresse.

Próximos passos da brincadeira:

(tools3) gabriel@localhost:~$ ps aux | grep chrome
gabriel   2523  0.0  0.0  13092   960 pts/9    S+   14:35   0:00 grep chrome
gabriel  17964  0.0  0.5 109156 11504 pts/21   Sl   13:46   0:00 chromedriver --port=34911
gabriel  17970  0.0  0.0      0     0 pts/21   Z    13:46   0:02 [chrome] <defunct>
gabriel  18716  0.0  0.5 109156 11308 pts/21   Sl   13:47   0:00 chromedriver --port=41969
gabriel  18722  0.0  0.0      0     0 pts/21   Z    13:47   0:01 [chrome] <defunct>
gabriel  19654  0.0  0.5 109156 11028 pts/21   Sl   13:49   0:00 chromedriver --port=47171
gabriel  19660  0.0  0.0      0     0 pts/21   Z    13:49   0:01 [chrome] <defunct>
gabriel  20402  0.0  0.5 109156 11140 pts/21   Sl   13:50   0:00 chromedriver --port=38771
gabriel  20408  0.0  0.0      0     0 pts/21   Z    13:51   0:01 [chrome] <defunct>
gabriel  22411  0.0  0.5 110180 12132 pts/21   Sl   13:57   0:00 chromedriver --port=57737
gabriel  22417  0.1  0.0      0     0 pts/21   Z    13:57   0:02 [chrome] <defunct>
gabriel  25967  0.0  0.5 110180 11756 pts/9    Sl   14:07   0:00 chromedriver --port=59725
gabriel  25973  0.2  0.0      0     0 pts/9    Z    14:07   0:04 [chrome] <defunct>
gabriel  26733  0.0  0.5 109156 11540 pts/9    Sl   14:08   0:00 chromedriver --port=43321
gabriel  26739  0.1  0.0      0     0 pts/9    Z    14:08   0:02 [chrome] <defunct>
gabriel  28621  0.0  0.5 109156 11080 pts/9    Sl   14:11   0:00 chromedriver --port=46083
gabriel  28627  0.1  0.0      0     0 pts/9    Z    14:11   0:02 [chrome] <defunct>
gabriel  29381  0.0  0.5 110180 11748 pts/9    Sl   14:12   0:00 chromedriver --port=34833
gabriel  29387  0.3  0.0      0     0 pts/9    Z    14:12   0:04 [chrome] <defunct>
gabriel  31321  0.0  0.5 110212 11824 pts/9    Sl   14:19   0:00 chromedriver --port=39535
gabriel  31327  0.5  0.0      0     0 pts/9    Z    14:19   0:05 [chrome] <defunct>
gmonnerat commented 6 years ago

if name == 'main': itau = ItauHome(Chrome()) try: itau.login('1234', '123456', 'MEUNOME', '000000', False) itau.go_to_ofx() print(itau.salvar_ofx(1, 1, 2018)) finally: itau.cleanup()

luxu commented 6 years ago

@henriquebastos dá pra reproduzir mesmo script mas pro BB?

ccrvlh commented 6 years ago

To tendo problema com o webdriver e sem sucesso, não to sabendo muito bem como resolver... Fiz o download e salvei o chromedriver tanto no meu usr/bin quanto no folder do próprio .py.

Traceback (most recent call last):
  File "itau.py", line 144, in <module>
    itau = ItauHome(Chrome())
  File "itau.py", line 24, in __init__
    return super().__init__(chrome_options=options)
  File "/usr/local/lib/python3.7/site-packages/selenium/webdriver/chrome/webdriver.py", line 68, in __init__
    self.service.start()
  File "/usr/local/lib/python3.7/site-packages/selenium/webdriver/common/service.py", line 83, in start
    os.path.basename(self.path), self.start_error_message)
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
ccrvlh commented 6 years ago

Consegui pegar o OFX finalmente! Alguma idéia de como fazer o parse do arquivo para um CSV, Dataframe, ou algo do tipo? Achei um OFXParser (https://github.com/jseutter/ofxparse), mas que não tá funcionando muito bem... alguém teve algum sucesso?

EDIT

Conseguir trabalhar com o OFX Parser (https://github.com/jseutter/ofxparse). Para quem se interessar, usei o módulo para converter em CSV e trabalhar com o arquivo em Excel.

import codecs
from ofxparse import OfxParser
import pandas as pd

with codecs.open('extrato.ofx', encoding="ISO-8859-1") as fileobj:
    ofx = OfxParser.parse(fileobj)

account     = ofx.account
statement   = account.statement
all_transactions = []

for transaction in statement.transactions:
  single_transaction = [
                        transaction.type,
                        transaction.date,
                        transaction.amount,
                        transaction.memo,
                        transaction.id
                        ]
  all_transactions.append(single_transaction)

df          = pd.DataFrame(all_transactions)
df.columns  = ['type', 'date', 'amount', 'memo', 'id']
df.to_csv('extrato.csv', sep=',', index=False)`
ccrvlh commented 6 years ago

@luxu O BB já tem API aberta com uma documentação razoável: https://developers.bb.com.br/docs/#!/Checking_Account/get_statements_checking_account

luxu commented 6 years ago

Dei uma olhada nessa doc mas achei dificil implementar, axo q o uso é restrito ao navegador.

junqueira commented 5 years ago

https://github.com/junqueira/bankscraper
docker-compose up -> itau ta OK, santander tem um qrcode que da ruim :(

eduardoluizm commented 5 years ago

Olá,

Alguem ja implemento ou viu em algum lugar um scraper pra conta empresarial?

Lá se autentica com codigo de Operador e senha.

Obrigado

edusouza commented 5 years ago

O Conta Azul faz.

https://ajuda.contaazul.com/hc/pt-br/articles/115007942347-Integra%C3%A7%C3%A3o-autom%C3%A1tica-Ita%C3%BA

Mas, infelizmente, não tem código disponível.

Em ter, 26 de fev de 2019 às 19:53, eduardoluizm notifications@github.com escreveu:

Olá,

Alguem ja implemento ou viu em algum lugar um scraper pra conta empresarial?

Lá se autentica com codigo de Operador e senha.

Obrigado

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/henriquebastos/itauscraper/issues/8#issuecomment-467648645, or mute the thread https://github.com/notifications/unsubscribe-auth/ACHdngvw6nJY-lcx8enFfffCq_j1nTOZks5vRbr0gaJpZM4VfUtd .

--

Eduardo Oliveira de Souza gtalk:souza.eduardo@gmail.com skype: esouza_cg twitter: @eduardosouza

eduardoluizm commented 5 years ago

Pois é, eu vi que eles fazem e imagino que seja algo nesse sentido.

Pra pessoa fisica, ainda existe o site mobile e da pra fazer um scrapper, porem pra PJ eles arrancaram e exige o APP.. o app empresarial aceita codigo de operador.. talvez precise uma engenharia reversa pra descobrir como essas chamadas de API são executadas e simular um "app mobile".

lucasrcezimbra commented 5 years ago

Como ficou esse script com Selenium? Tá funcionando?

Fiz uma engenharia reversa no site para desktop e consegui fazer um script que acessa o extrato utilizando requests. Vou organizar ele, remover meus dados e depois posto no GitHub.

lucasrcezimbra commented 5 years ago

Fiz script para fazer as requisições pro Itaú sem usar o Selenium, se alguém quiser ver/testar: https://github.com/Lrcezimbra/itau/

Carlosajunior commented 2 years ago

Eu tava atrás de uma solução para digitar a senha do itau com o selenium, me ajudou muito, obrigado