Hace un par de semanas me encontré con la necesidad de utilizar uno de esos sistemas que hay por ahí para acortar URL‘s. Necesitaba enviar una dirección por SMS y tenía que ocupar la menor cantidad de caracteres posible por aquello de optimizar el texto del mensaje. Mientras lo utilizaba pensaba en lo ingenioso de utilizar un sistema de numeración base36 para reducir exponencialmente el número de caracteres de la redirección. Esto iba a ser, pues, un artículo sobre las ventajas de los sistemas de numeración distintos al decimal para determinados proyectos, pero se acabó convirtiendo en un proyecto completo. Cuando estaba comenzando la explicación teórica pensé, ¿por qué no hacerlo? ¿por qué no demostrar lo rápido y fácil que se puede montar algo en Internet hoy en día?
Así, tras unas 15 horas de trabajo os presento IraUrl.me, un acortador de URL’s al estilo de TinyUrl o Bit.ly. Me ha costado más escribir el artículo que hacerlo realidad, curioso ¿eh?. En realidad a medida que iba preparando la aplicación se me iban ocurriendo más cosas que sería interesante montar, por lo que las 8 horas iniciales, más o menos, se convirtieron en 15.
La teoría
Para el que no lo sepa, un acortador de URL se basa en encontrar un dominio lo más corto posible y crear redirecciones HTTP 301 a otras URL‘s. El truco está en optimizar los parámetros que añadiremos a la URL para que sean lo más cortos posible, no queremos que éstos nos penalicen lo corto del dominio.
¿Cómo funcionan entonces estos acortadores de URL‘s? Mucho más fácil de lo que parece y seguramente como a ti se te habría ocurrido. Simplemente tenemos una base de datos donde vamos añadiendo registros a una tabla a medida que se van creando nuevas URL’s cortas. Esta tabla tiene un campo autonumérico, la clave de la tabla, que para cada nueva URL nos devuelve un identificador único, con lo que cada dirección podría ser accesible de la manera habitual:
http://dominio.com/1
http://dominio.com/1000000
Esa es exactamente la idea, lo único que hacemos es cambiar el identificador en cuestión de base10 (la de nuestro sistema métrico decimal) a base36 o base62 en mi caso. Otros sistemas de numeración conocidos son el hexadecimal (base16) y base64.
Vale, ya has hablado en chino. ¿De qué va esto? Veamos.
Sobre bases de numeración
El sistema decimal utiliza diez dígitos (de ahí lo de decimal :P) para formar todas las combinaciones de números posibles. Lo que ya conocemos, vamos. El binario utiliza dos dígitos (0 y 1), el hexadecimal 16 (0..9ABCDE), base36, como su nombre indica, treinta y seis (0..9a..z) y base62 utiliza los 62 dígitos que comprenden los números del 0 al 9 y las letras de la A a la Z en mayúsculas y minúsculas (0..9a..zA..Z). Veamos unos ejemplos:
Binario |
Decimal |
Hexadecimal |
Base36 |
Base62 |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
10 |
2 |
2 |
2 |
2 |
1010 |
10 |
A |
a |
a |
1100100 |
100 |
64 |
2s |
1c |
|
1000000 |
F4240 |
lfls |
4c92 |
|
10000000 |
989680 |
5yc1s |
FXsk |
Se puede observar de un vistazo cómo a medida que aumenta el número, cuanto mayor sea la base que manejamos menos dígitos tendrá . Los números, a fin de cuentas, son combinaciones continuas entre todos los dígitos posibles.Así, en función de la base y del número de dígitos, el mayor número representable representable sería:
Num. dígitos
|
Decimal |
Base62 |
1 |
10 |
62 |
2 |
100 |
3844 |
3 |
1000 |
238328 |
4 |
10000 |
14776336 |
5 |
100000 |
916132832 |
6 |
1000000 |
56800235584 |
7 |
10000000 |
3521614606208 |
8 |
100000000 |
218340105584896 |
9 |
1000000000 |
13537086546263552 |
O lo que es lo mismo, base(número de dígitos), 629 contra 109.Espero que se entienda la teoría. Como curiosidad:
Decimal: 10000000000000000000000
Base62: 36aHo5IWaicak
La pregunta ahora sería, ¿Por qué Base62 y no Base64, por ejemplo, mucho más conocida? Sencillo, porque además de los 62 caracteres de Base62, Base64 utiliza dos adicionales, generalmente + y / además del =, lo que convierten la cadena en no web safe, es decir, los caracteres especiales debieran traducirse para que su transporte no diese problemas, con lo que estaríamos perdiendo las ventajas de nuestro cifrado corto. Los 62 caracteres utilizados en Base62 son totalmente seguros, sólo letras (mayúsculas y minúsculas) y números.
Sabiendo ya cómo funciona el sistema, veremos cómo crear nuestra aplicación. Obviamente no contaré todo paso a paso ya que sino tardaría mucho más en escribir el artículo que en hacer la aplicación, me meteré sólo en las cosas que considere más importantes.
Para codificar/decodificar de base10 a base62 utilizaré estas librerías:
function dec2base($dec, $base, $digits = FALSE) {
if($base < 2 or $base > 256) {
die("Invalid Base: .$basen");
}
bcscale(0);
$value = '';
if(!$digits) {
$digits = digits($base);
}
while($dec > $base - 1) {
$rest = bcmod($dec,$base);
$dec = bcdiv($dec,$base);
$value = $digits[$rest].$value;
}
$value=$digits[intval($dec)].$value;
return (string) $value;
}
function base2dec($value, $base, $digits = FALSE) {
if($base < 2 or $base > 256) {
die("Invalid Base: .$basen");
}
bcscale(0);
if($base < 37) {
$value = strtolower($value);
}
if(!$digits) {
$digits = digits($base);
}
$size = strlen($value);
$dec = '0';
for($loop=0; $loop < $size; $loop++) {
$element = strpos($digits, $value[$loop]);
$power = bcpow($base, $size-$loop-1);
$dec = bcadd($dec, bcmul($element, $power));
}
return (string)$dec;
}
function digits($base) {
if($base < 64) {
return substr('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', 0, $base);
} else {
return substr("x0x1x2x3x4x5x6x7x8x9xaxbxcxdxexfx10x11x12x13x14x15x16x17x18x19x1ax1bx1cx1dx1ex1f !x22#x24%&'()*+,-./0123456789:;<=>x3f@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~x7fx80x81x82x83x84x85x86x87x88x89x8ax8bx8cx8dx8ex8fx90x91x92x93x94x95x96x97x98x99x9ax9bx9cx9dx9ex9fxa0xa1xa2xa3xa4xa5xa6xa7xa8xa9xaaxabxacxadxaexafxb0xb1xb2xb3xb4xb5xb6xb7xb8xb9xbaxbbxbcxbdxbexbfxc0xc1xc2xc3xc4xc5xc6xc7xc8xc9xcaxcbxccxcdxcexcfxd0xd1xd2xd3xd4xd5xd6xd7xd8xd9xdaxdbxdcxddxdexdfxe0xe1xe2xe3xe4xe5xe6xe7xe8xe9xeaxebxecxedxeexefxf0xf1xf2xf3xf4xf5xf6xf7xf8xf9xfaxfbxfcxfdxfexff", 0, $base);
}
}
function base_encode($value) {
return dec2base(base2dec($value, 256), 62);
}
function base_decode($value) {
return dec2base(base2dec($value, 62), 256);
}
Las dos últimas funciones son las que utilizaremos para las conversiones.
Paquetes y librerías utilizadas:
- Free CSS Templates: Para tener una bonita plantilla xhtml para nuestro proyecto 🙂 .
- Maxmind GeoLite Country: Para la geolocalización de un usuario a través de su IP.
- Wurfl: Para identificar el navegador/terminal de un visitante por su User Agent. Yo lo complemento con Tera-Wurfl para mantener la información en una base de datos.
- Fusion Charts Free: Para los gráficos de estadísticas.
- Zero Clipboard: Para copiar al portapapeles la url corta generada sin que el usuario deba seleccionarla, solo con un click.
- JqueryUI: Para el componente de navegación con pestañas.
- Google Safebrowsing API: Para comprobar si una url es potencialmente peligrosa.
- Adodb (opcional): Para abstraer el acceso a la base de datos. Yo suelo utilizarla en todos mis proyectos pero se pueden utilizar las funciones nativas de PHP.
- PHPExcel: Para generar Excel y PDF.
Adicionalmente:
- Un dominio y hosting donde alojarlo (6€).
- PHP y MySQL (tampoco son obligatorios, puedes hacerlo con cualquier tecnología).
- 15 horas de tu tiempo :P.
Estructura de la web
Cualquier proyecto web que se precie debe comenzarse describiendo qué queremos mostrar a nuestros visitantes, hay que recopilar todas las ideas, decidir las que interesan de verdad, estudiar cómo se van a disponer en el frontend y terminar con un mapa web que nos indique el flujo a seguir en el trabajo. Este será el nuestro:
Cuando un usuario vaya a una de nuestras url’s cortas en realidad estaremos reenviando la petición http internamente a un script encargado de hacer todo el proceso, link.php en mi caso.
Para ver las estadísticas de una URL me ha gustado el sistema de bit.ly, así que se lo copiamos :P. Añadiendo un “+” al final de la URL corta, en vez de saltar a la dirección larga mostraremos las estadísticas. Esto lo haremos, como en el caso anterior, dirigiendo internamente a otro script, stats.php.
Si el identificador que pretendemos usar para saltar a la url larga o ver estadísticas no existe, reenviaremos a index.php para que muestre un mensaje de error tipo “La url no existe“.
El dominio
Obviamente tendremos que buscar un dominio lo más corto posible, la mayoría estarán ya ocupados, pero buscando y buscando en TLD‘s extraños puedes encontrar algo. Yo he escogido un .me porque tiene un carácter menos que un .com 🙂 y no cuesta lo que un .es :P.
La base de datos
Muy sencilla, dos tablas solamente, en una mantendremos las urls generadas y en otra las estadísticas de acceso a las mismas.
CREATE TABLE IF NOT EXISTS `urls` (
`id` bigint(20) NOT NULL auto_increment,
`url` varchar(500) NOT NULL,
`titulo` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `stats` (
`id` bigint(20) NOT NULL auto_increment,
`idurl` bigint(20) NOT NULL,
`codpais` varchar(255) NOT NULL,
`referer` varchar(255) NOT NULL,
`hostreferer` varchar(255) NOT NULL,
`ua` varchar(255) NOT NULL,
`hora` datetime NOT NULL,
`pais` varchar(255) NOT NULL,
`marca` varchar(255) NOT NULL,
`modelo` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idurl` (`idurl`,`hora`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
La configuración de Apache
Como hemos comentado queremos que cuando alguien vaya a cualquier url de nuestro site del tipo
http://tudominio.com/prueba3
Se redirija internamente al script link.php que será el encargado de procesar la petición. De igual modo si alguien visita
http://tudominio.com/prueba3+
le mostraremos las estadísticas de esa URL (si existen). Configuramos Apache para que tenga en cuenta todas estas particularidades, mod_rewrite será nuestro amigo para conseguirlo. En mi caso he hecho que si la llamada no es un script php, ni una imagen ni un archivo javascript ni un css ni tiene el signo “+“, se vaya a link.php. Si tiene el signo “+” se irá a stats.php.
RewriteEngine on
RewriteCond %{REQUEST_URI} !^/(.*).php$
RewriteCond %{REQUEST_URI} !^/css/(.*)$
RewriteCond %{REQUEST_URI} !^/js/(.*)$
RewriteCond %{REQUEST_URI} !^/(.*)+$
RewriteCond %{REQUEST_URI} !^/images/(.*)$
RewriteRule ^(.+) /link.php?id=$1
RewriteCond %{REQUEST_URI} ^/(.*)+$
RewriteRule ^(.+) /stats.php?id=$1
Imagen y diseño
Para el diseño, o te lo haces tú mismo o si eres un negado creativo como yo te descargas una plantilla superchula de freecsstamplates.org y la adaptas a tus necesidades, no voy a entrar en más detalles.
Crear urls cortas
El primer script de nuestra aplicación. Un sencillo formulario donde el usuario introduce la URL que quiere acortar y al hacer submit… la acortamos integrando el sistema con la comprobación de malware que explicaba hace unos días ;).
$urlbase="";
if(isset($_POST['url'])){
$url=$_POST['url'];
try{
if(substr($url, 0, 4)!="http")
$url="http://".$url;
$class = new GoogleSafeBrowsing('ABQIAAAAQYvf-54bCBMuGY20SeONVxQ236Mc_IjryQBl-W_Repaw3fCykA', true);
$nomalware=$class->lookupsFor($url);
if($nomalware){
$htmltitle="";
$html=file_get_contents($url);
if($html!=""){
preg_match('/(.*)</title>/is', $html, $matches);
if(is_array($matches) && count($matches>0))
$htmltitle=trim($matches[1]);
}
$query="select * from urls where url='$url'";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
$id=$rs->fields['id'];
}else{
$query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
$rs=$conn->Execute($query);
$id=$conn->insert_id();
}
$base=base_encode($id);
$urlbase="http://iraurl.me/".$base;
}else{
$err=4;
}
}catch(exception $e){
$err=3;
}
}
Hemos añadido una pequeña comprobación. Si la URL que se quiere añadir ya existe, devolvemos la misma URL corta, yo he tomado esa decisión, tú puedes hacer lo que quieras. Además obtenemos el título de la URL final para tener una referencia hacia la misma, cuestión de sencillez visual :P.
Reenvío a urls largas
Ya tenemos nuestra URL corta, vamos ahora a reenviar las solicitudes a ella a la larga. Recordemos que nuestro Apache nos va a redirigir esa petición a link.php?id=XXXX. Nuestro script actualiza, además, las estadísticas de visitas de la url.
if(isset($_GET['id'])){
$idb=$_GET['id'];
$id=base_decode($idb)+0;
try{
$query="select * from urls where id=$id";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
$url=$rs->fields['url'];
$referer=@$_SERVER['HTTP_REFERER'];
$ua=@$_SERVER['HTTP_USER_AGENT'];
$ip=@$_SERVER['REMOTE_ADDR'];
$hostreferer="";
if(preg_match('@^(?:http://)?([^/]+)@i', $referer, $matches)>0)
$hostreferer = $matches[1];
$terminal=getMarcaModelo($_SERVER['HTTP_USER_AGENT']);
$marca=$terminal['marca'];
$modelo=$terminal['modelo'];
$temp=getGeoCodeAndPais($ip);
$codpais=$temp['code'];
$pais=$temp['pais'];
$query="insert into stats (idurl, codpais, referer, ua, hora, pais, marca, modelo, hostreferer) VALUES
($id, '$codpais', '$referer', '$ua', now(), '$pais', '$marca', '$modelo', '$hostreferer')";
$rs2=$conn->Execute($query);
header("HTTP/1.x 301 Moved");
header("Location: $url");
exit;
}else{
header("Location: http://iraurl.me/index.php?err=1");
exit;
}
}catch(exception $e){
header("Location: http://iraurl.me/index.php?err=2");
exit;
}
}
header("Location: http://iraurl.me/index.php?err=1");
Como veis, si la URL no existe redirigimos al usuario a index.php con un mensaje de error. Necesitaremos dos funciones adicionales, las que nos devuelven información del país de origen de una IP y los datos del terminal del usuario (móvi o web). No entraré en detalles sobre la instalación de Maxmind GeoLite Country o de Wurfl/Tera-Wurfl.
function getGeoCodeAndPais($ip){
require_once(dirname(__FILE__)."/geoip/geoip.inc");
$gi = geoip_open("/usr/share/GeoIP/GeoIP.dat",GEOIP_STANDARD);
$codpais=geoip_country_code_by_addr($gi, $ip);
$pais=geoip_country_name_by_addr($gi, $ip);
geoip_close($gi);
return array("pais"=>$pais, "code"=>$codpais);
}
function getCapabilities($ua){
require_once(dirname(__FILE__)."/Tera-WURFL/TeraWurfl.php");
$wurflObj = new TeraWurfl();
$matched = $wurflObj->GetDeviceCapabilitiesFromAgent($ua);
$movil = $wurflObj->capabilities;
return $movil;
}
Estadísticas
La teoría es la misma. Si existe la URL cargamos los datos, si no redirigimos a la home. En nuestra caso utilizamos el componente de pestañas de JqueryUI para organizar los distintos tipos de datos que permitiremos ver y añadiremos los botones para exportar a Excel y PDF.
$idb=substr($_GET['id'], 0, strlen($_GET['id'])-1);
$id=base_decode($idb)+0;
$query="select * from urls where id=$id";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
$urlbase="http://iraurl.me/".$idb;
$url=$rs->fields['url'];
$id=$rs->fields['id'];
$htmltitulo=$rs->fields['titulo'];
if($htmltitulo=="")
$htmltitulo=$url;
$query="select count(*) as nregs from stats where idurl=$id";
$rs=$conn->Execute($query);
$clicks=$rs->fields['nregs'];
}else{
header("Location: http://iraurl.me/index.php?err=1");
exit;
}
Muy sencillo.
Lo complicado en este caso es mostrar las gráficas con FusionCharts. Para cada una debemos añadir algo de código html:
<div id="chartClicks"></div>
<script type="text/javascript">
var myChart = new FusionCharts("images/Charts/FCF_Column3D.swf", "idChartClicks", "430", "400", "0", "1");
myChart.setDataURL(escape("xml.php?t=cli&id='.$idb.'"));
myChart.setTransparent(true);
myChart.render("chartClicks");
</script>
El script xml.php será el que devuelva los datos en el formato adecuado para FusionCharts. Por ejemplo:
$query="select DAY(hora) as dia, MONTH(hora) as mes, YEAR(hora) as ano, count(*) as nclicks
from stats
where idurl=$id
group by ano, mes, dia
order by hora";
$rs=$conn->Execute($query);
$xml='<graph caption="Clicks" rotateNames="1" xAxisName="Día" yAxisName="Clicks" showNames="1" decimalPrecision="0" formatNumberScale="0" chartLeftMargin="5" chartRightMargin="5" chartTopMargin="0">';
while($r=$rs->fetchrow()){
$xml.='<set name="'.$r['dia']."/".$r['mes']."/".$r['ano'].'" value="'.$r['nclicks'].'" color="#A1A1A1" />';
}
$xml.='</graph>';
Os doy sólo un ejemplo, el resto lo montáis por vuestra cuenta :).
Descifrar urls cortas
Todos los sistemas de acortar URL’s funcionan tal y como cuento en este artículo, haciendo un HTTP/301 redirect hacia la url original.
A partir de la URL corta podemos saber cual es la URL original simplemente siguiendo las redirecciones que hace. Muy sencillo con PHP y que, además nos sirve para, integrándola en nuestra API de malware, prevenir posibles problemas con la URL final.
function get_web_page( $url )
{
$options = array( 'http' => array(
'user_agent' => 'spider',
'max_redirects' => 10,
'timeout' => 120,
) );
$context = stream_context_create( $options );
$page = @file_get_contents( $url, false, $context );
$result = array( );
if ( $page != false )
$result['content'] = $page;
else if ( !isset( $http_response_header ) )
return null; // Bad url, timeout
// Save the header
$result['header'] = $http_response_header;
// Get the *last* HTTP status code
$nLines = count( $http_response_header );
for ( $i = $nLines-1; $i >= 0; $i-- )
{
$line = $http_response_header[$i];
if ( strncasecmp( "HTTP", $line, 4 ) == 0 )
{
$response = explode( ' ', $line );
$result['http_code'] = $response[1];
break;
}
}
return $result;
}
$url="";
if(isset($_POST['url'])){
$url=$_POST['url'];
$datos=get_web_page( $url );
if($datos){
$headers=$datos['header'];
$urls=array($url);
foreach($headers as $head){
$temp=explode(" ", $head);
if(strtolower($temp[0])=="location:"){
$urls[]=$temp[1];
}
}
$htmltitle="";
preg_match('/(.*)</title>/is', $datos['content'], $matches);
if(is_array($matches) && count($matches>0))
$htmltitle=trim($matches[1]);
}
}
Ya está, en $urls tendremos la lista de urls que van saltando hasta llegar a la final.
Api
Hoy en día todo tiene que tener Api. Para las estadísticas es muy sencillo, el propio XML que generamos para consumir con FusionCharts nos permite que clientes externos se alimenten del mismo. Para crear URL‘s cortas remotamente, simplemente creamos un archivo api.php:
if(isset($_GET['url'])){
$url=urldecode($_GET['url']);
try{
$htmltitle="";
if(substr($url, 0, 4)!="http")
$url="http://".$url;
$html=file_get_contents($url);
if($html!=""){
preg_match('/(.*)</title>/is', $html, $matches);
if(is_array($matches) && count($matches>0))
$htmltitle=trim($matches[1]);
}
$query="select * from urls where url='$url'";
$rs=$conn->Execute($query);
if($rs->recordcount()>0){
$id=$rs->fields['id'];
}else{
$query="insert into urls (url, titulo) VALUES ('$url', '$htmltitle')";
$rs=$conn->Execute($query);
$id=$conn->insert_id();
}
$base=base_encode($id);
$urlbase="http://iraurl.me/".$base;
echo $urlbase;
}catch(exception $e){
echo "ERROR";
}
}
Eso es todo. No olvides integrarlo también con el sistema de malware.
Conclusiones
Bueno, y todo este rollo ¿para qué?. Pues muy sencillo, para que veais que hoy en día la tecnología está al alcance de todos, es sencillo y rápido crear un proyecto en Internet, hay de todo por todas las esquinas, la tecnología no es lo importante, lo que verdaderamente cuenta es cómo mueves ese producto tecnológico para rentabilizarlo y obtener un beneficio de él.
Ya tengo mi proyecto superchulo funcionando, sólo me ha costado unas 15 horas de trabajo. Le he puesto un poco de Adsense por aquí y por allí. ¿Y ahora qué? ¿A esperar a que la gente entre y me haga millonario? 😛 Es mucho más complicado que eso como todos sabéis, primero tienes que tener una masa de usuarios elevada que le dé movimiento al proyecto y después tienes que conseguir que la mayoría de ellos sean gente normal, no gente técnica, usuarios avanzados que no pagamos por nada ni pinchamos en publicidad :P.
Hoy en día, en Internet, como en cualquier negocio, las técnicas de marketing y venta son mucho más importantes que la tecnología en sí misma, es duro reconocerlo, pero es así. De nada sirve que tengas el mejor producto del mundo mundial si no consigues que la gente lo utilice y se deje dinero, así de claro. Si tienes los conocimientos adecuados para mover el negocio, no te preocupes, la tecnología te la aporta cualquier partner por un módico precio, pero poner en manos de otro toda la estrategia de ventas de tu negocio no está tan claro ¿no?.
Espero que os sirva de algo el artículo. He querido mostrar, fundamentalmente, cómo utilizando algunas librerías que puedes obtener sin coste puedes hacer algo realmente útil y funcional con muy poco esfuerzo. Seguro que sacáis alguna idea.
Perdón por el rollo :P, al final me ha costado mucho más escribir el artículo que implementarlo.