青

一言

基于 Go 的站点下载工具和关于 GenAI 的思考

基于 Go 的站点下载工具和关于 GenAI 的思考

基于 Go 的站点下载工具和对 GenAI 的一些思考

在 Claude 的帮助下,我完成了一个基于 Go 的站点下载工具,全程自己没有写一行代码,仅仅依靠告诉 AI 我想要做什么,然后把获得的程序编译执行,告知 AI 需要优化和修改的地方就可以完整实现的可以下载指定站点的所有可下载资源的工具,久违地更新这篇文章,同时留下一些对 GenAI 的思考。

顺便一说,有了 AI 的帮助,之前搁置的一些问题也有了一定的眉目,也许几天之后就会有其他后续更新。

这是 AI 生成的内容:

GenAI(生成式人工智能)是指一种能够通过学习大量数据,生成与之相关的内容或信息的人工智能技术。这类技术的核心在于生成模型,能够根据输入的提示,生成文本、图像、音频、视频等各种形式的内容。

举个例子,像我这样的聊天机器人就是一种生成式AI,可以根据你给出的提示生成合理的回复。类似的技术也可以用来生成文章、代码、艺术作品,甚至模拟对话或创造虚拟人物。

生成式AI通常依赖深度学习模型,如变换器(Transformer)和生成对抗网络(GAN),它们能够通过大量的训练数据,不断提高生成内容的质量和准确性。

生成式人工智能(GenAI)

生成性人工智能(Generative AI,通常简称为 GenAI)如今正在快速改变着世界,在其出现之前,Transformer 曾一度是学习的主要内容,还没毕业的时候,记得当时其本属于自然语言处理领域的技术,用来处理图片取得了较好的评价成绩,甚是不解,甚至当时有想过用在 Transformer 的分析和应用上进行一些研究完 成学业,但毕竟不是这方面的料,还是选择了“好毕业”的内容。

无论当时还是现在的我都无法理解,为什么自然语言处理的方法运用到经过离散量化保存的数字图像上会有良好的效果,甚至一度以为是数据集或者刻意针对测试集进行训练的结果,就像无论当时,现在还是未来很长一段时间得我,也无法理解为什么 GenAI 会有如此强大的能力,特别是在 GPT 刚刚爆火的那段时间,认识到其背后的算法和训练逻辑和平时接触到的其他深度、强化学习方面的内容并没有明显突破和创新,仅仅是数据和参数规模巨量增加,就实现了“说人话”这看似简单实则极其复杂的目标,就像是在看一个人在短时间内完成了一个很复杂的题目,看不懂过程,答案近乎正确,在非常多学科和领域下,都用这一个看不懂的过程获得了接近正确答案的解答,确实让我无法理解。

也许是生活和教育环境的影响,对于这样无法理解的事,会产生一种难以言喻的情绪,不是恐惧,也不是好奇,也许是更偏向于敬畏的情绪,就像仰观宇宙之大,意识到地球和自己是多么的渺小,也像思考“我是谁”,“我从哪里来”这样的哲学问题一般,虽然冥冥间知道 GenAI 肯定有着理论和科学的依据,但就现在我的认知可以下次论断,在我有生之年可能也无法理解其背后的原理和逻辑。

但是无法理解并不代表无法使用,就如量子物理一般,虽然无法解释和理解其原因,大部分理论甚至是建立在一系列的假说之上,但其只要符合现实的观测,就可以被应用,改变生活,GenAI 也是如此。

前些日子 DeepSeek 成为了热点,一方面其有着较为良好的表现,为回答增加生成“思考”过程环节来提高准确和可靠,是一个非常有趣的想法,将问题进行拆分,对于需要思考的任务和不过多依赖思考条件反射就可以直接完成的任务进行拆分,对于需要思考的任务,增加生成“思考过程”的环节,从而有效提高了复杂问题的解答准确性,这是一个非常有趣的想法,也是一个非常好的应用。其产生的热度我也能完全理解,毕竟是第一次,看到了周围人无需特别的门槛就可以使用的 GenAI。虽然相比 GPT、Claude 等优秀 GenAI 还有着一定差距,“思考过程”这一有点也可以通过引导其他模型进行思考或者等待其他模型增加类似的功能来追赶,从技术上来说也是一时优势。 但就对经济和政治的是巨大的,在此简单提过,不进行详细讨论。

有人问,GenAI 如今这么强大,在各行各业有了一定的应用,是否会担心它导致失业或更大的问题?就我看来,从某些方面说,GenAI 解放了生产力,作为一个工具,改变了生产方式和一部分生产关系,这是更接近资源丰富的理想状态,但也不否认,在短期内也带来了恐惧和不确定性,不过这是一种必然的过程,也是一种必然的结果,如今给世界带来改变的是 GenAI,哪怕不是 GenAI 以当今信息传递和更新的速度,也会有其他技术和工具来改变世界。就如有人说“尽人事,听天命”,我想我们能做的就是努力的去适应和利用这些新技术,寻找到新的生存下去的方式,积极面对当前和未来发生的变化。能决定自己未来的总是自己,故步自封,和最新的生产工具脱节太远,才是被淘汰的原因。

基于 Go 的站点下载工具

完整程序
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
package main

import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"sync"
"syscall"
"time"

"golang.org/x/net/html"
)

// Configuration for the crawler
type Config struct {
BaseURL string
OutputDir string
Concurrency int
Timeout time.Duration
AllowedDomains []string
URLReplaceRules []ReplaceRule
SkipTLS bool
UserAgent string
QueueFile string // 新增:保存队列的文件路径
SaveInterval time.Duration // 新增:定期保存队列的时间间隔
}

// Rule for replacing URLs
type ReplaceRule struct {
Pattern *regexp.Regexp
Replacement string
}

// CrawlState 存储爬虫状态
type CrawlState struct {
Queue []string `json:"queue"`
VisitedURLs map[string]bool `json:"visited_urls"`
BaseURL string `json:"base_url"`
BaseURLPath string `json:"base_url_path"`
Timestamp time.Time `json:"timestamp"`
}

// Crawler struct
type Crawler struct {
config Config
visitedURLs map[string]bool
queue []string
client *http.Client
mutex sync.Mutex
wg sync.WaitGroup
baseURLPath string // 存储基础URL的路径,用于防止爬取父路径
shutdown chan bool // 新增:用于通知goroutine关闭的通道
stopped bool // 新增:标记爬虫是否已停止
}

// Create a new crawler
func NewCrawler(config Config) *Crawler {
// Initialize HTTP client with timeout and TLS configuration
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLS},
}

client := &http.Client{
Timeout: config.Timeout,
Transport: transport,
}

// 解析基础URL的路径,用于后续比较
baseURL, _ := url.Parse(config.BaseURL)
basePath := baseURL.Path
if !strings.HasSuffix(basePath, "/") {
// 确保路径以"/"结尾,方便后续处理
basePath = basePath + "/"
}

return &Crawler{
config: config,
visitedURLs: make(map[string]bool),
client: client,
baseURLPath: basePath,
shutdown: make(chan bool),
stopped: false,
}
}

// 新增:从文件加载之前的爬虫状态
func (c *Crawler) LoadState() error {
if c.config.QueueFile == "" {
return nil // 如果没有指定队列文件,则不加载
}

// 检查文件是否存在
if _, err := os.Stat(c.config.QueueFile); os.IsNotExist(err) {
fmt.Println("Queue file does not exist, starting fresh crawl")
return nil
}

// 读取文件内容
data, err := ioutil.ReadFile(c.config.QueueFile)
if err != nil {
return fmt.Errorf("failed to read queue file: %v", err)
}

// 解析JSON
var state CrawlState
err = json.Unmarshal(data, &state)
if err != nil {
return fmt.Errorf("failed to parse queue file: %v", err)
}

// 验证基础URL是否一致
if state.BaseURL != c.config.BaseURL {
fmt.Printf("Warning: Base URL in queue file (%s) differs from current base URL (%s)\n",
state.BaseURL, c.config.BaseURL)
// 可以选择继续或返回错误
}

// 恢复状态
c.mutex.Lock()
c.queue = state.Queue
c.visitedURLs = state.VisitedURLs
c.mutex.Unlock()

fmt.Printf("Loaded %d URLs in queue and %d visited URLs from %s\n",
len(state.Queue), len(state.VisitedURLs), c.config.QueueFile)

return nil
}

// 新增:保存爬虫状态到文件
func (c *Crawler) SaveState() error {
if c.config.QueueFile == "" {
return nil // 如果没有指定队列文件,则不保存
}

c.mutex.Lock()
state := CrawlState{
Queue: c.queue,
VisitedURLs: c.visitedURLs,
BaseURL: c.config.BaseURL,
BaseURLPath: c.baseURLPath,
Timestamp: time.Now(),
}
c.mutex.Unlock()

// 将状态转换为JSON
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal state: %v", err)
}

// 确保目录存在
dir := filepath.Dir(c.config.QueueFile)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory for queue file: %v", err)
}
}

// 写入文件
if err := ioutil.WriteFile(c.config.QueueFile, data, 0644); err != nil {
return fmt.Errorf("failed to write queue file: %v", err)
}

fmt.Printf("Saved %d URLs in queue and %d visited URLs to %s\n",
len(state.Queue), len(state.VisitedURLs), c.config.QueueFile)

return nil
}

// 新增:定期保存状态的goroutine
func (c *Crawler) autoSaveWorker() {
ticker := time.NewTicker(c.config.SaveInterval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
// 定期保存状态
if err := c.SaveState(); err != nil {
fmt.Printf("Error auto-saving state: %v\n", err)
}
case <-c.shutdown:
// 收到关闭信号
return
}
}
}

// Start crawling process
func (c *Crawler) Start() error {
// 尝试加载之前的状态
if err := c.LoadState(); err != nil {
fmt.Printf("Warning: Failed to load previous state: %v\n", err)
}

baseURL, err := url.Parse(c.config.BaseURL)
if err != nil {
return fmt.Errorf("invalid base URL: %v", err)
}

// Create output directory
err = os.MkdirAll(c.config.OutputDir, 0755)
if err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
}

// 如果队列为空,添加基础URL
c.mutex.Lock()
if len(c.queue) == 0 {
c.queue = append(c.queue, baseURL.String())
c.visitedURLs[baseURL.String()] = true
}
c.mutex.Unlock()

// 启动自动保存goroutine
if c.config.SaveInterval > 0 {
go c.autoSaveWorker()
}

// 设置优雅关闭的处理
setupSignalHandler(c)

// Create worker goroutines
for i := 0; i < c.config.Concurrency; i++ {
c.wg.Add(1)
go c.worker()
}

c.wg.Wait()

// 停止自动保存goroutine
if c.config.SaveInterval > 0 {
c.shutdown <- true
}

// 保存最终状态
c.stopped = true
return c.SaveState()
}

// Worker goroutine
func (c *Crawler) worker() {
defer c.wg.Done()

for {
// 检查是否应该停止
c.mutex.Lock()
if c.stopped {
c.mutex.Unlock()
return
}
c.mutex.Unlock()

// Get next URL to process
targetURL := c.getNextURL()
if targetURL == "" {
return
}

// Process the URL
c.processURL(targetURL)
}
}

// Get next URL from queue (thread-safe)
func (c *Crawler) getNextURL() string {
c.mutex.Lock()
defer c.mutex.Unlock()

if len(c.queue) == 0 {
return ""
}

// Get next URL and remove it from queue
nextURL := c.queue[0]
c.queue = c.queue[1:]
return nextURL
}

// 检查URL是否是父路径
func (c *Crawler) isParentPath(targetURL string) bool {
parsedURL, err := url.Parse(targetURL)
if err != nil {
return false
}

// 获取目标URL的路径
urlPath := parsedURL.Path
if !strings.HasSuffix(urlPath, "/") {
urlPath = urlPath + "/"
}

// 检查目标URL的路径是否是基础URL路径的父路径
baseURL, _ := url.Parse(c.config.BaseURL)
if parsedURL.Host == baseURL.Host && len(urlPath) < len(c.baseURLPath) {
// 如果在同一域名下,且路径长度更短,可能是父路径
return true
}

return false
}

// Add URL to queue (thread-safe)
func (c *Crawler) addURL(targetURL string) {
parsedURL, err := url.Parse(targetURL)
if err != nil {
return
}

// Make sure URL is absolute
baseURL, _ := url.Parse(c.config.BaseURL)
absoluteURL := baseURL.ResolveReference(parsedURL).String()

// Clean the URL - remove fragments
cleanURL, err := url.Parse(absoluteURL)
if err != nil {
return
}
cleanURL.Fragment = ""
absoluteURL = cleanURL.String()

// Skip if URL is already visited
c.mutex.Lock()
defer c.mutex.Unlock()

if c.visitedURLs[absoluteURL] {
return
}

// Check if URL is in allowed domains
if !c.isAllowedDomain(absoluteURL) {
return
}

// Mark URL as visited and add to queue
c.visitedURLs[absoluteURL] = true
c.queue = append(c.queue, absoluteURL)
fmt.Printf("Added to queue: %s\n", absoluteURL)
}

// Check if URL is in allowed domains
func (c *Crawler) isAllowedDomain(targetURL string) bool {
parsedURL, err := url.Parse(targetURL)
if err != nil {
return false
}

// If no domains specified, only allow the base domain
if len(c.config.AllowedDomains) == 0 {
baseURL, _ := url.Parse(c.config.BaseURL)
return parsedURL.Host == baseURL.Host
}

// Check against allowed domains
for _, domain := range c.config.AllowedDomains {
if parsedURL.Host == domain {
return true
}
}

return false
}

// 新增:设置关闭爬虫的方法
func (c *Crawler) Stop() {
c.mutex.Lock()
c.stopped = true
c.mutex.Unlock()

// 保存状态
if err := c.SaveState(); err != nil {
fmt.Printf("Error saving state during shutdown: %v\n", err)
}
}

// Process a URL
func (c *Crawler) processURL(targetURL string) {
// 因为一些原因,这里暂不公开该函数,请尝试自行完成,如果无法完成,请放弃尝试
}

// Parse links from HTML response
func (c *Crawler) parseLinksFromResponse(sourceURL, filePath string) {
// Read the saved file
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", filePath, err)
return
}
defer file.Close()

// Parse HTML
doc, err := html.Parse(file)
if err != nil {
fmt.Printf("Error parsing HTML from %s: %v\n", filePath, err)
return
}

// Parse base URL of the document
baseURL, _ := url.Parse(sourceURL)

// 检查HTML中是否有base标签
baseHref := findBaseHref(doc)
if baseHref != "" {
// 如果有base标签,更新baseURL
parsedBase, err := url.Parse(baseHref)
if err == nil {
baseURL = baseURL.ResolveReference(parsedBase)
}
}

// Extract links
var links []string
extractLinks(doc, &links)

// Process links
for _, link := range links {
// Skip non-HTTP/HTTPS links
if strings.HasPrefix(link, "mailto:") || strings.HasPrefix(link, "tel:") ||
strings.HasPrefix(link, "javascript:") || strings.HasPrefix(link, "#") {
continue
}

// Resolve URL
parsedURL, err := url.Parse(link)
if err != nil {
continue
}

absoluteURL := baseURL.ResolveReference(parsedURL).String()
c.addURL(absoluteURL)
}
}

// 查找HTML文档中的base标签
func findBaseHref(n *html.Node) string {
if n.Type == html.ElementNode && n.Data == "base" {
for _, attr := range n.Attr {
if attr.Key == "href" {
return attr.Val
}
}
}

// 递归处理子节点
for c := n.FirstChild; c != nil; c = c.NextSibling {
if href := findBaseHref(c); href != "" {
return href
}
}

return ""
}

// Extract links from HTML node
func extractLinks(n *html.Node, links *[]string) {
if n.Type == html.ElementNode {
var attrName string

// Determine which attribute to look for based on the element type
switch n.Data {
case "a", "link":
attrName = "href"
case "img", "script", "iframe", "source", "embed":
attrName = "src"
case "form":
attrName = "action"
case "object":
attrName = "data"
case "area":
attrName = "href"
}

// Extract attribute value if it exists
if attrName != "" {
for _, attr := range n.Attr {
if attr.Key == attrName {
*links = append(*links, attr.Val)
break
}
}
}
}

// Recursively process child nodes
for c := n.FirstChild; c != nil; c = c.NextSibling {
extractLinks(c, links)
}
}

// 新增:捕获操作系统信号,用于优雅关闭
func setupSignalHandler(c *Crawler) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nReceived shutdown signal. Saving state and exiting...")
c.Stop()
}()
}

func main() {
// Parse command line flags
baseURLFlag := flag.String("url", "", "Base URL to crawl (required)")
outputDirFlag := flag.String("output", "downloaded_site", "Output directory")
concurrencyFlag := flag.Int("concurrency", 5, "Number of concurrent workers")
timeoutFlag := flag.Int("timeout", 30, "Request timeout in seconds")
skipTLSFlag := flag.Bool("skip-tls", false, "Skip TLS certificate verification")
userAgentFlag := flag.String("user-agent", "Mozilla/5.0 (compatible; GoCrawler/1.0)", "User-Agent string")

// 新增命令行参数
queueFileFlag := flag.String("queue-file", "", "File to save/load queue state")
saveIntervalFlag := flag.Int("save-interval", 300, "Interval (in seconds) to auto-save queue state (0 to disable)")

// URL replacement rules
replaceFlag := flag.String("replace", "", "URL replacement rules (pattern:replacement,pattern:replacement,...)")

// Allowed domains
domainsFlag := flag.String("domains", "", "Allowed domains to crawl (comma-separated)")

flag.Parse()

// Validate base URL
if *baseURLFlag == "" {
fmt.Println("Error: Base URL is required")
flag.Usage()
os.Exit(1)
}

// Parse allowed domains
var allowedDomains []string
if *domainsFlag != "" {
allowedDomains = strings.Split(*domainsFlag, ",")
}

// Parse replacement rules
var replaceRules []ReplaceRule
if *replaceFlag != "" {
rules := strings.Split(*replaceFlag, ",")
for _, rule := range rules {
parts := strings.SplitN(rule, ":", 2)
if len(parts) != 2 {
fmt.Printf("Warning: Invalid replacement rule: %s\n", rule)
continue
}

pattern, err := regexp.Compile(parts[0])
if err != nil {
fmt.Printf("Warning: Invalid regex pattern: %s\n", parts[0])
continue
}

replaceRules = append(replaceRules, ReplaceRule{
Pattern: pattern,
Replacement: parts[1],
})
}
}

// 默认队列文件路径(如果未指定)
queueFile := *queueFileFlag
if queueFile == "" && *baseURLFlag != "" {
// 从 baseURL 生成默认的队列文件名
baseURL, err := url.Parse(*baseURLFlag)
if err == nil {
queueFile = filepath.Join(*outputDirFlag, fmt.Sprintf("queue_%s.json", baseURL.Hostname()))
}
}

// Create configuration
config := Config{
BaseURL: *baseURLFlag,
OutputDir: *outputDirFlag,
Concurrency: *concurrencyFlag,
Timeout: time.Duration(*timeoutFlag) * time.Second,
AllowedDomains: allowedDomains,
URLReplaceRules: replaceRules,
SkipTLS: *skipTLSFlag,
UserAgent: *userAgentFlag,
QueueFile: queueFile, // 新增:队列文件路径
SaveInterval: time.Duration(*saveIntervalFlag) * time.Second, // 新增:保存间隔
}

// Create and start crawler
crawler := NewCrawler(config)
err := crawler.Start()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}

fmt.Println("Crawl completed successfully")
}

这是 AI 生成的使用说明,我看过了,比我自己写的要好。

使用说明

简介

该程序是一个简单的网页爬虫,能够从指定的基础URL开始,抓取网站中的页面及其资源(如图片、脚本、CSS等),并将它们保存在本地文件系统中。它支持以下功能:

  • 并发抓取:支持多个并发爬虫实例(worker)来加速抓取过程。
  • URL替换规则:可以在下载的页面内容中替换URL。
  • 定期保存爬虫队列状态:能够在运行过程中定期保存当前爬虫状态,包括已抓取的页面和待抓取的URL。
  • 支持TLS/SSL配置:允许跳过TLS验证以抓取HTTPS页面。

配置参数

基本命令行参数

  • -url <url>:要抓取的基础URL(必须提供)。例如:-url https://example.com
  • -output <dir>:下载的文件将保存到指定目录。默认值为downloaded_site
  • -concurrency <num>:并发的工作线程数量。默认值为5。
  • -timeout <seconds>:请求的超时时间(秒)。默认值为30秒。
  • -skip-tls:是否跳过TLS证书验证。默认值为false,即不跳过TLS验证。
  • -user-agent <string>:设置爬虫的User-Agent字符串。默认值为Mozilla/5.0 (compatible; GoCrawler/1.0)
  • -queue-file <path>:指定一个文件路径,用于保存和加载爬虫的队列状态。爬虫将在此文件中保存当前队列状态,可以在程序重新启动后继续从中加载。
  • -save-interval <seconds>:设置定期保存爬虫状态的时间间隔(秒)。如果为0,则禁用定期保存。
  • -replace <pattern:replacement,...>:URL替换规则,使用正则表达式进行替换。例如:-replace http://example.com:https://newdomain.com
  • -domains <domain1,domain2,...>:指定允许抓取的域名列表,多个域名用逗号分隔。默认情况下,只允许抓取基础URL的域名。

示例

  1. 简单用例:抓取一个网站,并将文件保存在output目录中。
1
go run main.go -url https://example.com -output ./downloaded_site
  1. 并发抓取和队列保存:抓取https://example.com,并发5个线程,设置每5分钟保存一次爬虫队列。
1
go run main.go -url https://example.com -output ./downloaded_site -concurrency 5 -queue-file ./queue.json -save-interval 300
  1. URL替换和允许域名限制:抓取https://example.com,并将example.com替换为newdomain.com,只允许抓取example.comnewdomain.com的页面。
1
go run main.go -url https://example.com -output ./downloaded_site -replace "example.com:newdomain.com" -domains "example.com,newdomain.com"

主要功能

1. 加载和保存爬虫状态

  • 爬虫支持从文件加载之前的爬虫队列和已访问的URL,以便程序重新启动时能够从中断点继续抓取。
  • 爬虫还会定期保存当前抓取的状态(包括队列和已访问的URL),以便能够在进程崩溃或关闭时恢复。

2. 并发抓取

  • 通过配置并发线程数,程序能够提高抓取速度。多个工作线程(goroutines)将并行处理待抓取的URL。

3. HTML页面内容解析

  • 在抓取HTML页面时,程序会提取页面中的链接,并将新的链接添加到抓取队列中。它支持aimgscript等标签。

4. URL替换规则

  • 在抓取的页面内容中,程序支持根据指定的替换规则替换URL。例如,可以将页面中指向旧域名的链接替换为新域名。

5. 支持跳过TLS验证

  • 如果遇到TLS证书验证问题,可以使用-skip-tls选项跳过TLS验证,方便抓取HTTPS网站。

6. 优雅的关闭

  • 程序通过捕获操作系统的关闭信号(如SIGINT、SIGTERM),在关闭时会自动保存当前状态并安全退出。

退出和停止

您可以通过按下Ctrl + C来停止爬虫,程序会在接收到信号后优雅地停止,并保存当前的爬虫状态。

错误处理

如果在执行过程中遇到任何错误(例如,HTTP请求失败或文件写入错误),程序会显示错误消息,并继续处理下一个URL。

确实整个程序没有自己写任何一行代码,慢慢的接触也慢慢对其产生了依赖,有一种见过了美丽的风景,平庸的景象就很难再吸引人的感觉,可以毫不夸张的说,现在的我,已经是一个离开了 AI 没办法正常工作的“废物”了。很无奈,但确实是现实,以往需要一周完成的工作,现在在 AI 的帮助下只需要两天就可以完成,有时候,平台开始限制,就开始犯懒了,会选择等到平台解除限制再继续,思考的内容也很简单:我努力思考费精力数小时完成的内容,不如在 AI 帮助下,数分钟就可以完成,为什么要折磨自己呢?我对这样的想法其实并不排斥,也不抵触,就如同我不会排斥和抵触使用计算器一样,或许,这就是我所看到的生成式 AI 改变社会的最好证据吧。

本文作者:
本文链接:https://tdh6.top/%E6%9D%82%E9%A1%B9/website-crawl/
版权声明:本站文章采用 CC BY-NC-SA 3.0 CN 协议进行许可,翻译文章遵循原文协议。
图片来源:本站部分图像来源于网络,前往查看 相关说明。