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是一个能够直观地识别文档布局并有效地对其进行分…

GitFlow的四个分支

GitFlow中有四个分支&#xff1a; 常规分支&#xff1a;master & develop master主分支储存正式发布的历史&#xff0c;develop开发分支作为功能开发集成分支功能分支&#xff1a;feature 每个feature分支都对应一个要实现的具体功能或者改进点&#xff0c;这样可以保持每…

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 视…

cesium 实现批量divpoint气泡,及气泡碰撞测试与自动避让

需求背景解决效果index.vue 需求背景 需要实现一个上百点批量同时存在的 popup 弹框&#xff0c;为了提高用户体验 1.重叠的弹框&#xff0c;需要隐藏下一层级的 popup 2.为了让用户尽可能看到较全的弹框&#xff0c;需要做弹框的自动避让 解决效果 index.vue <!--/** * …

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

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

响应式Web设计:纯HTML和CSS的实现技巧

1. 简介 1.1. 概述 响应式Web设计(Responsive Web Design,简称RWD)是一种网络页面设计布局,它能够根据访问设备的屏幕尺寸、分辨率和其他特性动态地调整布局、图片和内容,以提供更好的用户体验。这种设计理念的核心在于“移动优先”,即首先考虑移动用户的体验,然后再扩…

Git使用——常见报错及其解决方法

一、报错关键词&#xff1a;OpenSSL、10054 fatal 1、在pull或push项目时&#xff0c;报错&#xff1a; fatal: unable to access https://github.com/../: OpenSSL SSL_read: Connection was reset, errno 10054 2、解决方法&#xff1a;进行解除/禁用Git SSL验证 项目里右…

【时时三省】C语言例题----华为机试题<字符串反转>

目录 1,题目 描述 输入描述: 输出描述: 示例1 2,代码

订单到期关闭如何实现?

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

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

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

【通用】C++ #pragma(特殊指令的预处理指令)

#pragma是一种用于向编译器发出特殊指令的预处理指令。它的作用是提供编译器特定的功能或控制编译行为。虽然 #pragma 不是标准 C 的一部分&#xff0c;但它被许多编译器实现并提供了不同的扩展。以下是一些常见的 #pragma 指令&#xff1a; 常见的 #pragma 指令 #pragma onc…

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

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

Pyramid学习笔记

Pyramid学习笔记 Static Assets&#xff1a; static assets 指那些非Python原文件&#xff0c;如&#xff1a;图片、css、js、还有目录&#xff08;没有__init__.py文件的目录&#xff09;以及Mako或Chamelon模板文件。 ####理解asset规范&#xff1a; render_to_response(m…

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(…

基于PHP的文件包含介绍

引言&#xff1a;在实际开发过程中&#xff0c;经常会遇到部分模块功能需要重复使用的情况&#xff0c;比如数据库的增删改查&#xff0c;文件包含通过将需要重复使用的功能模块代码引入其他文件的内容&#xff0c;实现重用代码、分离配置等。然而&#xff0c;如果文件包含操作…

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

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

报表系统之Redash

Redash 是一个开源的数据可视化和仪表板工具&#xff0c;旨在帮助用户轻松地从多个数据源中提取、查询、可视化数据&#xff0c;并分享结果。它的设计目标是让数据分析变得更加便捷&#xff0c;即使是非技术用户也能通过简单的操作生成复杂的数据报告和仪表板。 核心概念和功能…

sql基础语句题练

此篇文章所有题都来源于牛客网 牛客网在线编程_SQL篇_非技术快速入门 记录一下自己的做题的历程&#xff0c;废话不多说直接开整 1.查询所有列 select * from user_profile user_profile代表表table的意思 2.查询多列 select device_id,gender,age,university from user…

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

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