class CrawlerCei(): def __init__(self, headless=False, directory=None, debug=False): self.BASE_URL = 'https://ceiapp.b3.com.br/CEI_Responsivo/' self.driver = ChromeDriver() self.directory = directory self.debug = debug self.__colunas_df_cei = [ 'Data do Negócio', 'Compra/Venda', 'Mercado', 'Prazo/Vencimento', 'Código Negociação', 'Especificação do Ativo', 'Quantidade', 'Preço (R$)', 'Valor Total(R$)', 'Fator de Cotação' ] self.id_tabela_negociacao_ativos = 'ctl00_ContentPlaceHolder1_rptAgenteBolsa_ctl00_rptContaBolsa_ctl00_pnAtivosNegociados' self.id_mensagem_de_aviso = 'CEIMessageDIV' self.id_selecao_corretoras = 'ctl00_ContentPlaceHolder1_ddlAgentes' self.id_btn_consultar = 'ctl00_ContentPlaceHolder1_btnConsultar' def busca_trades(self): try: self.driver.get(self.BASE_URL) self.__login() df = self.__abre_consulta_trades() return self.__converte_dataframe_para_formato_padrao(df) except Exception as ex: raise ex finally: self.driver.quit() def __login(self): if self.debug: self.driver.save_screenshot(self.directory + r'01.png') txt_login = self.driver.find_element_by_id( 'ctl00_ContentPlaceHolder1_txtLogin') txt_login.clear() txt_login.send_keys(os.environ['CPF']) time.sleep(3.0) txt_senha = self.driver.find_element_by_id( 'ctl00_ContentPlaceHolder1_txtSenha') txt_senha.clear() txt_senha.send_keys(os.environ['SENHA_CEI']) time.sleep(3.0) if self.debug: self.driver.save_screenshot(self.directory + r'02.png') btn_logar = self.driver.find_element_by_id( 'ctl00_ContentPlaceHolder1_btnLogar') btn_logar.click() try: WebDriverWait(self.driver, 60).until( EC.visibility_of_element_located((By.ID, 'objGrafPosiInv'))) except Exception as ex: raise Exception( 'Nao foi possivel logar no CEI. Possivelmente usuario/senha errada ou indisponibilidade do site' ) if self.debug: self.driver.save_screenshot(self.directory + r'03.png') def __abre_consulta_trades(self): def consultar_click(driver): btn_consultar = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.ID, self.id_btn_consultar))) btn_consultar.click() def exists_and_not_disabled(id): def until_fn(driver): try: driver.find_element_by_id(id) except NoSuchElementException: return False return driver.find_element_by_id(id).get_attribute( "disabled") is None return until_fn dfs_to_concat = [] self.driver.get(self.BASE_URL + 'negociacao-de-ativos.aspx') if self.debug: self.driver.save_screenshot(self.directory + r'04.png') from selenium.webdriver.support.select import Select ddlAgentes = Select( self.driver.find_element_by_id(self.id_selecao_corretoras)) def __busca_trades_de_uma_corretora(i): if self.debug: self.driver.save_screenshot(self.directory + r'04_01.png') ddlAgentes = Select( self.driver.find_element_by_id(self.id_selecao_corretoras)) ddlAgentes.select_by_index(i) time.sleep(10) if self.debug: self.driver.save_screenshot(self.directory + r'04_02.png') WebDriverWait(self.driver, 15).until( exists_and_not_disabled(self.id_btn_consultar)) consultar_click(self.driver) if self.debug: self.driver.save_screenshot(self.directory + r'05.png') try: WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located( (By.ID, self.id_mensagem_de_aviso))) except: pass if self.debug: self.driver.save_screenshot(self.directory + r'06.png') # checa se existem trades para essa corretora aviso = self.driver.find_element_by_id(self.id_mensagem_de_aviso) if aviso.text == 'Não foram encontrados resultados para esta pesquisa.\n×': consultar_click(self.driver) WebDriverWait(self.driver, 60).until( exists_and_not_disabled(self.id_selecao_corretoras)) else: if not exists_and_not_disabled( self.id_tabela_negociacao_ativos)(self.driver): consultar_click(self.driver) WebDriverWait(self.driver, 30).until( EC.visibility_of_element_located( (By.ID, self.id_tabela_negociacao_ativos))) dfs_to_concat.append(self.__converte_trades_para_dataframe()) consultar_click(self.driver) WebDriverWait(self.driver, 60).until( exists_and_not_disabled(self.id_selecao_corretoras)) if len(ddlAgentes.options) == 1: __busca_trades_de_uma_corretora(0) else: for i in range(1, len(ddlAgentes.options)): __busca_trades_de_uma_corretora(i) if len(dfs_to_concat): return pd.concat(dfs_to_concat) else: return pd.DataFrame(columns=self.__colunas_df_cei) def __converte_trades_para_dataframe(self): soup = BeautifulSoup(self.driver.page_source, 'html.parser') top_div = soup.find('div', {'id': self.id_tabela_negociacao_ativos}) table = top_div.find(lambda tag: tag.name == 'table') df = pd.read_html(str(table), decimal=',', thousands='.')[0] df = df.dropna(subset=['Mercado']) return df def __converte_dataframe_para_formato_padrao(self, df): df = df.rename( columns={ 'Código Negociação': 'ticker', 'Compra/Venda': 'operacao', 'Quantidade': 'qtd', 'Data do Negócio': 'data', 'Preço (R$)': 'preco', 'Valor Total(R$)': 'valor' }) from src.stuff import calculate_add def formata_compra_venda(operacao): if operacao == 'V': return 'Venda' else: return 'Compra' def remove_fracionado_ticker(ticker): return ticker[:-1] if ticker.endswith('F') else ticker df['data'] = pd.to_datetime(df['data'], dayfirst=True) df['data'] = df['data'].dt.date df['ticker'] = df.apply( lambda row: remove_fracionado_ticker(row.ticker), axis=1) df['operacao'] = df.apply( lambda row: formata_compra_venda(row.operacao), axis=1) df['qtd_ajustada'] = df.apply(lambda row: calculate_add(row), axis=1) df['taxas'] = 0.0 df['aquisicao_via'] = 'HomeBroker' df.drop(columns=[ 'Mercado', 'Prazo/Vencimento', 'Especificação do Ativo', 'Fator de Cotação' ], inplace=True) return df
class CrawlerCei(): def __init__(self, headless=False, directory='./temp/', debug=False): self.BASE_URL = 'https://ceiapp.b3.com.br/CEI_Responsivo/' self.driver = ChromeDriver() self.directory = directory self.debug = debug self.__colunas_df_cei = ['Data do Negócio', 'Compra/Venda', 'Mercado', 'Prazo/Vencimento', 'Código Negociação', 'Especificação do Ativo', 'Quantidade', 'Preço (R$)', 'Valor Total(R$)', 'Fator de Cotação'] self.__colunas_df_carteira = ['Empresa', 'Tipo', 'ticker', 'isin', 'Preço (R$)', 'Quantidade', 'Fator de Cotação' 'Valor Total(R$)'] self.id_tabela_negociacao_ativos = 'ctl00_ContentPlaceHolder1_rptAgenteBolsa_ctl00_rptContaBolsa_ctl00_pnAtivosNegociados' self.id_mensagem_de_aviso = 'CEIMessageDIV' self.id_selecao_corretoras = 'ctl00_ContentPlaceHolder1_ddlAgentes' self.id_btn_consultar = 'ctl00_ContentPlaceHolder1_btnConsultar' self.id_selecao_data = 'ctl00_ContentPlaceHolder1_txtData' self.agentes_ignorar = {} if 'IGNORA_AGENTES_OPERACOES' in os.environ: self.agentes_ignorar = set(os.environ['IGNORA_AGENTES_OPERACOES'].split("-")) def busca_trades(self, dropExtras=True): try: print("Buscando operações CEI para " + os.environ['CPF']) if (os.getenv("SENHA_CEI","") == ""): raise Exception("Senha CEI não está definida") self.driver.get(self.BASE_URL) self.__login() df = self.__abre_consulta_trades() try: return self.__converte_dataframe_para_formato_padrao(df, dropExtras) except Exception as ex: if self.debug: print("*** Erro buscando operações") print(df) raise ex except Exception as ex: self.__save_screenshot('erro_busca_trades-' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M') + '.png', force_save=True) raise ex finally: self.driver.quit() def busca_carteira(self, data): try: print("Buscando carteira CEI para " + os.environ['CPF'] + " " + str(data)) if (os.getenv("SENHA_CEI","") == ""): raise Exception("Senha CEI não está definida") self.driver.get(self.BASE_URL) self.__login() df = self.__abre_consulta_carteira(data) return df except Exception as ex: self.__save_screenshot('erro_busca_carteira-' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M') + '.png', force_save=True) raise ex finally: self.driver.quit() def converte_html_carteira(self, html): try: df = self.__converte_custodia_html_para_dataframe(html, 0, 'Todos') return df except Exception as ex: raise ex def __save_screenshot(self, name, force_save=False): if self.debug or force_save: self.driver.save_screenshot(self.directory + name) def __login(self): self.__save_screenshot(r'01-inicio.png') txt_login = self.driver.find_element_by_id('ctl00_ContentPlaceHolder1_txtLogin') txt_login.clear() txt_login.send_keys(os.environ['CPF']) time.sleep(3.0) txt_senha = self.driver.find_element_by_id('ctl00_ContentPlaceHolder1_txtSenha') txt_senha.clear() txt_senha.send_keys(os.environ['SENHA_CEI']) time.sleep(3.0) self.__save_screenshot(r'02-dados-login.png') btn_logar = self.driver.find_element_by_id('ctl00_ContentPlaceHolder1_btnLogar') btn_logar.click() try: WebDriverWait(self.driver, 60).until(EC.visibility_of_element_located((By.ID, 'objGrafPosiInv'))) except Exception as ex: self.__save_screenshot(r'03-erro-login.png') raise Exception('Nao foi possivel logar no CEI. Possivelmente usuario/senha errada ou indisponibilidade do site') from ex self.__save_screenshot(r'03-pos-login.png') def consultar_click(self, driver): btn_consultar = WebDriverWait(driver, 20).until( EC.element_to_be_clickable((By.ID, self.id_btn_consultar))) btn_consultar.click(); def exists_and_not_disabled(self, id): def until_fn(driver): try: driver.find_element_by_id(id) except NoSuchElementException: return False return driver.find_element_by_id(id).get_attribute("disabled") is None return until_fn def __abre_consulta_trades(self): dfs_to_concat = [] self.driver.get(self.BASE_URL + 'negociacao-de-ativos.aspx') self.__save_screenshot(r'04-negociacao-ativos.png') from selenium.webdriver.support.select import Select ddlAgentes = Select(self.driver.find_element_by_id(self.id_selecao_corretoras)) def __busca_trades_de_uma_corretora(i, nomeAgente): print("Verificando Operações : " + str(i) + " " + nomeAgente) # O nome do agente é um número/ código e depois vem o nome # Utilizar isso para ignorar alguns agentes, não existe motivo para tentar buscar operações se o usuário não faz operações com o agente... # 3 - XP INVESTIMENTOS CCTVM S/A numeroAgente = nomeAgente.split(" ")[0] if numeroAgente in self.agentes_ignorar: print("\tIgnorando agente no download de operações") return self.__save_screenshot(r'05_01-' + str(i) + '.png') ddlAgentes = Select(self.driver.find_element_by_id(self.id_selecao_corretoras)) ddlAgentes.select_by_index(i) time.sleep(10) self.__save_screenshot(r'05_02-' + str(i) + '.png') WebDriverWait(self.driver, 15).until(self.exists_and_not_disabled(self.id_btn_consultar)) self.consultar_click(self.driver) self.__save_screenshot(r'05_03-' + str(i) + '.png') try: WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( (By.ID, self.id_mensagem_de_aviso))) except: pass self.__save_screenshot(r'05_04-' + str(i) + '.png') # checa se existem trades para essa corretora aviso = self.driver.find_element_by_id(self.id_mensagem_de_aviso) if aviso.text == 'Não foram encontrados resultados para esta pesquisa.\n×': self.consultar_click(self.driver) try: WebDriverWait(self.driver, 60).until(self.exists_and_not_disabled(self.id_selecao_corretoras)) self.__save_screenshot(r'05_05-sem-resultados-' + str(i) + '.png') except StaleElementReferenceException as ex: # Pode ignorar já que não teve dados pass print("\tNão existem operações") else: if not self.exists_and_not_disabled(self.id_tabela_negociacao_ativos)(self.driver): self.consultar_click(self.driver) WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( (By.ID, self.id_tabela_negociacao_ativos))) self.__save_screenshot(r'05_05-' + str(i) + '.png') operacoes = self.__converte_trades_para_dataframe(i, nomeAgente) print("\tOperações encontradas : " + str(len(operacoes))) dfs_to_concat.append(operacoes) self.consultar_click(self.driver) WebDriverWait(self.driver, 60).until(self.exists_and_not_disabled(self.id_selecao_corretoras)) # Durante as buscas os elementos se alteram e o selenium nao consegue obter o texto correto, então salva antes opcoesAgentes = ddlAgentes.options textosAgentes = [] for i in range(0,len(opcoesAgentes)): textosAgentes.append(opcoesAgentes[i].text) # Quando tem mais de um a primeira linha é um texto "Selecione" if len(opcoesAgentes) == 1: __busca_trades_de_uma_corretora(0, textosAgentes[0]) else: for i in range(1, len(opcoesAgentes)): __busca_trades_de_uma_corretora(i, textosAgentes[i]) if len(dfs_to_concat): return pd.concat(dfs_to_concat) else: return pd.DataFrame(columns=self.__colunas_df_cei) def __converte_trades_para_dataframe(self, i, nomeAgente): soup = BeautifulSoup(self.driver.page_source, 'html.parser') if self.debug: with open(self.directory + "trades-" + str(i) + ".html", "w") as file: file.write(soup.prettify()) top_div = soup.find('div', {'id': self.id_tabela_negociacao_ativos}) table = top_div.find(lambda tag: tag.name == 'table') if self.debug: with open(self.directory + "trades-" + str(i) + "-tabela.html", "w") as file: file.write(table.prettify()) df = pd.read_html(str(table), decimal=',', thousands='.')[0] df['agente'] = df.apply(lambda row: nomeAgente, axis=1) if self.debug: with open(self.directory + "trades-" + str(i) + ".txt", "w") as file: file.write(tabulate(df, headers=df.columns, showindex=True, tablefmt='psql')) # Remove totais que vem tambem na tabela df = df[df['Data do Negócio'].str.match('\\d+/\\d+/\\d+')] if self.debug: with open(self.directory + "trades-" + str(i) + "-filtro.txt", "w") as file: file.write(tabulate(df, headers=df.columns, showindex=True, tablefmt='psql')) return df def __converte_dataframe_para_formato_padrao(self, df, dropExtras): df = df.rename(columns={'Código Negociação': 'ticker', 'Compra/Venda': 'operacao', 'Quantidade': 'qtd', 'Data do Negócio': 'data', 'Preço (R$)': 'preco', 'Valor Total(R$)': 'valor', }) from src.stuff import calculate_add def formata_compra_venda(operacao): if operacao == 'V': return 'Venda' else: return 'Compra' def remove_fracionado_ticker(ticker): return ticker[:-1] if ticker.endswith('F') else ticker df['data'] = pd.to_datetime(df['data'], dayfirst=True) df['data'] = df['data'].dt.date df['ticker'] = df.apply(lambda row: remove_fracionado_ticker(row.ticker), axis=1) df['operacao'] = df.apply(lambda row: formata_compra_venda(row.operacao), axis=1) df['qtd_ajustada'] = df.apply(lambda row: calculate_add(row), axis=1) df['taxas'] = 0.0 df['aquisicao_via'] = 'HomeBroker' if dropExtras: df.drop(columns=['Mercado', 'Prazo/Vencimento', 'Especificação do Ativo', 'Fator de Cotação'], inplace=True) return df def __abre_consulta_carteira(self, data): dfs_to_concat = [] self.driver.get(self.BASE_URL + 'ConsultarCarteiraAtivos.aspx') self.__save_screenshot(r'04-carteira-ativos.png') from selenium.webdriver.support.select import Select ddlAgentes = Select(self.driver.find_element_by_id(self.id_selecao_corretoras)) def __busca_ativos_de_uma_corretora(i, nomeAgente): print("Verificando Carteira : " + str(i) + " " + nomeAgente) self.__save_screenshot(r'05_01-' + str(i) + '.png') ddlAgentes = Select(self.driver.find_element_by_id(self.id_selecao_corretoras)) ddlAgentes.select_by_index(i) time.sleep(10) self.__save_screenshot(r'05_02-' + str(i) + '.png') inputData = self.driver.find_element_by_id(self.id_selecao_data) inputData.clear() inputData.send_keys(data.strftime('%d/%m/%Y')) time.sleep(10) self.__save_screenshot(r'05_03-' + str(i) + '.png') WebDriverWait(self.driver, 15).until(self.exists_and_not_disabled(self.id_btn_consultar)) self.consultar_click(self.driver) self.__save_screenshot(r'05_04-' + str(i) + '.png') try: WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( (By.ID, self.id_mensagem_de_aviso))) except: pass self.__save_screenshot(r'05_05-' + str(i) + '.png') # checa se existem trades para essa corretora aviso = self.driver.find_element_by_id(self.id_mensagem_de_aviso) if aviso.text == 'Não foram encontrados resultados para esta pesquisa.\n×': self.consultar_click(self.driver) try: WebDriverWait(self.driver, 60).until(self.exists_and_not_disabled(self.id_selecao_corretoras)) self.__save_screenshot(r'05_06-sem-resultados-' + str(i) + '.png') except StaleElementReferenceException as ex: # Pode ignorar já que não teve dados pass else: self.__save_screenshot(r'05_06-' + str(i) + '.png') dfs_to_concat.append(self.__converte_custodia_para_dataframe(i, nomeAgente)) self.consultar_click(self.driver) WebDriverWait(self.driver, 60).until(self.exists_and_not_disabled(self.id_selecao_corretoras)) # Durante as buscas os elementos se alteram e o selenium nao consegue obter o texto correto, então salva antes opcoesAgentes = ddlAgentes.options textosAgentes = [] for i in range(0,len(opcoesAgentes)): textosAgentes.append(opcoesAgentes[i].text) # A primeira opção é Todos e depois tem as corretoras, parece funcionar bem com todas de uma vez __busca_ativos_de_uma_corretora(0, textosAgentes[0]) # if len(opcoesAgentes) == 1: # __busca_ativos_de_uma_corretora(0, textosAgentes[0]) # else: # for i in range(1, len(opcoesAgentes)): # __busca_ativos_de_uma_corretora(i, textosAgentes[i]) if len(dfs_to_concat): return pd.concat(dfs_to_concat) else: return pd.DataFrame(columns=self.__colunas_df_carteira) def __converte_custodia_para_dataframe(self, i, nomeAgente): return self.__converte_custodia_html_para_dataframe(self.driver.page_source, i, nomeAgente) # Separação desse metodo facilita nos testes utilizando html local def __converte_custodia_html_para_dataframe(self, html_source, i, nomeAgente): dfs_to_concat = [] soup = BeautifulSoup(html_source, 'html.parser') if self.debug: with open(self.directory + "custodia-" + str(i) + ".html", "w") as file: file.write(soup.prettify()) nomeEl = soup.findAll('span',id=re.compile('ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl\\d\\d_lblAgenteContas')) for el in nomeEl: agente = el.text.strip() conta = '' # ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl00_rptContaMercado_ctl00_lblContaPosicao nextId = el['id'].replace('lblAgenteContas','rptContaMercado_ctl\\d\\d_lblContaPosicao') next = el.find_parent('div').find_next_sibling('div').find(id=re.compile(nextId)) if next: conta = next.text.strip() # ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl00_rptContaMercado_ctl00_rprCarteira_ctl00_trBodyCarteira nextId = el['id'].replace('lblAgenteContas','rptContaMercado_ctl\\d\\d_rprCarteira_ctl\\d\\d_trBodyCarteira') nextTables = el.find_parent('div').find_next_sibling('div').findAll(id=re.compile(nextId)) for next in nextTables: # id="ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl02_rptContaMercado_ctl00_rprCarteira_ctl00_lblCarteira" nextId = next['id'].replace('trBodyCarteira','lblCarteira') lbl = next.find(id=re.compile(nextId)) carteira = '' if lbl: carteira = lbl.text.strip() table = next.find('table') df = pd.read_html(str(table), decimal=',', thousands='.')[0] df['agente'] = df.apply(lambda row: agente, axis=1) df['conta'] = df.apply(lambda row: conta, axis=1) df['carteira'] = df.apply(lambda row: carteira, axis=1) if self.debug: with open(self.directory + "custodia-" + str(i) + "-" + str(len(dfs_to_concat)) + ".html", "w") as file: file.write(table.prettify()) with open(self.directory + "custodia-" + str(i) + "-" + str(len(dfs_to_concat)) + "-filtro.txt", "w") as file: file.write(tabulate(df, headers=df.columns, showindex=True, tablefmt='psql')) dfs_to_concat.append(df) df = pd.concat(dfs_to_concat, ignore_index=True) # As linhas de total vem com Nan df = df.dropna() df = df.rename(columns={'Empresa': 'empresa', 'Tipo': 'tipo', 'Cód. de Negociação': 'ticker', 'Cod.ISIN': 'isin', 'Preço (R$)*': 'preco', 'Qtde.': 'qtd', 'Fator Cotação': 'fator', 'Valor (R$)': 'valor', }) if self.debug: with open(self.directory + "custodia-" + str(i) + "-" + str(len(dfs_to_concat)) + "-combinado.txt", "w") as file: file.write(tabulate(df, headers=df.columns, showindex=True, tablefmt='psql')) return df