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

在前一篇【手把手教你用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 mainimport ("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.FStype 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 *Crawlerfunc 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 = mainSiteerr = 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 = crawlerreturn crawler, nil
}func (crawler *Crawler) isCanceled() bool {select {case <-crawler.ctx.Done():return truedefault:return false}
}// Start 启动n个工作协程
func (crawler *Crawler) Start() {req := &URLRecord{URL:         crawler.Domain,URLType:     URLTypePage,Refer:       "",Depth:       1,FailedTimes: 0,}crawler.EnqueuePage(req)//todo 加 waitGroupfor i := 0; i < crawler.PageWorkerCount; i++ {go crawler.GetHTMLPage(i)}// only download need to work with assetsif 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 stringcontent, 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.Responseresp, 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.Headerbody, err = io.ReadAll(resp.Body)}return
}

看看软件的成果界面:

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

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

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

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

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

相关文章

Linux虚拟机磁盘管理-新分区磁盘挂载

挂载mount 注意&#xff1a;挂载前一定要对磁盘进行文件系统创建&#xff0c;否则无法挂载磁盘 比如mkfs.ext4系统文件 磁盘挂载前和挂载后&#xff1a; linux系统重启后磁盘挂载没有了怎么办(不建议&#xff0c;哪怕输错一个字系统起不来&#xff0c;自己操作的时候出现过起不…

大语言模型-PDF文档解析

PDF解析能够提升大语言模型系统的信息处理能力和应用范围&#xff0c;为用户提供更加便捷、高效、个性化的服务体验。本文介绍三种常用的pdf解析方式&#xff1a;Open Parse、pdfplumber、PyMuPD。 一、Open Parse Open Parse是一个能够直观地识别文档布局并有效地对其进行分…

Qt 系统相关 - 网络与音视频

目录 一、Qt 网络 1. UDP Socket 1.1 核心 API 概览 1.2 回显服务器 1.3 回显客户端 2. TCP Socket 2.1 核心 API 概览 2.2 回显服务器 2.3 回显客户端 3. HTTP Client 3.1 核心 API 3.2 代码示例 二、Qt 音视频 1. Qt 音频 1.1 核心API概览 1.2 示例 2. Qt 视…

加速网络体验,Squid缓存代理:让浏览如飞,畅享无限网络速度!

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言: squ…

订单到期关闭如何实现?

目录 一、被动关闭 二、定时任务 三、JDK自带的DelayQueue 四、Netty的时间轮 五、Kafka的时间轮 六、RocketMQ延迟消息 七、RabbitMQ死信队列 八、RabbitMQ插件 九、Redis过期监听 十、Redis的Zset 十一、Redisson 在电商、支付等系统中&#xff0c;一般都是先创建…

详解华为项目管理,附华为高级项目管理内训材料

&#xff08;一&#xff09;华为在项目管理中通过有效的沟通、灵活的组织结构、坚持不懈的努力、细致的管理和科学的考核体系&#xff0c;实现了持续的创新和发展。通过引进先进的管理模式&#xff0c;强调以客户需求为导向&#xff0c;华为不仅优化了技术管理和项目研发流程&a…

多重示例详细说明Eureka原理实践

Eureka原理&#xff08;Eureka Principle&#xff09;是指在长时间的思考和积累之后&#xff0c;通过偶然的瞬间获得灵感或发现解决问题的方法的一种认知现象。这个过程通常包括三个主要阶段&#xff1a;准备阶段、潜伏期以及突然的灵感爆发。下面详细说明Eureka原理的实践步骤…

vue3 组合式API

<!-- 深度监听 deep 点击按钮控制台&#xff0c;才输出count变化了: 1, 老值: 0;否则控制台不输出 --> <script setup>import { ref,watch } from vueconst state ref({count:0})const setCount () > {state.count.value}watch(state, () > {console.log(…

QT中通过TCP协议多线程的文件传输(客户端)

首先&#xff0c;新建一个项目&#xff0c;我命名为了SendFileClient 首先我们要在pro文件中 代码第一行加入network的后缀 一、窗口搭建 如图所示&#xff0c;在第一个QWidget中让客户端输入IP&#xff0c;端口号 连接服务器 第二个Qwidget 设置一个LineEdit,供客户端选择要…

[godot] 采用状态机时,如何处理攻击时移动?如“冲撞”

这里以‘史莱姆撞击’为例子&#xff0c;将‘空中跃进’定义为伤害帧。&#xff08;见下图&#xff09; 先梳理流程&#xff1a;a.史莱姆原地蓄力(起跳准备)--->b.跳起并移动一段距离(空中跃进)--->c.落地调整 一 当状态机进入‘攻击状态’时&#xff0c;在enter()中…

计算机毕业设计PySpark+Django农产品推荐系统 农产品爬虫 农产品商城 农产品大数据 农产品数据分析可视化 PySpark Hadoop

基于Spark的农产品个性推荐系统 相关技术介绍: 1. Python Python是一种高级编程语言&#xff0c;具有简洁、易读、易学的特点&#xff0c;被广泛应用于Web开发、数据分析、人工智能等领域。 在此系统中&#xff0c;我们使用Python进行后端开发&#xff0c;利用其强大的语法…

基本数据类型 --- 浮点型

float的机器码表示&#xff1a; 一个float数据 (pow(-1, sign) fraction) * pow(2, exponent - 127) 由上图&#xff0c;可得&#xff1a; (pow(-1, sign) fraction) * pow(2, exponent - 127) ( 1 2^(-2) ) * pow(2, 124-127) 0.15625 其他文章&#xff1a; https://b…

鸿蒙HarmonyOS之使用ArkTs语言实现层级树状目录选择UI

一、实现效果 二、实现步骤 代码示例中用到的颜色、图片等资源可以自行替换设置 1、Index.ets 里面调用 import { CategoryView} from ./CategoryView;//主页面 Entry Component struct Index {State tabsIndex: number 0;build() {...//层级目录ViewCategoryView()...} …

ReTagList标签列表(API)

组件实现基于 Vue3 + Element Plus + Typescript,同时引用 vueUse + lodash-es + tailwindCss (不影响功能,可忽略) 基于ElTag实现的Tag列表,支持Tag列表多选,动态Tag列表 ReTagList标签列表 基础 简单展示Tag列表,可通过size指定尺寸 查看 /demo/tag-list/basic.md …

Arduino开源四足蜘蛛机器人制作教程

视频教程&#xff1a;手把手叫你做四足蜘蛛机器人——1零件介绍_哔哩哔哩_bilibili 一、项目介绍 1.1 项目介绍 Arduino主控&#xff0c;图形化编程&#xff0c;趣味学习 Arduino nano开发板舵机扩展底板 4.8V可充电电池&#xff0c;支持Arduino C语言编程和米思齐图形化编程…

Yolov10网络详解与实战(附数据集)

文章目录 摘要模型详解模型实战训练COCO数据集下载数据集 COCO转yolo格式数据集&#xff08;适用V4&#xff0c;V5&#xff0c;V6&#xff0c;V7&#xff0c;V8&#xff09;配置yolov10环境训练断点训练测试 训练自定义数据集Labelme数据集格式转换训练测试 总结 摘要 模型详解…

CeresPCL 岭回归拟合(曲线拟合)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 由于在使用最小二乘插值拟合时,会涉及到矩阵求逆的操作,但是如果这个矩阵接近于奇异时,那么拟合的结果就会与我们期望的结果存在较大差距,因此就有学者提出在最小二乘的误差函数中添加正则项,即: 这里我们也可…

OpenGL-ES 学习(8) ---- FBO

目录 FBO OverViewFBO 优点使用FBO的步骤 FBO OverView FBO(FrameBuffer Object) 指的是帧缓冲对象&#xff0c;实际上是一个可以添加缓冲区容器&#xff0c;可以为其添加纹理或者渲染缓冲区对象(RBO) FBO(FrameBuffer Object) 本身不能用于渲染&#xff0c;只有添加了纹理或者…

Stability AI发布了单目视频转4D模型的新AI模型:Stable Video 4D

开放生成式人工智能初创公司Stability AI在3月发布了Stable Video 3D&#xff0c;是一款可以根据图像中的物体生成出可旋转的3D模型视频工具。Stability AI在7月24日发布了新一代的Stable Video 4D&#xff0c;增添了赋予3D模移动作的功能。 Stable Video 4D能在约40秒内生成8…