Começando
FTP com Python para trabalhar com servidores FTP, podemos usar ftplib , que vem com a biblioteca padrão do Python, então provavelmente você já a terá instalado. Caso não o faça, no entanto, você pode baixá-lo usando pip:
pip install ftplib
Para entrar em um site FTP, primeiro precisamos estabelecer uma conexão. Você pode fazer isso usando o método FTP . Basta substituir ftp.nasdaqtrader.com por qualquer site FTP em que você deseja fazer login.
'''Load the ftplib package '''
import ftplib
'''Connect to FTP site'''
ftp = ftplib.FTP('ftp.nasdaqtrader.com')
Assim que tivermos a conexão, podemos fazer login no site FTP usando o método de login apropriadamente nomeado :
'''Login'''
ftp.login()
Em nosso exemplo, não precisamos realmente fornecer um nome de usuário e senha, pois este servidor FTP está aberto a qualquer pessoa, mas não será o caso para muitos servidores FTP. Você pode enviar um nome de usuário e senha por meio do método de login como abaixo:
ftp.login('USERNAME' , 'PASSWORD')
Você só precisa substituir ‘USERNAME’ e ‘PASSWORD’ com seu nome de usuário e senha, respectivamente. Se o seu login for bem-sucedido, você deverá receber uma mensagem como esta:
‘230 User logged in.’
Depois de fazer o login, há uma coleção de comandos do pacote ftplib à sua disposição.
Navegando
Uma coisa que você pode querer fazer quando estiver conectado a um servidor FTP é descobrir seu diretório de trabalho atual. Você pode fazer isso usando o método pwd , semelhante ao Linux ou PowerShell.
ftp.pwd()
Listar o conteúdo de um diretório pode ser feito usando o método nlst . Para obter o conteúdo do diretório atual, nenhum parâmetro precisa ser passado. Se você deseja obter o conteúdo de outro diretório, basta passar seu nome como no exemplo abaixo, onde ‘SymbolDirectory’ é o nome de uma pasta no diretório de trabalho atual.
ftp.nlst()
ftp.nlst('SymbolDirectory')
Você também pode usar ftp.dir para obter o que está dentro de uma pasta. A diferença é que ftp.dir imprimirá o conteúdo, junto com uma etiqueta indicando se cada item é um subdiretório ou não, em vez de enviar os nomes dos itens na pasta para uma lista, como em ftp.nlst .
Para alterar o diretório de trabalho atual, usamos o comando cwd . Se desejar, você pode confirmar essa alteração chamando o método pwd novamente.
ftp.cwd('SymbolDirectory')
Baixando um único arquivo ftp com python
Agora, e se quisermos baixar programaticamente um arquivo de nosso site FTP? Isso é feito com bastante facilidade. Precisaremos carregar o pacote io , que também vem com a biblioteca padrão do Python.
import io
Em seguida, usamos o pacote io para criar um objeto BytesIO. Isso atua como um contêiner no qual gravaremos os dados de um arquivo no servidor FTP. Chamamos nosso objeto de r. Em seguida, usamos re o método retrbinary do objeto ftp para baixar o arquivo nasdaqlisted.txt, que contém uma lista dos tickers atualmente no NASDAQ.
r = io.BytesIO()
ftp.retrbinary('RETR nasdaqlisted.txt', r.write)
O objeto r nos permite acessar os dados baixados de nosso arquivo selecionado. Isso pode ser feito usando o método getvalue do objeto. O resultado baixado é um objeto de bytes, portanto, convertemos o resultado em uma string usando o método de decodificação . Portanto, nosso resultado baixado agora é armazenado na variável info .
info = r.getvalue().decode()
Nosso arquivo específico é delimitado por pipe (‘|’), então o código de yahoo_fin divide os dados por ‘|’ e analisa a lista de tickers listados atualmente no NASDAQ.
splits = info.split('|')
tickers = [x for x in splits if 'N\r\n' in x]
tickers = [x.strip('N\r\n') for x in tickers]
Baixando cada arquivo em um diretório FTP
Tudo bem, então temos um processo para obter o conteúdo de um único arquivo de um servidor FTP. Então, como obtemos o conteúdo de cada arquivo de uma determinada pasta? Precisamos apenas generalizar o código que temos acima, assim:
file_mapper = {}
for file in ftp.nlst():
r = io.BytesIO()
ftp.retrbinary('RETR ' + file , r.write)
file_mapper[file] = r
r.close()
Criando um dict vazio, file_mapper , preenchemos este dict com o conteúdo de cada arquivo no diretório em que estamos atualmente (‘SymbolDirectory’ em nosso exemplo). As chaves do dicionário são os nomes dos arquivos na pasta, enquanto os valores são os objetos BytesIO que podemos usar para obter os dados reais dentro de cada arquivo.
Assim, se quisermos obter o texto real de um desses arquivos – digamos, o arquivo options.txt na pasta SymbolDirectory , basta digitar:
file_mapper['options.txt'].getvalue().decode()
Se quiser gravar esse arquivo no disco, você pode passar os dados recuperados para a função aberta integrada, como a seguir:
data = file_mapper['options.txt'].getvalue().decode()
f = open('writing_options_to_disk.txt' , 'w+')
f.write(data)
f.close()
Isso gravará um arquivo em seu diretório de trabalho atual (ou seja, os.getcwd () ). Agora, alguns arquivos que você deseja baixar de um site FTP podem ser arquivos binários – como PDFs, arquivos do Excel etc., em vez de arquivos de texto bruto. Nesses casos, você vai querer pular o método de decodificação e passar o parâmetro ‘wb +’ para abrir , assim:
data = file_mapper['some_binary_file.pdf'].getvalue()
f = open('writing_binary_file_to_disk.pdf' , 'wb')
f.write(data)
f.close()
Baixando cada arquivo em um servidor FTP
Agora … estamos prontos. Como obtemos nosso código para baixar todos os arquivos em cada pasta disponível no servidor FTP? Existem algumas maneiras de fazer isso, mas a essência é que precisamos ser capazes de listar o conteúdo de cada diretório e, em seguida, fazer o download desse conteúdo. Basicamente, temos as peças de que precisamos acima.
Juntando tudo isso, a seguir está uma função para listar recursivamente todos os arquivos e subdiretórios que vivem em um servidor FTP. Como você pode ver, também precisaremos do pacote sys para fazer esse trabalho.
import sys
def list_all_ftp_files(ftp, dir):
dirs = []
non_dirs = []
'''Capture the print output of ftp.dir'''
stream = io.StringIO()
sys.stdout = stream
ftp.dir(dir)
streamed_result = stream.getvalue()
reduced = [x for x in streamed_result.split(' ') if x != '']
'''Clean up list'''
reduced = [x.split('\n')[0] for x in reduced]
'''Get the names of the folders by which ones are labeled <DIR>'''
indexes = [ix + 1 for ix,x in enumerate(reduced) if x == '<DIR>']
folders = [reduced[ix] for ix in indexes]
if dir == '/':
non_folders = [x for x in ftp.nlst() if x not in folders]
else:
non_folders = [x for x in ftp.nlst(dir) if x not in folders]
non_folders = [dir + '/' + x for x in non_folders]
folders = [dir + '/' + x for x in folders]
'''If currently scanning the root directory, just add the initial set of
of folders in that directory to our grand list'''
if dirs == []:
dirs.extend(folders)
'''Similarly, do the same for files that are not folders'''
if non_dirs == []:
non_dirs.extend(non_folders)
'''If there are still sub-folders within a directory, keep searching deeper
potential other sub-folders'''
if len(folders) > 0:
for sub_folder in sorted(folders):
result = list_all_ftp_files(ftp, sub_folder)
dirs.extend(result[0])
non_dirs.extend(result[1])
'''Return the list of all directories on the FTP Server, along
with the list of all non-folder files'''
return dirs , non_dirs
Resolvendo isso, a primeira parte da função usa ftp.dir para obter uma lista do conteúdo em um diretório de entrada. Conforme mencionado acima, isso não produz nada diretamente em uma lista ou alguma outra estrutura de dados que possa ser armazenada em uma variável. Em vez disso, ele imprime o conteúdo de um diretório no console. No entanto, usando o pacote sys , podemos capturar essa saída em nossa variável, streamed_result , de forma indireta. Em seguida, analisamos isso para obter as informações de que precisamos para nos dizer se cada arquivo é um subdiretório ou não.
'''Capture the print output of ftp.dir'''
stream = io.StringIO()
sys.stdout = stream
ftp.dir(dir)
streamed_result = stream.getvalue()
reduced = [x for x in streamed_result.split(' ') if x != '']
'''Clean up list'''
reduced = [x.split('\n')[0] for x in reduced]
Assim que tivermos uma lista de subdiretórios, continuaremos pesquisando em cada um deles por outras possíveis subpastas e arquivos que não sejam da pasta. Se mais subpastas forem encontradas, o processo será repetido até que nenhuma outra seja encontrada. O processo de pesquisa é repetido chamando recursivamente nossa função, list_all_ftp_files para pesquisar no próximo nível mais profundo de subdiretórios.
'''If there are still sub-folders within a directory, keep searching deeper
potential other sub-folders'''
if len(folders) > 0:
for sub_folder in sorted(folders):
result = list_all_ftp_files(ftp, sub_folder)
dirs.extend(result[0])
non_dirs.extend(result[1])
Podemos chamar nossa função assim:
'''Connect to FTP site'''
ftp = ftplib.FTP('ftp.nasdaqtrader.com')
'''Login'''
ftp.login()
'''Get all folders and files on FTP Server'''
folders , files = list_all_ftp_files(ftp , '/')
Agora, as pastas conterão uma lista de todos os subdiretórios em nosso site FTP, enquanto os arquivos conterão uma lista de todos os arquivos que não pertencem à pasta do site. Se você verificar o comprimento dos arquivos, verá que nosso exemplo tem mais de 26.000 arquivos! Na verdade, não vamos baixar todos eles em nosso exemplo, mas vou mostrar como fazer isso para o seu site FTP. Como um aviso, no entanto, se você estiver baixando um grande número de arquivos, é uma boa idéia verificar com o operador do servidor FTP para ter certeza de que você não afetará o desempenho do servidor se baixar em lote milhares de arquivos em seqüência curta.
Se você estiver pronto para baixar todos os arquivos em seu servidor FTP, poderá fazer isso basicamente como fizemos acima para um único diretório. Agora, no entanto, apenas percorremos todos os arquivos em arquivos , nossa lista de todas as não pastas que vivem no servidor.
file_mapper = {}
for file in files:
r = io.BytesIO()
ftp.retrbinary('RETR ' + file , r.write)
file_mapper[file] = r
r.close()
Você pode então escrever outro loop para realmente gravar esses arquivos no disco ou pode incorporar lógica no loop for acima para fazer isso. Conforme mencionado acima, é uma boa ideia tratar os arquivos binários de maneira diferente dos arquivos de texto brutos. Dependendo de como seus arquivos estão estruturados, você deve ser capaz de fazer algo assim:
import os
'''Write all files on FTP Server to disk'''
for key,val in file_mapper.items():
'''Create directory structure if it doesn't exist'''
if not os.path.exists(os.path.dirname(key)):
os.makedirs(os.path.dirname(key))
'''Check if file is a text file'''
if len(key.split('.txt')) > 1:
f = open(key , 'w+')
f.write(val.getvalue().decode())
f.close()
else:
f = open(key , 'wb+')
f.write(val.getvalue())
f.close()
O código acima verifica se cada arquivo em sequência é um arquivo de texto pela extensão (você também pode usar o método os.path.splitext do pacote os ). Em seguida, ele grava cada arquivo do servidor FTP no disco de forma apropriada.
Código completo
O código completo está abaixo:
import sys
import io
import ftplib
import os
def list_all_ftp_files(ftp, dir):
dirs = []
non_dirs = []
'''Capture the print output of ftp.dir'''
stream = io.StringIO()
sys.stdout = stream
ftp.dir(dir)
streamed_result = stream.getvalue()
reduced = [x for x in streamed_result.split(' ') if x != '']
'''Clean up list'''
reduced = [x.split('\n')[0] for x in reduced]
'''Get the names of the folders by which ones are labeled <DIR>'''
indexes = [ix + 1 for ix,x in enumerate(reduced) if x == '<DIR>']
folders = [reduced[ix] for ix in indexes]
if dir == '/':
non_folders = [x for x in ftp.nlst() if x not in folders]
else:
non_folders = [x for x in ftp.nlst(dir) if x not in folders]
non_folders = [dir + '/' + x for x in non_folders]
folders = [dir + '/' + x for x in folders]
'''If currently scanning the root directory, just add the initial set of
of folders in that directory to our grand list'''
if dirs == []:
dirs.extend(folders)
'''Similarly, do the same for files that are not folders'''
if non_dirs == []:
non_dirs.extend(non_folders)
'''If there are still sub-folders within a directory, keep searching deeper
potential other sub-folders'''
if len(folders) > 0:
for sub_folder in sorted(folders):
result = list_all_ftp_files(ftp, sub_folder)
dirs.extend(result[0])
non_dirs.extend(result[1])
'''Return the list of all directories on the FTP Server, along
with the list of all non-folder files'''
return dirs , non_dirs
'''Connect to FTP site'''
ftp = ftplib.FTP('ftp.nasdaqtrader.com')
'''Login'''
ftp.login()
'''Get all folders and files on FTP Server'''
folders , files = list_all_ftp_files(ftp , '/')
'''Get IO streams for each file'''
file_mapper = {}
for file in files:
r = io.BytesIO()
ftp.retrbinary('RETR ' + file , r.write)
file_mapper[file] = r
r.close()
'''Write all files on FTP Server to disk'''
for key,val in file_mapper.items():
'''Create directory structure if it doesn't exist'''
if not os.path.exists(os.path.dirname(key)):
os.makedirs(os.path.dirname(key))
'''Check if file is a text file'''
if len(key.split('.txt')) > 1:
f = open(key , 'w+')
f.write(val.decode())
f.close()
else:
f = open(key , 'wb+')
f.write(val)
f.close()
É isso para este post! Para aprender mais sobre ftplib , veja sua documentação oficial aqui .
Créditos: theautomatic