Programación web vía CGI – Una introducción

Versión: Python 2.5+, 3.x

CGI (Common Gateway Interface, Interfaz de entrada común) es un estándar de programación web que consiste en ejecutar un programa en el servidor y desplegar su resultado hacia el cliente (o navegador web). Se origina a mediados de la década del 90, y rápidamente se estandariza y globaliza, ya que se trata de una de las primeras tecnologías que permitió desarrollar webs de contenido dinámico. Por lo general, dicho programa o programas ejecutados en el servidor web están desarrollados en algún lenguaje de script (PHP, Perl, Python), por cuestiones de portabilidad entre las distintas plataformas; por lo tanto, se los denomina scripts CGI o simplemente CGIs.

En el área de desarrollo web con Python se pueden mencionar dos métodos distintios: vía CGI y vía un framework web como Django, Pyramid, web2py, entre otros. Este artículo no pretende debatir la eficiencia de la tecnología CGI en la web actual ni en comparación a las otras técnicas -tema para el cual, posiblemente, habrá un futuro artículo- sino asentar las bases para comenzar con su desarrollo y que puedas utilizarlo en base a tus necesidades y proyectos.

El Servidor

Para poder dar comienzo a la programación web vía CGI primero es necesario crear un servidor web para desarrollo local. Es decir, será éste el encargado de ejecutar el script CGI que solicita el navegador web y, al finalizar, retornar su contenido. Por lo tanto, crea un nuevo archivo cgiserver.py y añade lo siguiente:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from http.server import HTTPServer, CGIHTTPRequestHandler


class Handler(CGIHTTPRequestHandler):
    cgi_directories = ["/"]


httpd = HTTPServer(("", 8000), Handler)
httpd.serve_forever()

En Python 2, reemplaza from http.server import HTTPServer, CGIHTTPRequestHandler por:

from BaseHTTPServer import HTTPServer
from CGIHTTPServer import CGIHTTPRequestHandler

Realmente no es necesario comprender el código anterior, aunque tampoco es muy difícil de entender. Simplemente se crea un nuevo servidor HTTP utilizando la librería estándar y se le añade soporte para ejecutar scripts CGI. Reitero, se trata únicamente de un servidor para desarrollo local, no para producción. Nótese la línea número 8, en la que se indican los directorios que contendrán archivos CGIs (en este caso con extensión .py). Por último, la línea número 11 indica que el servidor escuchará peticiones vía el puerto 8000.

Una vez guardado el código anterior, ejecútalo.

El primer Script

Ya está corriendo el servidor de desarrollo local, listo para interpretar archivos CGI y retornar su resultado.

A la hora de preparar un script para ser ejecutado vía CGI debemos hacerlo como si se tratara de cualquier otro programa. Aquellos que programen en PHP deberán usar archivos *.php; programadores de Perl utilizarán archivos *.pl; por lo tanto, nosotros utilizaremos archivos *.py. De la misma manera, podrá tener el nombre que desees. Cada archivo CGI deberá realizar dos tareas fundamentales. La primera es indicar los headers o cabecera para que el navegador web sepa cómo interpretar el resultado. La segunda es imprimir el contenido con el formato que hemos especificado.

Vamos a comenzar creando un archivo script.py, en el mismo directorio en donde tenemos corriendo nuestro servidor cgiserver.py. Luego, inserta el siguiente código:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/plain")
print()

# Contenido (texto plano, como lo hemos especificado)
print("""Hola mundo!""")

Usuarios de Python 2.x deberán quitar los paréntesis al utilizar la sentencia print.

Veamos las porciones del código:

import cgi

Importa el módulo cgi. No es necesario importarlo si tu código simplemente imprime «Hola Mundo!»; de todas maneras, se le dará uso más adelante.

print("Content-Type: text/plain")
print()

Imprime el header el cual indica que se trata de un contenido de texto plano (text/plain). Se profundizará posteriormente. La segunda llamada a print() se utiliza para añadir un salto de línea. La cabecera debe incluír siempre dos saltos de línea al finalizar. La función print() añade automáticamente dicho carácter \n al final de la cadena. Entendido esto, también podría haberse utilizado:

print("Content-Type: text/plain\n")

Dando ambos códigos como resultado: Content-Type: text/plain\n\n (dos saltos de línea).

print("""Hola mundo!""")

Una vez especificada la cabecera, se imprime el contenido.

Nótese que en los scripts CGI la salida estándar es lo que visualizará el navegador. Por lo tanto, todo lo que se imprima en stdout será enviado al navegador.

Ahora ingresa http://localhost:8000/script.py en tu navegador web y verás el mensaje de bienvenida. Si en lugar de dicho mensaje lo que ves es el mismo contenido del archivo, es porque no has posicionado el script en la carpeta que contiene el servidor cgiserver.py.

Como se dijo anteriormente, el contenido del script será ejecutado por el navegador como éste lo disponga en la cabecera. Por ejemplo, si se imprime código HTML pero no se indica lo mismo en la cabecera, el navegador imprimirá simplemente el mismo código como si se tratase de texto plano.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/plain")
print()

print("""<html>
<head>
    <title>Título de la página</title>
</head>
<h3>Hola CGI!</h3>
</html>""")

Cambiando el contenido de script.py por este código e ingresando a http://localhost:8000/script.py en tu navegador verás que el código HTML no es interpretado.

Vista Previa

Para cambiar este comportamiento, la cabecera deberá indicar que se trata de código HTML de la siguiente manera:

print("Content-Type: text/html")

Código completo:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/html")
print()

print("""<html>
<head>
    <title>Título de la página</title>
</head>
<h3>Hola CGI!</h3>
</html>""")

Ahora sí verás:

Vista Previa

Existe una gran cantidad de opciones para esta cabecera (Content-Type). Por ejemplo, si quieres que el navegador interprete tu contenido como un archivo PDF deberás utilizar Content-Type: application/pdf. Puedes ver una abundante lista en este enlace.

Contenido dinámico

Ya hemos visto cómo retornar contenido a través de un script CGI, tanto texto plano como código HTML. El problema del ejemplo anterior es que siempre retornará lo mismo: un código HTML que imprime «Hola CGI!» en pantalla con un determinado tamaño. El objetivo de utilizar CGI es poder desplegar páginas dinámicas, que varíe el contenido dependiendo de las circunstancias y ocasiones. Por ejemplo, si quisieramos crear una página que salude a cada visitante deberíamos crear un archivo HTML para cada visitante con su respectivo nombre. Sería una infinidad de archivos como: saludar_pedro.html, saludar_juan.html, saludar_jorge.html. Teniendo estos tres archivos, si acaso Luis pasara por nuestra página no podríamos saludarlo, ya que no hay un archivo saludar_luis.html. Al utilizar CGI, podemos crear un único archivo que salude a todo visitante, sin importar su nombre.

A partir de ahora, tendré en cuenta que ya has agregado previamente a todos tus códigos las siguientes líneas:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/html")
print()

Todo código de aquí en adelante irá debajo de éste.

Para saludar a cada visitante utilizaremos el siguiente código:

url_input = cgi.FieldStorage()

print("""
<html>
<head>
    <title>Título de la página</title>
</head>
<h3>Hola %s!</h3>
</html>""" % url_input["nombre"].value)

En este código simplemente se obtiene la información enviada por el usuario a través del navegador web, utilizando la clase FieldStorage. Esta clase retornará los datos pasados después del ? en la URL junto con los de los formularios. Este script pretende ser llamado de la siguiente manera:

http://localhost:8000/script.py?nombre=Jorge

Así verás en pantalla «Hola Jorge!». La estructura del URL anterior es la siguiente:

http://dominio/script.py?variable=valor&otra_variable=valor&ultima_variable=valor

Como se observa, luego del nombre del script CGI se añade un ? (signo de interrogación) y a la derecha de éste el nombre de una variable junto con su valor, intercalados por = (signo igual) y separando cada grupo variable=valor con un & (et). Por ejemplo:

http://localhost:8080/script.py?nombre=Pedro&edad=25&idioma=Ingles

Estos datos serán accedidos desde el archivo script.py de la siguiente manera:

url_input = cgi.FieldStorage()

# Se trata como cualquier diccionario
url_input["nombre"].value  # 'Pedro'
url_input["edad"].value    # '25'
url_input["idioma"].value  # 'Ingles'

Nótese que el atributo FieldStorage.value retorna siempre una cadena, como en el caso de la edad, que por más de que se trate de un valor numérico es responsabilidad del programador convertirlo a un entero.

Al tratarse de un diccionario, puedes verificar rápidamente la presencia de las variables:

if "nombre" not in url_input:
    print("<html>¡No me has dicho tu nombre!</html>")

Código completo:

url_input = cgi.FieldStorage()

if "nombre" not in url_input:
    print("<html>¡No me has dicho tu nombre!</html>")
else:
    print("""
    <html>
    <head>
        <title>Título de la página</title>
    </head>
    <h3>Hola %s!</h3>
    </html>""" % (url_input["nombre"].value))

Por lo tanto, al ingresar a http://localhost:8000/script.py?nombre=Luis verás en pantalla «Hola Luis!». Por el contrario, en http://localhost:8000/script.py verás «¡No me has dicho tu nombre!».

Vista Previa

Vista Previa

Formularios

Una de las actividades más comunes es la de implementar formularios de contacto. Para esto, los campos rellenados por el usuario deben ser enviados hacia el servidor y manejados por nuestro script CGI. No solo este tipo de formularios, sino cualquier tipo de entrada por parte del visitante que necesite ser procesada.

La entrada vía formularios se recibe de la misma manera que vía URL. Por ejemplo, el siguiente código simula un formulario para iniciar sesión. Deberás crear dos archivos: index.py, el cual imprimirá el código HTML del formulario y login.py, que recibirá los datos ingresados por el usuario.

index.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/html")
print()

print("""<html>
    <head><title>Formulario</title></head>
    <form method="post" action="login.py">
        Nombre: <input name="name" type="text" /> <br />
        Contraseña: <input name="password" type="password" /> <br />
        <button>Ingresar</button>
    </form>
</html>""")

Simplemente código HTML. Se crea un formulario y dentro de éste se inserta un campo para el nombre, otro para la contraseña y un botón para ingresar. Nótese el atributo action de la etiqueta form, el cual deberá llevar el nombre del archivo al cual le serán enviados los datos ingresados vía una petición POST (especificado en el atributo method).

login.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi

# Headers
print("Content-Type: text/html")
print()

print("<html>")

form_input = cgi.FieldStorage()

if "name" not in form_input or "password" not in form_input:
    print("Debe rellenar todos los campos.")
else:
    print("Has iniciado sesión como %s." % form_input["name"].value)
    
print("</html>")

De la misma manera se obtienen los datos ingresados por el usuario. Se crea una instancia de la clase FieldStorage, y luego se accede a sus elementos como un diccionario. El nombre de los ítems del diccionario que contienen los valores del nombre y contraseña depende del atributo name especificado en las etiquetas input en el código HTML anterior. Nótese que el primero lleva el nombre de «name» y el segundo «password». Estos valores son totalmente a elección. Si se creara una caja de texto de la siguiente manera:

<input name="age" type="text" />

Luego debería ser accedida como:

form_input["age"].value

Siendo form_input una instancia de FieldStorage.

Vista Previa

Vista Previa

Problemas comúnes y soluciones

El texto original en inglés de este apartado se encuentra en este enlace, en la documentación del módulo CGI.

  • La mayoría de los servidores HTTP no envían el contenido que imprime el script CGI al navegador web hasta que éste no finalice, por lo que no es posible desarrollar una barra de progreso mientras el script está corriendo.
  • Chequea los registros del servidor HTTP (tail -f logfile en una ventana aparte puede ser útil).
  • Siempre chequea la sintaxis del script primero, ejecutándolo localmente (algo como python script.py).
  • Si el script no tiene errores de sintaxis, intenta añadiendo import cgitb; cgitb.enable() al comienzo del archivo. Recomiendo utilizar este módulo siempre, ya que crea registros de errores que serán mostrados en el navegador; te salvará en varias ocasiones.
  • Al invocar programar externos, asegúrate de que pueden ser encontrados. Generalmente, esto significa utilizar el nombre completo de las rutas; utilizar PATH no es muy conveniente en la programación vía CGI.
  • Al momento de leer o escribir archivos externos, asegúrate de que puedan ser utilizados por el userid que está ejecutando tu script CGI (que generalmente es el userid que ejecuta al servidor web).
  • No intenes dar un modo set-uid a tu script CGI. Esto no funciona en la mayoría de los sistemas, y también es una responsabilidad de seguridad.

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

19 comentarios.

  1. Buen dia amigo, por qué veo que la documentación para hacer controladores en Python esta enfocado es en Frameworks y no se aconseja el uso de CGI?

    • Recursos Python says:

      Hola, Luis. Ocurre que CGI es una tecnología vetusta. Es más fácil, rápido y seguro utilizar algún framework (Django, Flask, etc.) vía WSGI. Sin embargo, CGI sigue siendo de utilidad en pequeñas operaciones o para mantener sistemas antiguos.

      Saludos

  2. Maria Antonieta says:

    Hola ! Buenas tardes, gracias por tan excelentes contenidos. Una consulta, como detengo el servidor cgi? estos codigos de ejemplo me sirven para python3.5 en debian ?

    • Recursos Python says:

      Hola, ¿cómo estás? Sí, sirven en cualquier plataforma soportada por Python. El servidor lo detenés con CTRL + C (en Windows) o CTRL + D (en Linux).

      Saludos

  3. hola mi problema es el siguiente logre que el servidor funcionara dándole los permisos que requería y llamándolo desde consola para luego ejecutar el script en localhost desde el navegador pero al cargar no me muestra nada de contenido, ni siquiera el hola mundo copiado directamente de tu guiá lo cual me parece extraño y quiero saber si se debe aque no lo estoy haciendo desde un entorno virtual o es otra razón

  4. Domingo Román says:

    Duda: soy más que neófito (y además, mayorcito ya). Seguí las indicaciones al pie de la letra: tengo el cgi.server.py y es script.py en la misma carpeta pero cuando llego a la instrucción

    ingresa http://localhost:8000/script.py en tu navegador web

    Me aparece (Chrome):
    No se puede acceder a este sitio
    localhost rechazó la conexión.
    Buscar localhost 8000 script en Google
    ERR_CONNECTION_REFUSED

    y en Safari algo muy parecido.

    Tengo dudas con qué significa, respecto del archivo cgi.server
    «Una vez guardado el código anterior, ejecútalo.»
    Lo interpreté como ejecutarlo desde la consola. Cuando lo hice el cursor quedó como esperando algo. Imaginé que estaba bien.
    Muchas gracias y saludos

    • Recursos Python says:

      Hola. Exactamente, el servidor cgiserver.py se bloquea esperando a las peticiones HTTP. ¿El error lo obtienes estando el servidor abierto? Tal vez haya algún tipo de firewall bloqueando la conexión. Si el problema persiste te invito a que pases por el foro para verlo con mayor detalle.

      Saludos.

  5. Estoy tratando de mandar datos de un servidor a otro (lo que mando son xml codificados). Mi codigo es el siguiente.

    #!/usr/bin/python
    # Nombre de Fichero: urllib2_1.py

    import urllib2
    import sys
    import cjson
    import os

    #####################Lectura de un archivo xml##############################
    f=open(‘/usr/local/etc/service/cfdi_files/60ee2273-927b-481d-a36f-ce7465505a7b.xml’)
    xml=f.read()
    f.close()

    json = {‘data’:'»cfdi»:%s’ % (xml.encode(‘hex’))}
    print json

    data=cjson.encode(json)

    https = urllib2.Request(url=’https://172.16.150.187/py/index.py?’, data=data)
    f=urllib2.urlopen(https)
    print f.read()

    ##################Segundo archivo xml####################################

    f=open(‘/usr/local/etc/service/cfdi_files/8367832d-c465-4d5e-aec8-b532969f944c.xml’)
    xml=f.read()
    f.close()

    json = {‘data’:'»cfdi»%s’ % (xml.encode(‘hex’))}
    print json

    data=cjson.encode(json)

    https = urllib2.Request(url=’https://172.16.150.187/py/index.py?’, data=data)
    f=urllib2.urlopen(https)
    print f.read()

    Pero cuando hago la peticiò me marco el Error 502

    Me podrian explicar cual es mi error

    De lado del receptor el index es el siguiente

    #! /usr/bin/python
    # -*- coding:utf-8 -*-

    import cgi
    import cjson
    import sys
    import os
    import datetime
    import time
    import traceback

    class InputProcessed(object):
    def read(self, *args):
    pass #raise EOFError(‘The wsgi.input stream has already been consumed’)
    readline = readlines = __iter__ = read

    def get_post_form(environ):
    input = environ[‘wsgi.input’]
    post_form = environ.get(‘wsgi.post_form’)
    if (post_form is not None
    and post_form[0] is input):
    return post_form[2]
    # This must be done to avoid a bug in cgi.FieldStorage
    environ.setdefault(‘QUERY_STRING’, »)
    fs = cgi.FieldStorage(fp=input,
    environ=environ,
    keep_blank_values=1)
    new_input = InputProcessed()
    post_form = (new_input, input, fs)
    environ[‘wsgi.post_form’] = post_form
    environ[‘wsgi.input’] = new_input
    return fs

    def application(environ, start_response):
    if environ[‘REQUEST_METHOD’]==’GET’:
    ctx = cgi.FieldStorage(fp=environ[‘wsgi.input’], environ=environ)
    elif environ[‘REQUEST_METHOD’]==’POST’:
    ctx=get_post_form(environ)
    print ctx
    # datos=cjson.decode(ctx[‘data’].value)

    print ctx[‘data’].value
    datos = ctx[‘data’].value
    print ‘-‘*50

    # print datos
    # data1=json.decode(‘hex’)
    # print data1
    res={«success»:True,»msg_response»:’ok’}
    start_response(‘200 OK’, [(‘Content-Type’,’text/html; charset=utf-8′)])
    return [cjson.encode(res)]
    ~

    • Recursos Python says:

      Por lo que veo en tu servidor CGI estás imprimiendo varias cosas antes de la cabecera HTTP (que estimo que se imprime con la función start_response). Esto probablemente cause el error ya que la cabecera debe ser lo primero en imprimirse.

      De todas formas te invito a pasar por el foro si continuas con problemas para mayor comodidad.

      Un saludo.

    • Recursos Python says:

      No puedo ayudarte sin ver una porción de tu código, pero probablemente tengas problemas con la ruta a tus imágenes. Las rutas son relativas a la ubicación del archivo cgiserver.py. Si tu archivo está en, por ejemplo, /home/web/cgiserver.py y tu imagen en /home/web/imagenes/imagen1.png entonces el código HTML es <img src="imagenes/imagen1.png" />.

      Saludos.

  6. Hola! estoy comenzando en el mundo de python, mi pregunta es, si para correr en mi navegador web los archivos *.py tengo que intalar antes apache?.
    La otra pregunta es los archivos que menciona se deben guardar en la ubicacion de apache, porque esa parte no entiendo

    • Recursos Python says:

      ¡Hola! No es necesario instalar Apache; Python incluye en su librería estándar un servidor HTTP básico para utilizar durante el desarrollo. Es decir, el archivo cgiserver.py cumple el rol de Apache (o cualquier otro servidor web). Tanto este archivo como los demás podés guardarlos en cualquier ubicación.

      Un saludo.

    • Recursos Python says:

      Hola Dante. En primer lugar, habría que modificar el servidor CGI indicando una IP de acceso público o bien 0.0.0.0 para escuchar en todas las direcciones de IP disponibles.

      httpd = HTTPServer(("0.0.0.0", 8000), Handler)

      Ahora, la dirección de acceso debería ser «http://direccion-ip:8000/», siendo «direccion-ip» tu IP pública (puedes averiguarla en este enlace), siempre y cuando tu módem o router habilite el puerto 8000.

      Un saludo.

  7. Excelente manual, pero a mi al ejecutar los archivos en el navegador me los muestra como texto plano, osea me muestra el código, no lo ejecuta. ¿eso que puede ser?

    • Recursos Python says:

      Hola, me alegro que te haya servido el artículo. Respecto a tu problema, estimo que estás experimentando lo que comentas al ingresar a http://localhost:8000/script.py. Recuerda que debes colocar el archivo script.py junto con tu servidor, server.py, en el mismo directorio, ya que le hemos indicado que interprete los archivos CGI en el directorio actual (cgi_directories = ["/"]). Si quieres especificar otra ubicación, puedes añadirla a la lista anterior (por ejemplo, cgi_directories = ["/", "/scripts/"]).

      Espero que resulte para solucionar tu inconveniente. Un saludo.

Deja una respuesta