Go开发桌面客户端软件小试:网站Sitemap生成

news2025/1/12 19:57:28

在前一篇【手把手教你用Go开发客户端软件(使用Go + HTML)】中,我们详细介绍了如何通过Go语言开发一个简单的桌面客户端软件。本次,我们将继续这个系列,使用Go语言结合Sciter的Go绑定库——go-sciter,实战开发一个可以生成网站Sitemap的小工具。

请添加图片描述

Sitemap 是什么

Sitemap是指网站地图,主要用于列出网站的所有页面,以便搜索引擎更容易地爬取网站内容。通常情况下,Sitemap文件是一个XML格式的文件,里面包含了网站上所有希望被搜索引擎索引的链接。通过Sitemap,网站管理员可以更好地告知搜索引擎哪些页面是重要的、哪些页面需要更新。

Sitemap的好处包括:

  • 提升SEO:帮助搜索引擎更快更全面地索引网页。
  • 提高爬取效率:确保搜索引擎能发现和索引所有的页面,尤其是深层次或孤立页面。
  • 内容更新通知:搜索引擎可以根据Sitemap中的更新时间来判断页面是否需要重新爬取。

Sitemap生成思路

在开发这个Sitemap生成器时,我们的核心思路是遍历一个网站的所有链接,并根据需要生成相应的Sitemap。主要流程如下:

  1. 用户输入网址:用户在前端界面中输入目标网站的URL,并点击“生成”按钮。

  2. 异步调用生成逻辑:程序在后台异步执行Sitemap生成的逻辑,避免阻塞用户的操作体验。

  3. 请求网站页面:程序收到用户提交的网址后,从入口页面开始发起HTTP请求,获取页面内容。

  4. 遍历页面链接:程序对页面进行解析,提取页面中的所有链接,并将它们加入队列中。

  5. 检查现有Sitemap:如果网站已有Sitemap,程序会优先读取Sitemap中的链接,并将其加入队列,同时去除重复的链接。

  6. 协程处理链接:启动一个协程,从队列中逐一取出链接,继续对这些页面发起请求并解析内容,提取更多链接加入队列。

  7. 处理过的链接标记:每处理完一个链接,就会将其标记为已处理,并将该链接写入到生成的Sitemap文件中。

  8. 循环处理:重复上述过程,直到队列中的所有链接都被处理完毕,最后生成完整的Sitemap。

  9. 前端显示进度:在生成过程中,前端会定期刷新并显示当前的进度,例如已处理的链接数量和Sitemap的生成状态。

整个过程可以概括为爬取、分析、去重、保存四个步骤,确保在网站的大量链接中不漏掉重要页面,同时避免重复的链接被多次处理。

具体代码实现

1. 初始化项目

注意:go-sciter 需要使用最新的 v0.5.1-0.20220404063322-7f18ada7f2f5

main.go 的代码

该程序使用Sciter GUI库创建了一个窗口应用,主要功能包括:

加载并显示嵌入的HTML视图文件。
定义了打开URL、获取正在运行的任务信息和创建新任务的功能。
通过openUrl函数在系统默认浏览器中打开链接。
getRunningTask函数返回当前正在运行的任务信息。
createTask函数接收域名参数,创建一个新的爬虫任务,并保存站点地图到文件。

package main

import (
	"anqicms.com/sitemap/utils"
	"embed"
	"encoding/json"
	"github.com/sciter-sdk/go-sciter"
	"github.com/sciter-sdk/go-sciter/window"
	"github.com/skratchdot/open-golang/open"
	"log"
	"os"
	"path/filepath"
	"strings"
)

//go:embed all:views
var views embed.FS

type Map map[string]interface{}

func main() {
	w, err := window.New(sciter.SW_TITLEBAR|sciter.SW_RESIZEABLE|sciter.SW_CONTROLS|sciter.SW_MAIN|sciter.SW_ENABLE_DEBUG, &sciter.Rect{
		Left:   100,
		Top:    50,
		Right:  1100,
		Bottom: 660,
	})
	if err != nil {
		log.Fatal(err)
	}

	w.SetCallback(&sciter.CallbackHandler{
		OnLoadData: func(params *sciter.ScnLoadData) int {
			if strings.HasPrefix(params.Uri(), "home://") {
				fileData, err := views.ReadFile(params.Uri()[7:])
				if err == nil {
					w.DataReady(params.Uri()[7:], fileData)
				}
			}
			return 0
		},
	})

	w.DefineFunction("openUrl", openUrl)
	w.DefineFunction("getRunningTask", getRunningTask)
	w.DefineFunction("createTask", createTask)

	mainView, err := views.ReadFile("views/main.html")
	if err != nil {
		os.Exit(0)
	}
	w.LoadHtml(string(mainView), "")

	w.SetTitle("Sitemap 生成")
	w.Show()
	w.Run()
}

func openUrl(args ...*sciter.Value) *sciter.Value {
	link := args[0].String()
	_ = open.Run(link)

	return nil
}

// 获取运行中的task
func getRunningTask(args ...*sciter.Value) *sciter.Value {
	if RunningCrawler == nil {
		return nil
	}
	return jsonValue(RunningCrawler)
}

// 创建任务
func createTask(args ...*sciter.Value) *sciter.Value {
	domain := args[0].String()
	exePath, _ := os.Executable()
	sitemapPath := filepath.Dir(exePath) + "/" + utils.GetMd5String(domain, false, true) + ".txt"
	crawler, err := NewCrawler(CrawlerTypeSitemap, domain, sitemapPath)
	if err != nil {
		return jsonValue(Map{
			"msg":    err.Error(),
			"status": -1,
		})
	}
	crawler.OnFinished = func() {
		// 完成时处理函数
	}
	crawler.Start()

	return jsonValue(Map{
		"msg":    "任务已创建",
		"status": 1,
	})
}

func jsonValue(val interface{}) *sciter.Value {
	buf, err := json.Marshal(val)
	if err != nil {
		return nil
	}
	return sciter.NewValue(string(buf))
}

2 前端设计

使用go-sciter库实现前端界面,包含一个输入框和“生成”按钮。用户在输入框中填写目标网址后,点击按钮启动Sitemap生成。

views/task.html 的代码

HTML 结构:

定义了一个带有布局的简单网页,包括侧边栏 (aside) 和主要内容区域 (container)。
自定义标签与属性:

resizeable:指示页面可调整大小。
脚本 (text/tiscript):

变量与函数:
running:标记任务是否正在运行。
syncTask():同步并显示任务状态。
showResult(result):展示任务结果。
事件监听:
click 事件绑定到按钮,用于触发任务开始/取消操作。
定时器:
每秒调用 syncTask() 更新任务状态。

功能概述:

用户可以输入网站地址以生成网站地图。
提供“开始执行”和“停止”按钮控制任务。
显示任务进度和结果。

<html resizeable>
<head>
    <style src="home://views/style.css" />
    <meta charSet="utf-8" />
</head>
<body>
<div class="layout">
    <div class="aside">
        <h1 class="soft-title"><a href="home://views/main.html">Sitemap<br/>生成器</a></h1>
        <div class="aside-menus">
            <a href="home://views/task.html" class="menu-item active">开始使用</a>
            <a href="home://views/help.html" class="menu-item">使用教程</a>
        </div>
    </div>
    <div class="container">
        <div>
            <form class="control-form" #taslForm>
                <div class="form-header"><h3>Sitemap 生成</h3>
                </div>
                <div class="form-content">
                    <div class="form-item">
                        <div class="form-label">网址地址:</div>
                        <div class="input-block">
                            <input(domain) class="layui-input" type="text" placeholder="http://或https://开头的网站地址" />
                            <div class="text-muted">程序将抓取推送网址下的所有链接。</div>
                        </div>
                    </div>
                    <div>
                        <button type="default" class="stop-btn" #cancelTask>停止</button>
                        <button type="default" #taskSubmit>开始执行</button>
                    </div>
                </div>
            </form>
            <div class="result-list" #resultList>
                <div class="form-header">
                    <h3>查看结果</h3>
                </div>
                <div class="form-content">
                    <table>
                        <colgroup>
                            <col width="40%">
                            <col width="60%">
                        </colgroup>
                        <tbody>
                        <tr>
                            <td>目标站点</td>
                            <td #resultDomain></td>
                        </tr>
                        <tr>
                            <td>保存结果</td>
                            <td #resultPath></td>
                        </tr>
                        <tr>
                            <td>任务状态</td>
                            <td #resultStatus></td>
                        </tr>
                        <tr>
                            <td>发现页面</td>
                            <td #resultTotal></td>
                        </tr>
                        <tr>
                            <td>已处理页面</td>
                            <td #resultFinished></td>
                        </tr>
                        <tr>
                            <td>错误页面</td>
                            <td #resultNotfound></td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

</body>
</html>

<script type="text/tiscript">
    let running = false;
    function syncTask() {
        let res = view.getRunningTask()
        if (res) {
            let result = JSON.parse(res);
            running = true;
            $(#cancelTask).@.addClass("active");
            $(#resultList).@.addClass("active");
            $(#taskSubmit).text = "执行中";
            showResult(result);
        } else {
            running = false;
            $(#cancelTask).@.removeClass("active");
            $(#resultList).@.removeClass("active");
            $(#taskSubmit).text = "开始执行";
            return;
        }
    }
    event click $(#cancelTask){
        $(#cancelTask).@.removeClass("active");
        $(#resultList).@.removeClass("active");
    }
    event click $(#taskSubmit){
        let res = view.createTask($(#taslForm).value.domain)
        let result = JSON.parse(res)

        view.msgbox(#alert, result.msg);
        if (result.status == 1) {
           // 同步结果
            syncTask();
        }
    }
    // 打开本地路径
    event click $(#resultPath){
        view.openUrl($(#resultPath).text)
    }
    // 展示结果
    function showResult(result) {
        if (!result) {
            return;
        }
        $(#resultDomain).text = result.domain;
        $(#resultPath).text = result.save_path;
        $(#resultStatus).text = result.status;
        $(#resultTotal).text = result.total + "条";
        $(#resultFinished).text = result.finished + "条";
        $(#resultNotfound).text = result.notfound + "条";
    }
    // 进来的时候先执行一遍
    syncTask();
    // 1秒钟刷新一次
    self.timer(1000ms, function() {
        syncTask();
        return true;
    });
</script>

网页的抓取以及Sitemap的保存

限于篇幅,这里只列出了部分代码

简要说明一下:爬虫支持采集服务端渲染的静态页面,也支持采集客户端渲染的页面。如果网页是客户端渲染,则会调用ChromeDP来进行先渲染后抓取的操作步骤。

crawler.go 的部分代码

var RunningCrawler *Crawler

func NewCrawler(crawlerType string, startPage string, savePath string) (*Crawler, error) {
	if RunningCrawler != nil {
		RunningCrawler.Stop()
	}

	urlObj, err := url.Parse(startPage)
	if err != nil {
		log.Printf("解析起始地址失败: url: %s, %s", startPage, err.Error())
		return nil, err
	}
	if crawlerType != CrawlerTypeCollect {
		if crawlerType == CrawlerTypeDownload {
			_, err = os.Stat(savePath)
			if err != nil {
				log.Errorf("存储地址不存在")
				return nil, err
			}
		} else {
			// 检测上级目录
			_, err = os.Stat(filepath.Dir(savePath))
			if err != nil {
				log.Errorf("存储地址不存在")
				return nil, err
			}
		}
	}
	log.SetLevel(log.INFO)

	ctx, cancelFunc := context.WithCancel(context.Background())

	crawler := &Crawler{
		ctx:              ctx,
		Cancel:           cancelFunc,
		Type:             crawlerType,
		PageWorkerCount:  5,
		AssetWorkerCount: 5,
		SavePath:         savePath,
		PageQueue:        make(chan *URLRecord, 500000),
		AssetQueue:       make(chan *URLRecord, 500000),
		LinksPool:        &sync.Map{},
		LinksMutex:       &sync.Mutex{},
		Domain:           startPage,
		MaxRetryTimes:    3,
		IsActive:         true,
		lastActive:       time.Now().Unix(),
		gRWLock:          new(sync.RWMutex),
	}
	mainSite := urlObj.Host // Host成员带端口.
	crawler.MainSite = mainSite

	err = crawler.LoadTaskQueue()
	if err != nil {
		log.Errorf("加载任务队列失败: %s", err.Error())
		cancelFunc()
		return nil, err
	}
	crawler.Id = int(time.Now().Unix())

	if crawlerType == CrawlerTypeSitemap {
		crawler.sitemapFile = NewSitemapGenerator("txt", crawler.SavePath, false)
	}

	RunningCrawler = crawler

	return crawler, nil
}

func (crawler *Crawler) isCanceled() bool {
	select {
	case <-crawler.ctx.Done():
		return true
	default:
		return false
	}
}

// Start 启动n个工作协程
func (crawler *Crawler) Start() {
	req := &URLRecord{
		URL:         crawler.Domain,
		URLType:     URLTypePage,
		Refer:       "",
		Depth:       1,
		FailedTimes: 0,
	}
	crawler.EnqueuePage(req)

	//todo 加 waitGroup
	for i := 0; i < crawler.PageWorkerCount; i++ {
		go crawler.GetHTMLPage(i)
	}
	// only download need to work with assets
	if crawler.Type == CrawlerTypeDownload {
		for i := 0; i < crawler.AssetWorkerCount; i++ {
			go crawler.GetStaticAsset(i)
		}
	}
	//检查活动
	go crawler.CheckProcess()
}

func (crawler *Crawler) Stop() {
	if !crawler.IsActive {
		return
	}

	crawler.LinksMutex.Lock()
	crawler.IsActive = false
	//停止
	//time.Sleep(200 * time.Millisecond)
	close(crawler.AssetQueue)
	close(crawler.PageQueue)
	crawler.LinksMutex.Unlock()

	if crawler.sitemapFile != nil {
		_ = crawler.sitemapFile.Save()
	}

	log.Infof("任务完成", crawler.Domain)
	//开始执行抓取任务
	if crawler.OnFinished != nil && !crawler.canceled {
		crawler.OnFinished()
	}

	RunningCrawler = nil
}

// getAndRead 发起请求获取页面或静态资源, 返回响应体内容.
func (crawler *Crawler) getAndRead(req *URLRecord) (body []byte, header http.Header, err error) {
	err = crawler.UpdateURLRecordStatus(req.URL, URLTaskStatusPending)
	if err != nil {
		log.Infof("更新任务队列记录失败: req: %s, error: %s", req.URL, err.Error())
		return
	}

	if req.FailedTimes > crawler.MaxRetryTimes {
		log.Infof("失败次数过多, 不再尝试: req: %s", req.URL)
		return
	}
	if req.URLType == URLTypePage && crawler.Single && 1 < req.Depth {
		log.Infof("当前页面已达到最大深度, 不再抓取: req: %s", req.URL)
		return
	}

	if crawler.Render && req.URLType == URLTypePage {
		var content string
		content, err = ChromeDPGetArticle(req.URL)
		if err != nil {
			log.Errorf("请求失败, 重新入队列: req: %s, error: %s", req.URL, err.Error())
			req.FailedTimes++
			if req.URLType == URLTypePage {
				crawler.EnqueuePage(req)
			}
			return
		}
		header = http.Header{}
		header.Set("Content-Type", "text/html")
		body = []byte(content)
	} else {
		var resp *http.Response
		resp, err = getURL(req.URL, req.Refer)
		if err != nil {
			log.Errorf("请求失败, 重新入队列: req: %s, error: %s", req.URL, err.Error())
			req.FailedTimes++
			if req.URLType == URLTypePage {
				crawler.EnqueuePage(req)
			}
			return
		}
		defer resp.Body.Close()
		if resp.StatusCode >= 400 {
			crawler.Notfound++
			if crawler.Type == CrawlerType404 {
				crawler.SafeFile(req.URL, resp.StatusCode)
			}
			// 抓取失败一般是5xx或403, 405等, 出现404基本上就没有重试的意义了, 可以直接放弃
			err = crawler.UpdateURLRecordStatus(req.URL, URLTaskStatusFailed)
			log.Infof("页面404等错误: req: %s", req.URL)
			if err != nil {
				log.Errorf("更新任务记录状态失败: req: %s, error: %s", req.URL, err.Error())
			}
			err = errors.New(fmt.Sprintf("页面错误:%d", resp.StatusCode))
			return
		}

		header = resp.Header
		body, err = io.ReadAll(resp.Body)
	}

	return
}

看看软件的成果界面:

软件主界面:
请添加图片描述

爬虫任务界面:
请添加图片描述

如果你对完整的代码感兴趣,可以访问我的GitCode仓库:Go开发桌面软件小试-网站Sitemap生成 - https://gitcode.com/anqicms/sitemap。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2064668.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

14.C基础_结构体

定义与使用 1、定义 定义结构体&#xff1a; 定义结构体时&#xff0c;需要注意最后的分号必须加上。 定义结构体时&#xff0c;成员只去声明类型&#xff0c;不进行赋值。赋值在定义结构体变量时进行。 struct 结构体名{结构体成员列表 }; //注意这里的分…

Qt入门学什么?

Qt是一个跨平台的C图形用户界面应用程序框架&#xff0c;它为应用程序开发者提供建立图形界面所需的所有功能。Qt框架以其面向对象、易于扩展的特性而受到广泛欢迎&#xff0c;并且支持多种平台&#xff0c;包括桌面、嵌入式和移动平台 。 对于Qt的入门学习&#xff0c;可以通过…

uniapp+vue3的defineProps传递

//index.vue <view class"topic"><!-- 磨砂背景 --><view class"content"><matte v-for"(item,index) in 8" :key"index"></matte><matte isMore"false"></matte></view>&…

0成本学习Liunx系统【只需要一台笔记本电脑,无需购买云服务器】

【准备工作&#xff0c;需要软件】&#xff1a; 1&#xff1a;MobaXterm 【服务器连接工具&#xff08;免费开源&#xff09;】 2&#xff1a;CentOS-7-x86_64-DVD-2009.iso 【CentOS-7 镜像】 3&#xff1a;VirtualBox-7.0.20-163906-Win.exe 【虚拟机壳子】 4&…

朴素贝叶斯与决策树分类

朴素贝叶斯分类 1贝叶斯分类理论 选择高概率对应的类别 2条件概率 事件B发生的情况下&#xff0c;事件A发生的概率 &#x1d443;(&#x1d434;|&#x1d435;)&#x1d443;(&#x1d434;∩&#x1d435;)/&#x1d443;(&#x1d435;) > &#x1d443;(&#x1d43…

【前端面试】浏览器原理解读

前端进阶——浏览器篇-CSDN博客 浏览器工作原理与Javascript高级&#xff08;前后端异步&#xff09;-CSDN博客 DOM树的建立过程 前端DOM&#xff08;文档对象模型&#xff09;数的建立过程&#xff0c;实际上是浏览器解析HTML文档并构建DOM树的过程。这一过程大致可以分为以…

声音克隆GPT-SoVITS 2.0软件和详细的使用教程!

天命人&#xff0c;请允许我先蹭个热点&#xff01; 原始声音&#xff1a; 播放 克隆声音&#xff1a; 播放 文章写了一半&#xff0c;被《黑神话悟空》刷屏了。突发奇想&#xff0c;用里面的声音来做个素材试试看。 B站捞了一点声音素材&#xff0c;随便剪一剪&#xff0c…

IOS半越狱工具nathanlr越狱教程

简介 nathanlr 是一款半越狱工具&#xff0c;不是完整越狱。 半越狱只能使用一些系统范围的插件。 无法做到完整越狱 Dopamine 越狱一样插件兼容性。 nathanlr支持 iOS 16.5.1 – 16.6.1 系统。 支持 A12 及以上设备。 肯定有人问&#xff0c;为什么仅仅支持这些系统&#xff…

关于全球影像下载你需要知道这些参数

经常会有客户问我们&#xff0c;如果想要下载全球的影像应该怎么下载&#xff0c;这里我们用数字说话&#xff0c;为你介绍一下全球影像下载的那些关键参数。 TIF文件大小 在开始之前说明一下&#xff0c;以下表格中所有出现的级别均为标准级别&#xff0c;如果想对应水经微图…

Qt系列之数据库(一)

Qt 数据库开发是指在Qt框架下进行数据库操作的开发工作。Qt提供了一套强大的数据库模块&#xff0c;可以方便地与多种数据库进行交互&#xff0c;如SQLite、MySQL、PostgreSQL等。 该模块中接口是使用C语言&#xff0c;也就是说&#xff0c;学习相关的类及类的接口使用。 qt…

我的Markdown简历模板开源了!

我之前写过一篇文章&#xff0c;很详细的讲解了如何使用Markdown写出一份漂亮的简历&#xff0c;并且在各个博客平台都有发布。 为了方便&#xff0c;我在这贴一下这篇文章的链接&#xff1a;✨Markdown制作简历教程 如果你还没有读过&#xff0c;或者恰好需要做一份新的简历…

【Kubernetes】K8s中Container(容器)、Pod(小组)和node(节点)概念讲解

Kubernetes学习之路 第一章 Kubernetes学习入门之Container(容器)、Pod(小组)和node(节点)概念 文章目录 Kubernetes学习之路前言一、Container&#xff08;容器&#xff09;二、Pod&#xff08;小组&#xff09;1.单容器 Pod2.多容器 Pod 三、Container&#xff08;容器&…

132-横向移动-Exchange 服务有账户 CVE 漏洞无账户口令爆破

Exchange服务 Microsoft Exchange Server 是微软公司推出的一款企业级邮件服务器软件&#xff0c;它提供了一套全面的电子邮件服务组件&#xff0c;以及消息和协作系统。Exchange Server 不仅支持电子邮件服务&#xff0c;还提供了日历、联系人管理、任务管理、文档管理、实时会…

机器学习 之 决策树与随机森林的实现

引言 随着互联网技术的发展&#xff0c;垃圾邮件过滤已成为一项重要的任务。机器学习技术&#xff0c;尤其是决策树和随机森林&#xff0c;在解决这类问题时表现出色。本文将介绍随机森林的基本概念&#xff0c;并通过一个具体的案例——筛选垃圾电子邮件——来展示随机森林的…

【Qt】输入类控件QTextEdit

目录 输入类控件QTextEdit 例子&#xff1a;获取多行输入框的内容 例子&#xff1a;验证输入框的各种信号 输入类控件QTextEdit QTextEdit表示多行输入框&#xff0c;也是一个富文本&markdown编辑器。 并且能在内容超出编辑框范围时自动提供滚动条 在Qt中&#xff0c;有俩…

前端CSS选择器

css 和html 三种表示方式 行内样式 >内部样式>外部样式 元素选择器 属性选择器 id选择器 选择id为bb的 &#xff0c;给他增添样式 class选择器以 .开头 用法和id差不都 包含选择器和父子选择器 兄弟选择器 选择器组合 伪元素选择器 首字母格式不一样 首行格式不一样 …

java设计模式--创建型设计模式

创建型模式可分为&#xff1a;单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式 单例模式 单例模式 就是采取一定的方法保证在整个软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一个获取其对象的方法&#xff08;静态方法&#xf…

Unity编辑器扩展之Project视图扩展

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! Unity编辑器扩展之Project视图扩展 TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&#xff01…

河南萌新联赛2024第(六)场:郑州大学(补题ABCDFGIL)

文章目录 河南萌新联赛2024第&#xff08;六&#xff09;场&#xff1a;郑州大学A 装备二选一&#xff08;一&#xff09;简单介绍&#xff1a;思路&#xff1a;代码&#xff1a; B 百变吗喽简单介绍&#xff1a;思路&#xff1a;代码&#xff1a; C 16进制世界简单介绍&#x…

第二十七节、人物可互动标识

一、多个场景同时存在 方法&#xff1a;将另一个场景拖拽进 当前场景中 这样在一个场景中保存物体&#xff0c;另一个场景切换即可 创建一个场景名为上图&#xff08;这是一个持久化的场景&#xff09; 被激活的场景是粗体字的 二、代码 使用第二个代码获得player的子物体 …