IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Scrapix
Un « aspirateur » web (web crawler) simple orienté VCL qui gère : la récupération HTTP, l'extraction de liens et de ressources

Le , par XeGregory

0PARTAGES

Scrapix, un « aspirateur » web (web crawler) simple orienté VCL qui gère : la récupération HTTP, l’extraction de liens et de ressources (images, documents, audio, vidéo, ressources web), le respect optionnel de robots.txt, un mécanisme facultatif de téléchargement des ressources, des limites (nombre de fichiers trouvés, nombre de pages explorées), et des mises à jour UI thread-safe vers un TscListView et un TscStatusBar.

Le crawler est conçu pour être lancé depuis un thread d’arrière-plan et pour mettre à jour l’interface en toute sécurité via des wrappers TThread.Queue.
Il expose des commandes pour démarrer, mettre en pause, reprendre, annuler et attendre l’arrêt.

671302

Scrapix.Core.pas
unit Scrapix.Core;

interface

uses
{Winapi}
WinApi.Windows, WinApi.Messages,
{System}
System.SysUtils, System.Classes, System.Generics.Collections,
System.RegularExpressions, System.Types, System.Net.HttpClient,
System.Net.URLClient, System.IOUtils, System.Threading, System.SyncObjs,
{Vcl}
Vcl.ComCtrls, Vcl.StdCtrls, Vcl.CheckLst, Vcl.Graphics,
{StyleControls VCL}
scControls,
{Translate.Core}
Translate.Core;

const
// États d'exploration
STATE_RUNNING = 0;
STATE_PAUSED = 1;
STATE_CANCEL = 2;

// User-Agent utilisé pour toutes les requêtes HTTP
UserAgent = 'Scrapix/1.0';

type
TScrapix = class
private
{ --- Données de suivi et compteurs --- }
VisitedLinks: TDictionary;
FFoundFiles: TDictionary;
FBrokenLinks: TDictionary;
TotalLinks: Integer;
FileCount: Integer;
BrokenCount: Integer;
FRobotsBlocked: Integer;
FLinksTraversed: Integer;

{ --- Contrôle d'exécution --- }
FState: Integer;
FPauseEvent: TEvent;
FStoppedEvent: TEvent;

{ --- Paramètres de crawl --- }
RequestTimeoutMs: Integer;
RequestDelayMs: Integer;
SameDomainOnly: Boolean;
RootDomain: string;

{ --- Téléchargement --- }
FAutoDownload: Boolean;
DownloadFolder: string;

{ --- Robots.txt --- }
RobotsRules: TDictionary;
FRespectRobots: Boolean;

{ --- Filtres de ressources --- }
FSearchImages: Boolean;
FSearchDocuments: Boolean;
FSearchAudio: Boolean;
FSearchVideo: Boolean;
FSearchWeb: Boolean;

{ --- Exécution courante --- }
FRunning: Boolean;
FMaxDepth: Integer;

{ --- Chemins de rapports temporaires --- }
FVisitedFilePath: string;
FBrokenFilePath: string;
FFoundFilePath: string;

FLogFilePath: string;
FLogLock: TCriticalSection;

FDisableUIUpdates: Boolean;

{ --- Limites configurables --- }
FFoundFilesLimit: Integer;
FExploreLimit: Integer;

{ --- Fonctions internes --- }

// Retourne true si les mises à jour UI sont autorisées
function UIUpdatesAllowed: Boolean;

// Récupère le contenu d'une URL via GET et met à jour le ListView (thread-safe)
function GetWebContent(const URL: string; ListView: TscListView;
Depth: Integer; Logging: TscListBox): string;

// Extrait les liens et normalise les URL
procedure ExtractLinks(const HTML: string; BaseURL: string;
var LinkList: TStringList);

// Extrait sources médias (images, docs, audio, vidéo, ressources web) selon les flags actifs
procedure ExtractMediaSources(const HTML: string; BaseURL: string;
var ImageList, DocList, AudioList, VideoList, WebList: TStringList);

// Incrémente le compteur d'URL bloquées par robots.txt et met à jour le StatusBar
procedure IncrementRobotsBlocked(StatusBar: TscStatusBar);

// Incrémente le compteur de liens parcourus et applique la limite d'exploration
procedure IncrementLinksTraversed(StatusBar: TscStatusBar);

// Traite et enregistre un lien cassé : UI, dictionnaire, fichier rapport
procedure MarkBrokenLink(const URL: string; ListView: TscListView;
StatusBar: TscStatusBar; Logging: TscListBox);

// Vérifie la disponibilité d'un fichier via HEAD; fallback GET range si HEAD échoue
function IsFileAvailable(const URL: string; ListView: TscListView;
StatusBar: TscStatusBar; Depth: Integer; Logging: TscListBox): Boolean;

// Télécharge une ressource dans le dossier de téléchargement (organisé par type)
function DownloadFile(const URL: string; Client: THTTPClient;
out LocalPath: string; Logging: TscListBox): Boolean;

// Routine factorisée pour traiter une liste de ressources (vérif, robots, dispo, download)
procedure ProcessResourceGroup(ResourceList: TStringList;
const AcceptExts: array of string; StatusBar: TscStatusBar;
ListView: TscListView; Depth: Integer; const DefaultUIType: string;
Logging: TscListBox);

// Traite toutes les listes de ressources extraites d'une page (appel ProcessResourceGroup)
procedure ProcessFoundFiles(ImageList, DocList, AudioList, VideoList,
WebList: TStringList; StatusBar: TscStatusBar; ListView: TscListView;
CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox);

// Exploration récursive d'une page : récupération, extraction, traitement, récursion
procedure ExploreLinksRecursive(const URL: string; ListView: TscListView;
StatusBar: TscStatusBar; CheckList: TscCheckListBox; Depth: Integer;
Logging: TscListBox);

// Vérifie si LinkURL appartient au même domaine que BaseURL (ou sous-domaine)
function IsSameDomain(const BaseURL, LinkURL: string): Boolean;

// Normalise une URL relative ou absolue en URL absolue utilisable
function NormalizeURL(const BaseURL, RelOrAbsURL: string): string;

// Wrappers thread-safe pour mise à jour ListView / StatusBar
procedure SafeUpdateListViewStatus(ListView: TscListView;
const URL, StatusText: string; const Method: string = '');
procedure SafeUpdateListViewDownloadState(ListView: TscListView;
const URL, DownloadState: string);
procedure SafeUpdateListViewInfo(ListView: TscListView; const URL: string;
RespMs: Integer; const SizeBytes: string; Depth: Integer);
procedure SafeScrollListViewToBottom(ListView: TscListView);
procedure SafeSetStatusBarPanel(StatusBar: TscStatusBar;
PanelIndex: Integer; const Text: string);

// Logging thread-safe vers une TscListBox
procedure SafeLog(Logging: TscListBox; const Msg: string);

// robots.txt helpers : parse, cache et vérifie l'autorisation
function ParseRobots(const RobotsText: string;
OutList: TStringList): Boolean;
function EnsureRobotsForHost(const Host, Scheme: string): Boolean;
function IsAllowedByRobots(const URL: string): Boolean;

public
// Constructeur : initialise structure et valeurs par défaut
constructor Create;
// Destructeur : annule, attend et libère ressources
destructor Destroy; override;

// Configure les paramètres du crawl (timeouts, limites, options)
procedure ConfigureCrawl(const ARequestTimeoutMs, ARequestDelayMs: Integer;
ASameDomainOnly: Boolean; AAutoDownload: Boolean; ARespectRobots: Boolean;
AFoundFilesLimit: Integer; AExploreLimit: Integer);

// Démarre l'exploration synchroniquement ; crée rapports si demandé
procedure ExploreLinks(const URL: string; ListView: TscListView;
StatusBar: TscStatusBar; CheckList: TscCheckListBox; MaxDepth: Integer;
SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile: Boolean;
Logging: TscListBox);

// Mettre en pause, reprendre ou annuler l'exploration
procedure PauseExploration;
procedure ResumeExploration;
procedure CancelExploration;
function IsCanceled: Boolean;
function IsPaused: Boolean;
function IsRunning: Boolean;

// Attend l'arrêt complet (bloquant) ; supprime le paramètre Timeout
function WaitForStop: Boolean;

// Applique les filtres de types de fichiers depuis une CheckList UI
procedure ApplyFileTypeFiltersFromCheckList(CheckList: TscCheckListBox);

// Autorise la désactivation des mises à jour UI (tests, performances)
property DisableUIUpdates: Boolean read FDisableUIUpdates
write FDisableUIUpdates;
end;

implementation

uses
System.Net.Mime, System.StrUtils;

function TScrapix.UIUpdatesAllowed: Boolean;
begin
Result := not FDisableUIUpdates;
end;

procedure TScrapix.SafeScrollListViewToBottom(ListView: TscListView);
begin
if not UIUpdatesAllowed then
Exit;
if ListView = nil then
Exit;

if TThread.Current.ThreadID = MainThreadID then
begin
if (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
if ListView.Items.Count > 0 then
ListView.Items[ListView.Items.Count - 1].MakeVisible(False);
end
else
begin
TThread.Queue(nil,
procedure
begin
if not UIUpdatesAllowed then
Exit;
if (ListView = nil) or (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
if ListView.Items.Count > 0 then
ListView.Items[ListView.Items.Count - 1].MakeVisible(False);
end);
end;
end;

procedure TScrapix.SafeSetStatusBarPanel(StatusBar: TscStatusBar;
PanelIndex: Integer; const Text: string);
begin
if not UIUpdatesAllowed then
Exit;
if StatusBar = nil then
Exit;
if PanelIndex < 0 then
Exit;

if TThread.Current.ThreadID = MainThreadID then
begin
if (csDestroying in StatusBar.ComponentState) or
(not StatusBar.HandleAllocated) then
Exit;
if PanelIndex < StatusBar.Panels.Count then
StatusBar.Panels.Text := Text;
end
else
begin
TThread.Queue(nil,
procedure
begin
if not UIUpdatesAllowed then
Exit;
if (StatusBar = nil) or (csDestroying in StatusBar.ComponentState) or
(not StatusBar.HandleAllocated) then
Exit;
if PanelIndex < StatusBar.Panels.Count then
StatusBar.Panels.Text := Text;
end);
end;
end;

procedure TScrapix.SafeUpdateListViewStatus(ListView: TscListView;
const URL, StatusText: string; const Method: string = '');
var
sURL, sStatus, sMethod: string;
I: Integer;
Item: TListItem;
begin
if not UIUpdatesAllowed then
Exit;
if ListView = nil then
Exit;

sURL := NormalizeURL(URL, URL);
if sURL = '' then
sURL := URL;
sStatus := StatusText;
sMethod := Method;

if TThread.Current.ThreadID = MainThreadID then
begin
if (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;

for I := 0 to ListView.Items.Count - 1 do
begin
Item := ListView.Items;
if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then
begin
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sStatus;
Item.SubItems := sMethod;
SafeScrollListViewToBottom(ListView);
Exit;
end;
end;

Item := ListView.Items.Add;
Item.Caption := sURL;
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sStatus;
Item.SubItems := sMethod;
SafeScrollListViewToBottom(ListView);
end
else
begin
TThread.Queue(nil,
procedure
var
I2: Integer;
It: TListItem;
begin
if not UIUpdatesAllowed then
Exit;
if (ListView = nil) or (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
for I2 := 0 to ListView.Items.Count - 1 do
begin
It := ListView.Items;
if SameText(It.Caption, sURL) or SameText(It.Caption, URL) then
begin
while It.SubItems.Count < 6 do
It.SubItems.Add('');
It.SubItems := sStatus;
It.SubItems := sMethod;
SafeScrollListViewToBottom(ListView);
Exit;
end;
end;
It := ListView.Items.Add;
It.Caption := sURL;
while It.SubItems.Count < 6 do
It.SubItems.Add('');
It.SubItems := sStatus;
It.SubItems := sMethod;
SafeScrollListViewToBottom(ListView);
end);
end;
end;

procedure TScrapix.SafeUpdateListViewDownloadState(ListView: TscListView;
const URL, DownloadState: string);
var
sURL, sState: string;
I: Integer;
Item: TListItem;
begin
if not UIUpdatesAllowed then
Exit;
if ListView = nil then
Exit;

sURL := NormalizeURL(URL, URL);
if sURL = '' then
sURL := URL;
sState := DownloadState;

if TThread.Current.ThreadID = MainThreadID then
begin
if (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
for I := 0 to ListView.Items.Count - 1 do
begin
Item := ListView.Items;
if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then
begin
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sState;
SafeScrollListViewToBottom(ListView);
Exit;
end;
end;
Item := ListView.Items.Add;
Item.Caption := sURL;
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sState;
SafeScrollListViewToBottom(ListView);
end
else
begin
TThread.Queue(nil,
procedure
var
I2: Integer;
It: TListItem;
begin
if not UIUpdatesAllowed then
Exit;
if (ListView = nil) or (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
for I2 := 0 to ListView.Items.Count - 1 do
begin
It := ListView.Items;
if SameText(It.Caption, sURL) or SameText(It.Caption, URL) then
begin
while It.SubItems.Count < 6 do
It.SubItems.Add('');
It.SubItems := sState;
SafeScrollListViewToBottom(ListView);
Exit;
end;
end;
It := ListView.Items.Add;
It.Caption := sURL;
while It.SubItems.Count < 6 do
It.SubItems.Add('');
It.SubItems := sState;
SafeScrollListViewToBottom(ListView);
end);
end;
end;

procedure TScrapix.SafeUpdateListViewInfo(ListView: TscListView;
const URL: string; RespMs: Integer; const SizeBytes: string; Depth: Integer);
var
sURL, sRespMs, sSizeLocal, sDepth: string;
DepthLocal: Integer;
I: Integer;
Item: TListItem;
begin
if not UIUpdatesAllowed then
Exit;
if ListView = nil then
Exit;

sURL := NormalizeURL(URL, URL);
if sURL = '' then
sURL := URL;

if RespMs < 0 then
sRespMs := ''
else
sRespMs := IntToStr(RespMs);
sSizeLocal := SizeBytes;
DepthLocal := Depth;
if DepthLocal > 0 then
sDepth := IntToStr(DepthLocal)
else
sDepth := '';

if TThread.Current.ThreadID = MainThreadID then
begin
if (csDestroying in ListView.ComponentState) or
(not ListView.HandleAllocated) then
Exit;
for I := 0 to ListView.Items.Count - 1 do
begin
Item := ListView.Items;
if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then
begin
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sRespMs;
Item.SubItems := sSizeLocal;
if sDepth <> '' then
Item.SubItems := sDepth;
SafeScrollListViewToBottom(ListView);
Exit;
end;
end;
Item := ListView.Items.Add;
Item.Caption := sURL;
while Item.SubItems.Count < 6 do
Item.SubItems.Add('');
Item.SubItems := sRespMs;
Item.SubItems := sSizeLocal;
if sDepth <> '' then
Item.SubItems := sDepth;
SafeScrollListViewToBottom(ListView);
end
else
begin
TThread.Queue(nil,
procedure
begin
if not UIUpdatesAllowed then
Exit;
SafeUpdateListViewInfo(ListView, URL, RespMs, SizeBytes, Depth);
end);
end;
end;

procedure TScrapix.SafeLog(Logging: TscListBox; const Msg: string);
const
HORIZONTAL_MARGIN = 16;
var
NewWidth, CurrExtent, ClientW: Integer;
CanvasHandle: TCanvas;
S: string;
begin
S := Msg;

if UIUpdatesAllowed and (Logging <> nil) then
begin
if TThread.Current.ThreadID = MainThreadID then
begin
if (not(csDestroying in Logging.ComponentState)) and Logging.HandleAllocated
then
begin
try
Logging.Items.Add(S);
Logging.ItemIndex := Logging.Items.Count - 1;
except
end;
try
CanvasHandle := Logging.Canvas;
NewWidth := CanvasHandle.TextWidth(S) + HORIZONTAL_MARGIN;
except
NewWidth := 0;
end;
if NewWidth > 0 then
begin
CurrExtent := SendMessage(Logging.Handle,
LB_GETHORIZONTALEXTENT, 0, 0);
if NewWidth > CurrExtent then
SendMessage(Logging.Handle, LB_SETHORIZONTALEXTENT, NewWidth, 0)
else
begin
ClientW := Logging.ClientWidth;
if CurrExtent < ClientW then
SendMessage(Logging.Handle, LB_SETHORIZONTALEXTENT, ClientW, 0);
end;
end;
end;
end
else
begin
TThread.Queue(nil,
procedure
var
W: Integer;
Ce: Integer;
Cw: Integer;
begin
if not UIUpdatesAllowed then
Exit;
if (Logging = nil) or (csDestroying in Logging.ComponentState) or
(not Logging.HandleAllocated) then
Exit;
try
Logging.Items.Add(S);
Logging.ItemIndex := Logging.Items.Count - 1;
except
end;
try
W := Logging.Canvas.TextWidth(S) + HORIZONTAL_MARGIN;
except
W := 0;
end;
if W > 0 then
begin
Ce := SendMessage(Logging.Handle, LB_GETHORIZONTALEXTENT, 0, 0);
if W > Ce then
SendMessage(Logging.Handle, LB_SETHORIZONTALEXTENT, W, 0)
else
begin
Cw := Logging.ClientWidth;
if Ce < Cw then
SendMessage(Logging.Handle, LB_SETHORIZONTALEXTENT, Cw, 0);
end;
end;
end);
end;
end;

if (FLogFilePath <> '') and Assigned(FLogLock) then
begin
try
FLogLock.Acquire;
try
try
TFile.AppendAllText(FLogFilePath, S + sLineBreak, TEncoding.UTF8);
except
end;
finally
FLogLock.Release;
end;
except
end;
end;
end;

function TScrapix.NormalizeURL(const BaseURL, RelOrAbsURL: string): string;
var
S: string;
BaseUri: TURI;
SchemeHost, BasePath, BaseTrimmed: string;
Idx: Integer;
begin
Result := '';
S := Trim(RelOrAbsURL);
if S = '' then
Exit;

Idx := Pos('#', S);
if Idx > 0 then
Delete(S, Idx, MaxInt);

if S.StartsWith('mailto:', True) or S.StartsWith('javascript:', True) or
S.StartsWith('tel:', True) or S.StartsWith('data:', True) then
Exit;

if S.StartsWith('//') then
begin
Result := 'https:' + S;
Exit;
end;

if S.StartsWith('http://', True) or S.StartsWith('https://', True) then
begin
try
Result := TURI.Create(S).ToString
except
Result := S
end;
Exit;
end;

if BaseURL = '' then
Exit;
try
BaseUri := TURI.Create(BaseURL)
except
Exit
end;

SchemeHost := BaseUri.Scheme + '://' + BaseUri.Host;
if BaseUri.Port > 0 then
SchemeHost := SchemeHost + ':' + IntToStr(BaseUri.Port);

if S.StartsWith('/') then
begin
Result := SchemeHost + S;
Exit;
end;

BasePath := BaseUri.Path;
if (BasePath = '') or BasePath.EndsWith('/') then
BaseTrimmed := BasePath
else
BaseTrimmed := ExtractFilePath(BasePath);
if (BaseTrimmed = '') or (BaseTrimmed <> '/') then
BaseTrimmed := '/' + BaseTrimmed;

Result := SchemeHost + BaseTrimmed;
if not Result.EndsWith('/') then
Result := Result + '/';
Result := Result + S;

try
Result := TURI.Create(Result).ToString
except
end;
end;

function GetResponseHeaderValue(const Resp: IHTTPResponse;
const HeaderName: string): string;
var
I: Integer;
begin
Result := '';
if Resp = nil then
Exit;
for I := 0 to Length(Resp.Headers) - 1 do
if SameText(Resp.Headers.Name, HeaderName) then
Exit(Resp.Headers.Value);
end;

function FormatBytes(const SizeBytes: string): string;
var
Bytes: Int64;
d: Double;
DigitsOnly: string;
begin
Result := '';
if Trim(SizeBytes) = '' then
Exit;
if Trim(SizeBytes).ToLower = 'n/a' then
begin
Result := 'n/a';
Exit;
end;

try
Bytes := StrToInt64(Trim(SizeBytes))
except
DigitsOnly := TRegEx.Replace(SizeBytes, '[^0-9]', '');
if DigitsOnly = '' then
Exit;
try
Bytes := StrToInt64(DigitsOnly)
except
Exit
end;
end;

if Bytes = 0 then
begin
Result := 'n/a';
Exit;
end;

if Bytes < 1024 then
Result := Format('%d Octets', )
else if Bytes < Int64(1024) * 1024 then
begin
d := Bytes / 1024;
Result := FormatFloat('0.##', d) + ' Ko';
end
else if Bytes < Int64(1024) * 1024 * 1024 then
begin
d := Bytes / (1024 * 1024);
Result := FormatFloat('0.##', d) + ' Mo';
end
else
begin
d := Bytes / (1024 * 1024 * 1024);
Result := FormatFloat('0.##', d) + ' Go';
end;
end;

function TScrapix.ParseRobots(const RobotsText: string;
OutList: TStringList): Boolean;
var
Lines: TArray;
I: Integer;
CurrentAgents: TStringList;
L: string;
AgentMatched: Boolean;
PathPart: string;
begin
Result := False;
OutList.Clear;
if RobotsText = '' then
Exit;

Lines := RobotsText.Split([#13#10, #10, #13],
TStringSplitOptions.ExcludeEmpty);
CurrentAgents := TStringList.Create;
try
CurrentAgents.Clear;
AgentMatched := False;
for I := 0 to Length(Lines) - 1 do
begin
L := Trim(Lines);
if L = '' then
begin
CurrentAgents.Clear;
AgentMatched := False;
Continue;
end;

if StartsText('User-agent:', L) then
begin
CurrentAgents.Clear;
CurrentAgents.Add(Trim(Copy(L, Length('User-agent:') + 1, MaxInt)
).ToLower);
AgentMatched := SameText(CurrentAgents, 'Scrapix') or
SameText(CurrentAgents, '*');
Continue;
end;

if AgentMatched and StartsText('Disallow:', L) then
begin
PathPart := Trim(Copy(L, Length('Disallow:') + 1, MaxInt));
if PathPart = '' then
Continue;
if not PathPart.StartsWith('/') then
PathPart := '/' + PathPart;
if OutList.IndexOf(PathPart) = -1 then
OutList.Add(PathPart);
end;
end;
finally
CurrentAgents.Free;
end;
Result := True;
end;

function TScrapix.EnsureRobotsForHost(const Host, Scheme: string): Boolean;
var
Key: string;
RobotsURL, RobotsText: string;
Client: THTTPClient;
RespStream: TStringStream;
Resp: IHTTPResponse;
SL, NewSL: TStringList;
begin
if Host = '' then
Exit(False);
Key := LowerCase(Host);
if RobotsRules = nil then
RobotsRules := TDictionary.Create;
if RobotsRules.ContainsKey(Key) then
Exit(True);

RobotsURL := Scheme + '://' + Host + '/robots.txt';

Client := THTTPClient.Create;
try
Client.ConnectionTimeout := RequestTimeoutMs;
Client.ResponseTimeout := RequestTimeoutMs;
Client.UserAgent := UserAgent;
RespStream := TStringStream.Create('', TEncoding.UTF8);
try
try
Resp := Client.Get(RobotsURL, RespStream);
RobotsText := RespStream.DataString
except
RobotsText := '';
end;
finally
RespStream.Free;
end;
finally
Client.Free;
end;

SL := TStringList.Create;
try
ParseRobots(RobotsText, SL);
NewSL := TStringList.Create;
NewSL.Assign(SL);
RobotsRules.Add(Key, NewSL);
finally
SL.Free;
end;
Result := True;
end;

function TScrapix.IsAllowedByRobots(const URL: string): Boolean;
var
Host, Scheme, Path: string;
Rules: TStringList;
Key: string;
U: TURI;
I: Integer;
DisallowPath: string;
begin
if not FRespectRobots then
Exit(True);
Result := True;
if URL = '' then
Exit(True);

try
U := TURI.Create(URL);
Host := U.Host;
Scheme := U.Scheme;
Path := U.Path;
if Path = '' then
Path := '/';
except
Exit(True);
end;

Key := LowerCase(Host);
if (RobotsRules = nil) or (not RobotsRules.ContainsKey(Key)) then
EnsureRobotsForHost(Host, Scheme);

if (RobotsRules <> nil) and RobotsRules.ContainsKey(Key) then
begin
Rules := RobotsRules;
for I := 0 to Rules.Count - 1 do
begin
DisallowPath := Rules;
if Path.StartsWith(DisallowPath, True) then
begin
Result := False;
Exit;
end;
end;
end;
end;

constructor TScrapix.Create;
begin
inherited Create;
VisitedLinks := nil;
FFoundFiles := nil;
RequestTimeoutMs := 30000;
RequestDelayMs := 0;
SameDomainOnly := True;

FState := STATE_RUNNING;
FPauseEvent := TEvent.Create(nil, True, True, '');
FStoppedEvent := TEvent.Create(nil, True, True, '');

TotalLinks := 0;
FileCount := 0;
BrokenCount := 0;
FRobotsBlocked := 0;
FLinksTraversed := 0;

FAutoDownload := False;
DownloadFolder := '';

RobotsRules := nil;
FRespectRobots := True;

FSearchImages := True;
FSearchDocuments := True;
FSearchAudio := True;
FSearchVideo := True;
FSearchWeb := True;

FFoundFiles := TDictionary.Create;
FBrokenLinks := TDictionary.Create;

FRunning := False;
FMaxDepth := 0;

FFoundFilesLimit := 2000;
FExploreLimit := 100;

FVisitedFilePath := '';
FBrokenFilePath := '';
FFoundFilePath := '';

FLogFilePath := '';
FLogLock := TCriticalSection.Create;

FDisableUIUpdates := False;
end;

destructor TScrapix.Destroy;
var
SL: TStringList;
begin
CancelExploration;
WaitForStop;

FreeAndNil(VisitedLinks);
FreeAndNil(FFoundFiles);
FreeAndNil(FBrokenLinks);

FreeAndNil(FPauseEvent);
FreeAndNil(FStoppedEvent);

FreeAndNil(FLogLock);

if Assigned(RobotsRules) then
begin
for SL in RobotsRules.Values do
SL.Free;
RobotsRules.Free;
end;

inherited;
end;

procedure TScrapix.PauseExploration;
begin
TInterlocked.Exchange(FState, STATE_PAUSED);
if Assigned(FPauseEvent) then
FPauseEvent.ResetEvent;
end;

procedure TScrapix.ResumeExploration;
begin
TInterlocked.Exchange(FState, STATE_RUNNING);
if Assigned(FPauseEvent) then
FPauseEvent.SetEvent;
end;

procedure TScrapix.CancelExploration;
begin
TInterlocked.Exchange(FState, STATE_CANCEL);
if Assigned(FPauseEvent) then
FPauseEvent.SetEvent;
end;

function TScrapix.IsCanceled: Boolean;
begin
Result := TInterlocked.CompareExchange(FState, 0, 0) = STATE_CANCEL;
end;

function TScrapix.IsPaused: Boolean;
begin
Result := TInterlocked.CompareExchange(FState, 0, 0) = STATE_PAUSED;
end;

function TScrapix.IsRunning: Boolean;
begin
Result := TInterlocked.CompareExchange(PInteger(@FRunning)^, 0, 0) <> 0;
end;

function TScrapix.WaitForStop: Boolean;
begin
CancelExploration;

if Assigned(FStoppedEvent) then
begin
Result := FStoppedEvent.WaitFor(INFINITE) = wrSignaled;
Exit;
end;

while FRunning do
Sleep(20);
Result := not FRunning;
end;

procedure TScrapix.ConfigureCrawl(const ARequestTimeoutMs, ARequestDelayMs
: Integer; ASameDomainOnly: Boolean; AAutoDownload: Boolean;
ARespectRobots: Boolean; AFoundFilesLimit: Integer; AExploreLimit: Integer);
begin
if ARequestTimeoutMs <= 0 then
RequestTimeoutMs := 30000
else
RequestTimeoutMs := ARequestTimeoutMs;
if ARequestDelayMs < 0 then
RequestDelayMs := 0
else
RequestDelayMs := ARequestDelayMs;
SameDomainOnly := ASameDomainOnly;

FAutoDownload := AAutoDownload;
FRespectRobots := ARespectRobots;

if AFoundFilesLimit < 1 then
FFoundFilesLimit := 1
else if AFoundFilesLimit > 2000 then
FFoundFilesLimit := 2000
else
FFoundFilesLimit := AFoundFilesLimit;

if AExploreLimit < 1 then
FExploreLimit := 1
else if AExploreLimit > 100 then
FExploreLimit := 100
else
FExploreLimit := AExploreLimit;
end;

function TScrapix.GetWebContent(const URL: string; ListView: TscListView;
Depth: Integer; Logging: TscListBox): string;
var
Client: THTTPClient;
Mem: TMemoryStream;
Resp: IHTTPResponse;
ContentType, ContentLength: string;
StatusCode: Integer;
NormURL: string;
StartTick, EndTick, ElapsedMs: Cardinal;
S: RawByteString;
I: Integer;
begin
Result := '';
if URL = '' then
Exit;
NormURL := NormalizeURL(URL, URL);
if NormURL = '' then
Exit;

Client := THTTPClient.Create;
try
Client.ConnectionTimeout := RequestTimeoutMs;
Client.ResponseTimeout := RequestTimeoutMs;
Client.UserAgent := UserAgent;
Mem := TMemoryStream.Create;
try
try
StartTick := GetTickCount;
Resp := Client.Get(NormURL, Mem);
EndTick := GetTickCount;
ElapsedMs := EndTick - StartTick;

StatusCode := -1;
ContentType := '';
ContentLength := '';
if Resp <> nil then
begin
StatusCode := Resp.StatusCode;
ContentType := GetResponseHeaderValue(Resp, 'Content-Type');
ContentLength := GetResponseHeaderValue(Resp, 'Content-Length');
if ContentLength = '' then
ContentLength := GetResponseHeaderValue(Resp, 'Content-Range');
if (ContentLength <> '') and ContentLength.StartsWith('bytes', True)
then
begin
I := LastDelimiter('/', ContentLength);
if I > 0 then
ContentLength := Copy(ContentLength, I + 1, MaxInt);
end;
end;

if ContentLength = '' then
ContentLength := IntToStr(Mem.Size);

if Mem.Size > 0 then
begin
SetLength(S, Mem.Size);
Mem.Position := 0;
Mem.ReadBuffer(S, Mem.Size);
Result := string(S);
end
else
Result := '';

if Assigned(ListView) then
begin
SafeUpdateListViewStatus(ListView, NormURL,
Format('%d %s', [StatusCode, ContentType]), 'GET');
SafeUpdateListViewInfo(ListView, NormURL, ElapsedMs,
FormatBytes(ContentLength), Depth);
end;

if Assigned(Logging) then
SafeLog(Logging, Format('GET %s -> %d %s (%s)', [NormURL, StatusCode,
ContentType, FormatBytes(ContentLength)]));
except
on E: Exception do
begin
if Assigned(ListView) then
begin
SafeUpdateListViewStatus(ListView, NormURL,
'Exception : ' + E.Message, 'GET');
SafeUpdateListViewInfo(ListView, NormURL, -1, '', Depth);
end;
if Assigned(Logging) then
SafeLog(Logging, Format('GET Exception %s : %s',
[NormURL, E.Message]));
Result := '';
end;
end;
finally
Mem.Free;
end;

if RequestDelayMs > 0 then
TThread.Sleep(RequestDelayMs);
finally
Client.Free;
end;
end;

procedure TScrapix.ExtractLinks(const HTML: string; BaseURL: string;
var LinkList: TStringList);
var
Regex: TRegEx;
Match: TMatch;
RawLink, Link: string;
begin
Regex := TRegEx.Create(']*?\s+)?href="([^"]*)"', );
Match := Regex.Match(HTML);
while Match.Success do
begin
RawLink := Match.Groups.Value.Trim;
Match := Match.NextMatch;
if RawLink = '' then
Continue;
Link := NormalizeURL(BaseURL, RawLink);
if Link = '' then
Continue;
if (VisitedLinks = nil) or not VisitedLinks.ContainsKey(Link) then
if LinkList.IndexOf(Link) = -1 then
LinkList.Add(Link);
end;
end;

function RemoveURLParams(const URL: string): string;
var
P: Integer;
begin
Result := URL;
P := Pos('?', Result);
if P > 0 then
Result := Copy(Result, 1, P - 1);
end;

procedure TScrapix.ExtractMediaSources(const HTML: string; BaseURL: string;
var ImageList, DocList, AudioList, VideoList, WebList: TStringList);
var
RegexImg, RegexDoc, RegexAudio, RegexVideo: TRegEx;
RegexCss, RegexJs, RegexFont, RegexHtmlLink: TRegEx;
Match: TMatch;
RawSource, Source: string;

procedure AddIfNew(List: TStringList; const S: string);
begin
if (S <> '') and (TPath.GetExtension(S) <> '') and (List.IndexOf(S) = -1)
then
List.Add(S);
end;

begin
if FSearchImages then
begin
RegexImg := TRegEx.Create
(']*src="([^"]+\.(jpg|jpeg|png|gif|bmp|webp|svg))"',
);
Match := RegexImg.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(ImageList, Source);
Match := Match.NextMatch;
end;
end;

if FSearchDocuments then
begin
RegexDoc := TRegEx.Create
(']*href="([^"]+\.(pdf|zip|rtf|doc|docx|xls|xlsx|ppt|pptx))"',
);
Match := RegexDoc.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(DocList, Source);
Match := Match.NextMatch;
end;
end;

if FSearchAudio then
begin
RegexAudio := TRegEx.Create
('(?:]*>.*?]*src="([^"]+\.(mp3|wav|ogg|m4a|flac))")|href="([^"]+\.(mp3|wav|ogg|m4a|flac))"',
[roIgnoreCase, roSingleLine]);
Match := RegexAudio.Match(HTML);
while Match.Success do
begin
if Match.Groups.Value <> '' then
RawSource := Match.Groups.Value
else
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(AudioList, Source);
Match := Match.NextMatch;
end;
end;

if FSearchVideo then
begin
RegexVideo := TRegEx.Create
('(?:]*>.*?]*src="([^"]+\.(mp4|webm|mov|ogv|mkv))")|href="([^"]+\.(mp4|webm|mov|ogv|mkv))"',
[roIgnoreCase, roSingleLine]);
Match := RegexVideo.Match(HTML);
while Match.Success do
begin
if Match.Groups.Value <> '' then
RawSource := Match.Groups.Value
else
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(VideoList, Source);
Match := Match.NextMatch;
end;
end;

if FSearchWeb then
begin
RegexCss := TRegEx.Create(']*href="([^"]+\.(css))"[^>]*>',
);
Match := RegexCss.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(WebList, Source);
Match := Match.NextMatch;
end;

RegexJs := TRegEx.Create(']*src="([^"]+\.js)"',
);
Match := RegexJs.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(WebList, Source);
Match := Match.NextMatch;
end;

RegexHtmlLink := TRegEx.Create(']*href="([^"]+\.(html|htm))"',
);
Match := RegexHtmlLink.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(WebList, Source);
Match := Match.NextMatch;
end;

RegexFont := TRegEx.Create
('(?:href|src)=["'']([^"''\)]+?\.(woff2?|ttf|otf))["'']', );
Match := RegexFont.Match(HTML);
while Match.Success do
begin
RawSource := Match.Groups.Value;
Source := RemoveURLParams(RawSource);
Source := NormalizeURL(BaseURL, Source);
AddIfNew(WebList, Source);
Match := Match.NextMatch;
end;
end;
end;

procedure TScrapix.MarkBrokenLink(const URL: string; ListView: TscListView;
StatusBar: TscStatusBar; Logging: TscListBox);
begin
Inc(BrokenCount);
if Assigned(ListView) then
SafeUpdateListViewStatus(ListView, URL, GetTranslate('Broken'));
if Assigned(FBrokenLinks) and not FBrokenLinks.ContainsKey(URL) then
FBrokenLinks.Add(URL, True);
if FBrokenFilePath <> '' then
try
TFile.AppendAllText(FBrokenFilePath, URL + sLineBreak, TEncoding.UTF8)
except
end;
if Assigned(StatusBar) then
SafeSetStatusBarPanel(StatusBar, 3, IntToStr(BrokenCount));
if Assigned(Logging) then
SafeLog(Logging, Format(GetTranslate('BrokenLinkLog'),
Comportement :


Retour : True si parsing exécuté.

EnsureRobotsForHost(const Host, Scheme: string): Boolean
Rôle : Charger robots.txt pour un hôte donné et stocker les règles dans RobotsRules pour cache.
Comportement :

  • Construit l'URL robots.txt et effectue un GET avec THTTPClient.
  • Parse le contenu via ParseRobots et stocke une copie des règles dans RobotsRules[host en minuscule].
  • Ne relance pas si déjà en cache.

Retour : True sauf si Host vide.

IsAllowedByRobots(const URL: string): Boolean
Rôle : Vérifier si une URL est autorisée par les règles en cache ou en récupérant robots.txt si nécessaire.
Comportement :

  • Si FRespectRobots est false, renvoie true sans vérification.
  • Extrait Host, Scheme et Path via TURI.
  • S'assure que robots.txt est présent dans le cache pour l'hôte en appelant EnsureRobotsForHost.
  • Parcourt les chemins Disallow pour l'hôte et si Path commence par un Disallow, renvoie false.

Retour : True si autorisée, False si bloquée.



# Requêtes HTTP et utilitaires

GetResponseHeaderValue(const Resp: IHTTPResponse; const HeaderName: string): string
Rôle : Extraire la valeur d'un header HTTP donné depuis l'objet IHTTPResponse.
Comportement : Parcourt Resp.Headers et compare les noms en insensitif. Renvoie la première valeur correspondante.

FormatBytes(const SizeBytes: string): string
Rôle : Formater une taille binaire fournie en chaîne en représentation lisible avec suffixes Octets, Ko, Mo, Go et arrondissements.
Comportement :

  • Tente de convertir SizeBytes en entier. Si échoue, extrait les chiffres via regex.
  • Si valeur 0 ou vide, renvoie "n/a".
  • Convertit en unités en divisant et formatant avec FormatFloat.

Retour : Chaîne lisible.

GetWebContent(const URL: string; ListView: TscListView; Depth: Integer; Logging: TscListBox): string
Rôle : Effectue une requête HTTP GET sur l'URL normalisée et récupère le corps en texte brut, met à jour l'UI et le log.
Comportement :

  • Normalise l'URL.
  • Crée THTTPClient, configure timeout et user-agent.
  • Télécharge le contenu dans TMemoryStream et mesure le temps.
  • Récupère status, Content-Type et Content-Length depuis les headers.
  • Convertit le contenu binaire en string et retourne.
  • Met à jour ListView via SafeUpdateListViewStatus et SafeUpdateListViewInfo.
  • Journalise l'opération dans Logging via SafeLog.
  • Applique RequestDelayMs via TThread.Sleep si nécessaire.

Erreurs : Capture les exceptions, met à jour l'UI avec l'exception et renvoie chaîne vide.

IsFileAvailable(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; Depth: Integer; Logging: TscListBox): Boolean
Rôle : Vérifie la disponibilité d'une ressource en exécutant HEAD puis en Fallback GET avec Range bytes=0-0 si HEAD échoue.
Comportement :

  • Normalise l'URL et vérifie robots.txt via IsAllowedByRobots.
  • Tente Client.Head(NormURL) et mesure temps.
  • Si HEAD échoue, effectue un GET avec en-tête Range pour ne récupérer qu'un octet.
  • Extrait Content-Length ou Content-Range pour estimer la taille.
  • Détermine la disponibilité si StatusCode dans [200,299].
  • Met à jour ListView et Logging via SafeUpdateListViewStatus, SafeUpdateListViewInfo et SafeLog.

Retour : True si réponse HTTP 2xx, False sinon.



# Téléchargement de fichiers

DownloadFile(const URL: string; Client: THTTPClient; out LocalPath: string; Logging: TscListBox): Boolean
Rôle : Télécharger la ressource URL vers un fichier local dans DownloadFolder en organisant par type d'extension.
Comportement détaillé :

  • Normalise URL et vérifie robots.txt..
  • Extrait le chemin et le nom de fichier via TURI et TPath.
  • Si pas de nom, utilise "file" et obtient l'extension.
  • Détermine sous-dossier cible via DetermineSubFolderByExtension qui mappe extensions à Image, Document, Audio, Vidéo, Ressources Web/ CSS/JS/HTML/Fonts ou Autre.
  • Crée le dossier cible si nécessaire.
  • Si fichier existant, ajoute suffixe incrémental _n jusqu'à disponibilité.
  • Crée un TFileStream en mode fmCreate et effectue Client.Get pour écrire directement le flux dans le fichier.
  • Si la requête donne un code 2xx, considère le téléchargement réussi, sinon supprime le fichier partiel.
  • Log des succès ou échecs via SafeLog.

Sorties : LocalPath contenant le chemin absolu en cas de succès.
Retour : True si téléchargement réussi.



# Extraction de liens et ressources

ExtractLinks(const HTML: string; BaseURL: string; var LinkList: TStringList)
Rôle : Extraire toutes les URLs d'éléments
depuis le HTML et les normaliser.
Comportement :

Retour : Remplit LinkList avec URLs absolues.

RemoveURLParams(const URL: string): string
Rôle : Retire la partie query string après le '?' pour obtenir un chemin plus stable pour déduplication et nom de fichier.
Retour : URL sans paramètres.

ExtractMediaSources(const HTML: string; BaseURL: string; var ImageList, DocList, AudioList, VideoList, WebList: TStringList)
Rôle : Rechercher et récupérer les sources médias et ressources web dans le contenu HTML selon les flags d'extension activés.
Comportement :

  • Pour chaque catégorie active, exécute une expression régulière adaptée pour détecter src ou href vers extensions ciblées.
  • Nettoie via RemoveURLParams puis NormalizeURL.
  • Ajoute l'URL dans la liste correspondante si elle a une extension valide et n'existe pas déjà.

Types extraits :

  • Images : .jpg, .jpeg, .png, .gif, .bmp, .webp, .svg.
  • Documents : .pdf, .zip, .rtf, .doc, .docx, .xls, .xlsx, .ppt, .pptx.
  • Audio : .mp3, .wav, .ogg, .m4a, .flac depuis
  • Vidéo : .mp4, .webm, .mov, .ogv, .mkv depuis
  • Web : CSS, JS, HTML, fonts et liens vers fichiers HTML.

Retour : Les listes passées en paramètre sont remplies.



# Traitement des ressources trouvées

ProcessResourceGroup(ResourceList: TStringList; const AcceptExts: array of string; StatusBar: TscStatusBar; ListView: TscListView; Depth: Integer; const DefaultUIType: string; Logging: TscListBox)
Rôle : Routine générique qui traite une liste de ressources d'une catégorie: vérification d'extension, robots.txt, disponibilité, téléchargement ou marquage.
Comportement détaillé :

  • Si AcceptExts vide, accepte n'importe quelle extension.
  • Pour chaque URL de la liste :
  • Respecte les signaux d'annulation et de pause.
  • Filtre par extension si nécessaire.
  • Ignore si déjà dans FFoundFiles.
  • Vérifie robots.txt et marque Ignored si bloquée.
  • Appelle IsFileAvailable pour vérifier disponibilité.
  • Si disponible et FAutoDownload true, crée un THTTPClient local, met à jour UI en "Downloading", appelle DownloadFile et met à jour UI selon succès ou échec, ajoute l'URL à FFoundFiles et écrit FFoundFilePath si configuré.
  • Si disponible et FAutoDownload false, marque NotDownloaded et ajoute à FFoundFiles.
  • Si indisponible, si FAutoDownload false alors marque comme broken via MarkBrokenLink, si FAutoDownload true alors marque Broken_Ignored, ajoute à FBrokenLinks et logge.
  • Met à jour FileCount et vérifie FFoundFilesLimit pour annuler exploration si atteint.
  • Met à jour StatusBar panel pour FileCount à la fin et annule si limite atteinte.

Retour : Aucun. Effets sur dictionnaires, fichiers de rapport, UI et logs.

ProcessFoundFiles(ImageList, DocList, AudioList, VideoList, WebList: TStringList; StatusBar: TscStatusBar; ListView: TscListView; CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox)
Rôle : Appel ordonné de ProcessResourceGroup pour chaque type activé par les flags FSearch*.
Comportement : Pour chaque catégorie activée, appelle ProcessResourceGroup avec la liste et les extensions acceptées prédéfinies.
Effet : Centralise le traitement des ressources extraites depuis une page.

MarkBrokenLink(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; Logging: TscListBox)
Rôle : Incrémenter BrokenCount, mettre à jour UI et enregistrer le lien cassé.
Comportement :

  • Augmente BrokenCount.
  • Met à jour ListView via SafeUpdateListViewStatus en utilisant la traduction "Broken".
  • Ajoute à FBrokenLinks si non présent et écrit FBrokenFilePath si configuré.
  • Met à jour StatusBar panel pour BrokenCount.
  • Log l'événement.




# Exploration récursive

ExploreLinksRecursive(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox)
Rôle : Cœur de l'algorithme récursif d'exploration. Gère la visite d'une URL, extraction des liens et ressources, traitement des ressources, et récursion sur les liens extraits.
Étapes détaillées :

  • Vérifie signaux d'annulation et profondeur restante.
  • Calcule CurrentDepth en fonction de FMaxDepth pour affichage.
  • Normalise URL et skip si déjà visitée.
  • Vérifie robots.txt; si bloquée, incrémente compteur et sort.
  • Attend si en pause via FPauseEvent.
  • Ajoute NormURL à VisitedLinks, incrémente TotalLinks et appelle IncrementLinksTraversed, journalise la visite.
  • Vérifie limites FFoundFilesLimit et FExploreLimit et annule si dépassées.
  • Écrit NormURL dans FVisitedFilePath si configuré.
  • Ajoute une ligne "OnHold" au ListView de façon thread-safe pour indiquer URL en cours.
  • Récupère contenu via GetWebContent.
  • Si contenu vide : si FAutoDownload false marque lien cassé via MarkBrokenLink, sinon marque Broken_Ignored et log.

Si contenu présent :

Comportement d'arrêt : Respecte IsCanceled et IsPaused à de multiples points pour arrêt propre et responsive.
Notes : C'est la routine qui construit l'arbre d'exploration et déclenche le traitement des ressources.

ExploreLinks(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; CheckList: TscCheckListBox; MaxDepth: Integer; SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile: Boolean; Logging: TscListBox)
Rôle : Point d'entrée synchronique pour démarrer une exploration complète depuis une URL racine et gérer les rapports sur disque.
Comportement :

  • Initialise et reset des structures internes, compteurs et états.
  • Prépare les dossiers de rapport sous Documents\Scrapix

\\Report et le dossier download organisé par type :

  • Initialise FLogFilePath et vide le fichier de log.
  • Configure les chemins FBrokenFilePath, FVisitedFilePath, FFoundFilePath si les options Save* sont cochées et crée des fichiers vides.
  • Détermine RootDomain via TURI.Create(URL).
  • Configure FMaxDepth et log le lancement.
  • Appelle ExploreLinksRecursive pour débuter l'exploration.
  • Après la fin, si SaveBrokenToFile, SaveVisitedToFile ou SaveFoundFilesToFile, consolide les dictionnaires en fichiers placés dans le dossier Documents principal.
  • Journalise la fin et réinitialise les chemins et FRunning, signale FStoppedEvent.

Retour : Procédure synchrone qui ne retourne qu'à la fin du crawl ou après annulation.
Effets : Gère création de rapports et dossiers, et coordination globale du crawl.



# Wrappers thread-safe pour mise à jour UI et log

Les routines suivantes garantissent que les mises à jour de contrôles VCL se font depuis le thread principal ou via TThread.Queue si appelées depuis d'autres threads. Elles respectent FDisableUIUpdates et vérifient l'état des composants avant modification.


  • UIUpdatesAllowed: Boolean — Renvoie la possibilité d'effectuer des mises à jour UI selon FDisableUIUpdates.
  • SafeScrollListViewToBottom(ListView) — Rend visible la dernière ligne du ListView.
  • SafeSetStatusBarPanel(StatusBar, PanelIndex, Text) — Met à jour un panel du StatusBar identifié.
  • SafeUpdateListViewStatus(ListView, URL, StatusText, Method) — Ajoute ou met à jour une ligne dans ListView colonne Status et Method.
  • SafeUpdateListViewDownloadState(ListView, URL, DownloadState) — Met à jour la colonne état de téléchargement.
  • SafeUpdateListViewInfo(ListView, URL, RespMs, SizeBytes, Depth) — Met à jour colonnes temps de réponse, taille et profondeur.
  • SafeLog(Logging, Msg) — Ajoute une ligne au TscListBox et ajuste le horizon horizontal, écrit aussi de façon thread-safe dans FLogFilePath en utilisant FLogLock.

Chaque wrapper :

  • Vérifie UIUpdatesAllowed, paramètre nil et état ComponentState et HandleAllocated.
  • Si appel depuis le thread principal, met à jour directement.
  • Sinon, poste une closure via TThread.Queue qui répète les mêmes vérifications avant mise à jour.




# Filtres et options UI

ApplyFileTypeFiltersFromCheckList(CheckList: TscCheckListBox)
Rôle : Traduire les éléments cochés d'une CheckList en activation des flags FSearchImages, FSearchDocuments, FSearchAudio, FSearchVideo, FSearchWeb.
Comportement :

  • Réinitialise tous les flags à false.
  • Pour chaque item coché, compare le texte et active le flag correspondant.
  • Si libellés non reconnaissables, utilise la position de l'élément pour correspondance par index.

Effet : Permet de contrôler quels types de ressources sont recherchés.



# Comportement global et séquence d'opération


  • Créer une instance TScrapix et appeler ConfigureCrawl pour paramétrer timeouts, limites, téléchargement et respect robots.
  • Appeler ExploreLinks avec l'URL racine et options de rapport.
  • ExploreLinks initialise environnements, crée dossiers et fichiers de rapport, puis appelle ExploreLinksRecursive.
  • ExploreLinksRecursive normalise les URL, respecte robots.txt, récupère le HTML via GetWebContent, extrait links et ressources, traite les ressources via ProcessFoundFiles puis descend récursivement sur les liens filtrés par SameDomainOnly et profondeur restante.
  • ProcessResourceGroup vérifie la disponibilité via IsFileAvailable, télécharge si demandé via DownloadFile ou enregistre le fichier trouvé dans FFoundFiles, marque cassés et écrit les rapports.
  • Tout au long du processus, les wrappers Safe* mettent à jour l'UI et SafeLog journalise et écrit dans FLogFilePath de façon thread-safe.
  • Les opérations respectent les signaux Pause et Cancel afin d'arrêter proprement l'exploration et permettre la reprise.




    # Cycle de vie public

    TScrapix s’utilise comme un objet unique pour lancer, contrôler et terminer une session d’exploration. Séquence typique : création, configuration, démarrage (ExploreLinks), contrôles runtime (Pause/Resume/Cancel), attente d’arrêt (WaitForStop) et destruction.

    Étapes concrètes du cycle de vie

    • Create: instancier TScrapix pour initialiser structures internes et valeurs par défaut.
    • ConfigureCrawl: appeler pour fixer timeouts, délai entre requêtes, comportement same-domain, téléchargement automatique, respect robots, limite de fichiers trouvés et limite d’exploration.
    • ApplyFileTypeFiltersFromCheckList: appeler si l’état des filtres de type de fichier provient d’une CheckList UI; active/désactive FSearchImages, FSearchDocuments, FSearchAudio, FSearchVideo, FSearchWeb.
    • ExploreLinks(...): lancer l’exploration de manière synchrone en fournissant l’URL racine, contrôles UI (ListView, StatusBar, CheckList), profondeur maximale, options d’écriture des rapports (SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile) et logging UI. L’appel retourne seulement lorsque le crawl est fini ou annulé.
    • Pendant l’exploration: contrôler par PauseExploration, ResumeExploration et CancelExploration. Consulter IsPaused, IsCanceled, IsRunning pour l’état courant.
    • WaitForStop: si une attente bloquante de la fin est nécessaire, appeler pour s’assurer que toutes les tâches sont terminées.
    • Destroy: libérer l’objet TScrapix et toutes ses ressources; CancelExploration + WaitForStop sont exécutés dans le destructeur pour garantir arrêt propre.

    États et transitions publics

    • États internes accessibles via IsRunning, IsPaused, IsCanceled.


    Transitions d’état

    • Par défaut après Create, FState = STATE_RUNNING (prêt).
    • ConfigureCrawl n’affecte pas directement FState.
    • ExploreLinks met FRunning = True et TInterlocked.Exchange(FState, STATE_RUNNING).
    • PauseExploration met FState = STATE_PAUSED et reset FPauseEvent. Les routines checkent IsPaused et attendent FPauseEvent.
    • ResumeExploration met FState = STATE_RUNNING et SetEvent sur FPauseEvent.
    • CancelExploration met FState = STATE_CANCEL et SetEvent sur FPauseEvent pour réveiller les waiters. Les boucles respectent IsCanceled et quittent proprement.
    • À la fin d’ExploreLinks (fin normale ou après Cancel), FRunning devient False et FStoppedEvent est SetEvent. WaitForStop retourne alors true.


    Configuration publique détaillée
    ConfigureCrawl(ARequestTimeoutMs, ARequestDelayMs, ASameDomainOnly, AAutoDownload, ARespectRobots, AFoundFilesLimit, AExploreLimit)

    • ARequestTimeoutMs: timeout en millisecondes pour les requêtes HTTP; si ≤ 0, valeur par défaut 30000 ms.
    • ARequestDelayMs: pause entre requêtes en ms; si < 0 devient 0.
    • ASameDomainOnly: true pour n’explorer que les liens du même domaine racine. RootDomain est extrait dans ExploreLinks.
    • AAutoDownload: true pour télécharger automatiquement les ressources trouvées.
    • ARespectRobots: true pour activer la vérification robots.txt avant HEAD/GET et download.
    • AFoundFilesLimit: nombre maximal de fichiers trouvés/téléchargés; borne [1..2000], valeur par défaut 2000.
    • AExploreLimit: nombre maximal de liens parcourus; borne [1..100], valeur par défaut 100.
    • ApplyFileTypeFiltersFromCheckList(CheckList)
    • Active ou désactive les recherches par type en fonction des éléments cochés de la CheckList UI. Si CheckList est nil, laisse tous les flags à false. Utilise libellés (Image, Document, Audio, Vidéo, Web Document) ou l’index comme fallback.


    Propriétés publiques

    • DisableUIUpdates: booléen pour désactiver les mises à jour UI thread-safe; utile pour tests/performance.


    Entrée/sortie et rapports

    • ExploreLinks crée (si demandé) ces fichiers dans Documents\Scrapix
    • \\Report : BrokenLinks.txt, VisitedLinks.txt, FoundFiles.txt.. Le logger écrit Logging.txt dans Report\Logging. Les chemins sont initialisés au début du crawl et vidés à la fin.
    • FFoundFiles, FBrokenLinks et VisitedLinks sont tenus en mémoire pendant la session et écrits sur disque à la fin si les options Save* sont activées.
    • DownloadFolder est créé sous Documents\Scrapix
    • \\download et les fichiers téléchargés sont organisés en sous-dossiers par type (Image, Document, Audio, Vidéo, Ressources Web/JS/CSS/HTML/Fonts, Autre).


    Contrôle d’exécution en pratique

    • Toujours appeler ConfigureCrawl avant ExploreLinks pour fixer timeouts et limites.
    • Pour reprendre un crawl interrompu, détacher ou recréer TScrapix, reconfigurer et relancer ExploreLinks; l’état mémoire interne (VisitedLinks, FFoundFiles) n’est pas persistant entre instances.
    • Utiliser PauseExploration/ResumeExploration pour interruptions courtes; CancelExploration pour arrêter définitivement. Appeler WaitForStop après CancelExploration si on doit attendre la complétion avant Destroy.
    • Pour gros crawls, réduire UIUpdates ou définir DisableUIUpdates à true pour diminuer l’impact UI.
    • Vérifier FRespectRobots avant d’activer AAutoDownload pour respecter les sites.




    # Exemple de trace d’exécution pour une page racine contenant 2 liens et 3 images

    Contexte
    Instance TScrapix configurée avec :

    • RequestTimeoutMs=30000
    • RequestDelayMs=0
    • ASameDomainOnly=True
    • AAutoDownload=False
    • ARespectRobots=True
    • FFoundFilesLimit=2000
    • FExploreLimit=100


    Page racine http://example.com/index.html">));
    end;

    procedure TScrapix.IncrementRobotsBlocked(StatusBar: TscStatusBar);
    begin
    Inc(FRobotsBlocked);
    if Assigned(StatusBar) then
    SafeSetStatusBarPanel(StatusBar, 5, IntToStr(FRobotsBlocked));
    end;

    procedure TScrapix.IncrementLinksTraversed(StatusBar: TscStatusBar);
    begin
    Inc(FLinksTraversed);
    if Assigned(StatusBar) then
    SafeSetStatusBarPanel(StatusBar, 7, IntToStr(FLinksTraversed));
    if (FExploreLimit > 0) and (FLinksTraversed >= FExploreLimit) then
    CancelExploration;
    end;

    function TScrapix.IsFileAvailable(const URL: string; ListView: TscListView;
    StatusBar: TscStatusBar; Depth: Integer; Logging: TscListBox): Boolean;
    var
    Client: THTTPClient;
    Resp: IHTTPResponse;
    Headers: TNetHeaders;
    RespMem: TMemoryStream;
    StatusCode: Integer;
    ContentType: string;
    NormURL: string;
    StartTick, EndTick, ElapsedMs: Cardinal;
    ContentLength: string;
    I: Integer;
    begin
    Result := False;
    if URL = '' then
    Exit;
    NormURL := NormalizeURL(URL, URL);
    if NormURL = '' then
    Exit;

    if FRespectRobots and not IsAllowedByRobots(NormURL) then
    begin
    if Assigned(ListView) then
    SafeUpdateListViewStatus(ListView, NormURL,
    GetTranslate('BlockedByRobots'), 'HEAD');
    IncrementRobotsBlocked(StatusBar);
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('BlockedRobotsHEADLog'), ));
    Exit(False);
    end;

    Client := THTTPClient.Create;
    try
    Client.ConnectionTimeout := RequestTimeoutMs;
    Client.ResponseTimeout := RequestTimeoutMs;
    Client.UserAgent := UserAgent;
    Resp := nil;
    ContentLength := '';

    StartTick := 0;

    try
    try
    StartTick := GetTickCount;
    Resp := Client.Head(NormURL);
    EndTick := GetTickCount;
    except
    Resp := nil;
    EndTick := GetTickCount;
    end;

    ElapsedMs := EndTick - StartTick;

    if Resp = nil then
    begin
    SetLength(Headers, 1);
    Headers.Name := 'Range';
    Headers.Value := 'bytes=0-0';
    RespMem := TMemoryStream.Create;
    try
    try
    StartTick := GetTickCount;
    Resp := Client.Get(NormURL, RespMem, Headers);
    EndTick := GetTickCount;
    ElapsedMs := EndTick - StartTick;
    ContentLength := GetResponseHeaderValue(Resp, 'Content-Length');
    if ContentLength = '' then
    ContentLength := GetResponseHeaderValue(Resp, 'Content-Range');
    if (ContentLength <> '') and ContentLength.StartsWith('bytes', True)
    then
    begin
    I := LastDelimiter('/', ContentLength);
    if I > 0 then
    ContentLength := Copy(ContentLength, I + 1, MaxInt);
    end;
    if ContentLength = '' then
    ContentLength := IntToStr(RespMem.Size);
    except
    Resp := nil;
    ContentLength := '';
    end;
    finally
    RespMem.Free;
    end;
    SetLength(Headers, 0);
    end
    else
    begin
    ContentLength := GetResponseHeaderValue(Resp, 'Content-Length');
    end;

    if Resp <> nil then
    begin
    StatusCode := Resp.StatusCode;
    ContentType := GetResponseHeaderValue(Resp, 'Content-Type');

    Result := (StatusCode >= 200) and (StatusCode < 300);
    if Assigned(ListView) then
    begin
    if ContentLength = '' then
    ContentLength := '0';
    SafeUpdateListViewStatus(ListView, NormURL,
    Format('%d %s', [StatusCode, ContentType]), 'HEAD');
    SafeUpdateListViewInfo(ListView, NormURL, ElapsedMs,
    FormatBytes(ContentLength), Depth);
    end;
    if Assigned(Logging) then
    SafeLog(Logging, Format('HEAD %s -> %d %s (%s)', [NormURL, StatusCode,
    ContentType, FormatBytes(ContentLength)]));
    end
    else
    begin
    if Assigned(ListView) then
    SafeUpdateListViewStatus(ListView, NormURL,
    GetTranslate('NoResponse'), 'HEAD');
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('NoResponseHEADLog'),
    ));
    Result := False;
    end;
    except
    on E: Exception do
    begin
    if Assigned(ListView) then
    begin
    SafeUpdateListViewStatus(ListView, NormURL,
    'Exception : ' + E.Message, '');
    SafeUpdateListViewInfo(ListView, NormURL, -1, '', Depth);
    end;
    if Assigned(Logging) then
    SafeLog(Logging, Format('HEAD Exception %s : %s',
    [NormURL, E.Message]));
    Result := False;
    end;
    end;
    finally
    Client.Free;
    end;
    end;

    function TScrapix.DownloadFile(const URL: string; Client: THTTPClient;
    out LocalPath: string; Logging: TscListBox): Boolean;
    var
    FileName, Ext, UriPath, CandidateFolder, CandidateFile, BaseName: string;
    FS: TFileStream;
    Resp: IHTTPResponse;
    NormURL: string;
    Suffix: Integer;

    function DetermineSubFolderByExtension(const AExt: string): string;
    begin
    if AExt = '' then
    Exit('Autre');
    if SameText(AExt, '.jpg') or SameText(AExt, '.jpeg') or
    SameText(AExt, '.png') or SameText(AExt, '.gif') or SameText(AExt, '.bmp')
    or SameText(AExt, '.webp') or SameText(AExt, '.svg') then
    Exit('Image');
    if SameText(AExt, '.pdf') or SameText(AExt, '.zip') or
    SameText(AExt, '.rtf') or SameText(AExt, '.doc') or
    SameText(AExt, '.docx') or SameText(AExt, '.xls') or
    SameText(AExt, '.xlsx') or SameText(AExt, '.ppt') or
    SameText(AExt, '.pptx') or SameText(AExt, '.txt') then
    Exit('Document');
    if SameText(AExt, '.mp3') or SameText(AExt, '.wav') or
    SameText(AExt, '.ogg') or SameText(AExt, '.m4a') or SameText(AExt, '.flac')
    then
    Exit('Audio');
    if SameText(AExt, '.mp4') or SameText(AExt, '.webm') or
    SameText(AExt, '.mov') or SameText(AExt, '.ogv') or SameText(AExt, '.mkv')
    then
    Exit('Vidéo');
    if SameText(AExt, '.css') then
    Exit(TPath.Combine('Ressources Web', 'CSS'));
    if SameText(AExt, '.js') then
    Exit(TPath.Combine('Ressources Web', 'JS'));
    if SameText(AExt, '.html') or SameText(AExt, '.htm') then
    Exit(TPath.Combine('Ressources Web', 'HTML'));
    if SameText(AExt, '.woff') or SameText(AExt, '.woff2') or
    SameText(AExt, '.ttf') or SameText(AExt, '.otf') then
    Exit(TPath.Combine('Ressources Web', 'Fonts'));
    Result := 'Autre';
    end;

    begin
    Result := False;
    LocalPath := '';
    if (URL = '') or (Client = nil) then
    Exit;
    NormURL := NormalizeURL(URL, URL);
    if NormURL = '' then
    Exit;

    if FRespectRobots and not IsAllowedByRobots(NormURL) then
    begin
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('BlockedRobotsDownLog'), ));
    Exit(False);
    end;

    try
    try
    try
    UriPath := TURI.Create(NormURL).Path
    except
    UriPath := '';
    end;
    FileName := TPath.GetFileName(UriPath);
    if FileName = '' then
    FileName := 'file';
    BaseName := TPath.GetFileNameWithoutExtension(FileName);
    Ext := TPath.GetExtension(FileName);
    if Ext = '' then
    Ext := '';

    CandidateFolder := TPath.Combine(DownloadFolder,
    DetermineSubFolderByExtension(Ext));
    if not TDirectory.Exists(CandidateFolder) then
    try
    TDirectory.CreateDirectory(CandidateFolder)
    except
    Exit(False)
    end;

    CandidateFile := TPath.Combine(CandidateFolder, BaseName + Ext);
    Suffix := 0;
    while TFile.Exists(CandidateFile) do
    begin
    Inc(Suffix);
    CandidateFile := TPath.Combine(CandidateFolder,
    BaseName + '_' + IntToStr(Suffix) + Ext);
    if Suffix > 10000 then
    Break;
    end;

    LocalPath := CandidateFile;
    FS := TFileStream.Create(LocalPath, fmCreate);
    try
    Resp := Client.Get(NormURL, FS);
    if Resp <> nil then
    Result := (Resp.StatusCode >= 200) and (Resp.StatusCode < 300)
    else
    Result := False;
    finally
    FS.Free;
    if not Result then
    begin
    try
    if TFile.Exists(LocalPath) then
    TFile.Delete(LocalPath)
    except
    end;
    LocalPath := '';
    end;
    end;
    if Result then
    begin
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('DonwLog'),
    [NormURL, LocalPath]));
    end
    else
    begin
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('DonwFailedLog'), ));
    end;
    except
    on E: Exception do
    begin
    Result := False;
    LocalPath := '';
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('ExceptDonwLog'),
    [URL, E.Message]));
    end;
    end;
    finally
    end;
    end;

    procedure TScrapix.ProcessResourceGroup(ResourceList: TStringList;
    const AcceptExts: array of string; StatusBar: TscStatusBar;
    ListView: TscListView; Depth: Integer; const DefaultUIType: string;
    Logging: TscListBox);
    var
    I: Integer;
    URL: string;
    Available: Boolean;
    DLClient: THTTPClient;
    LocalPath: string;
    AddedCount: Integer;
    AcceptAny: Boolean;

    function IsExtAccepted(const AURL: string): Boolean;
    var
    E: string;
    AExt: string;
    begin
    if Length(AcceptExts) = 0 then
    begin
    Result := True;
    Exit;
    end;
    AExt := LowerCase(TPath.GetExtension(AURL));
    for E in AcceptExts do
    if AExt = LowerCase(E) then
    Exit(True);
    Result := False;
    end;

    begin
    if (ResourceList = nil) or (ResourceList.Count = 0) then
    Exit;
    AddedCount := 0;
    AcceptAny := Length(AcceptExts) = 0;

    for I := 0 to ResourceList.Count - 1 do
    begin
    if IsCanceled then
    Exit;
    while IsPaused do
    begin
    if IsCanceled then
    Exit;
    if Assigned(FPauseEvent) then
    FPauseEvent.WaitFor(250);
    end;

    URL := ResourceList;

    if (not AcceptAny) and (not IsExtAccepted(URL)) then
    Continue;
    if (FFoundFiles <> nil) and FFoundFiles.ContainsKey(URL) then
    Continue;

    if FRespectRobots and not IsAllowedByRobots(URL) then
    begin
    SafeUpdateListViewStatus(ListView, URL, GetTranslate('BlockedByRobots'));
    SafeUpdateListViewDownloadState(ListView, URL, GetTranslate('Ignored'));
    IncrementRobotsBlocked(StatusBar);
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('BlockedRobotsResLog'), ));
    Continue;
    end;

    Available := IsFileAvailable(URL, ListView, StatusBar, Depth, Logging);
    if Available then
    begin
    if FAutoDownload then
    begin
    DLClient := THTTPClient.Create;
    try
    DLClient.ConnectionTimeout := RequestTimeoutMs;
    DLClient.ResponseTimeout := RequestTimeoutMs;
    DLClient.UserAgent := UserAgent;

    SafeUpdateListViewDownloadState(ListView, URL,
    GetTranslate('Downloading'));
    if DownloadFile(URL, DLClient, LocalPath, Logging) then
    begin
    SafeUpdateListViewDownloadState(ListView, URL,
    GetTranslate('Downloaded'));
    Inc(AddedCount);
    if FFoundFiles <> nil then
    begin
    FFoundFiles.Add(URL, True);
    if FFoundFilePath <> '' then
    try
    TFile.AppendAllText(FFoundFilePath, URL + sLineBreak,
    TEncoding.UTF8)
    except
    end;
    end;
    end
    else
    begin
    SafeUpdateListViewDownloadState(ListView, URL,
    GetTranslate('DownloadFailed'));
    SafeUpdateListViewStatus(ListView, URL,
    GetTranslate('DownloadFailed'));
    end;
    finally
    DLClient.Free;
    end;
    end
    else
    begin
    Inc(AddedCount);
    SafeUpdateListViewDownloadState(ListView, URL,
    GetTranslate('NotDownloaded'));
    if FFoundFiles <> nil then
    begin
    FFoundFiles.Add(URL, True);
    if FFoundFilePath <> '' then
    try
    TFile.AppendAllText(FFoundFilePath, URL + sLineBreak,
    TEncoding.UTF8)
    except
    end;
    end;
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('FoundNotDownLog'), ));
    end;
    end
    else
    begin
    if not FAutoDownload then
    begin
    MarkBrokenLink(URL, ListView, StatusBar, Logging);
    SafeUpdateListViewDownloadState(ListView, URL, GetTranslate('Broken'));
    end
    else
    begin
    SafeUpdateListViewStatus(ListView, URL, GetTranslate('Broken_Ignored'));
    SafeUpdateListViewDownloadState(ListView, URL,
    GetTranslate('Broken_Ignored'));
    if Assigned(FBrokenLinks) and not FBrokenLinks.ContainsKey(URL) then
    FBrokenLinks.Add(URL, True);
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('CorruptedDownLog'), ));
    end;
    end;

    if (FFoundFilesLimit > 0) and (FileCount + AddedCount >= FFoundFilesLimit)
    then
    begin
    CancelExploration;
    Break;
    end;
    end;

    if AddedCount > 0 then
    Inc(FileCount, AddedCount);
    if Assigned(StatusBar) then
    SafeSetStatusBarPanel(StatusBar, 1, IntToStr(FileCount));
    if (FFoundFilesLimit > 0) and (FileCount >= FFoundFilesLimit) then
    CancelExploration;
    end;

    procedure TScrapix.ProcessFoundFiles(ImageList, DocList, AudioList, VideoList,
    WebList: TStringList; StatusBar: TscStatusBar; ListView: TscListView;
    CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox);
    begin
    if FSearchImages then
    ProcessResourceGroup(ImageList, ['.jpg', '.jpeg', '.png', '.gif', '.bmp',
    '.webp', '.svg'], StatusBar, ListView, Depth, 'Image', Logging);

    if FSearchDocuments then
    ProcessResourceGroup(DocList, ['.pdf', '.zip', '.rtf', '.doc', '.docx',
    '.xls', '.xlsx', '.ppt', '.pptx', '.txt'], StatusBar, ListView, Depth,
    'Document', Logging);

    if FSearchVideo then
    ProcessResourceGroup(VideoList, ['.mp4', '.webm', '.mov', '.ogv', '.mkv'],
    StatusBar, ListView, Depth, 'Vidéo', Logging);

    if FSearchAudio then
    ProcessResourceGroup(AudioList, ['.mp3', '.wav', '.ogg', '.m4a', '.flac'],
    StatusBar, ListView, Depth, 'Audio', Logging);

    if FSearchWeb then
    ProcessResourceGroup(WebList, ['.css', '.js', '.html', '.htm', '.woff',
    '.woff2', '.ttf', '.otf'], StatusBar, ListView, Depth,
    'RessourceWeb', Logging);
    end;

    function TScrapix.IsSameDomain(const BaseURL, LinkURL: string): Boolean;
    var
    HostBase, HostLink: string;
    TempBase: string;

    function HostIsSuffixOf(const SuffixHost, FullHost: string): Boolean;
    begin
    Result := (SuffixHost = FullHost) or FullHost.EndsWith('.' + SuffixHost);
    end;

    begin
    Result := False;
    if LinkURL = '' then
    Exit;
    TempBase := Trim(BaseURL);
    if TempBase = '' then
    Exit;

    if Pos('://', TempBase) = 0 then
    HostBase := LowerCase(TempBase)
    else
    try
    HostBase := LowerCase(TURI.Create(TempBase).Host)
    except
    HostBase := ''
    end;
    if HostBase = '' then
    Exit;

    try
    HostLink := LowerCase(TURI.Create(LinkURL).Host)
    except
    HostLink := ''
    end;
    if HostLink = '' then
    Exit;

    Result := HostIsSuffixOf(HostBase, HostLink);
    end;

    procedure TScrapix.ExploreLinksRecursive(const URL: string;
    ListView: TscListView; StatusBar: TscStatusBar; CheckList: TscCheckListBox;
    Depth: Integer; Logging: TscListBox);
    var
    HTMLContent: string;
    Links, Images, Docs, Audio, Video, Webs: TStringList;
    I: Integer;
    Item: TListItem;
    NormURL: string;
    NeedExtract: Boolean;
    CurrentDepth: Integer;
    begin
    if IsCanceled then
    Exit;
    if Depth <= 0 then
    Exit;

    if FMaxDepth <= 0 then
    CurrentDepth := Depth
    else
    CurrentDepth := FMaxDepth - Depth + 1;

    NormURL := NormalizeURL(URL, URL);
    if NormURL = '' then
    Exit;
    if (VisitedLinks <> nil) and VisitedLinks.ContainsKey(NormURL) then
    Exit;

    if FRespectRobots and not IsAllowedByRobots(NormURL) then
    begin
    if Assigned(ListView) then
    begin
    SafeUpdateListViewStatus(ListView, NormURL,
    GetTranslate('BlockedByRobots'));
    SafeUpdateListViewDownloadState(ListView, NormURL,
    GetTranslate('Ignored'));
    end;
    IncrementRobotsBlocked(StatusBar);
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('BlockedRobotsLog'), ));
    Exit;
    end;

    while IsPaused do
    begin
    if IsCanceled then
    Exit;
    if Assigned(FPauseEvent) then
    FPauseEvent.WaitFor(250);
    end;

    VisitedLinks.Add(NormURL, True);
    Inc(TotalLinks);
    IncrementLinksTraversed(StatusBar);

    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('VisitedLog'),
    [NormURL, CurrentDepth]));

    if IsCanceled then
    Exit;
    if (FFoundFilesLimit > 0) and (FileCount >= FFoundFilesLimit) then
    begin
    CancelExploration;
    Exit;
    end;
    if (FExploreLimit > 0) and (FLinksTraversed >= FExploreLimit) then
    begin
    CancelExploration;
    Exit;
    end;

    if FVisitedFilePath <> '' then
    try
    TFile.AppendAllText(FVisitedFilePath, NormURL + sLineBreak,
    TEncoding.UTF8)
    except
    end;

    if Assigned(ListView) and UIUpdatesAllowed then
    begin
    if TThread.Current.ThreadID = MainThreadID then
    begin
    Item := ListView.Items.Add;
    Item.Caption := NormURL;
    while Item.SubItems.Count < 6 do
    Item.SubItems.Add('');
    Item.SubItems := GetTranslate('OnHold');
    Item.SubItems := IntToStr(CurrentDepth);
    SafeScrollListViewToBottom(ListView);
    end
    else
    begin
    var
    LV := ListView;
    var
    sURL := NormURL;
    var
    sDepth := IntToStr(CurrentDepth);
    TThread.Queue(nil,
    procedure
    begin
    if not UIUpdatesAllowed then
    Exit;
    if (LV = nil) or (csDestroying in LV.ComponentState) or
    (not LV.HandleAllocated) then
    Exit;
    Item := LV.Items.Add;
    while Item.SubItems.Count < 6 do
    Item.SubItems.Add('');
    Item.Caption := sURL;
    Item.SubItems := GetTranslate('OnHold');
    Item.SubItems := sDepth;
    SafeScrollListViewToBottom(LV);
    end);
    end;
    end;

    HTMLContent := GetWebContent(NormURL, ListView, CurrentDepth, Logging);

    if HTMLContent = '' then
    begin
    if not FAutoDownload then
    begin
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('MarkBrokenLinkLog'), ));
    MarkBrokenLink(NormURL, ListView, StatusBar, Logging)
    end
    else
    begin
    SafeUpdateListViewStatus(ListView, NormURL,
    GetTranslate('Broken_Ignored'));
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('NoContentLog'), ));
    end;
    Exit;
    end;

    Links := TStringList.Create;
    Images := TStringList.Create;
    Docs := TStringList.Create;
    Audio := TStringList.Create;
    Video := TStringList.Create;
    Webs := TStringList.Create;
    try
    ExtractLinks(HTMLContent, NormURL, Links);

    NeedExtract := FSearchImages or FSearchDocuments or FSearchAudio or
    FSearchVideo or FSearchWeb;
    if NeedExtract then
    ExtractMediaSources(HTMLContent, NormURL, Images, Docs, Audio,
    Video, Webs);

    ProcessFoundFiles(Images, Docs, Audio, Video, Webs, StatusBar, ListView,
    CheckList, CurrentDepth, Logging);

    if IsCanceled then
    Exit;
    if (FFoundFilesLimit > 0) and (FileCount >= FFoundFilesLimit) then
    Exit;
    if (FExploreLimit > 0) and (FLinksTraversed >= FExploreLimit) then
    Exit;

    for I := 0 to Links.Count - 1 do
    begin
    if IsCanceled then
    Exit;
    while IsPaused do
    begin
    if IsCanceled then
    Exit;
    if Assigned(FPauseEvent) then
    FPauseEvent.WaitFor(250);
    end;

    if SameDomainOnly then
    begin
    if not IsSameDomain(RootDomain, Links) then
    Continue;
    end;

    ExploreLinksRecursive(Links, ListView, StatusBar, CheckList,
    Depth - 1, Logging);
    end;

    if Assigned(StatusBar) then
    begin
    SafeSetStatusBarPanel(StatusBar, 1, IntToStr(FileCount));
    SafeSetStatusBarPanel(StatusBar, 3, IntToStr(BrokenCount));
    SafeSetStatusBarPanel(StatusBar, 5, IntToStr(FRobotsBlocked));
    SafeSetStatusBarPanel(StatusBar, 7, IntToStr(FLinksTraversed));
    end;
    finally
    Links.Free;
    Images.Free;
    Docs.Free;
    Audio.Free;
    Video.Free;
    Webs.Free;
    end;
    end;

    procedure TScrapix.ExploreLinks(const URL: string; ListView: TscListView;
    StatusBar: TscStatusBar; CheckList: TscCheckListBox; MaxDepth: Integer;
    SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile: Boolean;
    Logging: TscListBox);
    var
    UriStart: TURI;
    RootFolder, StartUrlFolder, ReportFolder, StartFolderName: string;
    DocFolder, FileName, FilePath, LogFolder: string;
    SL: TStringList;
    Key: string;
    begin
    if Assigned(FStoppedEvent) then
    FStoppedEvent.ResetEvent;
    FRunning := True;
    try
    FreeAndNil(VisitedLinks);
    VisitedLinks := TDictionary.Create;
    if Assigned(FFoundFiles) then
    FFoundFiles.Clear;
    if Assigned(FBrokenLinks) then
    FBrokenLinks.Clear;
    TotalLinks := 0;
    FileCount := 0;
    BrokenCount := 0;
    FRobotsBlocked := 0;
    FLinksTraversed := 0;

    FVisitedFilePath := '';
    FBrokenFilePath := '';
    FFoundFilePath := '';

    TInterlocked.Exchange(FState, STATE_RUNNING);
    if Assigned(FPauseEvent) then
    FPauseEvent.SetEvent;

    if RequestTimeoutMs <= 0 then
    RequestTimeoutMs := 30000;
    if RequestDelayMs < 0 then
    RequestDelayMs := 0;

    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('StartingLog'), [URL, MaxDepth]));

    try
    try
    UriStart := TURI.Create(URL);
    RootDomain := UriStart.Host
    except
    RootDomain := ''
    end;

    RootFolder := TPath.Combine(TPath.GetDocumentsPath, 'Scrapix');
    StartFolderName := RootDomain;
    StartUrlFolder := TPath.Combine(RootFolder, StartFolderName);
    ReportFolder := TPath.Combine(StartUrlFolder, 'Report');

    try
    if not TDirectory.Exists(RootFolder) then
    TDirectory.CreateDirectory(RootFolder);
    if not TDirectory.Exists(StartUrlFolder) then
    TDirectory.CreateDirectory(StartUrlFolder);
    if not TDirectory.Exists(ReportFolder) then
    TDirectory.CreateDirectory(ReportFolder);
    except
    end;

    try
    LogFolder := TPath.Combine(ReportFolder, 'Logging');
    if not TDirectory.Exists(LogFolder) then
    TDirectory.CreateDirectory(LogFolder);
    FLogFilePath := TPath.Combine(LogFolder, 'Logging.txt');

    try
    TFile.WriteAllText(FLogFilePath, '', TEncoding.UTF8);
    except
    FLogFilePath := '';
    end;
    except
    FLogFilePath := '';
    end;

    DownloadFolder := TPath.Combine(StartUrlFolder, 'download');
    try
    if not TDirectory.Exists(DownloadFolder) then
    TDirectory.CreateDirectory(DownloadFolder);
    if not TDirectory.Exists(TPath.Combine(DownloadFolder, 'Image')) then
    TDirectory.CreateDirectory(TPath.Combine(DownloadFolder, 'Image'));
    if not TDirectory.Exists(TPath.Combine(DownloadFolder, 'Document')) then
    TDirectory.CreateDirectory(TPath.Combine(DownloadFolder, 'Document'));
    if not TDirectory.Exists(TPath.Combine(DownloadFolder, 'Audio')) then
    TDirectory.CreateDirectory(TPath.Combine(DownloadFolder, 'Audio'));
    if not TDirectory.Exists(TPath.Combine(DownloadFolder, 'Vidéo')) then
    TDirectory.CreateDirectory(TPath.Combine(DownloadFolder, 'Vidéo'));
    if not TDirectory.Exists(TPath.Combine(DownloadFolder, 'Web Document'))
    then
    TDirectory.CreateDirectory(TPath.Combine(DownloadFolder,
    'Web Document'));
    except
    end;

    if SaveBrokenToFile then
    begin
    FileName := 'BrokenLinks.txt';
    FBrokenFilePath := TPath.Combine(ReportFolder, FileName);
    try
    TFile.WriteAllText(FBrokenFilePath, '', TEncoding.UTF8)
    except
    FBrokenFilePath := ''
    end;
    end;

    if SaveVisitedToFile then
    begin
    FileName := 'VisitedLinks.txt';
    FVisitedFilePath := TPath.Combine(ReportFolder, FileName);
    try
    TFile.WriteAllText(FVisitedFilePath, '', TEncoding.UTF8)
    except
    FVisitedFilePath := ''
    end;
    end;

    if SaveFoundFilesToFile then
    begin
    FileName := 'FoundFiles.txt';
    FFoundFilePath := TPath.Combine(ReportFolder, FileName);
    try
    TFile.WriteAllText(FFoundFilePath, '', TEncoding.UTF8)
    except
    FFoundFilePath := ''
    end;
    end;

    FMaxDepth := MaxDepth;
    if Assigned(Logging) then
    SafeLog(Logging, GetTranslate('LaunchingLog'));
    ExploreLinksRecursive(URL, ListView, StatusBar, CheckList,
    MaxDepth, Logging);
    finally
    FreeAndNil(VisitedLinks);
    end;

    if SaveBrokenToFile and Assigned(FBrokenLinks) and (FBrokenLinks.Count > 0)
    then
    begin
    DocFolder := TPath.GetDocumentsPath;
    FileName := 'BrokenLinks.txt';
    FilePath := TPath.Combine(DocFolder, FileName);
    SL := TStringList.Create;
    try
    for Key in FBrokenLinks.Keys do
    SL.Add(Key);
    try
    SL.SaveToFile(FilePath, TEncoding.UTF8)
    except
    end;
    finally
    SL.Free;
    end;
    end;

    if SaveVisitedToFile and Assigned(VisitedLinks) and (VisitedLinks.Count > 0)
    then
    begin
    DocFolder := TPath.GetDocumentsPath;
    if FVisitedFilePath <> '' then
    FilePath := FVisitedFilePath
    else
    FilePath := TPath.Combine(DocFolder, 'VisitedLinks.txt');
    SL := TStringList.Create;
    try
    for Key in VisitedLinks.Keys do
    SL.Add(Key);
    try
    SL.SaveToFile(FilePath, TEncoding.UTF8)
    except
    end;
    finally
    SL.Free;
    end;
    end;

    if SaveFoundFilesToFile and Assigned(FFoundFiles) and (FFoundFiles.Count > 0)
    then
    begin
    DocFolder := TPath.GetDocumentsPath;
    if FFoundFilePath <> '' then
    FilePath := FFoundFilePath
    else
    FilePath := TPath.Combine(DocFolder, 'FoundFiles.txt');
    SL := TStringList.Create;
    try
    for Key in FFoundFiles.Keys do
    SL.Add(Key);
    try
    SL.SaveToFile(FilePath, TEncoding.UTF8)
    except
    end;
    finally
    SL.Free;
    end;
    end;
    finally
    if Assigned(Logging) then
    SafeLog(Logging, Format(GetTranslate('FinishedLog'),
    [FileCount, BrokenCount, TotalLinks]));
    FVisitedFilePath := '';
    FBrokenFilePath := '';
    FFoundFilePath := '';
    FRunning := False;
    if Assigned(FStoppedEvent) then
    FStoppedEvent.SetEvent;
    end;
    end;

    procedure TScrapix.ApplyFileTypeFiltersFromCheckList
    (CheckList: TscCheckListBox);
    var
    Idx: Integer;
    Txt: string;
    begin
    FSearchImages := False;
    FSearchDocuments := False;
    FSearchAudio := False;
    FSearchVideo := False;
    FSearchWeb := False;
    if CheckList = nil then
    Exit;

    for Idx := 0 to CheckList.Count - 1 do
    begin
    if not CheckList.Checked then
    Continue;
    Txt := Trim(CheckList.Items);
    if StartsText('Image', Txt) then
    FSearchImages := True
    else if StartsText('Document', Txt) then
    FSearchDocuments := True
    else if StartsText('Audio', Txt) then
    FSearchAudio := True
    else if StartsText('Vidéo', Txt) or StartsText('Video', Txt) then
    FSearchVideo := True
    else if StartsText('Web Document', Txt) then
    FSearchWeb := True
    else
    case Idx of
    0:
    FSearchImages := True;
    1:
    FSearchDocuments := True;
    2:
    FSearchAudio := True;
    3:
    FSearchVideo := True;
    4:
    FSearchWeb := True;
    end;
    end;
    end;

    end.

    Translate.Core.pas
    unit Translate.Core;

    interface

    uses
    {System}
    System.SysUtils, System.Classes, System.Generics.Collections, System.SyncObjs;

    type
    // Type énuméré représentant les langues supportées
    TLang = (lgFrench, lgEnglish);

    { Définitions publiques
    - SetLanguage : change la langue courante (thread-safe)
    - GetLanguage : retourne la langue courante (thread-safe)
    - GetTranslate : retourne la chaîne traduite correspondant à une clé
    - RegisterText : enregistre une traduction pour une clé donnée
    }

    procedure SetLanguage(ALang: TLang);
    function GetLanguage: TLang;
    function GetTranslate(const Key: string): string;
    procedure RegisterText(const Key, FrenchText, EnglishText: string);

    implementation

    var
    // Verrou pour protéger l'accès concurrent à CurrentLang et Texts
    LangLock: TCriticalSection;
    // Langue courante utilisée par GetTranslate
    CurrentLang: TLang;
    // Dictionnaire stockant les traductions : Key -> [fr, en]
    Texts: TDictionary>; // Key -> [fr, en]

    { InitDefaults
    Initialise les traductions par défaut utilisées par l'application.
    Appelle RegisterText pour chaque clé afin de remplir le dictionnaire.
    }
    procedure InitDefaults;
    begin
    // Boutons
    RegisterText('BtnStart', 'Démarrer', 'Start');
    RegisterText('BtnBreak_Pause', 'Pause', 'Break');
    RegisterText('BtnBreak_Resume', 'Reprendre', 'Resume');
    RegisterText('BtnStop', 'Arrêter', 'Stop');
    RegisterText('InProgress', 'En cours...', 'In progress...');

    // Messages utilisateur
    RegisterText('PleaseProvideUrl', 'Veuillez fournir une URL de départ.',
    'Please provide a starting URL.');
    RegisterText('StopOrPauseToOpen',
    'Arrêtez ou mettez en pause l''exploration pour ouvrir le lien.',
    'Stop or pause the crawl before opening the link.');
    RegisterText('InvalidUrl', 'URL invalide : ', 'Invalid URL: ');

    // Titres de colonnes ListView
    RegisterText('Col0', 'Exploration', 'Exploration');
    RegisterText('Col1', 'Statut', 'Statut');
    RegisterText('Col2', 'Téléchargement', 'Download');
    RegisterText('Col3', 'Temps de réponse (ms)', 'Response time (ms)');
    RegisterText('Col4', 'Taille', 'Size');
    RegisterText('Col5', 'Profondeur', 'Depth');
    RegisterText('Col6', 'Type de requête', 'Query type');

    // États et sous-items
    RegisterText('Broken', 'Corrompu', 'Broken');
    RegisterText('Broken_Ignored', 'Corrompu (ignoré)', 'Broken (ignored)');
    RegisterText('BlockedByRobots', 'Bloqué par robots.txt',
    'Blocked by robots.txt');
    RegisterText('Ignored', 'Ignoré', 'Ignored');
    RegisterText('Downloading', 'Téléchargement', 'Downloading');
    RegisterText('Downloaded', 'Téléchargé', 'Downloaded');
    RegisterText('DownloadFailed', 'Échec téléchargement', 'Download failed');
    RegisterText('NotDownloaded', 'Non téléchargé', 'Not downloaded');
    RegisterText('NoResponse', 'Pas de réponse', 'No response');
    RegisterText('OnHold', 'En attente', 'On hold');

    // Labels et cases à cocher
    RegisterText('LabDepth', 'Profondeur d''exploration', 'Exploration depth');
    RegisterText('CkSameDomain', 'Limiter au même domaine',
    'Limit to the same domain');
    RegisterText('CkRobot', 'Respecter les directives Robots.txt',
    'Respect Robots.txt directives');
    RegisterText('LabExploreLimit', 'Limite d''exploration', 'Exploration limit');
    RegisterText('LabFoundFilesLimit', 'Limite fichiers trouvés',
    'Limit files found');
    RegisterText('LabTimeout', 'Temps d''attente par requête (ms)',
    'Wait time per request (ms)');
    RegisterText('LabDelay', 'Délai entre requêtes (ms)',
    'Delay between requests (ms)');
    RegisterText('LabListFileTypes', 'Types de fichiers à rechercher',
    'File types to search for');
    RegisterText('CkAutoDownload', 'Téléchargement automatique',
    'Automatic download');
    RegisterText('LabReport', 'Rapport d''exploration', 'Crawl Report');
    RegisterText('CkSaveBrokenLinks', 'Rapport des liens corrompus',
    'Report corrupted links');
    RegisterText('CkSaveBrokenToFile', 'Rapport des pages visitées',
    'Report of visited pages');
    RegisterText('CkSaveFoundFilesToFile', 'Rapport des fichiers trouvés',
    'Report of found files');

    // Texte de la barre d'état
    RegisterText('Panel0', 'Fichiers trouvés ', 'Files found ');
    RegisterText('Panel2', 'Liens corrompus ', 'Corrupted links ');
    RegisterText('Panel4', 'Bloqué par robots.txt ', 'Blocked by robots.txt ');
    RegisterText('Panel6', 'Liens parcourus ', 'Links browsed ');

    // Observateur d'événements
    RegisterText('ExPanelLog', 'Observateur d''événements', 'Event Viewer');

    RegisterText('BrokenLinkLog', 'Lien corrompues : %s','Corrupted link: %s');
    RegisterText('BlockedRobotsHEADLog', 'Bloqué par le robots (HEAD) : %s',
    'Blocked by robots (HEAD): %s');
    RegisterText('NoResponseHEADLog', 'Aucune réponse HEAD pour %s',
    'No response HEAD for %s');
    RegisterText('BlockedRobotsDownLog',
    'Bloqué par le robots (Téléchargement) : %s',
    'Blocked by robots (Download): %s');
    RegisterText('DonwLog', 'Téléchargé %s -> %s', 'Downloaded %s -> %s');
    RegisterText('DonwFailedLog', 'Échec du téléchargement %s',
    'Download failed %s');
    RegisterText('ExcepDonwLog', 'Exception de téléchargement %s : %s',
    'Download exception %s : %s');
    RegisterText('BlockedRobotsResLog', 'Bloqué par le robots (Resource) : %s',
    'Blocked by robots (Resource) : %s');
    RegisterText('FoundNotDownLog', 'Trouvé (Non téléchargé) : %s',
    'Found (Not downloaded): %s');
    RegisterText('CorruptedDownLog',
    'Lien corrompue (Téléchargement automatique)*: %s',
    'Corrupted link (Automatic download) : %s');
    RegisterText('BlockedRobotsLog', 'Bloqué par le robots (Récursif) : %s',
    'Blocked by robots (Recursive) : %s');
    RegisterText('VisitedLog', 'Visité : %s (Profondeur = %d)',
    'Visited : %s (Depth = %d)');
    RegisterText('MarkBrokenLinkLog', 'Aucun contenu / lien corrompues : %s',
    'No content / Corrupted link : %s');
    RegisterText('NoContentLog', 'Aucun contenu (ignoré) : %s',
    'No content (ignored): %s');
    RegisterText('StartingLog',
    'Scrapix*: démarrage de l''exploration de %s (MaxDepth = %d)',
    'Scrapix: Starting to crawl %s (MaxDepth = %d)');
    RegisterText('LaunchingLog',
    'Scrapix : lancement de l''exploration récursive',
    'Scrapix: launching recursive exploration');
    RegisterText('FinishedLog',
    'Scrapix*: Exploration terminée. Fichiers trouvés = %d ,*Liens corrompus*=*%d ,*Liens parcourus*=*%d',
    'Scrapix: Crawling complete. Files found = %d , Corrupted links = %d , Links browsed = %d');
    end;

    { SetLanguage
    Définit la langue courante de façon thread-safe en protégeant l'affectation
    par un TCriticalSection afin d'éviter les conditions de concurrence.
    }
    procedure SetLanguage(ALang: TLang);
    begin
    LangLock.Enter;
    try
    CurrentLang := ALang;
    finally
    LangLock.Leave;
    end;
    end;

    { GetLanguage
    Retourne la langue courante de façon thread-safe en accédant à CurrentLang
    sous protection du verrou LangLock.
    }
    function GetLanguage: TLang;
    begin
    LangLock.Enter;
    try
    Result := CurrentLang;
    finally
    LangLock.Leave;
    end;
    end;

    { GetTranslate
    Recherche la traduction correspondant à Key dans le dictionnaire Texts.
    - Si la clé est vide, retourne immédiatement une chaîne vide.
    - Si la clé n'existe pas, retourne la clé elle-même comme fallback.
    - Sélectionne l'élément francais ou anglais suivant CurrentLang.
    L'accès au dictionnaire est protégé par LangLock pour être thread-safe.
    }
    function GetTranslate(const Key: string): string;
    var
    Arr: TArray;
    begin
    Result := Key; // fallback si aucune traduction trouvée
    if Key = '' then
    Exit;
    LangLock.Enter;
    try
    // Vérifie que Texts est initialisé et que la clé existe
    if (Texts <> nil) and Texts.TryGetValue(Key, Arr) then
    begin
    case CurrentLang of
    lgFrench:
    if Length(Arr) > 0 then
    Result := Arr; // français
    lgEnglish:
    if Length(Arr) > 1 then
    Result := Arr; // anglais
    end;
    end;
    finally
    LangLock.Leave;
    end;
    end;

    { RegisterText
    Enregistre ou met à jour la traduction pour une clé donnée.
    - Ignore les clés vides.
    - Alloue le tableau de 2 éléments [fr, en].
    - Si le dictionnaire n'existe pas encore, le crée.
    - Utilise AddOrSetValue pour ajouter ou remplacer la valeur existante.
    L'opération est thread-safe via LangLock.
    }
    procedure RegisterText(const Key, FrenchText, EnglishText: string);
    var
    Arr: TArray;
    begin
    if Key.IsEmpty then
    Exit;
    LangLock.Enter;
    try
    SetLength(Arr, 2);
    Arr := FrenchText;
    Arr := EnglishText;
    if Texts = nil then
    Texts := TDictionary < string, TArray < string >>.Create;
    Texts.AddOrSetValue(Key, Arr);
    finally
    LangLock.Leave;
    end;
    end;

    { Bloc d'initialisation
    - Crée le verrou LangLock.
    - Définit la langue par défaut (ici français).
    - Crée le dictionnaire Texts.
    - Remplit les traductions par défaut via InitDefaults.
    }
    initialization

    LangLock := TCriticalSection.Create;
    CurrentLang := lgFrench; // valeur par défaut
    Texts := TDictionary < string, TArray < string >>.Create;
    InitDefaults;

    { Bloc de finalisation
    - Libère les ressources allouées dans l'initialization.
    - Important de libérer Texts avant LangLock si le dictionnaire utilise des sections critiques.
    }
    finalization

    Texts.Free;
    LangLock.Free;

    end.


    UScrapix.pas Vcl UI
    unit UScrapix;

    interface

    uses
    {Winapi}
    Winapi.Windows, Winapi.Messages, Winapi.ShellAPI,
    {System}
    System.SysUtils, System.Variants, System.Classes, System.IOUtils,
    System.Net.URLClient, System.UITypes, System.StrUtils, System.ImageList,
    System.SyncObjs,
    {Vcl}
    Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls,
    Vcl.StdCtrls, Vcl.Mask, Vcl.CheckLst, Vcl.Themes, Vcl.ImgList,
    Vcl.VirtualImageList, Vcl.BaseImageCollection, Vcl.ImageCollection,
    {StyleControls VCL}
    scStyleManager, scControls, scModernControls, scDialogs, scExtControls,
    {Scrapix.Core}
    Scrapix.Core,
    {Translate.Core}
    Translate.Core;

    type
    TFScrapix = class(TForm)
    scStyleManager: TscStyleManager;
    Collection: TImageCollection;
    ImageList: TVirtualImageList;
    BoxMain: TscPanel;
    BtnOpenDir: TscButton;
    EdUrl: TscEdit;
    BtnStart: TscButton;
    BtnBreak: TscButton;
    BtnStop: TscButton;
    BtnSettings: TscButton;
    BtnResetUI: TscButton;
    BtnAbout: TscButton;
    BtnTranslate: TscButton;
    BoxScrap: TscPanel;
    ListView: TscListView;
    SplitView: TscSplitView;
    ScrollBox: TscScrollBox;
    LabDepth: TscLabel;
    SeDepth: TscSpinEdit;
    CkSameDomain: TscCheckBox;
    CkRobot: TscCheckBox;
    LabTimeout: TscLabel;
    SeTimeout: TscSpinEdit;
    LabDelay: TscLabel;
    SeDelay: TscSpinEdit;
    LabExploreLimit: TscLabel;
    SeExploreLimit: TscSpinEdit;
    LabFoundFilesLimit: TscLabel;
    SeFoundFilesLimit: TscSpinEdit;
    LabListFileTypes: TLabel;
    CkListFileTypes: TscCheckListBox;
    CkAutoDownload: TscCheckBox;
    LabReport: TscLabel;
    CkSaveBrokenLinks: TscCheckBox;
    CkSaveBrokenToFile: TscCheckBox;
    CkSaveFoundFilesToFile: TscCheckBox;
    ExPanelLog: TscExPanel;
    Logging: TscListBox;
    StatusBar: TscStatusBar;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure BtnOpenDirClick(Sender: TObject);
    procedure BtnStartClick(Sender: TObject);
    procedure BtnBreakClick(Sender: TObject);
    procedure BtnStopClick(Sender: TObject);
    procedure BtnSettingsClick(Sender: TObject);
    procedure BtnResetUIClick(Sender: TObject);
    procedure BtnAboutClick(Sender: TObject);
    procedure BtnTranslateClick(Sender: TObject);
    procedure ListViewDblClick(Sender: TObject);

    private
    Scrapix: TScrapix;

    { Met à jour l'état du bouton Pause/Resume selon l'état du crawler. }
    procedure UpdateBtnBreak;

    { Active / désactive les contrôles de l'UI selon le bool Running (thread-safe). }
    procedure UpdateUI(Running: Boolean);

    { Met à jour les libellés traduits dans l'UI. }
    procedure UpdateTranslateUI;

    { Vérifie rapidement que la chaîne donnée est une URL HTTP/HTTPS valide. }
    function IsUrl(const AUrl: string): Boolean;

    { Réinitialise les panneaux de la statusbar (thread-safe). }
    procedure ResetStatusPanels;

    { Routine interne pour restaurer l'UI (appelée toujours sur le thread principal). }
    procedure RestoreUIAfterRun;
    public
    end;

    var
    FScrapix: TFScrapix;

    implementation

    {$R *.dfm}

    { UpdateBtnBreak: adapte le libellé du bouton Pause/Resume selon l'état Scrapix. }
    procedure TFScrapix.UpdateBtnBreak;
    begin
    if not Assigned(Scrapix) then
    begin
    BtnBreak.Caption := GetTranslate('BtnBreak_Pause');
    Exit;
    end;

    if Scrapix.IsPaused then
    BtnBreak.Caption := GetTranslate('BtnBreak_Resume')
    else
    BtnBreak.Caption := GetTranslate('BtnBreak_Pause');
    end;

    { UpdateUI: active ou désactive les contrôles pertinents; exécute via Queue si appelé hors du MainThread. }
    procedure TFScrapix.UpdateUI(Running: Boolean);
    begin
    if TThread.Current.ThreadID <> MainThreadID then
    begin
    TThread.Queue(nil,
    procedure
    begin
    UpdateUI(Running);
    end);
    Exit;
    end;

    BtnStart.Enabled := not Running;
    BtnStop.Enabled := Running;
    BtnBreak.Enabled := Running;
    BtnSettings.Enabled := not Running;
    BtnResetUI.Enabled := not Running;
    BtnAbout.Enabled := not Running;
    BtnTranslate.Enabled := not Running;
    end;

    { UpdateTranslateUI: applique les traductions aux contrôles visibles. }
    procedure TFScrapix.UpdateTranslateUI;
    var
    I: Integer;
    begin
    BtnStart.Caption := GetTranslate('BtnStart');
    BtnStop.Caption := GetTranslate('BtnStop');
    BtnBreak.Caption := GetTranslate('BtnBreak_Pause');

    for I := 0 to ListView.Columns.Count - 1 do
    ListView.Columns.Caption := GetTranslate('Col' + IntToStr(I));

    LabDepth.Caption := GetTranslate('LabDepth');
    CkSameDomain.Caption := GetTranslate('CkSameDomain');
    CkRobot.Caption := GetTranslate('CkRobot');
    LabTimeout.Caption := GetTranslate('LabTimeout');
    LabDelay.Caption := GetTranslate('LabDelay');
    LabExploreLimit.Caption := GetTranslate('LabExploreLimit');
    LabFoundFilesLimit.Caption := GetTranslate('LabFoundFilesLimit');
    LabListFileTypes.Caption := GetTranslate('LabListFileTypes');
    CkAutoDownload.Caption := GetTranslate('CkAutoDownload');
    LabReport.Caption := GetTranslate('LabReport');
    CkSaveBrokenLinks.Caption := GetTranslate('CkSaveBrokenLinks');
    CkSaveBrokenToFile.Caption := GetTranslate('CkSaveBrokenToFile');
    CkSaveFoundFilesToFile.Caption := GetTranslate('CkSaveFoundFilesToFile');

    ExPanelLog.Caption := GetTranslate('ExPanelLog');

    StatusBar.Panels.Text := GetTranslate('Panel0');
    StatusBar.Panels.Text := GetTranslate('Panel2');
    StatusBar.Panels.Text := GetTranslate('Panel4');
    StatusBar.Panels.Text := GetTranslate('Panel6');
    end;

    { IsUrl: vérifie qu'une chaîne est une URL http(s) valide (sans lever d'exception). }
    function TFScrapix.IsUrl(const AUrl: string): Boolean;
    var
    U: TURI;
    begin
    Result := False;
    if AUrl.IsEmpty then
    Exit;
    try
    U := TURI.Create(AUrl);
    Result := ((U.Scheme = 'http') or (U.Scheme = 'https')) and (U.Host <> '');
    except
    Result := False;
    end;
    end;

    { FormCreate: initialise valeurs par défaut et UI. }
    procedure TFScrapix.FormCreate(Sender: TObject);
    begin
    SetLanguage(lgFrench);
    Scrapix := TScrapix.Create;

    SeDepth.MinValue := 1;
    SeDepth.MaxValue := 20;
    SeDepth.Value := 2;

    SeTimeout.MinValue := 1000;
    SeTimeout.MaxValue := 30000;
    SeTimeout.Value := 10000;

    SeDelay.MinValue := 1;
    SeDelay.MaxValue := 60000;
    SeDelay.Value := 100;

    SeExploreLimit.MinValue := 1;
    SeExploreLimit.MaxValue := 100;
    SeExploreLimit.Value := 20;

    SeFoundFilesLimit.MinValue := 1;
    SeFoundFilesLimit.MaxValue := 2000;
    SeFoundFilesLimit.Value := 500;

    CkSameDomain.Checked := False;

    with ListView do
    begin
    Columns.BeginUpdate;
    try
    Columns.Clear;
    with Columns.Add do
    Width := 600;
    with Columns.Add do
    Width := 200;
    with Columns.Add do
    Width := 200;
    with Columns.Add do
    Width := 200;
    with Columns.Add do
    Width := 120;
    with Columns.Add do
    Width := 120;
    with Columns.Add do
    Width := 200;
    finally
    Columns.EndUpdate;
    end;
    ViewStyle := vsReport;
    end;

    CkRobot.Checked := True;

    BtnBreak.Enabled := False;
    BtnStop.Enabled := False;

    with CkListFileTypes do
    begin
    Items.Clear;
    Items.Add('Image');
    Items.Add('Document');
    Items.Add('Audio');
    Items.Add('Vidéo');
    Items.Add('Web Document');

    Checked := True;
    Checked := True;
    Checked := True;
    Checked := True;
    Checked := False;
    end;

    SplitView.Close;
    UpdateUI(False);
    UpdateTranslateUI;

    ExPanelLog.RollUpState := True;

    {$IFDEF DEBUG}
    EdUrl.Text := 'https://github.com/';
    SeDepth.Value := 2;
    SeTimeout.Value := 10000;
    SeDelay.Value := 100;
    {$ENDIF}
    end;

    { FormClose: ordonne l'arrêt du crawler et empêche fuite d'objet. }
    procedure TFScrapix.FormClose(Sender: TObject; var Action: TCloseAction);
    begin
    if Assigned(Scrapix) then
    begin
    Scrapix.DisableUIUpdates := True;
    Scrapix.CancelExploration;
    Scrapix.WaitForStop;
    Action := caFree;
    end;
    end;

    { FormCloseQuery: empêche la fermeture tant que le crawler est en cours. }
    procedure TFScrapix.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    begin
    if Assigned(Scrapix) and Scrapix.IsRunning then
    begin
    Scrapix.DisableUIUpdates := True;
    Scrapix.CancelExploration;

    if not Scrapix.WaitForStop then
    begin
    CanClose := False;
    Exit;
    end;
    end;
    CanClose := True;
    end;

    { FormDestroy: libère l'objet Scrapix de manière sûre. }
    procedure TFScrapix.FormDestroy(Sender: TObject);
    begin
    if Assigned(Scrapix) then
    begin
    try
    Scrapix.DisableUIUpdates := True;
    Scrapix.CancelExploration;
    Scrapix.WaitForStop;
    FreeAndNil(Scrapix);
    except
    on E: Exception do
    scShowMessage(E.Message);
    end;
    end;
    end;

    { BtnOpenDirClick: ouvre le dossier de rapport / téléchargement lié à l'URL saisie. }
    procedure TFScrapix.BtnOpenDirClick(Sender: TObject);
    var
    Dir, DirDom: String;
    U: TURI;
    begin
    DirDom := EmptyStr;
    Dir := TPath.Combine(TPath.GetDocumentsPath, Application.Title);

    if IsUrl(EdUrl.Text) then
    begin
    try
    U := TURI.Create(EdUrl.Text);
    DirDom := TPath.Combine(Dir, U.Host);
    except
    DirDom := EmptyStr;
    end;
    end;

    if DirectoryExists(DirDom) then
    ShellExecute(0, 'open', PChar(DirDom), nil, nil, SW_SHOWNORMAL)
    else if DirectoryExists(Dir) then
    ShellExecute(0, 'open', PChar(Dir), nil, nil, SW_SHOWNORMAL);
    end;

    { ResetStatusPanels: remet à zéro les compteurs affichés sur la statusbar (thread-safe). }
    procedure TFScrapix.ResetStatusPanels;
    begin
    if TThread.Current.ThreadID <> MainThreadID then
    begin
    TThread.Queue(nil,
    procedure
    begin
    ResetStatusPanels;
    end);
    Exit;
    end;
    StatusBar.Panels.Text := '0';
    StatusBar.Panels.Text := '0';
    StatusBar.Panels.Text := '0';
    StatusBar.Panels.Text := '0';
    end;

    { RestoreUIAfterRun: restaure l'UI après exécution du thread (toujours MainThread). }
    procedure TFScrapix.RestoreUIAfterRun;
    begin
    UpdateUI(False);
    BtnStart.Caption := GetTranslate('BtnStart');
    UpdateBtnBreak;
    end;

    { BtnStartClick: lance l'exploration dans un thread anonyme, protège contre double démarrage. }
    procedure TFScrapix.BtnStartClick(Sender: TObject);
    begin
    if not IsUrl(Trim(EdUrl.Text)) then
    begin
    scShowMessage(GetTranslate('PleaseProvideUrl'));
    EdUrl.SetFocus;
    Exit;
    end;

    ListView.Items.Clear;
    SplitView.Close;
    UpdateUI(True);
    ResetStatusPanels;

    Logging.Items.Clear;
    ExPanelLog.RollUpState := False;

    if not Assigned(Scrapix) then
    Scrapix := TScrapix.Create;

    Scrapix.ConfigureCrawl(SeTimeout.ValueAsInt, SeDelay.ValueAsInt,
    CkSameDomain.Checked, CkAutoDownload.Checked, CkRobot.Checked,
    SeFoundFilesLimit.ValueAsInt, SeExploreLimit.ValueAsInt);

    BtnStart.Caption := GetTranslate('InProgress');

    TThread.CreateAnonymousThread(
    procedure
    begin
    try
    try
    Scrapix.ApplyFileTypeFiltersFromCheckList(CkListFileTypes);
    Scrapix.ExploreLinks(Trim(EdUrl.Text), ListView, StatusBar,
    CkListFileTypes, SeDepth.ValueAsInt, CkSaveBrokenLinks.Checked,
    CkSaveBrokenToFile.Checked, CkSaveFoundFilesToFile.Checked,
    Logging);
    except
    on E: Exception do
    TThread.Queue(nil,
    procedure
    begin
    scShowMessage('Explorer thread exception : ' + E.Message);
    end);
    end;
    finally
    TThread.Queue(nil,
    procedure
    begin
    if Assigned(Scrapix) and Scrapix.IsRunning then
    begin
    Scrapix.CancelExploration;
    Scrapix.WaitForStop;
    end;
    RestoreUIAfterRun;
    end);
    end;
    end).Start;
    end;

    { BtnBreakClick: bascule entre pause et reprise. }
    procedure TFScrapix.BtnBreakClick(Sender: TObject);
    begin
    if not Assigned(Scrapix) then
    Exit;

    if Scrapix.IsPaused then
    Scrapix.ResumeExploration
    else
    Scrapix.PauseExploration;

    UpdateBtnBreak;
    UpdateUI(True);
    end;

    { BtnStopClick: demande l'arrêt et attend la fin (bloquant court sur le thread UI). }
    procedure TFScrapix.BtnStopClick(Sender: TObject);
    begin
    if Assigned(Scrapix) then
    begin
    Scrapix.CancelExploration;
    Scrapix.WaitForStop;

    UpdateUI(False);
    BtnStart.Enabled := True;
    BtnStart.Caption := GetTranslate('BtnStart');
    BtnStop.Enabled := False;
    BtnBreak.Enabled := False;
    end;
    end;

    { BtnSettingsClick: ouvre/ferme le panneau de configuration. }
    procedure TFScrapix.BtnSettingsClick(Sender: TObject);
    begin
    SplitView.Opened := not SplitView.Opened;

    if SplitView.Opened then
    ScrollBox.VertScrollBar.Position := 0;
    end;

    { BtnResetUIClick: Réinitialise l'UI }
    procedure TFScrapix.BtnResetUIClick(Sender: TObject);
    begin
    EdUrl.Clear;
    ListView.Items.Clear;
    Logging.Items.Clear;
    end;

    { BtnAboutClick: A propos... }
    procedure TFScrapix.BtnAboutClick(Sender: TObject);
    begin
    with TStringList.Create do
    begin
    Add('Développé par : XeGregory');
    Add('IDE : Embarcadero Delphi 11');
    Add('');
    Add('Version :');
    Add('- Srapix UI : v1.0');
    Add('- Srapix.Core.pas : v1.0');
    Add('- Translate.Core : v1.0');
    scShowMessage(Text);
    Free;
    end;
    end;

    { BtnTranslateClick: change la langue de l'UI et met à jour les libellés. }
    procedure TFScrapix.BtnTranslateClick(Sender: TObject);
    begin
    case GetLanguage of
    lgFrench:
    begin
    SetLanguage(lgEnglish);
    BtnTranslate.ImageIndex := 3;
    end;
    lgEnglish:
    begin
    SetLanguage(lgFrench);
    BtnTranslate.ImageIndex := 2;
    end;
    end;
    UpdateTranslateUI;
    end;

    { ListViewDblClick: ouvre l'URL sélectionnée dans le navigateur, exige que le crawler soit stoppé ou en pause. }
    procedure TFScrapix.ListViewDblClick(Sender: TObject);
    var
    SelItem: TListItem;
    Url: string;
    begin
    SelItem := ListView.Selected;
    if SelItem = nil then
    Exit;

    Url := SelItem.Caption.Trim;
    if Url = '' then
    Exit;

    if Assigned(Scrapix) and not(Scrapix.IsPaused or Scrapix.IsCanceled) then
    begin
    scShowMessage(GetTranslate('StopOrPauseToOpen'));
    Exit;
    end;

    if not IsUrl(Url) then
    begin
    scShowMessage(GetTranslate('InvalidUrl') + Url);
    Exit;
    end;

    ShellExecute(0, 'open', PChar(Url), nil, nil, SW_SHOWNORMAL);
    end;

    end.




    # Interface Vcl

    671311



    # Champs et structures importantes


    • VisitedLinks : TDictionary — Dictionnaire des URL déjà visitées pour éviter les doublons pendant l'exploration.
    • FFoundFiles : TDictionary — Dictionnaire des fichiers repérés ou téléchargés.
    • FBrokenLinks : TDictionary — Dictionnaire des liens identifiés comme cassés.
    • TotalLinks, FileCount, BrokenCount, FRobotsBlocked, FLinksTraversed — Compteurs statistiques mis à jour pendant l'exploration.
    • FState, FPauseEvent, FStoppedEvent — Contrôle d'état de l'exploration pour pause, reprise et annulation.
    • RequestTimeoutMs, RequestDelayMs, SameDomainOnly, RootDomain — Paramètres du crawl et comportement d'URL.
    • FAutoDownload, DownloadFolder — Options et destination pour téléchargement automatique.
    • RobotsRules : TDictionary — Cache des règles robots.txt par hôte.
    • FRespectRobots, FSearchImages, FSearchDocuments, FSearchAudio, FSearchVideo, FSearchWeb — Flags de comportement.
    • FRunning, FMaxDepth — Indicateurs d'exécution et de profondeur.
    • FVisitedFilePath, FBrokenFilePath, FFoundFilePath, FLogFilePath, FLogLock — Chemins de rapports et synchronisation du log.
    • FDisableUIUpdates, FFoundFilesLimit, FExploreLimit — Contrôle d'UI et limites d'exploration.




    # Initialisation, destruction et contrôle d'exécution

    constructor Create
    Rôle : Initialise les champs, crée les dictionnaires FFoundFiles et FBrokenLinks, crée les events de pause/stop et le TCriticalSection pour le log.
    Valeurs par défaut importantes : RequestTimeoutMs = 30000 ms, RequestDelayMs = 0, SameDomainOnly = True, FRespectRobots = True, FFoundFilesLimit = 2000, FExploreLimit = 100, recherches de ressources activées.
    Effet : L'objet est prêt pour configurer et lancer une exploration.

    destructor Destroy
    Rôle : Annule toute exploration en cours, attend l'arrêt, libère les dictionnaires, events, lock, et libère et vide RobotsRules correctement en libérant chaque TStringList.
    Effet : Nettoyage sûr et libération des ressources.

    PauseExploration / ResumeExploration / CancelExploration
    Rôle : Modifier l'état FState pour PAUSED, RUNNING ou CANCEL respectivement et manipuler FPauseEvent afin d'ordonner le blocage ou la reprise des threads qui attendent.
    Effet : Permet au code récursif d'attendre ou d'interrompre proprement son exécution.

    IsCanceled / IsPaused / IsRunning
    Rôle : Fournir l'état courant par lecture atomique via TInterlocked pour IsCanceled et IsPaused et lecture de FRunning pour IsRunning.
    Retour : Boolean indiquant la condition demandée.

    WaitForStop
    Rôle : Met en CANCEL l'exploration puis attend que FStoppedEvent soit signalé ou boucle tant que FRunning reste vrai. Renvoie vrai si l'arrêt est confirmé.
    Usage : Bloquant pour attendre fin complète avant destruction ou autre action.

    ConfigureCrawl
    Rôle : Applique les paramètres fournis au crawler tels que timeouts, délai entre requêtes, restriction même domaine, téléchargement automatique, respect robots, limites de fichiers trouvés et limite d'exploration.
    Validation : Définit des bornes pour les limites et normalise les timeout/delay.



    # Normalisation d'URL et gestion domaines

    NormalizeURL(const BaseURL, RelOrAbsURL: string): string

    Rôle : Convertir une URL relative ou étrange en URL absolue normalisée.
    Comportement clé :

    • Supprime la partie fragment après '#'.
    • Ignore les URI de schéma non HTTP utiles mailto, javascript, tel, data.
    • Gère les protocoles relatifs en préfixant par https.
    • Si URL déjà absolue, tente TURI.Create pour normaliser.
    • Si URL relative et BaseURL fourni, combine scheme+host+port+chemin de base et concatène la partie relative.
    • Tente de normaliser le résultat via TURI.Create.

    Retour : URL normalisée ou chaîne vide si impossible.
    Impact : Utilisée partout pour uniformiser les comparaisons et requêtes.

    IsSameDomain(const BaseURL, LinkURL: string): Boolean
    Rôle : Déterminer si LinkURL appartient au même domaine ou sous-domaine du BaseURL.
    Logique :

    • Extrait les hôtes via TURI.Create ou prend BaseURL textuel si pas de schéma.
    • Compare en vérifiant si HostBase est suffixe du HostLink ou égal.

    Retour : True si même domaine ou sous-domaine.



    # robots.txt : parsing, caching et autorisations

    ParseRobots(const RobotsText: string; OutList: TStringList): Boolean
    Rôle : Lire le contenu de robots.txt et extraire les chemins "Disallow" applicables à l'agent "Scrapix" ou à "*" en tenant compte du bloc User-agent courant.
    Comportement :

    • Sépare en lignes, ignore les lignes vides, détecte les blocs User-agent.
    • Si le User-agent correspond à "Scrapix" ou "*", récupère les Disallow non vides et préfixe d'un slash si nécessaire.
    • Ajoute chaque chemin unique à OutList.

    Retour : True si parsing exécuté.

    EnsureRobotsForHost(const Host, Scheme: string): Boolean
    Rôle : Charger robots.txt pour un hôte donné et stocker les règles dans RobotsRules pour cache.
    Comportement :

    • Construit l'URL robots.txt et effectue un GET avec THTTPClient.
    • Parse le contenu via ParseRobots et stocke une copie des règles dans RobotsRules[host en minuscule].
    • Ne relance pas si déjà en cache.

    Retour : True sauf si Host vide.

    IsAllowedByRobots(const URL: string): Boolean
    Rôle : Vérifier si une URL est autorisée par les règles en cache ou en récupérant robots.txt si nécessaire.
    Comportement :

    • Si FRespectRobots est false, renvoie true sans vérification.
    • Extrait Host, Scheme et Path via TURI.
    • S'assure que robots.txt est présent dans le cache pour l'hôte en appelant EnsureRobotsForHost.
    • Parcourt les chemins Disallow pour l'hôte et si Path commence par un Disallow, renvoie false.

    Retour : True si autorisée, False si bloquée.



    # Requêtes HTTP et utilitaires

    GetResponseHeaderValue(const Resp: IHTTPResponse; const HeaderName: string): string
    Rôle : Extraire la valeur d'un header HTTP donné depuis l'objet IHTTPResponse.
    Comportement : Parcourt Resp.Headers et compare les noms en insensitif. Renvoie la première valeur correspondante.

    FormatBytes(const SizeBytes: string): string
    Rôle : Formater une taille binaire fournie en chaîne en représentation lisible avec suffixes Octets, Ko, Mo, Go et arrondissements.
    Comportement :

    • Tente de convertir SizeBytes en entier. Si échoue, extrait les chiffres via regex.
    • Si valeur 0 ou vide, renvoie "n/a".
    • Convertit en unités en divisant et formatant avec FormatFloat.

    Retour : Chaîne lisible.

    GetWebContent(const URL: string; ListView: TscListView; Depth: Integer; Logging: TscListBox): string
    Rôle : Effectue une requête HTTP GET sur l'URL normalisée et récupère le corps en texte brut, met à jour l'UI et le log.
    Comportement :

    • Normalise l'URL.
    • Crée THTTPClient, configure timeout et user-agent.
    • Télécharge le contenu dans TMemoryStream et mesure le temps.
    • Récupère status, Content-Type et Content-Length depuis les headers.
    • Convertit le contenu binaire en string et retourne.
    • Met à jour ListView via SafeUpdateListViewStatus et SafeUpdateListViewInfo.
    • Journalise l'opération dans Logging via SafeLog.
    • Applique RequestDelayMs via TThread.Sleep si nécessaire.

    Erreurs : Capture les exceptions, met à jour l'UI avec l'exception et renvoie chaîne vide.

    IsFileAvailable(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; Depth: Integer; Logging: TscListBox): Boolean
    Rôle : Vérifie la disponibilité d'une ressource en exécutant HEAD puis en Fallback GET avec Range bytes=0-0 si HEAD échoue.
    Comportement :

    • Normalise l'URL et vérifie robots.txt via IsAllowedByRobots.
    • Tente Client.Head(NormURL) et mesure temps.
    • Si HEAD échoue, effectue un GET avec en-tête Range pour ne récupérer qu'un octet.
    • Extrait Content-Length ou Content-Range pour estimer la taille.
    • Détermine la disponibilité si StatusCode dans [200,299].
    • Met à jour ListView et Logging via SafeUpdateListViewStatus, SafeUpdateListViewInfo et SafeLog.

    Retour : True si réponse HTTP 2xx, False sinon.



    # Téléchargement de fichiers

    DownloadFile(const URL: string; Client: THTTPClient; out LocalPath: string; Logging: TscListBox): Boolean
    Rôle : Télécharger la ressource URL vers un fichier local dans DownloadFolder en organisant par type d'extension.
    Comportement détaillé :

    • Normalise URL et vérifie robots.txt..
    • Extrait le chemin et le nom de fichier via TURI et TPath.
    • Si pas de nom, utilise "file" et obtient l'extension.
    • Détermine sous-dossier cible via DetermineSubFolderByExtension qui mappe extensions à Image, Document, Audio, Vidéo, Ressources Web/ CSS/JS/HTML/Fonts ou Autre.
    • Crée le dossier cible si nécessaire.
    • Si fichier existant, ajoute suffixe incrémental _n jusqu'à disponibilité.
    • Crée un TFileStream en mode fmCreate et effectue Client.Get pour écrire directement le flux dans le fichier.
    • Si la requête donne un code 2xx, considère le téléchargement réussi, sinon supprime le fichier partiel.
    • Log des succès ou échecs via SafeLog.

    Sorties : LocalPath contenant le chemin absolu en cas de succès.
    Retour : True si téléchargement réussi.



    # Extraction de liens et ressources

    ExtractLinks(const HTML: string; BaseURL: string; var LinkList: TStringList)
    Rôle : Extraire toutes les URLs d'éléments
    depuis le HTML et les normaliser.
    Comportement :

    Retour : Remplit LinkList avec URLs absolues.

    RemoveURLParams(const URL: string): string
    Rôle : Retire la partie query string après le '?' pour obtenir un chemin plus stable pour déduplication et nom de fichier.
    Retour : URL sans paramètres.

    ExtractMediaSources(const HTML: string; BaseURL: string; var ImageList, DocList, AudioList, VideoList, WebList: TStringList)
    Rôle : Rechercher et récupérer les sources médias et ressources web dans le contenu HTML selon les flags d'extension activés.
    Comportement :

    • Pour chaque catégorie active, exécute une expression régulière adaptée pour détecter src ou href vers extensions ciblées.
    • Nettoie via RemoveURLParams puis NormalizeURL.
    • Ajoute l'URL dans la liste correspondante si elle a une extension valide et n'existe pas déjà.

    Types extraits :

    • Images : .jpg, .jpeg, .png, .gif, .bmp, .webp, .svg.
    • Documents : .pdf, .zip, .rtf, .doc, .docx, .xls, .xlsx, .ppt, .pptx.
    • Audio : .mp3, .wav, .ogg, .m4a, .flac depuis
    • Vidéo : .mp4, .webm, .mov, .ogv, .mkv depuis
    • Web : CSS, JS, HTML, fonts et liens vers fichiers HTML.

    Retour : Les listes passées en paramètre sont remplies.



    # Traitement des ressources trouvées

    ProcessResourceGroup(ResourceList: TStringList; const AcceptExts: array of string; StatusBar: TscStatusBar; ListView: TscListView; Depth: Integer; const DefaultUIType: string; Logging: TscListBox)
    Rôle : Routine générique qui traite une liste de ressources d'une catégorie: vérification d'extension, robots.txt, disponibilité, téléchargement ou marquage.
    Comportement détaillé :

    • Si AcceptExts vide, accepte n'importe quelle extension.
    • Pour chaque URL de la liste :
    • Respecte les signaux d'annulation et de pause.
    • Filtre par extension si nécessaire.
    • Ignore si déjà dans FFoundFiles.
    • Vérifie robots.txt et marque Ignored si bloquée.
    • Appelle IsFileAvailable pour vérifier disponibilité.
    • Si disponible et FAutoDownload true, crée un THTTPClient local, met à jour UI en "Downloading", appelle DownloadFile et met à jour UI selon succès ou échec, ajoute l'URL à FFoundFiles et écrit FFoundFilePath si configuré.
    • Si disponible et FAutoDownload false, marque NotDownloaded et ajoute à FFoundFiles.
    • Si indisponible, si FAutoDownload false alors marque comme broken via MarkBrokenLink, si FAutoDownload true alors marque Broken_Ignored, ajoute à FBrokenLinks et logge.
    • Met à jour FileCount et vérifie FFoundFilesLimit pour annuler exploration si atteint.
    • Met à jour StatusBar panel pour FileCount à la fin et annule si limite atteinte.

    Retour : Aucun. Effets sur dictionnaires, fichiers de rapport, UI et logs.

    ProcessFoundFiles(ImageList, DocList, AudioList, VideoList, WebList: TStringList; StatusBar: TscStatusBar; ListView: TscListView; CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox)
    Rôle : Appel ordonné de ProcessResourceGroup pour chaque type activé par les flags FSearch*.
    Comportement : Pour chaque catégorie activée, appelle ProcessResourceGroup avec la liste et les extensions acceptées prédéfinies.
    Effet : Centralise le traitement des ressources extraites depuis une page.

    MarkBrokenLink(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; Logging: TscListBox)
    Rôle : Incrémenter BrokenCount, mettre à jour UI et enregistrer le lien cassé.
    Comportement :

    • Augmente BrokenCount.
    • Met à jour ListView via SafeUpdateListViewStatus en utilisant la traduction "Broken".
    • Ajoute à FBrokenLinks si non présent et écrit FBrokenFilePath si configuré.
    • Met à jour StatusBar panel pour BrokenCount.
    • Log l'événement.




    # Exploration récursive

    ExploreLinksRecursive(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; CheckList: TscCheckListBox; Depth: Integer; Logging: TscListBox)
    Rôle : Cœur de l'algorithme récursif d'exploration. Gère la visite d'une URL, extraction des liens et ressources, traitement des ressources, et récursion sur les liens extraits.
    Étapes détaillées :

    • Vérifie signaux d'annulation et profondeur restante.
    • Calcule CurrentDepth en fonction de FMaxDepth pour affichage.
    • Normalise URL et skip si déjà visitée.
    • Vérifie robots.txt; si bloquée, incrémente compteur et sort.
    • Attend si en pause via FPauseEvent.
    • Ajoute NormURL à VisitedLinks, incrémente TotalLinks et appelle IncrementLinksTraversed, journalise la visite.
    • Vérifie limites FFoundFilesLimit et FExploreLimit et annule si dépassées.
    • Écrit NormURL dans FVisitedFilePath si configuré.
    • Ajoute une ligne "OnHold" au ListView de façon thread-safe pour indiquer URL en cours.
    • Récupère contenu via GetWebContent.
    • Si contenu vide : si FAutoDownload false marque lien cassé via MarkBrokenLink, sinon marque Broken_Ignored et log.

    Si contenu présent :

    Comportement d'arrêt : Respecte IsCanceled et IsPaused à de multiples points pour arrêt propre et responsive.
    Notes : C'est la routine qui construit l'arbre d'exploration et déclenche le traitement des ressources.

    ExploreLinks(const URL: string; ListView: TscListView; StatusBar: TscStatusBar; CheckList: TscCheckListBox; MaxDepth: Integer; SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile: Boolean; Logging: TscListBox)
    Rôle : Point d'entrée synchronique pour démarrer une exploration complète depuis une URL racine et gérer les rapports sur disque.
    Comportement :

    • Initialise et reset des structures internes, compteurs et états.
    • Prépare les dossiers de rapport sous Documents\Scrapix

    \\Report et le dossier download organisé par type :

    • Initialise FLogFilePath et vide le fichier de log.
    • Configure les chemins FBrokenFilePath, FVisitedFilePath, FFoundFilePath si les options Save* sont cochées et crée des fichiers vides.
    • Détermine RootDomain via TURI.Create(URL).
    • Configure FMaxDepth et log le lancement.
    • Appelle ExploreLinksRecursive pour débuter l'exploration.
    • Après la fin, si SaveBrokenToFile, SaveVisitedToFile ou SaveFoundFilesToFile, consolide les dictionnaires en fichiers placés dans le dossier Documents principal.
    • Journalise la fin et réinitialise les chemins et FRunning, signale FStoppedEvent.

    Retour : Procédure synchrone qui ne retourne qu'à la fin du crawl ou après annulation.
    Effets : Gère création de rapports et dossiers, et coordination globale du crawl.



    # Wrappers thread-safe pour mise à jour UI et log

    Les routines suivantes garantissent que les mises à jour de contrôles VCL se font depuis le thread principal ou via TThread.Queue si appelées depuis d'autres threads. Elles respectent FDisableUIUpdates et vérifient l'état des composants avant modification.


    • UIUpdatesAllowed: Boolean — Renvoie la possibilité d'effectuer des mises à jour UI selon FDisableUIUpdates.
    • SafeScrollListViewToBottom(ListView) — Rend visible la dernière ligne du ListView.
    • SafeSetStatusBarPanel(StatusBar, PanelIndex, Text) — Met à jour un panel du StatusBar identifié.
    • SafeUpdateListViewStatus(ListView, URL, StatusText, Method) — Ajoute ou met à jour une ligne dans ListView colonne Status et Method.
    • SafeUpdateListViewDownloadState(ListView, URL, DownloadState) — Met à jour la colonne état de téléchargement.
    • SafeUpdateListViewInfo(ListView, URL, RespMs, SizeBytes, Depth) — Met à jour colonnes temps de réponse, taille et profondeur.
    • SafeLog(Logging, Msg) — Ajoute une ligne au TscListBox et ajuste le horizon horizontal, écrit aussi de façon thread-safe dans FLogFilePath en utilisant FLogLock.

    Chaque wrapper :

    • Vérifie UIUpdatesAllowed, paramètre nil et état ComponentState et HandleAllocated.
    • Si appel depuis le thread principal, met à jour directement.
    • Sinon, poste une closure via TThread.Queue qui répète les mêmes vérifications avant mise à jour.




    # Filtres et options UI

    ApplyFileTypeFiltersFromCheckList(CheckList: TscCheckListBox)
    Rôle : Traduire les éléments cochés d'une CheckList en activation des flags FSearchImages, FSearchDocuments, FSearchAudio, FSearchVideo, FSearchWeb.
    Comportement :

    • Réinitialise tous les flags à false.
    • Pour chaque item coché, compare le texte et active le flag correspondant.
    • Si libellés non reconnaissables, utilise la position de l'élément pour correspondance par index.

    Effet : Permet de contrôler quels types de ressources sont recherchés.



    # Comportement global et séquence d'opération


  • Créer une instance TScrapix et appeler ConfigureCrawl pour paramétrer timeouts, limites, téléchargement et respect robots.
  • Appeler ExploreLinks avec l'URL racine et options de rapport.
  • ExploreLinks initialise environnements, crée dossiers et fichiers de rapport, puis appelle ExploreLinksRecursive.
  • ExploreLinksRecursive normalise les URL, respecte robots.txt, récupère le HTML via GetWebContent, extrait links et ressources, traite les ressources via ProcessFoundFiles puis descend récursivement sur les liens filtrés par SameDomainOnly et profondeur restante.
  • ProcessResourceGroup vérifie la disponibilité via IsFileAvailable, télécharge si demandé via DownloadFile ou enregistre le fichier trouvé dans FFoundFiles, marque cassés et écrit les rapports.
  • Tout au long du processus, les wrappers Safe* mettent à jour l'UI et SafeLog journalise et écrit dans FLogFilePath de façon thread-safe.
  • Les opérations respectent les signaux Pause et Cancel afin d'arrêter proprement l'exploration et permettre la reprise.




    # Cycle de vie public

    TScrapix s’utilise comme un objet unique pour lancer, contrôler et terminer une session d’exploration. Séquence typique : création, configuration, démarrage (ExploreLinks), contrôles runtime (Pause/Resume/Cancel), attente d’arrêt (WaitForStop) et destruction.

    Étapes concrètes du cycle de vie

    • Create: instancier TScrapix pour initialiser structures internes et valeurs par défaut.
    • ConfigureCrawl: appeler pour fixer timeouts, délai entre requêtes, comportement same-domain, téléchargement automatique, respect robots, limite de fichiers trouvés et limite d’exploration.
    • ApplyFileTypeFiltersFromCheckList: appeler si l’état des filtres de type de fichier provient d’une CheckList UI; active/désactive FSearchImages, FSearchDocuments, FSearchAudio, FSearchVideo, FSearchWeb.
    • ExploreLinks(...): lancer l’exploration de manière synchrone en fournissant l’URL racine, contrôles UI (ListView, StatusBar, CheckList), profondeur maximale, options d’écriture des rapports (SaveBrokenToFile, SaveVisitedToFile, SaveFoundFilesToFile) et logging UI. L’appel retourne seulement lorsque le crawl est fini ou annulé.
    • Pendant l’exploration: contrôler par PauseExploration, ResumeExploration et CancelExploration. Consulter IsPaused, IsCanceled, IsRunning pour l’état courant.
    • WaitForStop: si une attente bloquante de la fin est nécessaire, appeler pour s’assurer que toutes les tâches sont terminées.
    • Destroy: libérer l’objet TScrapix et toutes ses ressources; CancelExploration + WaitForStop sont exécutés dans le destructeur pour garantir arrêt propre.

    États et transitions publics

    • États internes accessibles via IsRunning, IsPaused, IsCanceled.


    Transitions d’état

    • Par défaut après Create, FState = STATE_RUNNING (prêt).
    • ConfigureCrawl n’affecte pas directement FState.
    • ExploreLinks met FRunning = True et TInterlocked.Exchange(FState, STATE_RUNNING).
    • PauseExploration met FState = STATE_PAUSED et reset FPauseEvent. Les routines checkent IsPaused et attendent FPauseEvent.
    • ResumeExploration met FState = STATE_RUNNING et SetEvent sur FPauseEvent.
    • CancelExploration met FState = STATE_CANCEL et SetEvent sur FPauseEvent pour réveiller les waiters. Les boucles respectent IsCanceled et quittent proprement.
    • À la fin d’ExploreLinks (fin normale ou après Cancel), FRunning devient False et FStoppedEvent est SetEvent. WaitForStop retourne alors true.


    Configuration publique détaillée
    ConfigureCrawl(ARequestTimeoutMs, ARequestDelayMs, ASameDomainOnly, AAutoDownload, ARespectRobots, AFoundFilesLimit, AExploreLimit)

    • ARequestTimeoutMs: timeout en millisecondes pour les requêtes HTTP; si ≤ 0, valeur par défaut 30000 ms.
    • ARequestDelayMs: pause entre requêtes en ms; si < 0 devient 0.
    • ASameDomainOnly: true pour n’explorer que les liens du même domaine racine. RootDomain est extrait dans ExploreLinks.
    • AAutoDownload: true pour télécharger automatiquement les ressources trouvées.
    • ARespectRobots: true pour activer la vérification robots.txt avant HEAD/GET et download.
    • AFoundFilesLimit: nombre maximal de fichiers trouvés/téléchargés; borne [1..2000], valeur par défaut 2000.
    • AExploreLimit: nombre maximal de liens parcourus; borne [1..100], valeur par défaut 100.
    • ApplyFileTypeFiltersFromCheckList(CheckList)
    • Active ou désactive les recherches par type en fonction des éléments cochés de la CheckList UI. Si CheckList est nil, laisse tous les flags à false. Utilise libellés (Image, Document, Audio, Vidéo, Web Document) ou l’index comme fallback.


    Propriétés publiques

    • DisableUIUpdates: booléen pour désactiver les mises à jour UI thread-safe; utile pour tests/performance.


    Entrée/sortie et rapports

    • ExploreLinks crée (si demandé) ces fichiers dans Documents\Scrapix
    • \\Report : BrokenLinks.txt, VisitedLinks.txt, FoundFiles.txt.. Le logger écrit Logging.txt dans Report\Logging. Les chemins sont initialisés au début du crawl et vidés à la fin.
    • FFoundFiles, FBrokenLinks et VisitedLinks sont tenus en mémoire pendant la session et écrits sur disque à la fin si les options Save* sont activées.
    • DownloadFolder est créé sous Documents\Scrapix
    • \\download et les fichiers téléchargés sont organisés en sous-dossiers par type (Image, Document, Audio, Vidéo, Ressources Web/JS/CSS/HTML/Fonts, Autre).


    Contrôle d’exécution en pratique

    • Toujours appeler ConfigureCrawl avant ExploreLinks pour fixer timeouts et limites.
    • Pour reprendre un crawl interrompu, détacher ou recréer TScrapix, reconfigurer et relancer ExploreLinks; l’état mémoire interne (VisitedLinks, FFoundFiles) n’est pas persistant entre instances.
    • Utiliser PauseExploration/ResumeExploration pour interruptions courtes; CancelExploration pour arrêter définitivement. Appeler WaitForStop après CancelExploration si on doit attendre la complétion avant Destroy.
    • Pour gros crawls, réduire UIUpdates ou définir DisableUIUpdates à true pour diminuer l’impact UI.
    • Vérifier FRespectRobots avant d’activer AAutoDownload pour respecter les sites.




    # Exemple de trace d’exécution pour une page racine contenant 2 liens et 3 images

    Contexte
    Instance TScrapix configurée avec :

    • RequestTimeoutMs=30000
    • RequestDelayMs=0
    • ASameDomainOnly=True
    • AAutoDownload=False
    • ARespectRobots=True
    • FFoundFilesLimit=2000
    • FExploreLimit=100


    Page racine http://example.com/index.html
    contient 2 liens internes (/page1.html, /page2.html) et 3 images (/img1.jpg, /img2.png, /img3.svg) référencées dans l’HTML.

    État initial

    • VisitedLinks empty; FFoundFiles empty; FBrokenLinks empty.
    • FileCount=0; BrokenCount=0; TotalLinks=0; FLinksTraversed=0; FRobotsBlocked=0.
    • DownloadFolder et ReportFolder non créés jusqu’au début d’ExploreLinks.
    • UI: ListView vide; StatusBar panels vides; Logging vide.


    Appel ExploreLinks(url=http://example.com/index.html, MaxDepth=2, Save*: true)

    • ExploreLinks initialise dossiers sous Documents\Scrapix\example.com\Report et Logging\Logging.txt, crée DownloadFolder et sous-dossiers, initialise FVisitedFilePath, FBrokenFilePath, FFoundFilePath, met FRunning := True et logge "Starting crawl http://example.com/index.html depth 2" dans Logging.
    • RootDomain extrait "example.com".
    • ExploreLinks appelle ExploreLinksRecursive(url, Depth=2).


    ExploreLinksRecursive étape pour la racine

  • NormalizeURL normalise http://example.com/index.html en http://example.com/index.html..
  • IsAllowedByRobots vérifie robots.txt pour example.com, ajoute règles au cache si nécessaire; résultat true (autorisé).
  • VisitedLinks.Add("http://example.com/index.html"), TotalLinks := 1, IncrementLinksTraversed incrémente FLinksTraversed := 1 et met à jour StatusBar panel correspondant.
  • UI : ListView ajoute une ligne pour http://example.com/index.html avec colonne Status = "OnHold" et colonne Depth = "1".
  • GetWebContent effectue GET sur la racine, mesure elapsed ms, récupère Content-Type "text/html" et body HTML.
  • SafeUpdateListViewStatus remplace "OnHold" par "200 text/html" et SafeUpdateListViewInfo met RespMs et taille. Logging ajoute "GET http://example.com/index.html -> 200 text/html (X Ko)".
  • ExtractLinks trouve deux href normalisés : http://example.com/page1.html et http://example.com/page2.html..
  • ExtractMediaSources trouve trois images normalisées : http://example.com/img1.jpg, /img2.png, /img3.svg.
  • ProcessFoundFiles est appelé pour images ; AAutoDownload=false donc chaque image est marqué NotDownloaded et ajouté à FFoundFiles. Pour chaque image : FileCount incremente de 1. UI : pour chaque image SafeUpdateListViewDownloadState = "NotDownloaded" et SafeUpdateListViewStatus = statut HEAD info si IsFileAvailable a été appelé ou bien status initial. Logging ajoute "Found not downloaded" pour chaque image.
  • Après traitement des ressources : FileCount = 3, StatusBar panel fichiers mis à jour à "3".


    Récursion sur les deux liens
    Pour chaque lien (page1, page2) ExploreLinksRecursive est appelé avec Depth=1.
    Pour page1 :

  • NormalizeURL -> http://example.com/page1.html..
  • IsAllowedByRobots true.
  • VisitedLinks.Add(page1), TotalLinks := 2, FLinksTraversed := 2, StatusBar mis à jour.
  • UI ajoute ligne OnHold Depth=2.
  • GetWebContent GET page1 retourne 200 text/html avec HTML vide de ressources (hypothèse).
  • Aucun média trouvé ; ProcessFoundFiles ne fait rien. Logging "GET page1 -> 200 text/html".
  • Aucun lien supplémentaire ; fin de la branche page1.


    Pour page2 :

  • NormalizeURL -> http://example.com/page2.html..
  • IsAllowedByRobots true.
  • VisitedLinks.Add(page2), TotalLinks := 3, FLinksTraversed := 3, StatusBar mis à jour.
  • UI ajoute ligne OnHold Depth=2.
  • GetWebContent GET page2 retourne 404 (hypothèse d’un lien cassé).
  • SafeUpdateListViewStatus affiche "404 text/html" et SafeUpdateListViewInfo met RespMs et taille. Logging "GET page2 -> 404 text/html (n/a)".
  • HTMLContent vide ou non utile ; IsAutoDownload=false déclenche MarkBrokenLink(page2) qui : BrokenCount := 1, ajoute page2 à FBrokenLinks, écrit page2 dans BrokenLinks.txt, UI marque statut "Broken", StatusBar panel broken mis à jour à "1", Logging ajoute "Broken link: http://example.com/page2.html".


    Fin de l’exploration et écriture des rapports

  • Après retour de toutes les branches, ExploreLinks termine la récursion.
  • ExploreLinks écrit VisitedLinks.txt contenant les 3 URL visitées (index, page1, page2).
  • ExploreLinks écrit FoundFiles.txt contenant les 3 images.
  • ExploreLinks a déjà écrit BrokenLinks.txt contenant page2.
  • Logging final ajoute "Finished: files=3 broken=1 totalLinks=3".
  • FRunning := False et FStoppedEvent.SetEvent. UI StatusBar panels affichent FileCount=3, BrokenCount=1, RobotsBlocked=0, LinksTraversed=3.


    Exemple concret de lignes de log séquentielles




    TScrapix offre une implémentation complète d'un crawler synchronique orienté application VCL qui normalise les URL, applique robots.txt, extrait liens et ressources, vérifie disponibilité via HEAD/GET, télécharge les ressources en les classant par type, maintient des rapports et met à jour de façon thread-safe l'interface et les logs.
    Les primitives d'arrêt, pause et limites garantissent un fonctionnement contrôlé dans des explorations de taille limitée.



    Compatibilité générale
    TScrapix cible les environnements VCL Windows et nécessite des fonctionnalités RTL/CiE présentes dans les versions modernes de Delphi. En pratique, l’unité est utilisable avec Delphi récents (XE8 et ultérieurs) jusqu’aux versions récentes de RAD Studio

    Unités et fonctionnalités minimales requises

    • System.Net.HttpClient et System.Net.URLClient (THTTPClient, IHTTPResponse) pour les requêtes HTTP.
    • System.Threading (TTask, TThread) pour exécution asynchrone et Sleep non bloquant.
    • System.Generics.Collections (TDictionary), System.SyncObjs (TEvent, TCriticalSection).
    • System.Types / System.SysUtils / System.Classes / System.IOUtils (TURI, TPath, TFile, TDirectory, TStringList).
    • System.RegularExpressions (TRegEx).
    • Vcl controls (TListView/TStatusBar/TCheckListBox replacements utilisés ici : TscListView, TscStatusBar, TscCheckListBox, TscListBox — fournis par StyleControls ou à remplacer par composants VCL natifs si nécessaire).




    Développé par : XeGregory
    IDE : Embarcadero Delphi 11
    Composants utilisés Vcl : StyleControls VCL
  • Vous avez lu gratuitement 2 951 articles depuis plus d'un an.
    Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

    Une erreur dans cette actualité ? Signalez-nous-la !