Red de conocimientos turísticos - Información de alquiler - Programación back-end Programación de base de datos Python3

Programación back-end Programación de base de datos Python3

Para la mayoría de los desarrolladores de software, el término base de datos suele referirse a RDBMS (sistemas de gestión de bases de datos relacionales), que utilizan tablas (cuadrículas similares a hojas de cálculo) en las que las filas representan registros y las columnas representan campos registrados. Las tablas y los datos almacenados en ellas se crean y manipulan mediante declaraciones escritas en SQL (lenguaje de consulta estructurado). Python proporciona una API (Interfaz de programación de aplicaciones) para manipular bases de datos SQL, que generalmente se incluye con la base de datos SQLite 3 como estándar.

Otro tipo de base de datos es un DBM (administrador de base de datos), que almacena cualquier número de elementos clave-valor. La biblioteca estándar de Python proporciona varias interfaces DBM, incluidas algunas específicas para plataformas UNIX. La forma en que funciona un DBM es similar a un diccionario en Python, la diferencia es que un DBM generalmente se almacena en el disco en lugar de en la memoria, y sus claves y valores son siempre objetos de bytes y pueden estar limitados por la longitud. El módulo shelve explicado en la primera sección de este capítulo proporciona una interfaz DBM conveniente que nos permite usar cadenas como claves y objetos arbitrarios (seleccionables) como valores.

Si las bases de datos DBM y SQLite disponibles no son suficientes, el índice de paquetes de Python, pypi.python.org/pypi, proporciona una gran cantidad de paquetes relacionados con bases de datos, incluido bsddb DBM ("Berkeley DB"), Mapeadores relacionales de objetos, como SQLAlchemy (www.sqlalchemy.org), e interfaces de datos cliente/servidor populares, como DB2, Informix, Ingres, MySQL, ODBC y PostgreSQL.

En este capítulo, implementaremos dos versiones de un programa que mantiene una lista de DVD y realiza un seguimiento del título, año de lanzamiento, duración y editor de cada DVD. La primera versión del programa usaba un DBM (a través del módulo shelve) para almacenar sus datos, y la segunda versión usaba una base de datos SQLite. Ambos programas pueden cargar y guardar formatos XML simples, lo que permite exportar datos de DVD desde un programa e importarlos a otro. En comparación con la versión DBM, el programa basado en SQL proporciona más funciones y su diseño de datos también es un poco más limpio.

12.1 Base de datos DBM

El módulo shelve proporciona un contenedor para DBM. Con esto, podemos tratarlo como un diccionario cuando interactuamos con DBM. Aquí asumimos que solo usamos claves de cadena. y valores seleccionables, el módulo de estantería convertirá las claves y los valores en objetos de bytes (o viceversa) durante el procesamiento real.

Dado que el módulo shelve utiliza el DBM subyacente, si el mismo DBM no está disponible en otras computadoras, es posible que el archivo DBM guardado en una determinada computadora no se pueda leer en otras máquinas. Para resolver este problema, una solución común es proporcionar funciones de importación y exportación XML para archivos que deben ser transferibles entre máquinas, que es lo que hacemos en el programa de DVD dvds-dbm.py en esta sección.

Para la clave, usamos el título del DVD; para el valor, usamos una tupla, que almacena el editor, el año de lanzamiento y la hora. Con la ayuda del módulo shelve, no necesitamos realizar ninguna conversión de datos y podemos procesar el objeto DBM como un diccionario.

El programa es similar en estructura al programa basado en menús que vimos anteriormente, por lo que lo que se muestra aquí principalmente es la parte relacionada con la programación DBM. Lo que se proporciona a continuación es parte de la función main() del programa y algunos códigos para el procesamiento del menú se ignoran.

db = Ninguno

prueba:

db = shelve.open(filename, protocol=pickle.HIGHEST_PROTOCOL)

finalmente:

si db no es Ninguno:

db.dose()

Aquí hemos abierto (creado si no existe) el archivo DBM especificado para facilitarlo. realiza operaciones de lectura y escritura. El valor de cada elemento se guarda como un pepinillo usando el protocolo de pepinillo especificado. Los elementos existentes se pueden leer, incluso si se guardaron usando un protocolo de nivel inferior, porque Python puede descubrir el protocolo correcto para leer el pepinillo. Finalmente, se cierra el DBM; esto se hace para borrar el caché interno del DBM y garantizar que los archivos del disco reflejen cualquier cambio que se haya realizado. Además, es necesario cerrar los archivos.

El programa proporciona las opciones correspondientes para agregar, editar, enumerar, eliminar, importar y exportar datos de DVD. Ignoraremos la mayor parte del código de la interfaz de usuario excepto las adiciones, nuevamente porque se han mostrado en otros contextos.

def add_dvd(db):

título = Console.get_string("Título", "título")

si no título:

devolver

director = Console.get_string("Director", "director")

si no es director:

devolver

año = Console.get_integer("Año", "año",mínimo=1896,

máximo=datetime,date.today().año)

duración = Console.get_integer( "Duración (minutos)", "minutos", mínimo=0, máximo=60*48)

db[título] = (director, año, duración)

db.sync ()

Como todas las funciones llamadas por el menú del programa, esta función también toma un objeto DBM (db) como único parámetro. La mayor parte del trabajo de esta función es obtener los detalles del DVD. En la penúltima línea, almacenamos los elementos clave-valor en el archivo DBM, con el título del DVD como clave, el editor y el año. y el tiempo (recogido por el módulo de estantería juntos) como valor.

Para sincronizar con la consistencia habitual de Python, DBM proporciona la misma API que el diccionario, por lo que además de la función shelve.open() (mostrada anteriormente) y el método shelve.Shelf.sync() (el El método se utiliza para borrar el caché interno de shelve y sincronizar los datos del archivo en el disco con los cambios realizados (en este caso, agregando un nuevo elemento), no necesitamos aprender ninguna sintaxis nueva.

def edit_dvd(db):

old_title = find_dvd(db, "edit")

si old_title es Ninguno:

return

título = Console.get.string("Título", "título", título_antiguo)

si no es título:

devolver

director, año, duración = db[old_title]

...

db[title]= (director, año, duración)

if título ! = old_title:

del db[old_title]

db.sync()

Para poder editar un DVD, el usuario primero debe seleccionar el DVD para operar , es decir, obtener el título del DVD, porque el título se usa como clave y el valor se usa para almacenar otros datos relacionados. Dado que la funcionalidad necesaria también es necesaria en otras situaciones (como extraer un DVD), la implementamos en una función find_dvd() separada, que veremos más adelante. Si se encuentra el DVD, obtenemos los cambios realizados por el usuario y utilizamos los valores existentes como predeterminados para acelerar la interacción. (Para esta función, ignoramos la mayor parte del código de la interfaz de usuario, porque es casi el mismo que cuando agregamos el DVD). Finalmente, guardamos los datos, tal como lo hicimos cuando los agregamos. Si el título no ha cambiado, el valor asociado se sobrescribe; si el título ha cambiado, se crea un nuevo par clave-valor y es necesario eliminar el elemento original.

def find_dvd(db, mensaje):

mensaje = "(Inicio de) título de " + mensaje

while True:

coincide =[]

inicio = Console.get_string(message, "title")

si no es inicio:

devuelve Ninguno

para título en base de datos:

if title.lower().startswith(start.lower()):

matches.append(title)

if len(coincidencias) == 0:

print("No hay DVD que comiencen con", inicio)

continuar

elif len(coincidencias) == 1:

return coincidencias[0]

elif len(matches) > DISPLAY_LIMIT:

print("Demasiados DVD comienzan con {0}; intente ingresar más del título".format(start)

continuar

else:

coincidencias = sorted(matches, key=str.lower)

para i, coincide en enumerate(matches):

print("{0}: {1}".format(i+1, match))

cual = Console.get_integer("Número (o 0 para cancelar)",

"número", mínimo=1, máximo=len(coincidencias))

devuelve coincidencias[que - 1] if which != 0 else Ninguno

Para que descubrir un DVD sea lo más rápido y sencillo posible, requerimos que el usuario ingrese solo uno o los primeros caracteres de su título. Una vez que tenemos los caracteres iniciales del título, recorremos el DBM y creamos una lista de coincidencias. Si solo hay una coincidencia, devuelva ese elemento si hay varias coincidencias (pero menos que DISPLAY_LIMIT, un número entero establecido en otra parte del programa), muéstrelos todos en orden sin distinguir entre mayúsculas y minúsculas y establezca un número para cada elemento de modo que el usuario puede seleccionar un título simplemente ingresando el número. (La función Console.get_integer() puede aceptar 0, incluso si el valor mínimo es mayor que 0, de modo que 0 se puede usar como valor de eliminación. Este comportamiento se puede deshabilitar usando el parámetro enable_zero=False. No podemos usar el Tecla Enter, es decir, no hay ¿Qué significa cancelar? Porque no ingresar nada significa aceptar el valor predeterminado.

)

def list_dvds(db):

start =””

if len(db)> DISPLAY.LIMIT:

inicio = Console.get_string("Enumere los que comienzan con [Enter=all]", "start")

print()

para el título en sorted(db, key=str.lower ):

si no es inicio o título.Iower().startswith(start.lower()):

director, año, duración = db[título]

print("{título} ({año}) {duración} minuto{0}, por "

"{director}".format(Util.s(duración),**locals() ))

Enumerar todos los DVD (o aquellos títulos que comienzan con alguna subcadena) es iterar sobre todos los elementos del DBM.

La función Util.s() es simplemente s = lambda x: "" if x == 1 else "s", por lo tanto, si la duración del tiempo no es 1 minuto, se devuelve "s".

def remove_dvd(db):

título = find_dvd(db, "remove")

si el título es Ninguno:

return

ans = Console.get_bool("¿Eliminar {0}?".format(title), "no")

if ans:

del db[ title]

db.sync()

Para eliminar un DVD, primero debe encontrar el DVD que el usuario desea eliminar y solicitar confirmación. Después de obtenerlo, elimine el elemento. de DBM.

Hasta ahora, hemos mostrado cómo usar el módulo shelve para abrir (o crear) un archivo DBM y cómo agregarle elementos, editarlos, iterar sobre ellos y eliminarlos.

Desafortunadamente, hay un error en el diseño de nuestros datos. Los nombres de los editores se repiten, lo que fácilmente puede generar inconsistencias. Por ejemplo, el editor Danny DeVito puede ingresarse como "Danny DeVito" para una película o "Danny deVito" para otra. Para resolver este problema, se pueden usar dos archivos DBM: el archivo de DVD principal usa la clave de título y el valor (año, duración, ID del editor); el archivo del editor usa la clave de ID del editor (entero) y el valor del nombre del editor. La versión de base de datos SQL del programa que se muestra en la siguiente sección evitará este defecto mediante el uso de dos tablas, una para DVD y otra para editores.

12.2 Base de datos SQL

Las interfaces para las bases de datos SQL más populares están disponibles en módulos de terceros. Python viene con el módulo sqlite3 (así como con la base de datos SQLite 3), por lo que en Python. , puede iniciar la programación de la base de datos directamente.

SQLite es una base de datos SQL liviana que carece de muchas características de bases de datos como PostgreSQL, pero es muy conveniente para construir sistemas prototipo y es suficiente en muchos casos.

Para que el cambio entre bases de datos backend sea lo más fácil posible, PEP 249 (Especificación API de base de datos Python v2.0) proporciona una especificación API llamada DB-API 2.0. La interfaz de la base de datos debe seguir esta especificación. Por ejemplo, el módulo sqlite3 sigue esta especificación, pero no todos los módulos de terceros siguen esta especificación. La especificación API especifica dos objetos principales, a saber, objetos de conexión y objetos de cursor. La Tabla 12-1 y la Tabla 12-2 enumeran las API que estos dos objetos deben admitir respectivamente. En el módulo sqlite3, además de los requeridos por la especificación DB-API 2.0, sus objetos de conexión y objetos de cursor proporcionan muchas propiedades y métodos adicionales.

La versión SQL del programa de DVD es dvds.sql.py, que almacena los datos de los editores y del DVD por separado para evitar la duplicación y proporciona un nuevo menú para que los usuarios enumeren los editores. Las dos tablas utilizadas por este programa se encuentran en la Figura 12-1

def connect(filename):

create= not os.path.exists(filename)

db = sqlite3.connect(nombre de archivo)

si crear:

cursor = db.cursor()

cursor.execute("CREAR TABLA directores ("

"id INTEGER PRIMARY KEY AUTOINCREMENT ÚNICO NO NULO, "

"nombre TEXTO ÚNICO NO NULO)")

cursor.execute("CREAR TABLA dvds ( "

"id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, "

"title TEXT NOT NULL, "

"año INTEGER NOT NULL,"

"duración INTEGER NOT NULL, "

"director_id INTEGER NOT NULL, "

"CLAVE EXTRANJERA (director_id) REFERENCIAS directores)")

db .commit()

return db

La función sqlite3.connect() devolverá un objeto de base de datos y abrirá su archivo de base de datos especificado. Si el archivo no existe, se crea un archivo de base de datos vacío. En vista de esto, antes de llamar a sqlite3.connect(), debemos prestar atención a si la base de datos se va a crear desde cero. Si es así, se deben crear las tablas que utilizará el programa. Todas las consultas se realizan a través de un cursor de base de datos, que se puede obtener desde el método cursor() del objeto de base de datos.

Tenga en cuenta que ambas tablas se crearon con un campo ID, y el campo ID tiene una restricción AUTOINCREMENT; esto significa que SQLite asignará automáticamente un valor único al campo ID, por lo que al insertar un nuevo registro, Puede dejar estos campos a SQLite.

SQLite admite un conjunto limitado de tipos de datos (booleanos, números y cadenas, en realidad) pero se pueden ampliar utilizando "adaptadores" de datos o a tipos de datos predefinidos (como los utilizados para. fechas y horas), o tipos personalizados para representar tipos de datos arbitrarios. El programa DVD no requiere esta funcionalidad, y la documentación del módulo sqlite3 proporciona una explicación detallada de lo que usamos. La sintaxis de clave externa puede diferir de la utilizada para otros. bases de datos y, en cualquier caso, simplemente documentar nuestras intenciones, debido a que SQLite no necesita imponer la integridad relacional como muchas otras bases de datos, otra cosa que distingue a sqlite3 es su comportamiento predeterminado. El comportamiento es admitir el procesamiento de transacciones implícitas, por lo tanto, no hay "inicio" explícito. Se proporciona el método "transacción"

def add_dvd(db):

title = Console.get_string("Title. ", "title")

si no es título :

return

director = Console.get_string("Director", "director")

si no es director:

return

año = Console.get_integer("Año", "año", mínimo=1896,

máximo=datetime .date.today().year)

duración = Console.get_integer("Duración (minutos)", "minutos",

mínimo=0,máximo=60*48)

director_id = get_and_set_director(db, director)

cursor = db.cursor()

cursor.execute("INSERT INTO dvds ”

"(título, año, duración, director_id)"

"VALUES (?, ?, ?, ?)",

(título, año, duración, director_id))

db.commit()

El código inicial de esta función es el mismo que el de la función correspondiente en el programa dvds-dbm.py, pero después de completar la recopilación de datos, es el mismo que la función original. Hay una gran diferencia. El emisor ingresado por el usuario puede o no estar en la tabla de directores, por lo que tenemos una función get_and_set_director() que inserta un emisor en la base de datos si aún no está en la base de datos, devolviendo en cualquier caso la ID del editor lista para ser insertada en la mesa de DVD cuando sea necesario. Una vez que todos los datos estén disponibles, ejecutamos una declaración SQL INSERT. No necesitamos especificar el ID del registro porque SQLite nos lo proporcionará automáticamente.

En la consulta, utilizamos signos de interrogación (?) como marcadores de posición, y cada ? se reemplaza por el valor en la secuencia que sigue a la cadena que contiene la declaración SQL. También se pueden utilizar marcadores de posición con nombre, como veremos más adelante al editar registros.

Aunque es posible evitar el uso de marcadores de posición (y simplemente formatear la cadena SQL con los datos incrustados en ella), recomendamos usar siempre marcadores de posición y dejar el trabajo de codificar y escapar correctamente los elementos de datos al módulo de base de datos para completarlos. Otro beneficio de utilizar marcadores de posición es una mayor seguridad, ya que esto evita que se inserte SQL arbitrario de forma maliciosa en una consulta.

def get_and_set_director(db, director):

director_id = get_director_id(db, director)

si directorjd no es Ninguno:

return director_id

cursor = db.cursor()

cursor.execute("INSERT INTO directors (name) VALUES (?)",(director,))

db.commit()

return get_director_id(db, director)

Esta función devuelve el ID del emisor dado e inserta un nuevo registro de emisor si es necesario. Si se inserta un registro, primero intentamos recuperar su ID usando la función get_director_id().

def get_director_id(db, director):

cursor = db.cursor()

cursor.execute("SELECCIONE la identificación DE los directores DONDE nombre=?" ,(director,))

campos = cursor.fetchone()

devuelve campos[0] si los campos no son Ninguno más Ninguno

función get_director_id() Devuelve el ID del editor determinado, o Ninguno si no hay ningún editor especificado en la base de datos. Usamos el método fetchone() porque hay un registro coincidente o no. (Sabemos que no habrá editores duplicados porque el campo de nombres de la tabla de directores tiene una restricción ÚNICA. En cualquier caso, antes de agregar un nuevo editor, siempre verificamos si existe primero). Este tipo El método de recuperación siempre devuelve un secuencia de campos (Ninguno si no hay más registros). Aun así, aquí solo solicitamos que se devuelva un solo campo.

def edit_dvd(db):

título, identidad = find_dvd(db, "edit")

si el título es Ninguno:

devolver

título = Console.get_string("Título","título", título)

si no es título:

devolver

cursor = db.cursor()

cursor.execute("SELECCIONE dvds.año, dvds.duración, directores.nombre"

"DE dvds, directores "

"DONDE dvds.director_id = directores.id AND "

"dvds.id=:id", dict(id=identidad))

año, duración, director = cursor.fetchone()

director = Console.get_string("Director", "director", director)

si no es director:

return

año = Console,get_integer("Año","año", año, 1896,datetime.date.today().año)

duración = Console.get_integer("Duración (minutos) ", "minutos",

duración, mínimo=0, máximo=60*48)

director_id = get_and_set_director(db, director)

cursor.execute ("ACTUALIZAR dvds SET título=:título, año=:año,"

"duración=:duración, director_id=:directorjd "

"DONDE id=:identidad", locales ())

db.commit()

Para editar un registro de DVD, primero debemos encontrar el registro que el usuario necesita manipular. Si se encuentra un registro, le damos al usuario la oportunidad de modificar su título y luego recuperar los otros campos del registro para que los valores existentes se utilicen como valores predeterminados, minimizando el trabajo de entrada del usuario y el usuario solo necesita presione Enter. Se puede aceptar el valor predeterminado. Aquí, utilizamos marcadores de posición con nombre (de la forma: nombre) y debemos usar un mapa para proporcionar el valor correspondiente. Para las declaraciones SELECT, usamos un diccionario recién creado; para las declaraciones UPDATE, usamos el diccionario devuelto por locals().

Podemos usar un nuevo diccionario para ambas declaraciones al mismo tiempo. En este caso, para la declaración UPDATE, podemos pasar dict(title=title, año=año, duración=duración, director_id=director_id. , id=identidad)), no locales().

Después de que todos los campos estén disponibles y el usuario haya ingresado los cambios que deben realizarse, recuperamos el ID del emisor correspondiente (inserte un nuevo registro de emisor si es necesario) y luego actualizamos la base de datos con los nuevos datos. Adoptamos un enfoque simplificado y actualizamos todos los campos del registro, no solo aquellos que han sido modificados.

Cuando se utilizan archivos DBM, el título del DVD se utiliza como clave, por lo que si se modifica el título, debemos crear una nueva entrada clave-valor y eliminar la entrada original. Sin embargo, aquí cada registro de DVD tiene una identificación única, que se crea cuando el registro se inserta por primera vez, por lo que solo necesitamos cambiar el valor de cualquier otro campo, no se requieren otras operaciones.

def find_dvd(db, mensaje):

mensaje = "(Inicio de) título de " + mensaje

cursor = db.cursor()

p>

mientras es Verdadero:

inicio = Console.get_stnng(mensaje, "título")

si no inicio:

return (Ninguno, Ninguno)

cursor.execute("SELECCIONAR título, id DE DVD"

"¿DÓNDE LE GUSTA el título? ORDENAR POR título",

(start +"% ",))

registros = cursor.fetchall()

if len(records) == 0:

print("Hay No hay DVD que comiencen con ", inicio)

continuar

elif len(records) == 1:

return records[0]

elif len( registros) > DISPLAY_LIMIT:

print("Demasiados DVD ({0}) comienzan con {1}; intente ingresar "

"más del título" .format(len( registros),inicio))

continuar

más:

para i, registrar en enumerar(registros):

print(" {0}:{1}".format(i + 1, record[0]))

cual = Console.get_integer("Número (o 0 para cancelar)",

"número", mínimo=1, máximo=len(registros))

devuelve registros[cuáles -1] si cuál != 0 más (Ninguno, Ninguno)

this La función tiene la misma función que la función find_dvd() en el programa dvdsdbm.py y devuelve una tupla (título de DVD, ID de DVD) o (Ninguno, Ninguno), dependiendo de si se encuentra un registro. No es necesario iterar sobre todos los datos. En su lugar, se utiliza el carácter comodín de SQL (%), por lo que solo se recuperan los registros relevantes.

Como queremos que el número de registros coincidentes sea pequeño, los volvemos a colocar todos en la secuencia de secuencias a la vez.

Si hay más de un registro coincidente, pero el número es lo suficientemente pequeño como para mostrarlo, imprimimos los registros y adjuntamos un número numérico a cada registro para que el usuario pueda seleccionar el registro requerido de la misma manera que en dvds-dbm. programa py Similar a lo que se hace en:

def list_dvds(db):

cursor = db.cursor()

sql = ("SELECT dvds. título, dvds .año, dvds.duración, "

"directores.nombre DE dvds, directores "

"DONDE dvds.director_id = directores.id")

start = Ninguno

if dvd_count(db) > DISPLAY_LIMIT:

start = Console.get_string("Enumere los que comienzan con [Enter=all]", "start")

sql += "Y dvds.title COMO?"

sql += "ORDENAR POR dvds.title"

print()

si el inicio es Ninguno:

cursor.execute(sql)

más:

cursor.execute(sql, (start +"%",))

para registrar en el cursor:

print("{0[0]} ({0[1]}) {0[2]} minutos, por {0[3] }". format(record))

Para listar los detalles de cada DVD, ejecutamos una consulta SELECT. Esta consulta une dos tablas, y si el número de registros (devueltos por la función dvd_count()) excede el límite de visualización, el segundo elemento se agrega a la rama WHERE y luego la consulta se ejecuta e itera sobre los resultados. Cada registro es una secuencia cuyos campos coinciden con la consulta SELECT.

def dvd_count(db):

cursor = db.cursor()

cursor.execute("SELECT COUNT(*) FROM dvds")

p>

return cursor.fetchone()[0]

Colocamos estas líneas de código en una función separada porque las necesitamos en varios códigos de funciones diferentes.

Ignoramos el código de la función list_directors() porque esta función es muy similar en estructura a la función list_dvds(), pero es más simple porque esta función solo lista un campo (nombre).

def remove_dvd(db):

título, identidad = find_dvd(db, "remove")

si el título es Ninguno:

return

ans = Console.get_bool("¿Eliminar {0}?".format(title), "no")

si ans:

cursor = db.cursor()

cursor.execute("BORRAR DE DVD DONDE id=?", (identidad,))

db.commit()

Esta función se llamará cuando el usuario necesite eliminar un registro, y esta función es muy similar a la función correspondiente en el programa dvds-dbm.py.

En este punto, hemos revisado completamente el programa dvds-sql.py y hemos aprendido cómo crear tablas de bases de datos, seleccionar registros, iterar sobre registros seleccionados e insertar, actualizar y eliminar registros. Usando el método ejecutar(), podemos ejecutar cualquier declaración SQL compatible con la base de datos subyacente.

SQLite ofrece muchas más funciones de las que usaremos aquí, incluido el modo de confirmación automática (así como cualquier otro tipo de control de transacciones) y la capacidad de crear funciones que se pueden ejecutar dentro de consultas SQL. También es posible proporcionar una función de fábrica que controle lo que se devuelve para cada registro recuperado (por ejemplo, un diccionario o un tipo personalizado en lugar de una secuencia de campos). Además, es posible crear una base de datos SQLite en memoria pasando ":memory:" como nombre de archivo.

El contenido anterior está parcialmente extraído del curso en video 05 Programación back-end Programación de bases de datos Python22. Para obtener más ejemplos prácticos, consulte la explicación en video. Aprender programación es más fácil con las enseñanzas de Zhang Yuanwai y puedes aprender habilidades reales sin gastar dinero.