Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
Fanfois et ses papillons
27 novembre 2011

Aspirateur (4 – sauvegarde des images)

Maintenant que nous avons une IHM qui fonctionne, nous pouvons nous concentrer sur le fonctionnel. Notre aspirateur de site web sait récupérer le code source d'une page web, identifier les tags HTML <img> contenus dans cette dernière et extraire les URL des images indiquées dans ces tags.

Pour commencer à justifier sa dénomination d'aspirateur de site web, il faut que notre programme sauve localement le code de la page et les images contenues dans cette dernière. Pour que la page locale fonctionne avec les images locales, il faut modifier l'URL des tags <img> correspondants dans le code source de la page.

Grâce à l'IHM que nous avons mis en place dans le précédent billet, nous disposons des informations principales nécessaires à la réalisation de ces tâches.
Cela dit, tout n'est pas réglé pour autant.
Si écrire un fichier sur notre disque local n'est pas très compliquer, déterminer où nous allons le placer peut être nettement moins évident. Pour comprendre le problème, considérons les deux types d'URL que nous pouvons rencontrer :

  • URL absolues. Elles indiquent un chemin commençant par / ou carrément http(s)://. Dans le cas de / il faut partir de la racine du site courant. Supposons que nous soyons sur le site http://www.canalblog.com, l'URL /sharedDocs/images/frontend/logo.gif est identique à http://www.canalblog.com/sharedDocs/images/frontend/logo.gif ;
  • URL relatives. Elles indiquent un chemin qui ne commence ni par /, ni par http(s)://. Souvent le chemin commence par ../, qui indique qu'il faut remonter d'un niveau dans l'arborescence des répertoires, ou ./ qui indique qu'il faut rester au même niveau. En fait ./logo.gif et logo.gif signifient la même chose. Dans les tags <img> récupérés dans le second billet il n'y avait pas d'adresse relative, il n'y a donc pas d'exemple utilisable dans ce que nous avons déjà rencontré.

Les URL relatives ne nous posent pas de problème dans notre version locale. Nous pouvons les conserver dans le code source de la page et tout fonctionnera correctement, pour peu que nous ayons reconstruit localement l'arborescence exacte du site que nous aspirons. Cependant pour récupérer l'image nous devons calculer l'URL absolue.

Les URL absolues nous simplifient le travail pour récupérer l'image car nous savons directement où aller les chercher. Par contre, pour ce qui est du stockage local, nous ne pouvons pas en rester là. Il nous faut obligatoirement avoir un chemin relatif dans la version locale de la page. A priori la chose peut paraître simple, voir trivial.
On met des ../ pour remonter à la racine du site, et on concatène le chemin absolu et enlevant l'éventuel http(s)://www.xxx.yyy.
Ben justement, NON !
Non, parce que ce http(s)://www.xxx.yyy peut ne pas correspondre au site courant.
Reprenons le résultat du second billet.
Nous demandons la page par défaut de canalblog, nous sommes donc sur le site http://www.canalblog.com. Au niveau des images nous trouvons par exemple https://storage.canalblog.com/90/50/581044/69236175.jpg. Certes nous sommes toujours chez "canalblog.com", mais pas exactement au même endroit. C'est un peu comme si nous étions toujours dans la même rue, mais que nous ayons changé de maison. Dans ces conditions, il n'est pas garanti que nous n'ayons pas des collisions de noms de répertoire ou de noms de fichier si nous considérons que http://www.canalblog.com/ et https://storage.canalblog.com sont interchangeables. Si nous avions changé de domaine, ne pas tenir compte du changement dans l'organisation locale des fichiers serait encore plus risqué (les images peuvent très bien être externalisées sur un site spécialisé).

Cela va nous obliger à gérer ce que nous récupérons ou pas. Pour ce que nous récupérons, il faudra que nous décidions où nous le mettons. Dit autrement, nous devons avoir une table qui dise que les éléments issus de tel domaine vont dans tel répertoire. Par exemple avec canalblog nous devons dire où vont les données issues de www.canalblog.com et où vont celles issues de storage.canalblog.com. Evidement si nous voulons éviter d'en mettre un peu partout sur le disque local, il est souhaitable que le répertoire indiqué pour storage.canalblog.com ne soit pas très éloigné de celui indiqué pour www.canalblog.com, un sous répertoire par exemple.

Nous devons également gérer un autre problème avec les URL de pages.
Il est possible d'indiquer un fichier par défaut dans un répertoire virtuel de site web. Lorsque cela est fait, cela permet au serveur web de retourner une page, alors que l'URL qui lui est fournie n'indique qu'un répertoire virtuel. Typiquement nous avons demandé la page http://www.canalblog.com, or ce n'est pas une URL de page. Si nous regardons l'URL affichée par un butineur nous verrons d'ailleurs http://www.canalblog.com/, ce qui indique bien une URL de répertoire et non de fichier. Pourtant notre butineur affiche bien une page et non un répertoire. Cela est dû à la présence d'une page par défaut, dont nous ignorons le nom mais qui a bien un nom (souvent quelque chose comme default, index ou blank). C'est cette page que nous avons reçu. Le problème c'est que nous devons lui donner un nom pour pouvoir la sauver localement. Tant qu'à faire autant utiliser un nom qui parle un peu à l'utilisateur, c'est pour cela que nous avons prévu un nom par défaut dans notre IHM.

Nous pouvons également avoir un problème avec l'extension du fichier. Si nous récupérons une page dynamique cette dernière aura probablement une extension liée au moteur de pages qui la traite, aspx par exemple. Localement nous ne pouvons pas conserver cette extension qui pourrait être source de problème lors d'un chargement de la page (à l'aide d'un double click par exemple). Nous avons donc un champ extension par défaut dans notre IHM qui permet à l'utilisateur d'indiquer s'il souhaite utiliser "HTM" ou "HTML" (extensions standards pour une page web statique).

 

Maintenant que nous avons présenté ce que nous allons faire, commençons par améliorer l'encapsulation en ajoutant une classe ImageProcessor à notre librairie AspirateurUtil. Naturellement, nous déplaçons l'expression régulière de traitement des tags <img> et la fonction ImageEvaluator dans cette nouvelle classe. Pour avoir une bonne encapsulation, nous ne souhaitons rendre visible que le minimum. Dans ce contexte il est inutile d'exposer comment nous faisons pour traiter la page. L'utilisation d'une expression régulière est un "détail d'implémentation". Nous ne l'exposons donc pas et proposons à l'utilisateur une fonction ProcessPage. Le code de cette nouvelle classe est alors :

using System;
using System.Text;
using System.Text.RegularExpressions;
 
namespace AspirateurUtil
{
    /// <summary>
    /// Classe de traitement des tags HTML img.
    /// </summary>
    public class ImageProcessor
    {
        #region Membres
        // L'expression régulière permettant le traitement des tags img.
        private static Regex _regex = new Regex("<\\s*img\\b[^>]*\\bsrc\\s*=\\s*[\"']?([^\"'\\s]+)[\"']?[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
        #endregion
 
        #region Méthodes publiques
        public string ProcessPage(string pageContent)
        {
            return _regex.Replace(pageContent, new MatchEvaluator(ImageEvaluator));
        }
        #endregion
 
        #region Méthodes privées
        /// <summary>
        /// Fonction appelée lorsqu'un tag IMG est trouvé.
        /// </summary>
        /// <param name="m">Le tag img trouvé.</param>
        /// <returns>Le tag img avec l'URL locale.</returns>
        private string ImageEvaluator(Match m)
        {
            // On prépare le terrain pour une future substitution.
            StringBuilder strURL = new StringBuilder();
            int iDebut = m.Groups[1].Index - m.Groups[0].Index;
            strURL.Append(m.Groups[0].Value.Substring(0, iDebut));
            strURL.Append("../../../URL/locale.jpg");
            strURL.Append(m.Groups[0].Value.Substring(iDebut + m.Groups[1].Length));
            return strURL.ToString();
        }
    }
}

Coté appelant, btnGo_Click, la ligne de code devient :

// On cherche les images.
ImageProcessor imgProc = new ImageProcessor();
strPage = imgProc.ProcessPage(strPage);

Il nous faut maintenant récupérer l'image dans la fonction ImageEvaluator et la sauver localement. Rien de plus simple, le code suivant le fait très bien :

WebClient wc = new WebClient();
wc.DownloadFile(absUrl, path);

Tout le problème consiste à avoir les bons noms en appliquant les règles présentées précédemment. Nous aurons besoin de ce traitement à chaque fois que nous voudrons récupérer un élément sur le web, cela n'est absolument pas spécifique aux images. Nous ajoutons donc une nouvelle classe, UrlFileMatcher, à notre projet AspirateurUtil. L'idée est d'avoir dans cette classe une méthode publique qui calcule tout ce dont nous avons besoin à partir d'une URL trouvée dans une page. La signature de la fonction est assez facile à imaginer :

/// <summary>
/// Calcule, à partir d'une URL trouvée dans une page web,
/// toutes les informations nécessaires au traitement par l'aspirateur.
/// </summary>
/// <param name="url">L'URL à traiter.</param>
/// <param name="isPage">L'URL est-elle celle d'une page?</param>
/// <param name="absUrl">L'URL absolue à utiliser pour récupérer l'élément.</param>
/// <param name="relUrl">L'URL relative à utiliser dans le HTML local.</param>
/// <param name="filePath">Le chemin complet à utiliser pour sauver localement l'élément.</param>
/// <returns>true si tout a pu être calculé, false en cas de problème.</returns>
public bool ProcessUrl(string url, bool isPage, out Uri absUrl, out Uri relUrl, out string path)

Pour écrire son code, nous allons utiliser la classe Uri de .NET. Cette dernière sait faire pratiquement toute les manipulations d'URL dont nous avons besoin. Avec ce que nous avons fait dans le précédent billet, nous avons l'URL de départ de l'aspiration dans une instance de la classe Uri. Nous allons faire la même chose pour le répertoire de base locale. Pour que la classe Uri comprenne bien que c'est un répertoire il faut que nous ayons un \ à la fin du nom. Nous modifions donc l'expression régulière de validation du chemin.
Nous avons également besoin du nom de fichier par défaut complet (DefaultName + DefaultExtension), nous l'ajoutons.
Le type énuméré permet facilement de limiter les erreurs sur la saisie de l'extension des fichiers et permettait de voir un binding non triviale à réaliser dans le troisième billet. Cela dit pour les noms de fichiers, une version texte serait la bienvenue.
Pour finir, BaseUrl peut contenir l'URL d'un élément et non d'un site, cela dépend uniquement de l'utilisateur. Ce qui nous intéresse pour le calcul des URL, c'est une URL de répertoire virtuel. Tout cela nous amène à modifier fortement notre classe WebSiteModele :

using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
 
namespace Aspirateur.Model
{
    public partial class WebSiteModele : ModeleBase
    {
        #region Constructeurs
        /// <summary>
        /// Il nous faut un constructeur par défaut.
        /// </summary>
        public WebSiteModele() : base() { }
 
        /// <summary>
        /// Ce constructeur permet d'éviter les problèmes liés aux validations des propriétés.
        /// </summary>
        /// <param name="url"></param>
        /// <param name="directory"></param>
        /// <param name="name"></param>
        /// <param name="extension"></param>
        public WebSiteModele(Uri url, Uri directory, string name, Extension extension)
            : base()
        {
            _baseUrl = url;
            _baseVirtualDiretory = ExtractVirtualDirectory(_baseUrl);
            _baseDirectory = directory;
            _defaultName = name;
            _defaultExtension = extension;
            _defaultFileName = _defaultName + DefaultExtensionString;
        }
        #endregion
 
        #region BaseUrl
        private Uri _baseUrl = null;
 
        /// <summary>
        /// L'URL de base à aspirer.
        /// </summary>
        [Required(ErrorMessage = "Il faut obligatoirement une URL de départ.")]
        [RegularExpression(@"^http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&amp;=]*)?$", ErrorMessage = "URL non reconnue, URL absolu et complète obligatoire.")]
        public Uri BaseURL
        {
            get { return _baseUrl; }
            set
            {
                if (_baseUrl != value)
                {
                    RaisePropertyChanging<Uri>(() => BaseURL);
                    _baseUrl = value;
                    _baseVirtualDiretory = ExtractVirtualDirectory(_baseUrl);
                    RaisePropertyChanged<Uri>(() => BaseURL);
                }
            }
        }
        #endregion
 
        #region BaseVirtualDiretory
        private Uri _baseVirtualDiretory = null;
 
        /// <summary>
        /// L'URL du site ou du répertoire de base à aspirer.
        /// </summary>
        public Uri BaseVirtualDiretory
        {
            get { return _baseVirtualDiretory; }
        }
        #endregion
 
        #region BaseDirectory
        private Uri _baseDirectory = null;
 
        /// <summary>
        /// Le répertoire de base de la version locale du site.
        /// </summary>
        [Required(ErrorMessage = "Il faut obligatoirement un répertoire racine.")]
        [RegularExpression(@"^(([a-zA-Z]:)(?=(\\(\w[\w ]*)))(\\\w[\w ]*)*)\\$", ErrorMessage = "Répertoire non reconnu. Chemin absolu et \\ final obligatoires.")]
        public Uri BaseDirectory
        {
            get { return _baseDirectory; }
            set
            {
                if (_baseDirectory != value)
                {
                    RaisePropertyChanging<Uri>(() => BaseDirectory);
                    _baseDirectory = value;
                    RaisePropertyChanged<Uri>(() => BaseDirectory);
                }
            }
        }
        #endregion
 
        #region DefaultName
        private string _defaultName = null;
 
        /// <summary>
        /// Le nom par défaut donné aux fichiers sans nom.
        /// </summary>
        [Required(ErrorMessage = "Il faut obligatoirement un nom de fichier par défaut.")]
        [StringLength(30, MinimumLength = 3, ErrorMessage = "Le nom par défaut doit faire entre 3 et 30 caractères. ")]
        public string DefaultName
        {
            get { return _defaultName; }
            set
            {
                if (_defaultName != value)
                {
                    RaisePropertyChanging<String>(() => DefaultName);
                    _defaultName = value;
                    _defaultFileName = _defaultName + DefaultExtensionString;
                    RaisePropertyChanged<String>(() => DefaultName);
                }
            }
        }
        #endregion
 
        #region DefaultFileName
        private string _defaultFileName = null;
 
        /// <summary>
        /// Le nom complet par défaut donné aux fichiers sans nom.
        /// </summary>
        public string DefaultFileName
        {
            get { return _defaultFileName; }
        }
        #endregion
 
        #region DefaultExtension
        private Extension _defaultExtension = Extension.HTM;
 
        /// <summary>
        ///  L'extension par défaut donnée aux fichiers HTML locaux.
        /// </summary>
        public Extension DefaultExtension
        {
            get { return _defaultExtension; }
            set
            {
                if (_defaultExtension != value)
                {
                    RaisePropertyChanging<Extension>(() => DefaultExtension);
                    _defaultExtension = value;
                    _defaultFileName = _defaultName + DefaultExtensionString;
                    RaisePropertyChanged<Extension>(() => DefaultExtension);
                }
            }
        }
        #endregion
 
        #region DefaultExtensionString
        private string[] _defaultExtensions = new string[] {".htm", ".html"};
 
        /// <summary>
        ///  Version texte de l'extension par défaut donnée aux fichiers HTML locaux.
        /// </summary>
        public string DefaultExtensionString
        {
            get { return _defaultExtensions[(int)_defaultExtension]; }
        }
        #endregion
 
        #region Méthodes privées
        /// <summary>
        /// Extrait de l'URL fournie la partie répertoire virtuelle.
        /// Dit autrement, élimine le nom de fichier s'il y en a un.
        /// La fonction considère qu'il y a un nom de fichier si le dernier caractère n'est pas un /.
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private Uri ExtractVirtualDirectory(Uri url)
        {
            string absPath = url.AbsolutePath;
            if (absPath.EndsWith("/"))
            {
                return url;
            }
            else
            {
                return new Uri(url, absPath.Substring(0, absPath.LastIndexOf('/') + 1));
            }
        }
        #endregion
    }
}

Au final seul BaseVirtualDiretory n'a pas été modifié. Du côté du code appelant nous effectuons une modification équivalente :

// Initialisation du binding.
_leSite = new WebSiteModele(new Uri("http://www.canalblog.com"), new Uri(@"D:\test\canalblog\"), "canalblog", Extension.HTM);
DataContext = _leSite;

Pour travailler UrlFileMatcher a besoin des informations contenues dans notre instance de WebSiteModele et de l'URL de la page en cours de traitement. Nous créons donc les membres correspondant et ajoutons le constructeur idoine :

using System;
using Aspirateur.Model;
using System.Net;
 
namespace AspirateurUtil
{
    /// <summary>
    /// Classe de traitement des URL.
    /// </summary>
 
    public class UrlFileMatcher
    {
        #region Membres
        // Les données décrivant le site à traiter.
        private WebSiteModele _leSite;
 
        // L'URL de la page courante.
        private Uri _currentUrl;
        #endregion
 
        #region Constructeurs
        public UrlFileMatcher(WebSiteModele leSite, Uri currentUrl)
        {
            _leSite = leSite;
            _currentUrl = currentUrl;
        }
        #endregion
 
        #region Méthodes publiques
        /// <summary>
        /// Calcule, à partir d'une URL trouvée dans une page web,
        /// toutes les informations nécessaires au traitement par l'aspirateur.
        /// </summary>
        /// <param name="url">L'URL à traiter.</param>
        /// <param name="absUrl">L'URL absolue à utiliser pour récupérer l'élément.</param>
        /// <param name="relUrl">L'URL relative à utiliser dans le HTLL local.</param>
        /// <param name="filePath">Le chemin complet à utiliser pour sauver localement l'élément.</param>
        /// <returns>TRue si tout a pu être calculé, false en cas de problème.</returns>
        public bool ProcessUrl(string url, out Uri absUrl, out Uri relUrl, out string path)
        {
            return false;
        }
        #endregion
    }
}

Nous avons maintenant tout ce qu'il nous faut pour traiter les URL pointant sur le site que nous voulons aspirer. Le code de ProcessUrl peut alors prendre la forme suivante :

using System.IO;
 
public bool ProcessUrl(string url, bool isPage, out Uri absUrl, out Uri relUrl, out string path)
{
    // Initialisation.
    Uri filePath = null;
    absUrl = relUrl = null;
    path = null;
 
    try
    {
        // Construction de l'adresse absolue.
        absUrl = new Uri(_currentUrl, url);
 
        // On vérifie que l'adresse absolue pointe bien vers le site principal.
        if (_leSite.BaseURL.IsBaseOf(absUrl))
        {
            // Construction de l'URL relative.
            relUrl = _currentUrl.MakeRelativeUri(absUrl);
 
            // Construction du chemin complet local.
            filePath = new Uri(_leSite.BaseDirectory, _leSite.BaseVirtualDiretory.MakeRelativeUri(absUrl));
        }
        else
        {
            // Pour l'instant on ne fait rien.
            return false;
        }
 
        // On s'assure que le répertoire de destination existe.
        path = filePath.LocalPath;
        Directory.CreateDirectory(Path.GetDirectoryName(path));
 
        // Est-ce bien une URL de fichier ?
        if (string.IsNullOrWhiteSpace(Path.GetFileName(path)))
        {
            // Non, on ajoute le nom complet par défaut.
            path = Path.Combine(path, _leSite.DefaultFileName);
        }
        // Faut-il modifier l'extension ?
        else if (isPage)
        {
            path = Path.ChangeExtension(path, _leSite.DefaultExtensionString);
        }
 
        // Tout va bien.
        return true;
    }
    catch (Exception Ex)
    {
        // Il nous faudrait loguer l'erreur mais nous ne savons pas faire.
        return false;
    }
}

Ce code ne présente pas de difficulté particulière, les commentaires permettant de savoir ce qui est fait. L'utilisation des classes .NET Uri, Path et Directory retire toute la complexité potentielle (même si, ni Path, ni Directory, ne fonctionne avec Uri, d'où la conversion en chaîne de caractères).
La seule chose qui peut paraître surprenante c'est le fait que l'on demande si l'URL concerne une page ou pas.
C'est pour pouvoir forcer l'extension des pages sans devoir imaginer un algorithme permettant de déterminer de manière sûr si l'URL est celle d'une page ou pas. Cela reporte évidement le problème sur l'appelant, mais nous verrons que l'appelant n'a pas de difficulté à savoir dans quel cas il se trouve.

Remarque : Le catch génère un warning à la compilation car, ne savant pas loguer, nous n'utilisons pas l'exception. Le code reste tout de même, car il peut être utile sous débogueur et rien ne vous empêche d'ajouter une écriture dans un fichier de trace.

Il ne nous reste plus qu'à initialiser une instance de UrlFileMatcher avant d'initialiser l'instance de ImageProcessor dans la fonction btnGo_Click. Evidemment nous devrons passer cette instance à l'instance de ImageProcessor pour qu'elle puisse l'utiliser. Cela nous donne dans btnGo_Click :

// On prépare le traitement des URL.
UrlFileMatcher urlFileMatcher = new UrlFileMatcher(_leSite, _leSite.BaseURL);
 
// On cherche les images.
ImageProcessor imgProc = new ImageProcessor(urlFileMatcher);
strPage = imgProc.ProcessPage(strPage);

Sans surprise, il faut modifier ImageProcessor en conséquence (seules les parties modifiées sont présentées) :

#region Membres
// La classe à utiliser pour le traitement de l'URL.
private UrlFileMatcher _urlFileMatcher;
#endregion
 
#region Constructeurs
public ImageProcessor(UrlFileMatcher urlFileMatcher)
{
    _urlFileMatcher = urlFileMatcher;
}
#endregion
 
#region Méthodes privées
/// <summary>
/// Fonction appelée lorsqu'un tag IMG est trouvé.
/// </summary>
/// <param name="m">Le tag img trouvé</param>
/// <returns>Le tag img avec l'URL locale.</returns>
private string ImageEvaluator(Match m)
{
    Uri absUrl, relUrl;
    string path;
 
    // Génération des noms nécessaires au traitement.
    if (!_urlFileMatcher.ProcessUrl(m.Groups[1].Value, false, out absUrl, out relUrl, out path))
    {
        // On ne sait pas traiter.
        return m.ToString();
    }
 
    // Récupération de l'image.
    if (!DownloadHelper.GetFile(absUrl, path))
    {
        // Rien en locale, on garde le tag d'origine.
        return m.ToString();
    }
 
    // On substitue l'URL locale à l'URL distante.
    StringBuilder strURL = new StringBuilder();
    int iDebut = m.Groups[1].Index - m.Groups[0].Index;
    strURL.Append(m.Groups[0].Value.Substring(0, iDebut));
    strURL.Append(relUrl.ToString());
    strURL.Append(m.Groups[0].Value.Substring(iDebut + m.Groups[1].Length));
    return strURL.ToString();
}
#endregion

Nous avons utilisé une fonction de DownloadHelper qui n'existe pas. Nous l'ajoutons et le code complet de DownloadHelper devient :

using System;
using System.Net;
 
namespace AspirateurUtil
{
    /// <summary>
    /// Diverses fonctions aidant à la récupération de données sur le web.
    /// </summary>
    public class DownloadHelper
    {
        /// <summary>
        /// Téléchargement du fichier indiqué.
        /// </summary>
        /// <param name="targetUrl">L'URL cible.</param>
        /// <param name="filePath">Le fichier destination.</param>
        /// <returns></returns>
        public static bool GetFile(Uri targetUrl, string filePath)
        {
            using (WebClient wc = new WebClient())
            {
                try
                {
                    wc.DownloadFile(targetUrl, filePath);
                    return true;
                }
                catch (Exception Ex)
                {
                    // Il nous faudrait loguer l'erreur mais nous ne savons pas faire.
                    return false;
                }
            }
        }
 
        /// <summary>
        /// Récupération du code HTML d'une page web.
        /// </summary>
        /// <param name="targetURL">L'URL cible.</param>
        /// <returns></returns>
        public static string GetPage(Uri targetURL)
        {
            using (WebClient wc = new WebClient())
            {
                try
                {
                    return wc.DownloadString(targetURL);
                }
                catch (Exception Ex)
                {
                    // Il nous faudrait loguer l'erreur mais nous ne savons pas faire.
                    return string.Empty;
                }
            }
        }
    }
}

Si vous lancer le programme, vous pourrez constater la création de répertoires et l'apparition de fichiers images.
Pour avoir quelque chose d'un peu utilisable nous allons sauver le HTML modifié de notre page. Nous faisons donc appelle à UrlFileMatcher pour obtenir le chemin complet du fichier HTML que nous voulons sauver. Il apparait alors qu'il serait utile d'essayer de générer le nom du fichier avant de traiter les tags contenus dans la page. En effet, si nous ne pouvons générer le nom local, nous ne pourrons pas sauver le HTML. Il est alors inutile de récupérer les éléments liés à la page. Le code de btnGo_Click devient alors :

private void btnGo_Click(object sender, RoutedEventArgs e)
{
    // On indique que l'on travaille.
    Cursor oldCursor = this.Cursor;
    this.Cursor = Cursors.Wait;
 
    try
    {
        string strPage = DownloadHelper.GetPage(_leSite.BaseURL);
 
        // On génère le nom local associé à la page récupérée.
        Uri absUrl, relUrl;
        string path;
        UrlFileMatcher urlFileMatcher = new UrlFileMatcher(_leSite, _leSite.BaseURL);
        if (urlFileMatcher.ProcessUrl(_leSite.BaseURL.ToString(), true, out absUrl, out relUrl, out path))
        {
            // On cherche les images.
            ImageProcessor imgProc = new ImageProcessor(urlFileMatcher);
            strPage = imgProc.ProcessPage(strPage);
 
            // On sauve la page.
            StreamWriter sw = null;
            try
            {
                sw = new StreamWriter(path, false, System.Text.Encoding.ASCII);
                sw.WriteLine(strPage);
                sw.Close();
            }
            catch (Exception Ex)
            {
                MessageBox.Show(Ex.ToString(), "btnGo_Click");
                if (null != sw)
                {
                    sw.Close();
                    File.Delete(path);
                }
            }
        }
    }
    finally
    {
        this.Cursor = oldCursor;
    }
}

Si nous lançons le programme et cliquons directement sur le bouton "Aspire", il fonctionne correctement. Mais si nous avons la mauvaise idée de mettre une URL d'image dans l'URL de départ, http://www.canalblog.com/sharedDocs/images/frontend/logo.gif par exemple, les choses se compliquent. Notre image semble présente mais sous le nom logo.htm.
Cela est dû au fait que nous avons forcé le programme à considérer que l'utilisateur ne fournira jamais une URL de ressource. Rien ne le dit. Nous devons donc vérifier la nature de ce que l'utilisateur nous a demandé de récupérer.
La meilleure méthode consiste à le demander au serveur web. Pour cela nous avons besoin du header HTTP associé à l'élément demandé par l'utilisateur. Un problème se pose alors car WebClient ne sait pas récupérer juste le header d'une ressource web.
Nous devons donc lui apprendre. Cela se fait en créant une classe dérivée de WebClient et en surchargeant les méthodes qui vont bien. Nous ajoutons donc à AspirateurUtil une nouvelle classe CustomWebClient :

using System;
using System.Net;
 
namespace AspirateurUtil
{
    /// <summary>
    /// Classe étendant les possibilités de WebClient.
    /// </summary>
    internal class CustomWebClient : WebClient
    {
        public bool HeaderOnly { get; set; }
 
        protected override WebRequest GetWebRequest(Uri address)
        {
            WebRequest req = base.GetWebRequest(address);
            if (HeaderOnly && req.Method == "GET")
            {
                req.Method = "HEAD";
            }
            return req;
        }
    }
}

Cela fait nous ajoutons une nouvelle méthode à la classe DownloadHelper :

/// <summary>
/// L'URL correspond-t-elle à une page web ?
/// </summary>
/// <param name="targetURL">L'URL cible.</param>
/// <returns></returns>
public static bool IsPage(Uri targetURL)
{
    // Notre version de WebClient nous permet de ne récupérer que le header HTTP.
    using (CustomWebClient wc = new CustomWebClient())
    {
        try
        {
            wc.HeaderOnly = true;
            byte[] body = wc.DownloadData(targetURL);
            string type = wc.ResponseHeaders["content-type"];
            return type.Contains(@"text/html");
        }
        catch (Exception Ex)
        {
            // Il nous faudrait loguer l'erreur mais nous ne savons pas faire.
            return false;
        }
    }
}

Nous pouvons maintenant traiter correctement la demande de l'utilisateur :

private void btnGo_Click(object sender, RoutedEventArgs e)
{
    // On indique que l'on travaille.
    Cursor oldCursor = this.Cursor;
    this.Cursor = Cursors.Wait;
 
    try
    {
        Uri            absUrl, relUrl;
        string         path;
        UrlFileMatcher urlFileMatcher = new UrlFileMatcher(_leSite, _leSite.BaseURL);
 
        // L'utilisateur a-t-il demandé une page ?
        if (!DownloadHelper.IsPage(_leSite.BaseURL))
        {
            // Génération des noms nécessaires au traitement et récupération du fichier.
            if (urlFileMatcher.ProcessUrl(_leSite.BaseURL.ToString(), false, out absUrl, out relUrl, out path))
            {
                DownloadHelper.GetFile(absUrl, path);
            }
            return;
        }
 
        // On est maintenant certain qu'il s'agit d'une page.
        string strPage = DownloadHelper.GetPage(_leSite.BaseURL);
 
        // On génère le nom local associé à la page récupérée.
        if (urlFileMatcher.ProcessUrl(_leSite.BaseURL.ToString(), true, out absUrl, out relUrl, out path))
        {
            // On cherche les images.
            ImageProcessor imgProc = new ImageProcessor(urlFileMatcher);
            strPage = imgProc.ProcessPage(strPage);
 
            // On sauve la page.
            StreamWriter sw = null;
            try
            {
                sw = new StreamWriter(path, false, System.Text.Encoding.ASCII);
                sw.WriteLine(strPage);
                sw.Close();
            }
            catch (Exception Ex)
            {
                MessageBox.Show(Ex.ToString(), "btnGo_Click");
                if (null != sw)
                {
                    sw.Close();
                    File.Delete(path);
                }
            }
        }
    }
    finally
    {
        this.Cursor = oldCursor;
    }
}

Nous pouvons maintenant fournir n'importe quelle URL valide à notre programme, il fonctionnera correctement.

Certes notre aspirateur n'est pas encore très puissant.
Il n'aspire qu'une page et encore qu'avec les images qui lui sont associées et qui ont le bon goût d'être sur le site principal de l'URL initiale.
Nous ferons évoluer tout cela dans le prochain billet, qui verra le retour en force des expressions régulières. Préparez-vous à une petite prise de tête.

En attendant voici le code complet de ce billet.

Publicité
Publicité
Commentaires
Fanfois et ses papillons
Publicité
Archives
Publicité