Optimización del sitio web de Hawley Lane Dental

La clínica dental Hawley Lane Dental, situada en Connecticut, Estados Unidos, dispone de un sitio web bien diseñado y funcional, pero presentaba unos inconvenientes para sus usuarios en los que me he puesto a trabajar:

El primero: ya solucionado, era la llegada indiscriminada de spam al sitio.
El segundo: una cierta lentitud del sitio sobre la que voy a trabajar para optimizar los tiempos de descarga. No tan sólo a los usuarios les cansan los sitios lentos, también los buscadores puntúan algo mejor a los sitios que descargan rápido.

Desde luego cada uno de estos temas da, como mínimo, para un artículo propio pero de momento dejemos éste por finalizado.

Drupal: en mi opinión el mejor CMS en PHP

Existen bastantes páginas donde podemos encontrar comparativas y opiniones de diferentes CMS’s. La opinión que aquí expreso está basada en mis años de experiencia y me centraré en los 3 más populares desarrollados en PHP: Drupal, Joomla y WordPress. En Python me parece excelente Plone, aunque complejo, en .NET todavía no he conocido ninguno bueno pero en este artículo no hablaré de ellos.

WordPress fue concebido para los blogs pero hoy en día dispone de muchos plugins alguno de los cuales pretende que WordPress pueda ir más allá. Un ejemplo representativo es crear una tienda con WordPress y mediante un plugin añadirle la funcionalidad de e-commerce: he tenido que sufrir ese plugin y ver que simplemente WordPress no da la talla. Probablemente es el mejor CMS para gestionar blogs y sitios web muy estándar pero tiene carencias importantes sacado de ese contexto. Por supuesto eso no significa que no pueda salir del mundo de los blogs, pero hay CMS’s mejores para ello. Es el que hago servir para este sitio web y es realmente muy fácil, rápido y cómodo de mantener, así como también lo fue crearlo.

Joomla es uno de los CMS más populares. Su principal ventaja sobre Drupal es que es más rápido desarrollar un sitio, siempre y cuando sea un sitio web que no se aleje demasiado de aquello para lo que Joomla fue ideado. En otras palabras: cuando debemos personalizar y empezar a tocar el corazón para dotar al sitio de nuevas funcionalidades las cosas se complican sobremanera.

Cuando se quiere un sitio personalizado me parece que Drupal es ideal. No tan solo por la creación de diseños personalizados mediante temas (themes), como cualquier otro CMS, sino también por la capacidad de crear nuevas funcionalidades y modificar las existentes mediante «hooks» o «callbacks». Los «hooks» nos permiten tanto la modificación del núcleo del CMS como la de los módulos sin tocar una sola línea del código original, lo que permite actualizar tanto el núcleo como los módulos sin que se pierdan las funcionalidades implementadas en los hooks. Y hablando de módulos, la cantidad de estos es ingente: desde módulos para hacer copias de seguridad de la base de datos hasta módulos para crear tipos de contenidos y campos personalizados pasando por módulos para optimizar el sitio para los motores de búsqueda, módulos para mostrar galerías de imágenes o vídeos, etc. Hay tantos módulos que no es muy frecuente que debamos desarrollar uno propio. La perfecta separación entre por un lado el núcleo del CMS y los módulos desarrollados por terceros y por el otro nuestras modificaciones hacen que un sitio en Drupal sea muy agradable de mantener, siempre y cuando en el desarrollo se haya respetado dicha separación.

Al hablar de las actualizaciones he tocado el tema de la seguridad y ahí Drupal también se sitúa mejor: la reputación de Joomla en lo que a seguridad se refiere no es muy buena.

Obviamente no todo son ventajas con Drupal. Su principal defecto es que el código sea un poco lo que en la jerga de los programadores llamamos «spaghetti code». Drupal, al menos en su versión 6, no utiliza muchas de las características de orientación a objetos que proporciona PHP 5 aunque a cambio puede funcionar con PHP 4.4.0. Si miramos el código la primera impresión es que es procedimental: no encontraremos clases. Si bien esa herencia de la versión 4 de PHP ahora le pesa, en su diseño se incluyeron algunos de los principios de la orientación a objetos que lo mitigan parcialmente. Componentes como los módulos, temas, nodos y usuarios encajan en la descripción de objeto. Incluso se emuló la herencia, la abstracción, la encapsulación y el polimorfismo. Por otro lado, los usuarios menos avezados que tengan la tarea de administrar el sitio web pueden encontrar algunos aspectos del back-end difíciles de usar, por ejemplo he oído quejas sobre la dificultad del módulo vistas pero es también éste uno de los módulos más poderosos de Drupal y no es fácil simplificarlo manteniendo al mismo tiempo toda su potencia. Lo que sucede con dicho módulo es un buen resumen de la idea detrás de Drupal: muy configurable y versátil pero eso tiene un coste en su facilidad de uso.

Otra característica de Drupal, que no es positiva ni negativa a mi entender, es que no usa el hoy en día tan popular patrón MVC (Modelo Vista Controlador) sino el menos conocido PAC (Presentación Abstracción Control). (Aunque por ahí hay gente intentado hacer encajar Drupal en el patrón MVC). Que el patrón MVC sea el más popular no implica necesariamente que sea el mejor para la web, frecuentemente en el mundo de la informática lo más popular (léase usado) no es lo mejor pues hay otros factores que entran en juego, pero todo eso ya sería tema para muchos artículos 😉 y no tiene cabida en esta breve oda de Drupal.

Guía Cirugía

Colaboro conjuntamente con Net Midas, en el aspecto técnico (me encargo de la programación e implementación) del interesante proyecto «Guía Cirugía». Guía Cirugía será un directorio donde usuarios de todo el mundo podrán ponerse en contacto con cirujanos plásticos de toda América Latina y España. Entre otros aspectos, el portal también será un punto de encuentro entre usuarios, un lugar donde los cirujanos publicarán artículos especializados, donde los usuarios podrán plantear sus dudas a los cirujanos, incluirá una sección para cirujanos y más funcionalidades y apartados de los cuales les iremos informando aquí en el blog de VIC Services.

Si bien la colaboración será inicialmente en el desarrollo de todo el apartado técnico pero espero poder aportar también ideas y conocimientos en otros aspectos.

¡Les mantendré informados!

Nuevos proyectos

Empiezo nuevos proyectos conjuntamente con Net Midas. El primero de ellos será el desarrollo y la implementación de un website para un cliente final norteamericano que de momento prefiere el anonimato. Mis tareas serán la maquetación, la programación del gestor de contenidos y la implementación del sistema en el servidor. El cliente lo usará como blog, como lugar donde publicar su información financiera, vídeos y demás contenidos. Cuando el proyecto esté más desarrollado y el cliente esté dispuesto a ello, les daré más información.

Confío en que la relación con Net Midas, empresa especializada en la consultoría del marketing online, será fructífera y que éste proyecto va a ser el primero de muchos.

Mis primeros 4 scripts en Python.

Os presento mis primeros 4 scripts en Python. Me parece un lenguaje muy interesante con unas librerías muy potentes, creo que en parte es debido a que es fácil portar librerías de C a Python y en este lenguaje se ha programado de todo. Desde luego que no son para un manual de programación en Python, os los presento solo para que veáis la potencia del lenguaje.

Lo que hace éste script está comentado en el mismo código:

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

# El script espera dos argumentos:
# 1 - Una fecha en formato '%d-%m-%Y %H:%M', por ejemplo '21-01-2011 11:40'
# Mostrará sólo los ficheros que se hayan modificado después de esta fecha.
# 2 - Un path en formato *nix, por ejemplo '/home/vic/python'. No acepta '~' para referirse a 'home'.
# Buscará recursivamente, subdirectorios incluidos, sin profundizar en enlaces simbólicos, todos los ficheros a partir de ese path.
#
# os.path.getmtime -> Da el número de segundos desde epoch (medianoche UTC desde el 1 de enero de 1970).
# time.strptime devuelve una estructura time (struct_time).
# time.mktime(struct_time) devuelve el nº de segundos desde epoch. time.localtime(segundos_desde_epoch) hace la función inversa.
# El nº de segundos desde epoch se expresa en coma flotante.
#
#Para la fecha de creación de un fichero o dir: stat -c %w nombre
#Para saber la fecha de la última molificación: stat -c %y nombre
#Cuando se crea un fichero dentro de un directorio se modifica solo la fecha de modificacion del dir y no la de creación.
#Cuando se modifica un fichero dentro de un directorio no se altera la fecha de modificación ni de creación del dir.

import os, time, sys
from sys import argv

stModDesde = time.strptime(argv[1], '%d-%m-%Y %H:%M')
fModDesde = time.mktime(stModDesde)

top = argv[2]
for root, dirs, files in os.walk(top, topdown=False):
    for name in files:
        elemPath = os.path.join(root, name)
        fModificado = os.path.getmtime(elemPath)
        if (fModificado > fModDesde):
            print 'Fichero ' + elemPath + ' Modificado:' + time.ctime(fModificado)
#     for name in dirs:
#         elemPath = os.path.join(root, name)
#         Si queremos trabajar también con los directorios descomentar este for / in

¡Daros cuenta de que tan solo son 11 líneas! Aquí una función en Object Pascal que hice hará unos 7 años que aun teniendo que hacer algo más sencillo (buscar un fichero en el directorio actual y subdirectorios) era más compleja: recursiva y con más líneas de código.

unit Llibreria;

interface
uses
SysUtils, Classes, Forms;

procedure busca(path: string; carpet: string; coincidencias: TStrings);

implementation

procedure busca(path: string; carpet: string; coincidencias: TStrings);
    var
    sr: TSearchRec;
    res: byte;
    lg: integer;
    begin
        lg:=strLen(PChar(path)); //lg:=length(path);
        path:=path+carpet;
        res:=FindFirst(path, faAnyFile, sr);
        if (res=0) then coincidencias.Add(copy(path,1,lg) + sr.Name);
        FindClose(sr);
        path:=copy(path,1,lg) + '*.*';
        res:=FindFirst(path, faAnyFile, sr);
        while (res=0) do begin
            if (((sr.Attr and faDirectory)<>0) and (sr.Name[1]<>'.')) then begin
                path:=copy(path,1,lg);
                path:=path + sr.Name + '\';
                application.ProcessMessages;
                busca(path,carpet,coincidencias);
            end;
            res:=FindNext(sr);
        end;
    FindClose(sr);
    path:=copy(path,1,lg);
    end;
end.

Aquí un script para criptografía simétrica, usa el algoritmo Blowfish. El modo de cifrado es CTR, pero no le hagáis mucho caso a la función del contador. Una vez más creo que los comentarios ya explican bastante. (Python es el primer lenguaje en que me veo obligado a usar más líneas de comentarios que de código)

#! /usr/bin/env python
# -*- coding: latin-1 -*-

# El script espera 3 argumentos:
# 1º: Encriptar / Desencriptar: e / d
# 2º y 3º: paths a ficheros.
#     Con 'e' el 2º parámetro es el fichero a encriptar y el 3º el del encriptado (el resultado).
#     Con 'd' el 2º parámetro es el fichero encriptado y el 3º el desencriptado (el resultado).
#     ATENCIÓN: ¡en ambos casos el fichero resultante se sobreescribirá si ya existe!
#
# A continuación pedirá la clave.

from Crypto.Cipher import Blowfish
from os import path
from sys import argv, exit
import getpass

if not path.isfile(argv[2]):
    exit(argv[2] + " no existe o no es un fichero.")

# Añadir 'b' (binary) lo hace portable a Windows que sí distingue entre ficheros de texto y binarios,
# permitiendo encriptar tanto ficheros de texto como binarios.
f1 = open(argv[2], 'rb')

passw = getpass.getpass("Contraseña:")

class contador(object):
    def __init__(self, inicio):
        self.cont = inicio
    def next(self):
        self.cont = self.cont + 1
        sCont = str(self.cont)
        while len(sCont) < 8:
            sCont = sCont + '0'
        return sCont

c = contador(0)

oBlow = Blowfish.new(passw, Blowfish.MODE_CTR, counter=c.next)
# El primer param. es la clave. El segundo es el modo.
# Parece que el modo ECB es el más inseguro y CTR el más seguro. Ver http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher_feedback_.28CFB.29
# El tercer parámetro no es opcional en el caso de CTR: requiere el contador. Debe ser una cadena de 8 carácteres.
# Se instancia el objeto antes del if pues el contador debe ser el mismo para la encriptación y la desencriptación.
# Usar algo como "sAleatoria = os.urandom(8)" tiene el inconveniente de tener que almacenar en el fichero ese contador para luego poder desencriptar.

if argv[1] == 'e':
    texto = f1.read() # Texto a encriptar.

    # La cadena a encriptar debe tener una longitud que sea múltiple de 8. Independientemente de la extensión de la clave.
    while (len(texto) % 8 != 0):
        texto = texto + chr(3) # El ASCII 3 es End of TeXt (ETX).

        textoEncriptado = oBlow.encrypt(texto)
        #print "Texto encriptado: " + textoEncriptado
        #textoDesencriptado = oBlow.decrypt(textoEncriptado)
        #print "Texto desencriptado: " + textoDesencriptado
        f2 = open(argv[3], 'wb') # Sobreescribe el fichero si ya existe.
        f2.write(textoEncriptado)
        f1.close()
        f2.close()
elif argv[1] == 'd':
    textoEncriptado = f1.read()
    texto = oBlow.decrypt(textoEncriptado)
    f2 = open(argv[3], 'wb')
    f2.write(texto)
    f1.close()
    f2.close()
else:
    print "Opción desconocida. 'e' para encriptar y 'd' para desencriptar"

¿Y Python para la web? He aquí un pequeño script que lee todas las referencias de productos de un fichero csv para después buscar en la web de la empresa las imágenes que les corresponden y guardarlas en el disco duro.

Creo que con las librerías «mechanize» y «BeautifulSoup» podemos construir fantásticas aplicaciones de testeo web, de una manera más rápida, eficiente y consumiendo muchos menos recursos que con herramientas como Selenium. Aquí el código:

#! /usr/bin/env python
# coding=utf-8

import mechanize
import cookielib
from BeautifulSoup import BeautifulSoup
import urllib

br = mechanize.Browser()

cj = cookielib.LWPCookieJar()
br.set_cookiejar(cj)
#Inicio configuración browser
br.set_handle_equiv(True)
br.set_handle_gzip(False)
br.set_handle_redirect(True)
br.set_handle_referer(True)
br.set_handle_robots(False)

#Para debug:
#br.set_debug_http(True)
#br.set_debug_redirects(True)
#br.set_debug_responses(True)

# User-Agent (no es Linux! pero lo engañamos O:-)
br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1')]

br.set_proxies({'http' : '192.168.4.4:3128'})

# Fin configuración browser
# Abre fichero de referencias:
f = open('/cygdrive/c/users/vic/documents/Empresa/Empresa.csv', 'r')
fContent = f.readlines()
for ref in fContent:
    ref = ref.strip()
    print "'" + ref + "'"
    #Busca por cada referencia su imagen:
    response1 = br.open("http://www.empresa.com/en/search/node/" + urllib.quote(ref))
    #print br.title() Título de la página
    #print response1.info() # Cabeceras
    #print response1.read() # HTML
    # Si no obtenemos HTML da un error:
    if not br.viewing_html():
        continue
    sHtml = response1.read()
    if sHtml.find('http://www.empresa.com/en/products/') == -1:
        continue
    #Sigue el link del resultado de la búsqueda:
    response1 = br.follow_link(url_regex = "http://www.empresa.com/en/products/")
    if not response1:
        continue;
    sHtml = response1.read()
    #Pone el HTML en la sopa para procesarlo:
    soup = BeautifulSoup(sHtml)
    #Busca la imagen y obtiene su valor src
    bsTagImg = soup.find(id='productimage')
    src = bsTagImg['src']
    #Descarga la imagen en /tmp
    fTemp = br.retrieve(src)[0]
    #La copiamos al directorio que queramos:
    fh = open(fTemp, 'rb')
    fContentImg = fh.read()
    f2 = open('/home/vic/python/ref_' + ref + '.jpg', 'wb')
    f2.write(fContentImg)
    fh.close()
    f2.close()
f.close()

Realmente un lenguaje potente, ¿verdad?