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.


Scrapix.Core.pas
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
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<string, Boolean>; 
    FFoundFiles: TDictionary<string, Boolean>; 
    FBrokenLinks: TDictionary<string, Boolean>; 
    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<string, TStringList>; 
    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 <a href="..."> 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[PanelIndex].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[PanelIndex].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[I]; 
      if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then 
      begin 
        while Item.SubItems.Count < 6 do 
          Item.SubItems.Add(''); 
        Item.SubItems[0] := sStatus; 
        Item.SubItems[5] := sMethod; 
        SafeScrollListViewToBottom(ListView); 
        Exit; 
      end; 
    end; 
 
    Item := ListView.Items.Add; 
    Item.Caption := sURL; 
    while Item.SubItems.Count < 6 do 
      Item.SubItems.Add(''); 
    Item.SubItems[0] := sStatus; 
    Item.SubItems[5] := 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[I2]; 
          if SameText(It.Caption, sURL) or SameText(It.Caption, URL) then 
          begin 
            while It.SubItems.Count < 6 do 
              It.SubItems.Add(''); 
            It.SubItems[0] := sStatus; 
            It.SubItems[5] := sMethod; 
            SafeScrollListViewToBottom(ListView); 
            Exit; 
          end; 
        end; 
        It := ListView.Items.Add; 
        It.Caption := sURL; 
        while It.SubItems.Count < 6 do 
          It.SubItems.Add(''); 
        It.SubItems[0] := sStatus; 
        It.SubItems[5] := 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[I]; 
      if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then 
      begin 
        while Item.SubItems.Count < 6 do 
          Item.SubItems.Add(''); 
        Item.SubItems[1] := sState; 
        SafeScrollListViewToBottom(ListView); 
        Exit; 
      end; 
    end; 
    Item := ListView.Items.Add; 
    Item.Caption := sURL; 
    while Item.SubItems.Count < 6 do 
      Item.SubItems.Add(''); 
    Item.SubItems[1] := 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[I2]; 
          if SameText(It.Caption, sURL) or SameText(It.Caption, URL) then 
          begin 
            while It.SubItems.Count < 6 do 
              It.SubItems.Add(''); 
            It.SubItems[1] := sState; 
            SafeScrollListViewToBottom(ListView); 
            Exit; 
          end; 
        end; 
        It := ListView.Items.Add; 
        It.Caption := sURL; 
        while It.SubItems.Count < 6 do 
          It.SubItems.Add(''); 
        It.SubItems[1] := 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[I]; 
      if SameText(Item.Caption, sURL) or SameText(Item.Caption, URL) then 
      begin 
        while Item.SubItems.Count < 6 do 
          Item.SubItems.Add(''); 
        Item.SubItems[2] := sRespMs; 
        Item.SubItems[3] := sSizeLocal; 
        if sDepth <> '' then 
          Item.SubItems[4] := sDepth; 
        SafeScrollListViewToBottom(ListView); 
        Exit; 
      end; 
    end; 
    Item := ListView.Items.Add; 
    Item.Caption := sURL; 
    while Item.SubItems.Count < 6 do 
      Item.SubItems.Add(''); 
    Item.SubItems[2] := sRespMs; 
    Item.SubItems[3] := sSizeLocal; 
    if sDepth <> '' then 
      Item.SubItems[4] := 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[1] <> '/') 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[I].Name, HeaderName) then 
      Exit(Resp.Headers[I].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', [Bytes]) 
  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<string>; 
  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[I]); 
      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[0], 'Scrapix') or 
          SameText(CurrentAgents[0], '*'); 
        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<string, TStringList>.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[Key]; 
    for I := 0 to Rules.Count - 1 do 
    begin 
      DisallowPath := Rules[I]; 
      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<string, Boolean>.Create; 
  FBrokenLinks := TDictionary<string, Boolean>.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[1], 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('<a\s+(?:[^>]*?\s+)?href="([^"]*)"', [roIgnoreCase]); 
  Match := Regex.Match(HTML); 
  while Match.Success do 
  begin 
    RawLink := Match.Groups[1].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 
      ('<img\s+[^>]*src="([^"]+\.(jpg|jpeg|png|gif|bmp|webp|svg))"', 
      [roIgnoreCase]); 
    Match := RegexImg.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(ImageList, Source); 
      Match := Match.NextMatch; 
    end; 
  end; 
 
  if FSearchDocuments then 
  begin 
    RegexDoc := TRegEx.Create 
      ('<a\s+[^>]*href="([^"]+\.(pdf|zip|rtf|doc|docx|xls|xlsx|ppt|pptx))"', 
      [roIgnoreCase]); 
    Match := RegexDoc.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(DocList, Source); 
      Match := Match.NextMatch; 
    end; 
  end; 
 
  if FSearchAudio then 
  begin 
    RegexAudio := TRegEx.Create 
      ('(?:<audio\b[^>]*>.*?<source[^>]*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[1].Value <> '' then 
        RawSource := Match.Groups[1].Value 
      else 
        RawSource := Match.Groups[3].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(AudioList, Source); 
      Match := Match.NextMatch; 
    end; 
  end; 
 
  if FSearchVideo then 
  begin 
    RegexVideo := TRegEx.Create 
      ('(?:<video\b[^>]*>.*?<source[^>]*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[1].Value <> '' then 
        RawSource := Match.Groups[1].Value 
      else 
        RawSource := Match.Groups[3].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(VideoList, Source); 
      Match := Match.NextMatch; 
    end; 
  end; 
 
  if FSearchWeb then 
  begin 
    RegexCss := TRegEx.Create('<link\s+[^>]*href="([^"]+\.(css))"[^>]*>', 
      [roIgnoreCase]); 
    Match := RegexCss.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(WebList, Source); 
      Match := Match.NextMatch; 
    end; 
 
    RegexJs := TRegEx.Create('<script\s+[^>]*src="([^"]+\.js)"', 
      [roIgnoreCase]); 
    Match := RegexJs.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(WebList, Source); 
      Match := Match.NextMatch; 
    end; 
 
    RegexHtmlLink := TRegEx.Create('<a\s+[^>]*href="([^"]+\.(html|htm))"', 
      [roIgnoreCase]); 
    Match := RegexHtmlLink.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].Value; 
      Source := RemoveURLParams(RawSource); 
      Source := NormalizeURL(BaseURL, Source); 
      AddIfNew(WebList, Source); 
      Match := Match.NextMatch; 
    end; 
 
    RegexFont := TRegEx.Create 
      ('(?:href|src)=["'']([^"''\)]+?\.(woff2?|ttf|otf))["'']', [roIgnoreCase]); 
    Match := RegexFont.Match(HTML); 
    while Match.Success do 
    begin 
      RawSource := Match.Groups[1].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'), [URL])); 
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'), [NormURL])); 
    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[0].Name := 'Range'; 
        Headers[0].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'), 
            [NormURL])); 
        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'), [NormURL])); 
    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'), [NormURL])); 
      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[I]; 
 
    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'), [URL])); 
      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'), [URL])); 
      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'), [URL])); 
      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'), [NormURL])); 
    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[0] := GetTranslate('OnHold'); 
      Item.SubItems[4] := 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[0] := GetTranslate('OnHold'); 
          Item.SubItems[4] := 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'), [NormURL])); 
      MarkBrokenLink(NormURL, ListView, StatusBar, Logging) 
    end 
    else 
    begin 
      SafeUpdateListViewStatus(ListView, NormURL, 
        GetTranslate('Broken_Ignored')); 
      if Assigned(Logging) then 
        SafeLog(Logging, Format(GetTranslate('NoContentLog'), [NormURL])); 
    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[I]) then 
          Continue; 
      end; 
 
      ExploreLinksRecursive(Links[I], 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<string, Boolean>.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[Idx] then 
      Continue; 
    Txt := Trim(CheckList.Items[Idx]); 
    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
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
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<string, TArray<string>>; // 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<string>; 
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[0]; // français 
        lgEnglish: 
          if Length(Arr) > 1 then 
            Result := Arr[1]; // 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<string>; 
begin 
  if Key.IsEmpty then 
    Exit; 
  LangLock.Enter; 
  try 
    SetLength(Arr, 2); 
    Arr[0] := FrenchText; 
    Arr[1] := 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
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
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[I].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[0].Text := GetTranslate('Panel0'); 
  StatusBar.Panels[2].Text := GetTranslate('Panel2'); 
  StatusBar.Panels[4].Text := GetTranslate('Panel4'); 
  StatusBar.Panels[6].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[0] := True; 
    Checked[1] := True; 
    Checked[2] := True; 
    Checked[3] := True; 
    Checked[4] := 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[1].Text := '0'; 
  StatusBar.Panels[3].Text := '0'; 
  StatusBar.Panels[5].Text := '0'; 
  StatusBar.Panels[7].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






# Champs et structures importantes

  • VisitedLinks : TDictionary<string, Boolean> — Dictionnaire des URL déjà visitées pour éviter les doublons pendant l'exploration.
  • FFoundFiles : TDictionary<string, Boolean> — Dictionnaire des fichiers repérés ou téléchargés.
  • FBrokenLinks : TDictionary<string, Boolean> — 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<string, TStringList> — 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 <a href="..."> depuis le HTML et les normaliser.
Comportement :
  • Utilise une expression régulière insensible à la casse pour trouver href dans les balises <a>.
  • Pour chaque href non vide, normalise via NormalizeURL et ajoute à LinkList si non visité et non déjà présent.

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 <audio> ou href.
  • Vidéo : .mp4, .webm, .mov, .ogv, .mkv depuis <video> ou href.
  • 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 :
  • Crée listes temporaires Links, Images, Docs, Audio, Video, Webs.
  • ExtractLinks pour récupérer <a href>.
  • Si besoin, ExtractMediaSources pour extraire les ressources selon flags.
  • Appelle ProcessFoundFiles pour traiter et éventuellement télécharger ces ressources.
  • Vérifie à nouveau signaux d'annulation et limites.
  • Parcourt chaque lien extrait et, si SameDomainOnly est true, filtre via IsSameDomain en se basant sur RootDomain, puis appelle ExploreLinksRecursive récursivement avec Depth - 1.
  • Met à jour panels StatusBar avec compteurs.
  • Libère toutes les listes temporaires.

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

\[rootdomain]\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

  1. Créer une instance TScrapix et appeler ConfigureCrawl pour paramétrer timeouts, limites, téléchargement et respect robots.
  2. Appeler ExploreLinks avec l'URL racine et options de rapport.
  3. ExploreLinks initialise environnements, crée dossiers et fichiers de rapport, puis appelle ExploreLinksRecursive.
  4. 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.
  5. 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.
  6. Tout au long du processus, les wrappers Safe* mettent à jour l'UI et SafeLog journalise et écrit dans FLogFilePath de façon thread-safe.
  7. 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
  • \[rootdomain]\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
  • \[rootdomain]\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
  1. NormalizeURL normalise http://example.com/index.html en http://example.com/index.html..
  2. IsAllowedByRobots vérifie robots.txt pour example.com, ajoute règles au cache si nécessaire; résultat true (autorisé).
  3. VisitedLinks.Add("http://example.com/index.html"), TotalLinks := 1, IncrementLinksTraversed incrémente FLinksTraversed := 1 et met à jour StatusBar panel correspondant.
  4. UI : ListView ajoute une ligne pour http://example.com/index.html avec colonne Status = "OnHold" et colonne Depth = "1".
  5. GetWebContent effectue GET sur la racine, mesure elapsed ms, récupère Content-Type "text/html" et body HTML.
  6. 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)".
  7. ExtractLinks trouve deux href normalisés : http://example.com/page1.html et http://example.com/page2.html..
  8. ExtractMediaSources trouve trois images normalisées : http://example.com/img1.jpg, /img2.png, /img3.svg.
  9. 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.
  10. 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 :
  1. NormalizeURL -> http://example.com/page1.html..
  2. IsAllowedByRobots true.
  3. VisitedLinks.Add(page1), TotalLinks := 2, FLinksTraversed := 2, StatusBar mis à jour.
  4. UI ajoute ligne OnHold Depth=2.
  5. GetWebContent GET page1 retourne 200 text/html avec HTML vide de ressources (hypothèse).
  6. Aucun média trouvé ; ProcessFoundFiles ne fait rien. Logging "GET page1 -> 200 text/html".
  7. Aucun lien supplémentaire ; fin de la branche page1.


Pour page2 :
  1. NormalizeURL -> http://example.com/page2.html..
  2. IsAllowedByRobots true.
  3. VisitedLinks.Add(page2), TotalLinks := 3, FLinksTraversed := 3, StatusBar mis à jour.
  4. UI ajoute ligne OnHold Depth=2.
  5. GetWebContent GET page2 retourne 404 (hypothèse d’un lien cassé).
  6. SafeUpdateListViewStatus affiche "404 text/html" et SafeUpdateListViewInfo met RespMs et taille. Logging "GET page2 -> 404 text/html (n/a)".
  7. 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
  1. Après retour de toutes les branches, ExploreLinks termine la récursion.
  2. ExploreLinks écrit VisitedLinks.txt contenant les 3 URL visitées (index, page1, page2).
  3. ExploreLinks écrit FoundFiles.txt contenant les 3 images.
  4. ExploreLinks a déjà écrit BrokenLinks.txt contenant page2.
  5. Logging final ajoute "Finished: files=3 broken=1 totalLinks=3".
  6. 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 1 446 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 !