O PyODPS permite criar, ler, gravar e excluir tabelas do MaxCompute programaticamente. Também é possível gerenciar schemas, partições e transferências de dados em massa pelo MaxCompute Tunnel.
Pré-requisitos
Antes de começar, verifique se você tem:
Um projeto do MaxCompute
PyODPS instalado e configurado
Um AccessKey ID e um AccessKey secret
Um objeto de entrada do MaxCompute inicializado (
o)
Início rápido
Crie uma tabela, grave dados e leia-os:
from odps.models import Schema
# Define columns
schema = Schema.from_lists(['id', 'name'], ['bigint', 'string'])
# Create the table
table = o.create_table('my_table', schema, if_not_exists=True)
# Write records
o.write_table('my_table', [[1, 'Alice'], [2, 'Bob']])
# Read records
for record in o.read_table('my_table'):
print(record)
Resumo dos métodos da API
|
Operação |
Método |
Parâmetros principais |
Descrição |
|
Listar tabelas |
|
|
Lista todas as tabelas de um projeto |
|
Verificar existência |
|
|
Verifica se uma tabela existe |
|
Obter tabela |
|
|
Obtém um objeto de tabela |
|
Criar tabela |
|
|
Cria uma tabela |
|
Gravar dados |
|
|
Adiciona registros a uma tabela |
|
Ler dados |
|
|
Lê registros de uma tabela |
|
Excluir tabela |
|
|
Exclui uma tabela |
|
Converter para DataFrame |
|
— |
Converte uma tabela em um DataFrame do PyODPS |
|
Sincronizar metadados |
|
— |
Atualiza o objeto de tabela local com dados do servidor |
Para consultar a referência completa de métodos do PyODPS, visualize Visão geral do SDK Python.
Listar tabelas
Liste todas as tabelas de um projeto:
for table in o.list_tables():
print(table)
Filtre pelo prefixo do nome:
for table in o.list_tables(prefix="table_prefix"):
print(table.name)
Por padrão, list_tables() retorna apenas os nomes das tabelas. O acesso a propriedades como table_schema ou creation_time aciona solicitações adicionais e aumenta a latência. No PyODPS 0.11.5 ou superior, passe extended=True para recuperar essas propriedades em uma única chamada:
for table in o.list_tables(extended=True):
print(table.name, table.creation_time)
Filtre pelo tipo de tabela:
# Valid types: managed_table, external_table, virtual_view, materialized_view
managed_tables = list(o.list_tables(type="managed_table"))
external_tables = list(o.list_tables(type="external_table"))
virtual_views = list(o.list_tables(type="virtual_view"))
materialized_views = list(o.list_tables(type="materialized_view"))
Verificar se uma tabela existe
print(o.exist_table('pyodps_iris'))
# Returns True if the table exists
Obter informações da tabela
Obtenha um objeto de tabela com get_table():
t = o.get_table('pyodps_iris')
Imprima o schema da tabela:
print(t.schema)
Saída:
odps.Schema {
sepallength double # Sepal length (cm)
sepalwidth double # Sepal width (cm)
petallength double # Petal length (cm)
petalwidth double # Petal width (cm)
name string # Type
}
Acesse detalhes das colunas:
# All columns
print(t.schema.columns)
# A specific column
print(t.schema['sepallength'])
# Column comment
print(t.schema['sepallength'].comment)
Acesse as propriedades da tabela:
print(t.lifecycle) # Table lifecycle
print(t.creation_time) # Creation time
print(t.is_virtual_view) # Whether the table is a view
print(t.size) # Table size in bytes
print(t.comment) # Table comment
Acessar tabelas entre projetos
Passe o parâmetro project para obter uma tabela de outro projeto:
t = o.get_table('table_name', project='other_project')
Criar um schema de tabela
Há dois métodos disponíveis para criar schemas.
Método 1: Objetos Column e Partition
Use objetos Column e Partition do módulo odps.models quando precisar de controle total sobre as definições de colunas, incluindo comentários:
from odps.models import Schema, Column, Partition
columns = [
Column(name='num', type='bigint', comment='the column'),
Column(name='num2', type='double', comment='the column2'),
]
partitions = [Partition(name='pt', type='string', comment='the partition')]
schema = Schema(columns=columns, partitions=partitions)
Acesse as propriedades do schema:
# All columns including partition columns
print(schema.columns)
# Partition columns only
print(schema.partitions)
# Non-partition column names
print(schema.names)
# Non-partition column types
print(schema.types)
Método 2: Schema.from_lists()
O método Schema.from_lists() é mais simples, mas não oferece suporte a comentários em colunas:
from odps.models import Schema
schema = Schema.from_lists(
['num', 'num2'], # Column names
['bigint', 'double'], # Column types
['pt'], # Partition names
['string'] # Partition types
)
print(schema.columns)
Criar uma tabela
A partir de um objeto schema
from odps.models import Schema
schema = Schema.from_lists(['num', 'num2'], ['bigint', 'double'], ['pt'], ['string'])
# Create a table
table = o.create_table('my_new_table', schema)
# Skip creation if the table already exists
table = o.create_table('my_new_table', schema, if_not_exists=True)
# Set the lifecycle (days before auto-deletion)
table = o.create_table('my_new_table', schema, lifecycle=7)
A partir de definições de coluna como strings
# Partitioned table (columns, partition columns)
table = o.create_table('my_new_table', ('num bigint, num2 double', 'pt string'), if_not_exists=True)
# Non-partitioned table
table = o.create_table('my_new_table02', 'num bigint, num2 double', if_not_exists=True)
Ativar tipos de dados estendidos
Por padrão, apenas estes tipos de dados têm suporte: BIGINT, DOUBLE, DECIMAL, STRING, DATETIME, BOOLEAN, MAP e ARRAY.
Para usar tipos estendidos, como TINYINT e STRUCT, ative a extensão de tipos de dados do MaxCompute V2.0:
from odps import options
options.sql.use_odps2_extension = True
table = o.create_table('my_new_table', 'cat smallint, content struct<title:varchar(100), body:string>')
Sincronizar atualizações da tabela
Quando outro programa modificar uma tabela, chame reload() para atualizar o objeto local com os metadados mais recentes do servidor:
from odps.models import Schema
schema = Schema.from_lists(['num', 'num2'], ['bigint', 'double'], ['pt'], ['string'])
table = o.create_table('my_new_table', schema)
# Fetch the latest table metadata from the server
table.reload()
Gravar dados em uma tabela
write_table()
Use write_table() para gravações simples e únicas, quando todos os registros já estiverem prontos. Esse método adiciona dados à tabela e gerencia partições em uma única chamada.
records = [
[111, 1.0],
[222, 2.0],
[333, 3.0],
[444, 4.0]
]
# Write to a partition; create the partition if it does not exist
o.write_table('my_new_table', records, partition='pt=test', create_partition=True)
Cada chamada a write_table() cria um arquivo no servidor. Essa operação consome tempo, e muitos arquivos pequenos degradam o desempenho das consultas. Grave vários registros por chamada ou passe um objeto gerador.
O método write_table() sempre adiciona dados. Para substituir dados existentes:
Tabelas sem partição: Chame
table.truncate().Tabelas particionadas: Exclua e recrie a partição.
open_writer()
Use open_writer() para gravações em streaming ou para gravar registros incrementalmente em uma sessão gerenciada.
t = o.get_table('my_new_table')
with t.open_writer(partition='pt=test02', create_partition=True) as writer:
records = [
[1, 1.0],
[2, 2.0],
[3, 3.0],
[4, 4.0]
]
writer.write(records) # Accepts any iterable
Grave em partições de vários níveis:
t = o.get_table('test_table')
with t.open_writer(partition='pt1=test1,pt2=test2') as writer:
records = [
t.new_record([111, 'aaa', True]),
t.new_record([222, 'bbb', False]),
t.new_record([333, 'ccc', True]),
t.new_record([444, 'Chinese', False])
]
writer.write(records)
Gravação paralela multiprocessos
Vários processos podem gravar na mesma tabela simultaneamente ao compartilhar um ID de sessão e gravar em blocos separados. Cada bloco corresponde a um arquivo no servidor. O processo principal confirma a operação após a conclusão de todos os workers.
import random
from multiprocessing import Pool
from odps.tunnel import TableTunnel
def write_records(tunnel, table, session_id, block_id):
# Reuse the existing session
local_session = tunnel.create_upload_session(table.name, upload_id=session_id)
# Write to this process's block
with local_session.open_record_writer(block_id) as writer:
for i in range(5):
record = table.new_record([random.randint(1, 100), random.random()])
writer.write(record)
if __name__ == '__main__':
N_WORKERS = 3
table = o.create_table('my_new_table', 'num bigint, num2 double', if_not_exists=True)
tunnel = TableTunnel(o)
upload_session = tunnel.create_upload_session(table.name)
# Share the session ID across processes
session_id = upload_session.id
pool = Pool(processes=N_WORKERS)
futures = []
block_ids = []
for i in range(N_WORKERS):
futures.append(pool.apply_async(write_records, (tunnel, table, session_id, i)))
block_ids.append(i)
[f.get() for f in futures]
# Commit all blocks
upload_session.commit(block_ids)
Ler dados de uma tabela
read_table()
Use read_table() para iterar sobre todos os registros de uma tabela ou partição:
for record in o.read_table('my_new_table', partition='pt=test'):
print(record)
head()
Visualize até 10.000 registros sem varrer toda a tabela:
t = o.get_table('my_new_table')
for record in t.head(3):
print(record)
open_reader()
Use open_reader() quando precisar de acesso fatiado ou da contagem de registros antes da leitura. Esse método expõe um atributo count e oferece suporte a fatiamento por índice:
t = o.get_table('my_new_table')
with t.open_reader(partition='pt=test') as reader:
count = reader.count
for record in reader[5:10]: # Read a slice of records
print(record)
Sem um bloco with:
reader = t.open_reader(partition='pt=test')
count = reader.count
for record in reader[5:10]:
print(record)
Excluir uma tabela
# Delete only if the table exists
o.delete_table('my_table_name', if_exists=True)
# Or call drop() on a table object
t.drop()
Converter uma tabela em um DataFrame
O método to_df() converte uma tabela em um DataFrame do PyODPS. Para mais detalhes, consulte DataFrame (não recomendado).
table = o.get_table('my_table_name')
df = table.to_df()
Gerenciar partições
Verificar se uma tabela é particionada
table = o.get_table('my_new_table')
if table.schema.partitions:
print('Table %s is partitioned.' % table.name)
Iterar sobre partições
table = o.get_table('my_new_table')
# All partitions
for partition in table.partitions:
print(partition.name)
# Sub-partitions under pt=test
for partition in table.iterate_partitions(spec='pt=test'):
print(partition.name)
# Partitions matching a condition (PyODPS 0.11.3 and later)
for partition in table.iterate_partitions(spec='dt>20230119'):
print(partition.name)
A partir do PyODPS 0.11.3, iterate_partitions() aceita expressões lógicas como dt>20230119.
Verificar se uma partição existe
table = o.get_table('my_new_table')
table.exist_partition('pt=test,sub=2015')
Obter informações da partição
table = o.get_table('my_new_table')
partition = table.get_partition('pt=test')
print(partition.creation_time)
print(partition.size)
Criar uma partição
t = o.get_table('my_new_table')
t.create_partition('pt=test', if_not_exists=True)
Excluir uma partição
t = o.get_table('my_new_table')
t.delete_partition('pt=test', if_exists=True)
# Or call drop() on a partition object
partition.drop()
Registros e mapeamentos de tipos de dados
Um registro representa uma única linha em uma tabela do MaxCompute. Todos os quatro métodos de E/S — open_reader(), open_writer(), open_record_reader() e open_record_writer() — usam registros.
Crie um registro chamando new_record() em um objeto de tabela.
Dado este schema de tabela:
odps.Schema {
c_int_a bigint
c_string_a string
c_bool_a boolean
c_datetime_a datetime
c_array_a array<string>
c_map_a map<bigint,string>
c_struct_a struct<a:bigint,b:string>
}
Crie e manipule registros:
import datetime
t = o.get_table('mytable') # o is the MaxCompute entry object
# Create a record with initial values
# The number of values must match the number of fields in the schema
r = t.new_record([1024, 'val1', False, datetime.datetime.now(), None, None])
# Create an empty record
r2 = t.new_record()
# Set values by index
r2[0] = 1024
# Set values by field name
r2['c_string_a'] = 'val1'
# Set values by attribute
r2.c_string_a = 'val1'
# Set ARRAY value
r2.c_array_a = ['val1', 'val2']
# Set MAP value
r2.c_map_a = {1: 'val1'}
# Set STRUCT value (PyODPS 0.11.5 and later)
r2.c_struct_a = (1, 'val1') # tuple
r2.c_struct_a = {"a": 1, "b": 'val1'} # dict
# Get values
print(r[0]) # By index
print(r['c_string_a']) # By field name
print(r.c_string_a) # By attribute
print(r[0: 3]) # Slice
print(r[0, 2, 3]) # Multiple indices
print(r['c_int_a', 'c_double_a']) # Multiple field names
Mapeamentos de tipos de dados
|
Tipo do MaxCompute |
Tipo Python |
|
TINYINT, SMALLINT, INT, BIGINT |
int |
|
FLOAT, DOUBLE |
float |
|
STRING |
str |
|
BINARY |
bytes |
|
DATETIME |
datetime.datetime |
|
DATE |
datetime.date |
|
BOOLEAN |
bool |
|
DECIMAL |
decimal.Decimal |
|
MAP |
dict |
|
ARRAY |
list |
|
STRUCT |
tuple / namedtuple |
|
TIMESTAMP |
pandas.Timestamp |
|
TIMESTAMP_NTZ |
pandas.Timestamp |
|
INTERVAL_DAY_TIME |
pandas.Timedelta |
Tratamento de STRING
Por padrão, STRING é mapeado para strings Unicode (str no Python 3, unicode no Python 2). Para armazenar dados binários em uma coluna STRING, defina options.tunnel.string_as_binary = True.
Comportamento de fuso horário
O PyODPS usa o fuso horário local por padrão. O MaxCompute não armazena valores de fuso horário; ele converte valores datetime em timestamps UNIX para armazenamento.
Para usar UTC:
options.local_timezone = False
Para usar um fuso horário específico:
options.local_timezone = 'Asia/Shanghai'
DECIMAL no Python 2
Quando o pacote cdecimal está instalado, o PyODPS usa cdecimal.Decimal em vez de decimal.Decimal no Python 2.
Comportamento do tipo STRUCT
Antes do PyODPS 0.11.5, STRUCT era mapeado para dict. A partir do PyODPS 0.11.5, STRUCT é mapeado para namedtuple por padrão.
Para restaurar o comportamento anterior com dict:
options.struct_as_dict = True
Em ambientes DataWorks, struct_as_dict tem como padrão False para compatibilidade histórica. O PyODPS 0.11.5 e versões posteriores aceitam tanto dict quanto tuple para valores STRUCT. Versões anteriores aceitam apenas dict.
MaxCompute Tunnel
O MaxCompute Tunnel é o canal de dados de baixo nível para uploads e downloads em massa. Na maioria dos casos, write_table() e read_table() são mais simples. Use o Tunnel quando precisar de gravações multiprocessos ou controle refinado de sessões.
O PyODPS não oferece suporte a upload de dados por meio de tabelas externas (por exemplo, tabelas baseadas em OSS ou Tablestore).
Se o CPython estiver instalado, o PyODPS compila código C durante a instalação para acelerar transferências baseadas no Tunnel.
Fazer upload de dados
from odps.tunnel import TableTunnel
table = o.get_table('my_table')
tunnel = TableTunnel(o)
upload_session = tunnel.create_upload_session(table.name, partition_spec='pt=test')
with upload_session.open_record_writer(0) as writer:
record = table.new_record()
record[0] = 'test1'
record[1] = 'id1'
writer.write(record)
record = table.new_record(['test2', 'id2'])
writer.write(record)
# Commit outside the with block. Committing before all data is written causes an error.
upload_session.commit([0])
Fazer download de dados
from odps.tunnel import TableTunnel
tunnel = TableTunnel(o)
download_session = tunnel.create_download_session('my_table', partition_spec='pt=test')
with download_session.open_record_reader(0, download_session.count) as reader:
for record in reader:
print(record)