Le language MathML


6 avril 2007
Essayer de trouver une syntaxe simple pour représenter graphiquement des formules à partir de code du genre :

    dx(sup(n)sub(i))/ds=~=(x(sup(n)sub(i+1))-x(sup(n)sub(i-1)))/2delta()s
    dy(sup(n)sub(i))/ds=~=(y(sup(n)sub(i+1))-y(sup(n)sub(i-1)))/2delta()s
    d(sup(2))x(sup(n)sub(i+1))/ds(sup(2))=~=(x(sup(n)sub(i+1))-2x(sub(i)sup(n))+x(sup(n)sub(i-1)))/delta()s(sup(2))
    d(sup(2))y(sup(n)sub(i+1))/ds(sup(2))=~=(y(sup(n)sub(i+1))-2y(sub(i)sup(n))+y(sup(n)sub(i-1)))/delta()s(sup(2))

Les parenthèses représentent des sous-blocs à rendre relativement à un bloc parent
Une double parenthèse (un bloc lui-même entre parenthèses) affiche des parenthèses
Des fonctions pré-existantes (built-ins) pour des opérations de base :
sup(%bloc%) : bloc en exposant
sub(%bloc%) : bloc en indice
delta() : signe delta minuscule
Delta() : signe delta majuscule
=~= : pour le signe "presque égal"

Proximité des villes françaises


17 décembre 2006

SIG - Les villes de France et leurs proximité

J'ai pour but d'analyser les 38052 communes de France par rapport à une ville "pivot", et parmi celles qui comptent plus de 5000 habitants, d'en déduire une liste triée des villes les plus proches. Bien évidemment les données sont stockées dans une base de données (MySQL pour ne pas la nommer), que j'ai reprise depuis ce site très intéressant et la volonté ici est d'être le plus performant possible, d'où mon choix du C++.

Tout d'abord il me faut présenter la structure de la base de données, dans un pseudo-langage de description qui m'est propre mais simple à comprendre :

<component name='erpCity' basedOn="qbase" language="FR" fullName="Villes" Gender="female" tableName='erpCities'>
    <fields>
        <shorttext name='inseeCode' formHeader='Code INSEE'/>
        <shorttext name='name' formHeader='Nom de la ville'/>
        <float name='latitude' formHeader='Latitude (en radians)'/>
        <float name='longitude' formHeader='Longitude (en radians)'/>
        <shorttext name='zipCode' formHeader='Code postal'/>
        <integer name='pop' formHeader='Population'/>
        <float name='density' formHeader='Densité de population'/>
    </fields>
</component>

L'insertion de ces données dans une base MySQL se fait de façon directe en PHP :

<html>
<head>
    <title>Création de la base de données des villes</title>
    <meta http-equiv='Content-Type' value='test/html; charset=utf-8'/>
</head>
<body>
<?

include_once "./dir.php";
include_once $gIncludeDir."/__helpers.php";
include_once "component.php";

    $Datas = file_get_contents("villes.csv");
    $FieldNames = array("Insee"=>"inseeCode", "Nom"=>"name",
                        "LatitudeRadian"=>"latitude", "LongitudeRadian"=>"longitude",
                        "CodePostal"=>"zipCode", "NombreHabitants"=>"pop", "Densite"=>"density");
    $Lines = explode("\n", $Datas);
    $NbLines = count($Lines);
    echo "Nombre de communes : $NbLines<br/>\n";
    $Comp = new Component("city");
    $Comp->CreateTable();       //On réinitialise la table à chaque fois
    $GlobalRet = true;
    for($i=0; $i<$NbLines; $i++)        {
        if (!$i)        continue;       //On ignore la première ligne
        $Values = explode(";", $Lines[$i]);
        //
        $Query = "insert into qt_erpCities (id, inseeCode, name, latitude, longitude, zipCode, pop, density) values (0, \"$Values[0]\", \"$Values[1]\", $Values[2], $Values[3], \"$Values[4]\", $Values[5], $Values[6]);";
        if (!($Ret = DB_ExecQuery($Query)))     echo "Problème d'insertion : <blockquote>".mysql_error()."</blockquote>\n";
        $GlobalRet &= $Ret;
    }
    if (!$GlobalRet)        echo "<p>Il y a eu au moins un problème d'insertion.</p>";

?>

</body>
</html>

L'exécutable qui tournera sur le serveur utilise un tri Radix et un wrapper MySQL pour simplifier l'écriture :

class   CityRanks : public Application
{
    struct  City
    {
        float   latitude, longitude;
        float   distance;
    };
    public:
        CityRanks()     {       }
        virtual     int run()
        {
            int     i;
            //On parse les paramètres de la ligne de commandes
            int     NbParamsToParse = _argc-1;
            if (NbParamsToParse < 2)        return EXIT_FAILURE;
            if (NbParamsToParse > 2)        NbParamsToParse = 2;
            int*    Params = new int[2];
            for(i=0; i<NbParamsToParse; i++)        sscanf(_argv[i+1], "%d", &Params[i]);
            //Paramètres de la ligne de commande :
            int Pivot = Params[0];
            int NbResults = Params[1];
            //
            MySQL*  DB = new MySQL();
            City**  Cities = new City*[1700];
            char**  CityNames = new char*[1700];
            memset(CityNames, 0, sizeof(char*)*1700);
            int     NbCities = 0;
            //
            DB->serverInit();
            if (DB->init() && DB->connect("Nom de la base de données", "Nom de l'utilisateur", "Mot de passe de l'utilisateur"))        {
                if (DB->query("select name,latitude,longitude from qt_erpCities where pop>=5000 group by inseeCode order by pop desc;"))        {
                    City*   CurCity = NULL;
                    MYSQL_ROW   Row;
                    while (DB->fetchRow(Row))       {
                        CurCity = Cities[NbCities] = new City;
                        string_affectCopy(&CityNames[NbCities], Row[0]);
                        sscanf(Row[1], "%f", &CurCity->latitude);
                        sscanf(Row[2], "%f", &CurCity->longitude);
                        NbCities++;
                    }
                    DB->freeResult();
                }   else    return EXIT_FAILURE;
            }   else    return EXIT_FAILURE;
            //Je me suis inspiré de http://en.wikipedia.org/wiki/Great-circle_distance
            float   EarthRadius = 6372.795f;
            float   PivotLongitude = Cities[Pivot]->longitude;
            for(i=0; i<NbCities; i++)       {
                //On calcule la distance entre la ville <i> et la ville témoin/pivot
                float   Lat1 = Cities[i]->latitude;
                float   Lat2 = Cities[Pivot]->latitude;
                float   dLat = Lat1 - Lat2;
                float   dLong = Cities[i]->longitude - PivotLongitude;
                float   SinLat = sin(dLat/2);
                float   SinLong = sin(dLong/2);
                float   dSigma = 2.0f * asin(sqrt(SinLat*SinLat+cos(Lat1)*cos(Lat2)*SinLong*SinLong));
                Cities[i]->distance = EarthRadius * dSigma;
            }
            //On trie les résultats en fonction de la distance
            //On prépare le tableau à trier
            int*    Distances = new int[NbCities*2];
            for(i=0; i<NbCities; i++)       {
                Distances[2*i] = (int) Cities[i]->distance;
                Distances[2*i+1] = i;       //La clé
            }
            int*    Result = new int[NbCities*2];
            Radix_sort(Distances, NbCities, 2, Result);
            printf("Distance par rapport à %s :\n", CityNames[Pivot]);
            for(i=1; i<NbResults+1; i++)        printf("%s : %d km\n", CityNames[Result[2*i+1]], Result[2*i]);
            Radix_release();
            ReleaseArray(Distances);
            ReleaseArray(Result);
            //
            Release(DB);
            for(i=0; i<NbCities; i++)       Release(Cities[i]);
            ReleaseArray(Cities);
            for(i=0; i<NbCities; i++)       string_release(&CityNames[i]);
            ReleaseArray(CityNames);
            //
            ReleaseArray(Params);
            //
            return EXIT_SUCCESS;
        }

};

FFW_MAINENTRY(CityRanks);

Le temp moyen d'exécution sur un Sempron 2200+, avec 768Mo de RAM, est de l'ordre de 120ms, sachant que plus de 99% (si, si) de ce temps est consacré à l'accès à la DB. A noter que je voulais avoir un exécutable le plus petit possible (on pourra certainement faire mieux que moi, mais je suis déjà très content !). Avec la commande ci-dessous, ce programme prend 7860 octets :

strip --strip-unneeded -R .comment -R .gnu.version cityquery

Pour exécuter la requête depuis un navigateur, qui est le but de cette entrée, le script PHP suivant se chargera du travail (qui n'est pas bien lourd) :

<html>
<head>
    <title>Requête de distance géographiques</title>
    <meta http-equiv='Content-Type' value='test/html; charset=utf-8'/>
</head>
<body>
<?
    $Pivot = isset($_GET["pivot"]) ? $_GET["pivot"] : "";
    $NbResults = isset($_GET["nbResults"]) ? $_GET["nbResults"] : 30;
    if (!strlen($Pivot) || !$NbResults)     die("");
    if (!is_numeric($Pivot)  || !is_numeric($NbResults))        die("");
    if ($Pivot > 38052 || $Pivot <= 0 || $NbResults > 100 || $NbResults <=0)        die("");
    $Output = shell_exec("/usr/local/bin/cityquery $Pivot $NbResults");
    echo str_replace("\n", "<br/>\n", $Output);
?>
</body>
</html>

On pourra consulter le résultat de tout ceci sur les images ci-dessous, en attendant que je donne accès à ce script directement depuis ce site (ça ne serait peut-être pas très prudent) :

Le tri linéaire multi-usage


16 décembre 2006

Radix Sort

Je veux avoir une routine de tri qui ne soit pas aussi lente qu'un Heap Sort ou autre QuickSort, et pour cela utiliser une méthode digne de ce qu'on apprend quand on programme en assembleur 68000 avec un nombre ridicule de cycles par frames : le radix !

Le plus facile pour comprendre la méthode est de la voir à l'oeuvre. Prenons la liste de nombres suivante qui se trouve en désordre :

{ 12, 203, 3, 17, 801, 76, 45 }

Si on trie cette liste par rapport aux chiffres de unités on obtient :

{ 801, 12, 203, 3, 45, 76, 17 }

Si on trie cette liste intermédiaire par rapport aux dizaines :

{ 801, 203, 3, 12, 17, 45, 76 }

On voit ici que les cinq derniers chiffres sont triés. Passons aux centaines à présent :

{ 3, 12, 17, 45, 76, 203, 801 }

Et la liste est triée !

Le but est donc d'implémenter cette méthode non pas sur des chiffres en base 10 mais en base 2 bien évidemment. Pour réaliser ces passes successives on peut constater que l'on peut exécuter des passes de préparation sur la liste de nombres à trier (une passe par radix) : une passe pour compter les chiffres et une autre pour en déduire des segments. Je m'explique... Pour la liste de départ on obtiendrait les compteurs suivant :

{ 0, 1, 1, 2, 0, 1, 1, 1, 0, 0, 0 } pour les unités
{ 3, 2, 0, 0, 1, 0, 0, 1, 0, 0, 0 } pour les dizaines
    { 5, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0 } pour les centaines

Ces tableaux contiennent, à l'indice i, le nombre d'éléments qui possèdent le chiffre i à la position 0 (unités), 1 (dizaines) et 2 (centaines). La seule originalité de l'algorithme est de constater que, de ces listes de compteurs, on peut déduire des listes d'offsets qui indiquent le départ des segments des nombres qui possèdent le radix considéré :

{ 0, 0, 1, 2, 4, 4, 5, 6, 7, 7, 7 } pour les unités
{ 0, 3, 5, 5, 5, 6, 6, 6, 7, 7, 7 } pour les dizaines
    { 0, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7 } pour les centaines

A partir de ces listes d'offsets on peut exécuter les n passes de tri avec une inner loop simpliste, que l'on peut illustrer en revenant à la liste de départ de nombres à trier, en n'exécutant que la passe des unités :

Liste d'offset pour la passe considérée : { 0, 0, 1, 2, 4, 4, 5, 6, 7, 7, 7 }
            Résultat de la passe courante : { X, X, X, X, X, X, X }
Nombre '12', radix=2, offset=1 que l'on incrémente dans sa liste d'offset
Liste d'offset pour la passe considérée : { 0, 0, 2, 2, 4, 4, 5, 6, 7, 7, 7 }
    Résultat de la passe courante : { X, 12, X, X, X, X, X }
Nombre '203', radix=3, offset=2
Liste d'offset pour la passe considérée : { 0, 0, 2, 3, 4, 4, 5, 6, 7, 7, 7 }
    Résultat de la passe courante : { X, 12, 203, X, X, X, X }
Nombre '3', radix=3, offset=3
Liste d'offset pour la passe considérée : { 0, 0, 2, 4, 4, 4, 5, 6, 7, 7, 7 }
    Résultat de la passe courante : { X, 12, 203, 3, X, X, X }
Nombre '17', radix=7, offset=6
Liste d'offset pour la passe considérée : { 0, 0, 2, 4, 4, 4, 5, 7, 7, 7, 7 }
    Résultat de la passe courante : { X, 12, 203, 3, X, X, 17 }
Nombre '801', radix=1, offset=0
Liste d'offset pour la passe considérée : { 0, 1, 2, 4, 4, 4, 5, 7, 7, 7, 7 }
    Résultat de la passe courante : { 1, 12, 203, 3, X, X, 17 }
Nombre '76', radix=6, offset=5
Liste d'offset pour la passe considérée : { 0, 0, 2, 4, 4, 4, 6, 7, 7, 7, 7 }
    Résultat de la passe courante : { 1, 12, 203, 3, X, 76, 17 }
Nombre '45', radix=5, offset=4
Liste d'offset pour la passe considérée : { 0, 0, 2, 4, 5, 4, 6, 7, 7, 7, 7 }
    Résultat de la passe courante : { 1, 12, 203, 3, 45, 76, 17 }

Et la passe est terminée : les nombres sont effectivement triés selon leurs unités. En première implémentation on obtient le code suivant (qui ne fonctionne pas encore pour les nombres négatifs) :

void Radix_sort(int* values, int nbValues, bool verbose)
{
    int i, j;
    //On établit les décomptes des chiffres en fonctions de leurs positions
    int**   Counters = new int*[4];
    for(i=0; i<4; i++)      for(Counters[i] = new int[256], j=0; j<256; j++)        Counters[i][j] = 0;
    //On procède aux décomptes d'occurence des chiffres pour toutes les valeurs à trier
    int Value;
    for(i=0; i<nbValues; i++)   for(Value = values[i], j=0; j<4; j++, Value>>=8)    Counters[j][Value&0xFF]++;
    //Arrivé ici, on a par exemple : Counters[2][4] représente le nombre de valeurs à trier qui ont le chiffre '4' en deuxième position
    if (verbose)    for(i=0; i<4; i++)      Radix_dump(Counters[i], 256);
    //On cumule les compteurs précédents pour qu'ils représentent les offsets des valeurs 'triées' à l'intérieur d'une liste de tri
    int CurOffset;
    for(j=0; j<4; j++)
        for(i=0, CurOffset = 0; i<256; i++)     {
        int CurCounter = Counters[j][i];
        Counters[j][i] = CurOffset;
        CurOffset += CurCounter;
    }
    //Arrivé ici Counters[2][4], par exemple, représente l'offset de départ auquel on peut placer les valeurs qui ont 4 en deuxième position.
    if (verbose)    for(i=0; i<4; i++)      Radix_dump(Counters[i], 256);
    //On va trier sur deux listes en parallèle
    int*    List1 = new int[nbValues];
    int*    List2 = new int[nbValues];
    int*    SrcList = values;
    int*    DestList = List1;
    for(j=0; j<4; j++)      {
        int*    Offsets = Counters[j];
        //Voilà la ligne immonde !
        for(i=0; i<nbValues; i++)       *(DestList + Offsets[(SrcList[i]>>(j*8))&0xFF]++) = SrcList[i];
        if (SrcList == values)      SrcList = List2;
        int*    TmpList = SrcList;
        SrcList = DestList;
        DestList = TmpList;
    }
    Radix_dump(DestList, nbValues);
    //Releases des listes temporaires
    ReleaseArray(List1)
    ReleaseArray(List2)
    //Release des listes internes du radix
    for(i=0; i<4; i++)      ReleaseArray(Counters[i])
    ReleaseArray(Counters)
}
On peut voir que le réel code de tri ne prend qu'un quart de la source.

La ligne de code immonde de la boucle de tri interne est l'équivalente des lignes ci-dessous, plus lisibles :

int Value = SrcList[i];
int Digit = (Value>>(j*4))&0xF;
int*    Ptr = DestList + Offsets[Digit];
*Ptr = Value;
Offsets[Digit]++;

Pour tester cette routine, appelons-là avec le code suivant :

#define     NBVALUES    12
int     Src[NBVALUES] = { 170, 13, 75, 45, 90, 1985, 24, 802, 2, 66, 3, 76 };
    Radix_sort(Src, NBVALUES, false);

Une analyse rapide nous donne, pour N éléments à trier :

Occupation mémoire : 2*N+128+8 int (pour info, l'opération de tri de 1000 éléments entraîne l'allocation, en chiffrage-4, de 2136 int = 8544 octets = 8.34 ko

Temps machine : Init1{4*256*1W} + Init2{N*(1R+1W)} + Init3{4*256*(1R+1W)} + BoucleInterne{4*(1R+N*(2R+2W))} = 1028R+2048W + N*(9R+9W)

On est donc bien en O(N) pour la mémoire ET pour le temps machine.

Ã?a n'est pas encore fini !

Ceci étant fait, il reste encore beaucoup de petites choses à faire et à arranger :

voir comment trier des clés mais garder leur correspondance s'il y en a une gérer des doublons de valeurs à trier : ils sont gérés nativement, sans avoir rien à faire ! gérer les valeurs négatives : les valeurs négatives étaient déjà séparées des valeurs positives dans le tri de base. Il a juste fallu faire une recopie à la fin... J'aurais préféré faire l'équivalent d'un buffer cyclique mais ça ne marche pas des masses. gérer les flottants faire un reverse sort une liste Counters[x] qui ne contient qu'une seule valeur différente de zéro peut entraîner une optim ! ne peut-on faire du in-place ? Du in-place avec une seule liste à priori non, mais avec aucune autre liste que celles d'entrées OUI ! Le seul souci étant que la liste avec les valeurs a trier est trashée après l'appel... faire du radix un service qui s'initialise en créant ses tableaux de compteurs une seule fois. Transformer les fonctions en une classe fournissant un service de tri thread-safe.
void Radix_dump(int* toDump, int nb, int itemSize)
{
    int i, j;
    for(i=0; i<nb*itemSize; i+=itemSize)        {
        for(j=0; j<itemSize; j++)   {
            if (j)      printf("-");
            printf("%d", toDump[i+j]);
        }
        if (i != (nb-1)*itemSize)       printf(", ");
    }
    printf("\n");
}

//Le tableau de compteurs, utilisé pour chaque appel à la fonction de tri
int**   Counters = NULL;

//Allocation mémoire des listes internes
bool Radix_init()
{
    if (!Counters)      {
        Counters = new int*[4];
        memset(Counters, 0, 4*sizeof(int*));
    }
    int i;
    for(i=0; i<4; i++)      {
        if (!Counters[i])   Counters[i] = new int[256];
        memset(Counters[i], 0, 256*sizeof(int));
    }
    return true;
}

//Release des listes internes du radix
bool Radix_release()
{
    int i;
    for(i=0; i<4; i++)      ReleaseArray(Counters[i])
    ReleaseArray(Counters)
}

//Trie par la méthode "Radix" une liste de n-tuples <clé>-<donnée>.
//Les données peuvent avoir n fois la taille d'une clé (<itemSize>=n) ou être carrément absent (<itemSize>=0).
//La liste triée figure dans <result>, la liste à trier, <values>, est modifiée (attention !).
bool    Radix_sort(int* values, int nbValues, int itemSize, int* result)
{
    int i, j, Value;
    if (!Radix_init())      return false;
    //On procède aux décomptes d'occurence des chiffres pour toutes les valeurs à trier
    int NbNegativeValues = 0;
    for(i=0; i<nbValues*itemSize; i+=itemSize)      {
        Value = values[i];
        if (Value < 0)      NbNegativeValues++;
        for(j=0; j<4; j++, Value>>=8)   Counters[j][Value&0xFF]++;
    }
    //On cumule les compteurs pour qu'ils représentent les offsets des valeurs 'triées' à l'intérieur d'une liste de tri
    int CurOffset;
    for(j=0; j<4; j++)
        for(i=0, CurOffset = 0; i<256; i++)     {
        int CurCounter = Counters[j][i];
        Counters[j][i] = CurOffset;
        CurOffset += CurCounter*itemSize;
    }
    //On va trier en parallèle sur les deux listes fournies en entrée : values et result
    int*    SrcList = values;
    int*    DestList = result;
    for(j=0; j<4; j++)      {
        int*    Offsets = Counters[j];
        int*    SrcPtr = SrcList;
        for(i=0; i<nbValues*itemSize; i+=itemSize)      {
            int     ValueIndex = ((*SrcPtr)>>(j*8))&0xFF;
            int*    DestPtr = DestList + Offsets[ValueIndex];
            while (SrcPtr < SrcList+i+itemSize)     *DestPtr++ = *SrcPtr++;
            Offsets[ValueIndex] += itemSize;
        }
        int*    TmpList = SrcList;  SrcList = DestList; DestList = TmpList;     //Swap
    }
    //On réordonne les deux segments : valeurs négatives puis valeurs positives
    SrcList = values+(nbValues-NbNegativeValues)*itemSize;
    DestList = result;
    for(i=0; i<NbNegativeValues*itemSize; i++)          *DestList++ = *SrcList++;
    SrcList = values;
    for(i=0; i<(nbValues-NbNegativeValues)*itemSize; i++)   *DestList++ = *SrcList++;
    //
    return true;
}
Un code un peu plus stable et sûr.

Tri inversé

Pour que la même routine puisse fournir le tri ascendant (comme ici) ou descendant il suffit de changer la partie qui détermine les offsets à partir des compteurs d'occurence de radix. Comme suit :

    //reverse est un bool en entrée de la fonction de tri.
    int iLimit = reverse ? -1 : 256;
    int iStep = reverse ? -1 : 1;
    for(j=0; j<4; j++)      {
        int CurOffset = 0;
        i = reverse ? 255 : 0;
        while (i != iLimit)     {
            int CurCounter = Counters[j][i];
            Counters[j][i] = CurOffset;
            CurOffset += CurCounter*itemSize;
            i += iStep;
        }
    }

Les nombres flottants

La gestion des nombres flottants se fait naturellement avec un tri qui ordonne les éléments en fonction de radix : ce qui marche naturellement sur des entiers fonctionne également sur des flottants IEEE 754, avec le bout de code suivant tout de même :

bool    Radix_sortf(float* values, int nbValues, int itemSize, float* result, bool reverse)
{
    //Première passe : on trie toutes les valeurs, les négatives seront inversées.
    if (!Radix_sort((int*) values, nbValues, itemSize, (int*) result, reverse))     return false;
    //Deuxième passe : on ne trie que les négatives pour les inverser.
    bool    Ret = Radix_sort((int*) result, Radix_NbNegativeValues, itemSize, (int*) values, !reverse);
    int i;
    for(i=0; i<Radix_NbNegativeValues; i++)     result[i] = values[i];
    return Ret;
}

Pourquoi ce bout de code ? Parce que sans rien changer, les flottants positifs sont gérés correctement mais les négatifs sont inversés, d'où...

Il n'y a pas de cas pathologique (même NaN et inf sont correctement gérés).

Utilisation du Radix Sort

Module serveur :: requêtes géographiques

Des scripts utiles en Bash


10 octobre 2006

Zone de détente pour stocker tous les scripts qu'il m'est arrivé de faire pour le plaisir (si, si, c'est vrai), pour le travail ou pour répondre à un besoin bien précis. La plupart ne doivent pas être optimisés et doivent bien exposer mes manques de connaissance globaux de Bash/Linux, mais, comme on dit en Californie du moment que ça marche.

Télécharger une collection de Mod/XM/S3M

Si, comme moi, vous aimez les modules Amiga de la belle époque, ce petit script va vous plaire : il rapatrie l'ensemble de la collection du site Amiga Module Preservation et ceci sans s'embêter. �a me permet d'illustrer l'intérêt de la conjonction de sed et awk (ou la version de GNU, gawk) pour écrire des scripts très courts qui peuvent faciliter la vie (et entre autre les tâches d'admin).

Pour l'instant je remplis un fichier (modlist) avec les URLs de tous les modules, il reste quelques lignes à écrire pour les télécharger effectivement.

#!/bin/bash

#Ou comment choper tous les modules Amiga qu'on veut en un seul coup
#TODO : décompresser les gz on the fly
#TODO : fournir simplement la liste des fichiers

RootURL=\"http://amp.dascene.net/modules/\"
wget -O amp.download  \$RootURL

#On déduit les dossiers à choper
for folder in `cat amp.download | sed -n 's/^.*<a href=\"\([^\"]*\)\">\1.*$/\1/p'`; do
    :>artistslist
    wget -O amp.download.tmp $RootURL/$folder
    cat amp.download.tmp | sed -n 's/^.*<a href=\"\([^\"]*\)\">\\1.*\$/\1/p' | awk '{ print "'$folder'\"$0 }' >> artistslist
    for artist in `cat artistslist`; do
        :>modlist
        wget -O artist.page $RootURL$artist
        cat artist.page | sed -n 's/^.*<a href=\"\([^\"\/?]\+\)\">.*$/\1/p' | awk '{ print "'$RootURL$artist'"$0 }' >> modlist
        for module in `cat modlist`; do
            Path=${module%/*}
            Artist=${Path##*/}
            ModName=${module##*/}
            mkdir -p ${Artist:0:1}$Artist
            wget -P ${Artist:0:1}$Artist/ $module
        done
    done
done

rm -f amp.download artist.page modlist amp.download.tmp
Le résultat de ce script est un fichier modlist de 4 454 104 octets, la taille totale des mods téléchargés arrive plus tard... Il faudrait faire la même chose pour ftp://ftp.modland.com/pub/, leur serveur a l'air bien foireux et c'est très pénible ne serait-ce que de le visiter. La même chose pour http://it.aminet.net/~aminet/dirs/tree_mods.html. Et bien sûr ensuite faire un script qui supprime les doublons des trois site :)

Nettoyage des noms des modules

Une fois les modules précédents téléchargés, on se rend compte que les habitudes de nommage de fichiers ne correspondent pas à ceux en vigueur actuellement, on peut facilement les renommer avec le script suivant :

Dir="Jester"; for item in $Dir/MOD*; do NewName=$(echo `basename ${item}` | awk 'BEGIN{FS="."} {print $2"."tolower($1)}'); mv "$item" "$Dir/$NewName"; done
Le dossier et l'extension sont en dur, mais c'est un exemple

Commandes Bash intéressantes

Ne riez pas mais ça faisait un bout de temps que je voulais avoir la possibilité de trier des fichiers par taille (ou tout autre critère) mais sans avoir jamais vraiment cherché. C'est fait, et c'est très facile.

//Trier la sortie de ls -l (aka ll) en fonction la taille des fichiers (descendant).
ls -l | sort -nrk 5,5
//Pour les trier dans l'ordre ascendant il faut virer le 'r' de sort.

//Extension : renvoie les 4 plus gros fichiers (il faut virer la 1ère ligne de ls -l qui est la taille totale).
ls -l | awk 'BEGIN{i=0}{if (i++) print $0}' | sort -rnk 5,5 | tail -n 4
//Pour les 4 plus petits, il faut virer le 'r' de sort.

//Mais on peut arranger le bloc awk par une commande sed (c'est très original...)
ls -l | sed -n '1!p' | sort -rnk 5,5 | tail -n 4
//C'est équivalent : on dit à sed de faire le contraire d'imprimer (flag p) la ligne 1.

//Si on veut trier par date, le tri par nombre (-n) ne va plus il faut trier les chaînes directement (-d), ce qui donne :
ls -l | sed -n '1!p' | sort -rdk 6,6 | tail -n 4
//Et on trie alors par date croissante (les plus récents en dernier)

Petite application : imaginons que l'on ait un ensemble de dossiers contenant chacun plusieurs fichiers (par exemple des PDF de docs amassés dans plusieurs domaines), comment avoir les fichiers triés du plus petit au plus gros ? Réponse :

for file in */*; do [[ -f $file ]] && echo `ls -l "$file"`; done | sort -nrk 5,5
Bien penser au guillemets qui encadre $file sinon un fichier ayant un nom avec des espaces va provoquer un bordel...

La version deux ci-dessous de ce oneliner peut paraître plus compliquée mais une fois sauvée dans un script et placée dans le PATH elle convient à tout ce dont j'ai besoin. Elle permet d'utiliser l'option -0 de la commande ls qui gère tous les types de noms de fichiers.

find . -name "*pdf" -print0 | xargs -0 ls -l | awk '{ O=""; for (i=8; i<=NF; i++) O=O$i; print $5" "O;}' | sort -rnk1,1

Si on appelle le script précédent findex on peut ainsi de chercher tous les fichiers PDF et de les ordonner à partir du répertoire courant et dans tous ses sous-répertoires en une seule ligne intuitive : findex . pdf.

Tagage, classement et indexation automatiques de données textuelles

Je cherche à calculer une empreinte d'un texte à partir des mots qui le constitue.

cat <mon_fichier> | sed -e "s/''/\n/g" | sed -e 's/ /\n/g' -e 's/[=:;,\{\}"\$<>&|#\*\(\)\\\/\-\?\+\.]/ /g' | sed -e 's/ /\n/g' | sort | uniq -c | sort -n

En ignorant la casse, en filtrant les mots inutiles (les plus courants etc), en les triant par taille et en n'en gardant que 10, on commence à pouvoir tagguer automatiquement un bloc de texte.

cat <mon_fichier> | sed -e \"s/\'\`/\\n/g\" | sed -e 's/ /\\n/g' -e 's/[=:;,\{\}\"\\$<>&|#\*\(\)\\\/\-\?\+\.]/ /g' | sed -e 's/ /\\n/g' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -n | awk -f filter.awk | sort -nk 3,3 | tail | awk '{print $2}'

Voilà le résultat avec un bout de descriptif de Awk :

consecutive
declaration
differences
fundamental
programming
additionally
conceptually
corresponding
superficially
one-dimensional
Ca peut sembler peu intéressant mais si on affecte manuellement une série de tags à quelques éléments on peut avoir un tagging automatique pour les documents ultérieurs. Il faut pouvoir laisser la possibilité de changer ces affectations au besoin Page spéciale d'extraction de torrents (obligatory pr0n page)

LDD extended

Je ne sais pas vraiment si ce script est utile : peut-être ldd fait-il déjà tout ce travail, mais je n'en ai pas l'impression. Toujours est-il que je voulais obtenir le résultat de ldd pour un exécutable et ensuite pour chacune des librairies renvoyées. D'où ce script qui m'a bien énervé, surtout pour le passage de tableau à la fonction contains...

#!/bin/bash

function    displayHelp
{
    echo "Extension de récursive de <ldd> :"
    echo "affiche toutes les librairies en dépendance d'une autre ou d'un exécutable"
    echo "et affiche les dépendances des dépendances ad finitum."
    echo
    echo "Syntaxe :"
    echo
    echo "    $0 <exelibs>+|--help|--version"
    echo
    echo "Options :"
    echo
    echo "<exelibs>+ : suite d'exécutables ou de librairies dont on veut les dépendances."
    echo
    echo "--help : cette page d'aide."
}

#Vérifie que ${1} n'est pas contenu dans le tableau dont le nom est passé en second paramètre
function    contains
{
    Value="${1}"
    ArrName=${2}
    Arr=\${"$ArrName"[@]}; Arr=`eval echo $Arr`
    Temp=( ${Arr[@]/$Value/XXX$Value} )
    Verif=( ${Arr[@]} )
    Str=${Temp[@]};     Len1=${#Str}
    Str=${Verif[@]};    Len2=${#Str}
    [[ $Len1 != $Len2 ]] && return 0;
    return 1
}

NbParams=${#*}
[[ $NbParams == 0 ]] && echo "Pas de paramètres : il faut le chemin d'une librairie ou d'un exécutable." && echo && displayHelp && exit 1;

while [[ true ]];   do
    [[ ${#*} == 0 ]] && break;
    CurBinary=${1}
    shift
    [[ $CurBinary == "--help" ]] && displayHelp && continue;
    echo "Examen de $CurBinary :"
    [[ ! -f $CurBinary ]] && echo "Le fichier n'existe pas !" && continue;
    unset Todo; unset Handled
    Todo[0]=$CurBinary
    PassIndex=0
    while [[ ${#Todo[@]} != 0 ]];   do
    #   echo "Passe $PassIndex : il reste "${#Todo[@]}" élément(s) à examiner"
        for lib in ${Todo[@]}; do
            List=( `ldd $lib | awk '{if ($1!~"linux-gate" && $1!~"statically") { if (!length($3)) print $1"\n"$1; else print $1"\n"$3}}'` )
            let "nbLibs = -1 + ${#List[@]} / 2"
            for i in `seq 0 $nbLibs`; do
                LibName=${List[((i*2))]}
                LibPath=${List[((2*i+1))]}
                contains $LibPath Handled;  Ret1=$?
                contains $LibPath Todo;     Ret2=$?
                if [[ $Ret1 -eq 1 ]];   then
                    if [[ $Ret2 -eq 1 ]];   then
                        Todo[${#Todo[@]}]=$LibPath          #On l'ajoute au tableau todo
                    fi
                fi
            done
            newArray=( ${Todo[@]%$lib} ); unset Todo; Todo=( ${newArray[@]} )
            contains $lib Handled;  Ret=$?
            if [[ $Ret -eq 1 ]];    then    Handled[${#Handled[@]}]=$lib;   fi
        done
        ((PassIndex++))
    done

    let "MaxItemsIndex = ${#Handled[@]} -1"
    for i in `seq 1 $MaxItemsIndex`; do echo ${Handled[$i]}; done
done

Copie de CDs

Un petit script sans aucune prétention mais que j'apprécie beaucoup pour m'aider à copier mes cds sur un disque dur sans avoir à utiliser de souris ou de commandes répétitives. Il crée un dossier par CD, place tout à l'intérieur, modifie les droits d'écriture et sort le CD du lecteur en me disant si des erreurs se sont produites. Je l'appelle généralement depuis l'endroit ou je veux stocker les données avec le commande : grabcd /media/cdrom

#!/bin/bash

[[ ${#} -ne 1 ]] && echo "J'attends la partition source..." && exit 1;
Src="${1}"

ErrorLog="./errors.log"
rm -f $ErrorLog

mount $Src 2> /dev/null

let NbFolders=`ls -ld cd* 2> /dev/null | wc -l`+1
Dest=cd$NbFolders
mkdir $Dest

cp -vr $Src/* $Dest/ 2>$ErrorLog
chmod -R u+w $Dest/

[[ -s $ErrorLog ]] && echo "Des erreurs de lecture se sont produites :" && cat $ErrorLog;

  eject

Taux de changes

Pour obtenir le taux de conversion du dollar face à l'euro (en gros combien il faut de dollars pour avoir un euro) il suffit d'utiliser ce script :

wget -q -O- www.federalreserve.gov/releases/h10/Update/ | awk '{if ($2=="MEMBERS" && $3=="EURO") print $NF}'

J'aime beaucoup l'option de wget -O- pour envoyer la ressource HTTP téléchargée sur stdout, très pratique !

(Dé)Compression de plusieurs dossiers (archives)

Parcours de tous les dossiers du répertoire courant et crée une archive BZ2 pour chacun en indiquant la progression totale (attention aux dossiers contenant des espaces...).

fList=`find -maxdepth 1 -type d | tail -n+2`; nb=0; for item in $fList; do echo -n "Dossier : $item..."; tar cjf "$item.tar.bz2" "$item"/*; ((nb++)); echo $(echo "scale=1; 100*$nb/`echo "$fList" | wc -l`" | bc)" %"; done

Ajout automatique de domaines à un serveur DNS

Voici un script autosuffisant (pas besoin de ressource externe) pour ajouter ou modifier un domaine auprès de Bind. �a évite de s'embêter à éditer et/ou copier-coller des fichiers, ça fait tout tout seul.
Bien sûr rien de compliqué mais au moins c'est disponible.

#!/bin/bash

function    displayHelp
{
    echo "Ajoute un domaine à Bind et en option relance ce serveur."
    echo && echo "Syntaxe :"
    echo "  $0 [--add <domain>] [--label <label>] [--restart] [--help]"
    echo && echo "Options :"
    echo && echo "  --add <domain> : ajoute le domaine <domain> à la liste des zones gérées par Bind."
    echo && echo "  --label <txt> : ajoute le champ texte <txt> à la zone DNS (via une ligne TXT)."
    echo && echo "  --restart : redémarre le serveur de nom pour qu'il prenne en compte toutes ses zones."
    echo && echo "  --help : affiche cette aide (équivalent à appeler ce programme sans aucun paramètre)."
    echo && echo "###TODO : reste à modifier le fichier principal de configuration de Bind."
    echo && echo "(c) Qitools 2007."
}

function    createNamedFile
{
    domainName=$1
    domainHost=$2
    hostIP=$3
    label=$4
    #Création d'un fichier temporaire contenant la template de la zone
    TempFileName=mktemp
    echo "; The zone file for the %domainName% domain
\$TTL 3D
%domainName%.    IN      SOA     ns.%domainName%. postmaster.%domainName%. (
                        %date%
                        2M
                        1M
                        1W
                        1H )
                TXT     \"%label%\"
                IN      NS      ns.%domainName%.
                IN      NS      ns.ovh.net.
                MX      10      mail.%domainName%.

localhost           IN      A       127.0.0.1
ns                  IN      A       %domainIP%
mail                IN      A       %domainIP%
%domainName%.       IN      A       %domainIP%
www                 IN      CNAME   %domainHost%.
" > $TempFileName
    #Transformation de la template
    cat $TempFileName | sed "s/%domainName%/$domainName/g" | sed "s/%domainIP%/$hostIP/g" | sed "s/%domainHost%/$domainHost/g" > $TempFileName.1
    if [[ ${#label} != 0 ]]; then
        cat $TempFileName.1 | sed "s/%label%/$label/g" > $TempFileName.2
    else
        cat $TempFileName.1 | sed "/%label%/d" > $TempFileName.2
    fi
    #Gestion épineuse de la date...
    ZoneFile=/var/named/zones/$domainName
    ZoneDir=`dirname $ZoneFile`
    mkdir -p $ZoneDir
    if [[ -f $ZoneFile ]]; then
        OldDate=`cat $ZoneFile | grep "$domainName.*IN.*SOA" --after-context=1 | tail -n +2 | awk '{print $1}'`; OldRev=${OldDate:8}
        #On nettoie la chaîne des 0 en préfixe sinon le let suivant ne fonctionnera pas
        OldRev=`echo $OldRev | sed "s/^0*//g"`
        let OldRev++
        OldRev=`printf "%02d\n" $OldRev`
        ZoneDate="`date +%Y%m%d`$OldRev"
    else
        ZoneDate="`date +%Y%m%d`01"
    fi
    #on reprend la date existante et on incrémente la révision
    cat $TempFileName.2 | sed "s/%date%/$ZoneDate/g" > $ZoneFile
    #Arrivé ici on peut ajouter quelque chose au fichier /etc/named.conf si ça n'est pas déjà fait...
    MainBindConf="/etc/named.conf"
    #Soit on ajoute un bloc de zone soit on réécrit le bloc correct (on ne sait jamais)
}

RestartNamed=false
[[ ${#} == 0 ]] && displayHelp && exit 0;
while [[ ${#} -gt 0 ]]; do
    Param=$1
    case $Param in
        "--help")   displayHelp && exit 0;;
        "--add")    shift; Domain=$1;;
        "--label")  shift; ZoneLabel=$1;;
        "--restart")    RestartNamed=true;;
        ".*")       echo "Option <$Param> inconnue !";;
    esac
    shift
done

#On nettoie le nom de domaine...
Domain=`echo $Domain | tr [A-Z] [a-z] | sed "s/ //g" | sed "s/\///g"`
#On vérifie qu'on est bien root
[[ `id -u` != 0 ]] && echo "Ce script nécessite d'être en root pour fonctionner." && exit 3;
#Vérifier que named tourne bien ou du moins qu'il existe...
NamedRunning=`ps -Af | grep [n]amed | head -n 1 | awk '{print $1}'`
if [[ $NamedRunning != "named" ]]; then
    #Le serveur Bind ne tourne pas, on va vérifier qu'il existe quand même sur la machine
    NamedPath=`which named`
    [[ $? == 1 ]] && echo "Le serveur de nom Bind n'existe pas, le script ne sert à rien sur cette machine." && exit 4;
fi

[[ $RestartNamed -eq "false" && ${#Domain} -eq 0 && ${#MainDNSServer} -eq 0 ]] && echo "Aucune option sélectionnée." && exit 2;

CurrentHost=`hostname`
CurrentHostIP=`host $CurrentHost | sed '/alias/d' | sed '/NXDOMAIN/d' | head -n 1 | awk '{print $4}'`
[[ ${#CurrentHostIP} -eq 0 ]] && echo "Aucune adresse IP trouvée pour le domaine <$CurrentHost>. Vérifiez votre entrée et recommencez." && exit 1;

createNamedFile $Domain $CurrentHost $CurrentHostIP "$ZoneLabel"

if [[ $RestartNamed == "true" ]]; then
    /etc/init.d/named stop 1>/dev/null
    /etc/init.d/named start 1>/dev/null
fi

Récupérer ses mots de passe Windows

Une petite note pour me souvenir de la façon dont on récupère les mots de passe d'une install de Windows sur le même PC qu'un Linux.
Ici j'utilise (K)Ubuntu, mais la logique est suffisamment simpliste pour être adapté sur n'importe quelle distribution.
En tous cas ça m'a permis de récupérer facilement mon mot de passe root - pardon Administrator - que j'avais oublié, n'ayant pas démarré Windows depuis un bon bout de temps...

#Installe les softs qui peuvent manquer
apt-get install bkhive samdump2 john
#On récupère la SYSKEY
bkhive /media/hda1/WINNT/system32/config/system syskey
#On utilise la SYSKEY pour décrypter les hash des mots de passe
samdump2 /media/hda1/WINNT/system32/config/SAM syskey > hashes
#On cherche les mots de passe dont on connaît les hash
john hashes

J'ai extrait ces infos de cette page intéressante. D'autres informations peuvent être obtenues depuis la page de John The Ripper, logiciel utilisé pour réellement cracker les mots de passe.

ImageMagick sympathique

Je devais faire des manipulations sur des images et forcément le réflexe est d'utiliser ImageMagick ! Le problème était que j'avais une petite centaine de feuilles papier dont les numéros en bas de page étaient incorrects et qui devait produire un PDF regroupant deux pages d'origine par page. J'ai créé deux dossiers, un dossier jpg/ pour les fichiers nettoyés et un autre doubles/ pour les images finales. Le script est :

i=1; j=0; k=0; for item in *png; do echo $item; convert "$item" -fill white -draw 'rectangle 484,3256,2184,3516' -fill black -font "Times-Bold" -pointsize 60 -draw "text 1300,3400 {$i}" -quality 80 "jpg/page_"$j"_"$k".jpg"; ((i++)); ((k++)); [[ $k == 2 ]] && k=0 && ((j++)); done

for i in `find product/*jpg | awk 'match($0,"page_([0-9]+)",a) {print a[1]}' | sort -nk1,1 | uniq`; do PicIndex=`echo "0"$i | awk '{print substr($0, length($0)-1, 2)}'`; echo $i; montage "jpg/page_"$i"_0.jpg" "jpg/page_"$i"_1.jpg" -geometry +2+2 "doubles/page_"$PicIndex".jpg"; done

convert -monitor doubles/*jpg -monitor -compress jpeg dest.pdf

La première ligne efface le numéro de page existant et le remplace par quelque chose de standardisé et en profite pour nommer les fichiers produits de manière à préparer l'étape suivante. Etape qui justement, sur la deuxième ligne, crée des images contenant des groupes de deux images combinées. La dernière ligne générant le fichier PDF voulu.

Commandes Ubuntu pour la gestion des paquets

Quelques commandes pour se faciliter la vie sous Debian/Ubuntu :

Repair:dpkg --configure -a
Reconfigure:dpkg-reconfigure pkg (-a)
Fix:apt-get -f install (pkg)
Nuke:dpkg --force-remove-reinstreq -r pkg
Corrupt deb:rm /var/cache/apt/archives/pkg
Hack dpkg:rm /var/lib/dpkg/info/pkg*

Mon fichier sources.list

# 
deb cdrom:[Ubuntu-Server 6.10 _Edgy Eft_ - Release i386 (20061025.1)]/ edgy main restricted

deb http://fr.archive.ubuntu.com/ubuntu/ edgy main restricted
deb-src http://fr.archive.ubuntu.com/ubuntu/ edgy main restricted

## Major bug fix updates produced after the final release of the
## distribution.
deb http://fr.archive.ubuntu.com/ubuntu/ edgy-updates main restricted
deb-src http://fr.archive.ubuntu.com/ubuntu/ edgy-updates main restricted

deb http://fr.archive.ubuntu.com/ubuntu edgy-security main restricted
deb http://fr.archive.ubuntu.com/ubuntu edgy-proposed main restricted
deb-src http://fr.archive.ubuntu.com/ubuntu edgy-security main restricted
deb-src http://fr.archive.ubuntu.com/ubuntu edgy-proposed main restricted

deb http://fr.archive.ubuntu.com/ubuntu edgy universe multiverse
deb http://fr.archive.ubuntu.com/ubuntu edgy-updates universe multiverse
deb http://fr.archive.ubuntu.com/ubuntu edgy-security universe multiverse
deb http://fr.archive.ubuntu.com/ubuntu edgy-proposed universe multiverse
deb-src http://fr.archive.ubuntu.com/ubuntu edgy universe multiverse
deb-src http://fr.archive.ubuntu.com/ubuntu edgy-updates universe multiverse
deb-src http://fr.archive.ubuntu.com/ubuntu edgy-security universe multiverse
deb-src http://fr.archive.ubuntu.com/ubuntu edgy-proposed universe multiverse

deb http://archive.canonical.com/ubuntu edgy-commercial main

deb http://kubuntu.org/packages/kde-latest edgy main
deb-src http://kubuntu.org/packages/kde-latest edgy main

deb http://kubuntu.org/packages/koffice-latest edgy main
deb-src http://kubuntu.org/packages/koffice-latest edgy main

deb http://kubuntu.org/packages/amarok-latest edgy main
deb-src http://kubuntu.org/packages/amarok-latest edgy main

deb http://medibuntu.sos-sts.com/repo/ edgy free non-free
deb-src http://medibuntu.sos-sts.com/repo/ edgy free non-free

deb http://ubuntu.beryl-project.org edgy all
deb-src http://ubuntu.beryl-project.org edgy all
deb http://gandalfn.club.fr/ubuntu/ edgy stable
deb-src http://gandalfn.club.fr/ubuntu/ edgy stable

deb http://ubuntu.beryl-project.org/ edgy main

deb http://www.albertomilone.com/drivers/edgy/latest/32bit binary/

## Uncomment the following two lines to add software from the 'universe'
## repository.
## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
## team, and may not be under a free licence. Please satisfy yourself as to
## your rights to use the software. Also, please note that software in
## universe WILL NOT receive any review or updates from the Ubuntu security
## team.
# deb http://fr.archive.ubuntu.com/ubuntu/ edgy universe
# deb-src http://fr.archive.ubuntu.com/ubuntu/ edgy universe

## Uncomment the following two lines to add software from the 'backports'
## repository.
## N.B. software from this repository may not have been tested as
## extensively as that contained in the main release, although it includes
## newer versions of some applications which may provide useful features.
## Also, please note that software in backports WILL NOT receive any review
## or updates from the Ubuntu security team.
# deb http://fr.archive.ubuntu.com/ubuntu/ edgy-backports main restricted universe multiverse
# deb-src http://fr.archive.ubuntu.com/ubuntu/ edgy-backports main restricted universe multiverse


deb http://security.ubuntu.com/ubuntu edgy-security main restricted
deb-src http://security.ubuntu.com/ubuntu edgy-security main restricted
# deb http://security.ubuntu.com/ubuntu edgy-security universe
# deb-src http://security.ubuntu.com/ubuntu edgy-security universe
1 2 3..... 9 10 11 12..... 18 19 20