赞
踩
编写一个热点查看程序,包含百度热搜、微博热搜、头条、知乎等,废话不说上效果图:
效果图1:
效果图2
使用golang 1.9 编写代码
使用Wails技术实现GUI渲染,页面组件使用ant-design-vue,vite进行前端资源打包。
Wails技术
https://wails.io/zh-Hans/docs/introduction
Wails 是一个可让您使用 Go 和 Web 技术编写桌面应用的项目。
将它看作为 Go 的快并且轻量的 Electron 替代品。 您可以使用 Go 的灵活性和强大功能,结合丰富的现代前端,轻松的构建应用程序。
colly v2 实现数据抓取:
Go colly爬虫框架精简高效【杠杠的】入门到精通 - 掘金 (juejin.cn)
window环境为例:wails build -clean
将资源文件和程序打包程成独立的exe文件。
从 Go 下载页面 下载 Go,并配置好环境变量,还需要确保的 PATH
环境变量包含您的 ~/go/bin
目录路径
npm --version 检查环境
在window环境下运行,需要保证WebView2,现在window10/11默认已经安装好了,微软强制内置的环境,可以忽略,如果后续环境检测不通过可以再额外进行安装。
命令行运行 go install github.com/wailsapp/wails/v2/cmd/wails@latest
安装 Wails CLI
命令行运行 wails doctor
命令,类似如下结果,说明完成环境配置了。
如果提示 wails 找不到命令,检查 …go/bin 是否配置path环境
PS C:\Users\14639> wails doctor DEB | Using go webview2loader Wails CLI v2.5.1 SUCCESS Done. # System OS | Windows 10 Home China Version | 2009 (Build: 22000) ID | 21H2 Go Version | go1.19.9 Platform | windows Architecture | amd64 # Wails Version | v2.5.1 # Dependencies Dependency | Package Name | Status | Version WebView2 | N/A | Installed | 113.0.1774.57 Nodejs | N/A | Installed | 16.14.2 npm | N/A | Installed | 8.5.0 *upx | N/A | Available | *nsis | N/A | Available | * - Optional Dependency # Diagnosis Your system is ready for Wails development! Optional package(s) installation details: - upx : Available at https://upx.github.io/ - nsis : More info at https://wails.io/docs/guides/windows-installer/ ♥ If Wails is useful to you or your company, please consider sponsoring the project: https://github.com/sponsors/leaanthony
具体环境配置细节可以参考wails官网:安装 | Wails
直接使用wails脚手架创建,wails init -n wails-demo -t vue
,使用vue进行开发,这里模式使用的vue3,打包使用的vite。相关技术不了解的同学可以自行学习。
frontend
文件夹下根目录
即可wails dev
wails build -clean
npm install xxx
先给出源码仓库(码云):wails-demo: wails-demo (gitee.com) 感兴趣的可以下载一下本地运行。下载后直接运行wails dev
即可
main.go
程序的运行启动入口
package main import ( "embed" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) // 下面代码不能删除,是为了go打包资源文件 //go:embed all:frontend/dist var assets embed.FS func main() { // Create an instance of the app structure app := NewApp() // NewMenu 窗口操作菜单 //newMenu := menu.NewMenu() //FileMenu := newMenu.AddSubmenu("菜单") //FileMenu.AddText("设置", keys.CmdOrCtrl("t"), func(data *menu.CallbackData) { // runtime.EventsEmit(app.ctx, "open-file", time.Now().Format("2006-01-02 15:04:05")) //}) //FileMenu.AddSeparator() //FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) { // runtime.Quit(app.ctx) //}) // Create application with options err := wails.Run(&options.App{ Title: "实时热点", Width: 1024, Height: 768, DisableResize: true, //Menu: newMenu, AssetServer: &assetserver.Options{ Assets: assets, }, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, Bind: []interface{}{ app, }, }) if err != nil { println("Error:", err.Error()) } }
App.go
主要承担和前端js的通信和方法绑定Bind。
package main import ( "context" ) // App struct type App struct { ctx context.Context hsr *HotSearchRouter } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx hsr := &HotSearchRouter{} hsr.Init() a.hsr = hsr } // Greet returns a greeting for the given name func (a *App) Greet(index int) []HotSearchDto { if index == Last { return []HotSearchDto{} } return a.hsr.Route(index).Visit() }
hot_search.go
爬取热搜数据
package main import ( "encoding/json" "fmt" "github.com/PuerkitoBio/goquery" "github.com/gocolly/colly/v2" "github.com/gocolly/colly/v2/extensions" "github.com/labstack/gommon/log" "net/http" "net/http/cookiejar" "net/url" "strconv" "strings" ) const ( BaiDu = iota WeiBo TouTiao ZhiHu Last ) // HotSearchDto 搜索结果项 type HotSearchDto struct { Sort int `json:"sort"` Title string `json:"title"` Desc string `json:"desc"` Url string `json:"url"` Hot string `json:"hot"` } // IHotSearch 热搜接口 type IHotSearch interface { BindHTMLSelector() Visit() []HotSearchDto } // BaseSearch 基础搜索服务 实现接口的三个方法 type BaseSearch struct { Url string Collector *colly.Collector Data []HotSearchDto Limit int } //func (bs *BaseSearch) Ajax() { // fmt.Println("base ajax") // bs.Data = []HotSearchDto{} //} func (bs *BaseSearch) BindHTMLSelector() { fmt.Println("Nothing to do") } func (bs *BaseSearch) Visit() []HotSearchDto { bs.Data = []HotSearchDto{} err := bs.Collector.Visit(bs.Url) if err != nil { fmt.Printf("%v\n", err) return []HotSearchDto{} } return bs.Data } // HotSearchRouter 路由选择器 type HotSearchRouter struct { Router map[int]IHotSearch } func (r *HotSearchRouter) newCollector() *colly.Collector { return colly.NewCollector( colly.IgnoreRobotsTxt(), colly.AllowURLRevisit(), func(collector *colly.Collector) { // 设置随机ua extensions.RandomUserAgent(collector) // 设置cookiejar cjar, err := cookiejar.New(nil) if err == nil { collector.SetCookieJar(cjar) } }) } func (r *HotSearchRouter) Init() { r.Router = make(map[int]IHotSearch) r.Router[BaiDu] = &BaiDuHotSearch{BaseSearch{ Url: "https://top.baidu.com/board?tab=realtime", Collector: r.newCollector(), }} r.Router[BaiDu].BindHTMLSelector() r.Router[WeiBo] = &WeiBoHotSearch{BaseSearch{ Url: "https://weibo.com/ajax/side/hotSearch", Collector: r.newCollector(), }} r.Router[WeiBo].BindHTMLSelector() r.Router[TouTiao] = &TouTiaoHotSearch{BaseSearch{ Url: "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc", Collector: r.newCollector(), }} r.Router[TouTiao].BindHTMLSelector() r.Router[ZhiHu] = &ZhiHuHotSearch{BaseSearch{ Url: "https://tophub.today/n/mproPpoq6O", Collector: r.newCollector(), }} r.Router[ZhiHu].BindHTMLSelector() } func (r *HotSearchRouter) Route(key int) IHotSearch { return r.Router[key] } // BaiDuHotSearch 百度 type BaiDuHotSearch struct { BaseSearch } func (hs *BaiDuHotSearch) BindHTMLSelector() { hs.Collector.OnHTML(".container-bg_lQ801", func(element *colly.HTMLElement) { element.DOM.Find(".category-wrap_iQLoo").Each(func(index int, itemSelection *goquery.Selection) { contentSelection := itemSelection.ChildrenFiltered(".content_1YWBm") title := contentSelection.Find(".c-single-text-ellipsis").Text() href, _ := contentSelection.ChildrenFiltered("a").Attr("href") desc := contentSelection.Find(".hot-desc_1m_jR").First().Text() if len(desc) > 0 { desc = strings.ReplaceAll(desc, "查看更多>", "") } hot := itemSelection.Find(".trend_2RttY .hot-index_1Bl1a").Text() hs.Data = append(hs.Data, HotSearchDto{ Sort: index, Title: title, Url: href, Desc: desc, Hot: hot, }) }) }) } // WeiBoHotSearch 微博 type WeiBoHotSearch struct { BaseSearch } // BindHTMLSelector 微博 重写父类ajax获取数据 func (hs *WeiBoHotSearch) BindHTMLSelector() { hs.Collector.OnResponse(func(response *colly.Response) { if response.StatusCode == http.StatusOK { var tempMap = make(map[string]interface{}) err := json.Unmarshal(response.Body, &tempMap) if err != nil { log.Errorf("json反序列化失败:%v", err) } realtimeArr := tempMap["data"].(map[string]interface{})["realtime"].([]interface{}) for i, v := range realtimeArr { word := v.(map[string]interface{})["word"].(string) wordScheme := word wsi := v.(map[string]interface{})["word_scheme"] if wsi != nil { wordScheme = wsi.(string) } ci := v.(map[string]interface{})["category"] category := "分类" if ci != nil { category = ci.(string) } hot := v.(map[string]interface{})["num"].(float64) hs.Data = append(hs.Data, HotSearchDto{ Sort: i, Title: word, Url: "https://s.weibo.com/weibo?q=" + url.QueryEscape(wordScheme), Desc: fmt.Sprintf("%s: %s", category, word), Hot: strconv.Itoa(int(hot)), }) } return } log.Errorf("读取微博ajax接口失败:%s", string(response.Body)) }) } // 头条 type TouTiaoHotSearch struct { BaseSearch } func (hs *TouTiaoHotSearch) BindHTMLSelector() { hs.Collector.OnResponse(func(response *colly.Response) { if response.StatusCode == http.StatusOK { var tempMap = make(map[string]interface{}) err := json.Unmarshal(response.Body, &tempMap) if err != nil { log.Errorf("json反序列化失败:%v", err) } dataArr := tempMap["data"].([]interface{}) for i, v := range dataArr { title := v.(map[string]interface{})["Title"].(string) link := v.(map[string]interface{})["Url"].(string) hot := v.(map[string]interface{})["HotValue"].(string) labelInter := v.(map[string]interface{})["LabelDesc"] desc := title if labelInter != nil { desc = labelInter.(string) + ":" + desc } hs.Data = append(hs.Data, HotSearchDto{ Sort: i, Title: title, Url: link, Desc: desc, Hot: hot, }) } return } log.Errorf("读取头条ajax接口失败:%s", string(response.Body)) }) } // 知乎 type ZhiHuHotSearch struct { BaseSearch } func (hs *ZhiHuHotSearch) BindHTMLSelector() { hs.Collector.OnHTML(".Zd-p-Sc", func(element *colly.HTMLElement) { element.DOM.Find(".cc-dc-c tbody").First().Find("tr").Each(func(i int, selection *goquery.Selection) { title := selection.Find(".al a").Text() href, _ := selection.Find(".al a").Attr("href") hot := selection.Find("td:nth-child(3)").Text() hs.Data = append(hs.Data, HotSearchDto{ Sort: i, Title: title, Url: element.Request.AbsoluteURL(href), Desc: title, Hot: hot, }) }) }) }
前端核心代码 App.vue
<script setup> import {reactive} from 'vue' // import HelloWorld from './components/HelloWorld.vue' import txImg from './assets/images/tx.gif' import {Greet} from '../wailsjs/go/main/App' import { onMounted } from 'vue' import { PieChartOutlined, BarChartOutlined, DotChartOutlined, LineChartOutlined} from '@ant-design/icons-vue'; onMounted(() => { tabClick(0) }) const data = reactive({ activeKey: 0, image: [ "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png", txImg, "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png", txImg, "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png", ], hotData: { 0: [], 1: [], 2: [], 3: [] }, loading: false }) function handleMouse(e) { // e.preventDefault(); } function tabClick(index) { data.loading = true Greet(index).then(result => { console.log(result) data.loading = false data.hotData[index] = result }) } function urlClick(url) { window.runtime.BrowserOpenURL(url) return false } </script> <template> <div style="width: 100%; height: 100%;overflow: hidden;padding-bottom: 200px" @contextmenu="handleMouse"> <div style="text-align: center"> <a-image :width="200" :src="data.image[data.activeKey]"/> </div> <div style="padding: 10px;overflow: auto;height: 100%;"> <a-tabs v-model:activeKey="data.activeKey" type="card" @tabClick="tabClick"> <a-tab-pane :key="0"> <template #tab> <span> <pie-chart-outlined /> 百度 </span> </template> <a-list item-layout="horizontal" :data-source="data.hotData[0]" rowKey="sort" :loading="data.loading"> <template #renderItem="{ item }"> <a-list-item> <a-list-item-meta :description="item.desc"> <template #title> <a href="javascript:" @click="urlClick(item.url)">{{ item.title }}</a> </template> <template #avatar> <a-avatar v-if="item.sort < 3" style="background-color: red">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=3 && item.sort < 6" style="background-color: #f56a00">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=6 && item.sort < 9" style="background-color: #c8b50eff">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else>{{ item.sort + 1 }}</a-avatar> </template> </a-list-item-meta> <div>热度: <span v-if="item.sort < 3" style="color: red">{{ item.hot }}</span> <span v-else-if="item.sort >=3 && item.sort < 6" style="color: #f56a00">{{ item.hot }}</span> <span v-else-if="item.sort >=6 && item.sort < 9" style="color: #c8b50eff">{{ item.hot }}</span> <span v-else>{{ item.hot }}</span> </div> </a-list-item> </template> </a-list> </a-tab-pane> <a-tab-pane :key="1"> <template #tab> <span> <bar-chart-outlined /> 微博 </span> </template> <a-list item-layout="horizontal" :data-source="data.hotData[1]" rowKey="sort" :loading="data.loading"> <template #renderItem="{ item }"> <a-list-item> <a-list-item-meta :description="item.desc"> <template #title> <a href="javascript:" @click="urlClick(item.url)">{{ item.title }}</a> </template> <template #avatar> <a-avatar v-if="item.sort < 3" style="background-color: red">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=3 && item.sort < 6" style="background-color: #f56a00">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=6 && item.sort < 9" style="background-color: #c8b50eff">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else>{{ item.sort + 1 }}</a-avatar> </template> </a-list-item-meta> <div>热度: <span v-if="item.sort < 3" style="color: red">{{ item.hot }}</span> <span v-else-if="item.sort >=3 && item.sort < 6" style="color: #f56a00">{{ item.hot }}</span> <span v-else-if="item.sort >=6 && item.sort < 9" style="color: #c8b50eff">{{ item.hot }}</span> <span v-else>{{ item.hot }}</span> </div> </a-list-item> </template> </a-list> </a-tab-pane> <a-tab-pane :key="2"> <template #tab> <span> <dot-chart-outlined /> 头条 </span> </template> <a-list item-layout="horizontal" :data-source="data.hotData[2]" rowKey="sort" :loading="data.loading"> <template #renderItem="{ item }"> <a-list-item> <a-list-item-meta :description="item.desc"> <template #title> <a href="javascript:" @click="urlClick(item.url)">{{ item.title }}</a> </template> <template #avatar> <a-avatar v-if="item.sort < 3" style="background-color: red">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=3 && item.sort < 6" style="background-color: #f56a00">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=6 && item.sort < 9" style="background-color: #c8b50eff">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else>{{ item.sort + 1 }}</a-avatar> </template> </a-list-item-meta> <div>热度: <span v-if="item.sort < 3" style="color: red">{{ item.hot }}</span> <span v-else-if="item.sort >=3 && item.sort < 6" style="color: #f56a00">{{ item.hot }}</span> <span v-else-if="item.sort >=6 && item.sort < 9" style="color: #c8b50eff">{{ item.hot }}</span> <span v-else>{{ item.hot }}</span> </div> </a-list-item> </template> </a-list> </a-tab-pane> <a-tab-pane :key="3"> <template #tab> <span> <line-chart-outlined /> 知乎 </span> </template> <a-list item-layout="horizontal" :data-source="data.hotData[3]" rowKey="sort" :loading="data.loading"> <template #renderItem="{ item }"> <a-list-item> <a-list-item-meta :description="item.desc"> <template #title> <a href="javascript:" @click="urlClick(item.url)">{{ item.title }}</a> </template> <template #avatar> <a-avatar v-if="item.sort < 3" style="background-color: red">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=3 && item.sort < 6" style="background-color: #f56a00">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else-if="item.sort >=6 && item.sort < 9" style="background-color: #c8b50eff">{{ item.sort + 1 }}</a-avatar> <a-avatar v-else>{{ item.sort + 1 }}</a-avatar> </template> </a-list-item-meta> <div>热度: <span v-if="item.sort < 3" style="color: red">{{ item.hot }}</span> <span v-else-if="item.sort >=3 && item.sort < 6" style="color: #f56a00">{{ item.hot }}</span> <span v-else-if="item.sort >=6 && item.sort < 9" style="color: #c8b50eff">{{ item.hot }}</span> <span v-else>{{ item.hot }}</span> </div> </a-list-item> </template> </a-list> </a-tab-pane> </a-tabs> </div> </div> </template> <style> .ant-layout-header { background-color: #7cb305; } </style>
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。