300行Go代码实现钉钉或微信的网页解析功能

我们平时在使用微信或者钉钉聊天时,当我们发送一个网址到微信或钉钉聊天框时,微信或钉钉会帮我们进行解析,生成一个卡片式的网址预览样式,闲来无事,我用golang试着写了一个能够实现类似功能的代码。

需求分析

  • 首先分析钉钉展示卡片样式如下:
    图片描述
    卡片内容包含:网址标题、网页描述、网页地址、网页的icon等信息。
  • 所以我们需要从网址中解析出以上信息,明白了需求,现在就开始撸代码了。

话不多说直接上代码,设计思路在代码中有注释描述:

package main

import (
	"fmt"
	"git.forchange.cn/manage/boss/dto"
	"github.com/PuerkitoBio/goquery"
	"golang.org/x/text/encoding/simplifiedchinese"
	"net/http"
	"net/url"
	"strings"
)

const (
	GBK     string = "GBK"
	UTF8    string = "UTF8"
	UNKNOWN string = "UNKNOWN"
)

const (
	href         = "href"
	content      = "content"
	defaultTitle = "无标题"
	defaultDesc  = "无描述"
	defaultIcon  = "https://www.baidu.com/favicon.ico"
)

type UrlParse struct {
	Link  string `json:"link"`  // 链接地址
	Title string `json:"title"` // 链接标题
	Desc  string `json:"desc"`  // 链接描述
	Image string `json:"image"` // 链接封面
}

func UrlCrawler(link string) (urlParse UrlParse, err error) {
	// 替换为http请求
	link = strings.ReplaceAll(link, "https://", "http://")
	urlParse.Link = link
	// 请求html页面
	res, err := http.Get(link)
	if err != nil {
		return
	}
	defer res.Body.Close()
	if res.StatusCode != 200 {
		return
	}
	// 加载 HTML document对象
	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		return
	}
	// 获取网站标题
	title, err := getTitle(doc)
	if err != nil {
		return
	}
	urlParse.Title = title

	// 获取网站描述
	desc, err := getDesc(doc)
	if err != nil {
		return
	}
	urlParse.Desc = desc

	// 获取网站封面
	icon, err := getIcon(doc, link)
	if err != nil {
		return
	}
	urlParse.Image = icon
	return
}

func getTitle(doc *goquery.Document) (title string, err error) {
	title = doc.Find("title").Text()
	if title == "" {
		title = getElementVal(doc, "meta[property='og:title']", content)
	}
	if title == "" {
		title = getElementVal(doc, "meta[name='og:title']", content)
	}
	if title == "" {
		title = getElementVal(doc, "meta[property='twitter:title']", content)
	}
	if title == "" {
		title = getElementVal(doc, "meta[name='twitter:title']", content)
	}
	if title == "" {
		// 没有获取到,设置默认值
		title = defaultTitle
		return
	}
	// UTF-8转换
	title, err = transformUtf8(title)
	if err != nil {
		return
	}
	// 去除多余的空字符
	title = strings.TrimSpace(title)
	return
}

func getDesc(doc *goquery.Document) (desc string, err error) {
	desc = getElementVal(doc, "meta[name='description']", content)
	if desc == "" {
		desc = getElementVal(doc, "meta[property='og:description']", content)
	}
	if desc == "" {
		desc = getElementVal(doc, "meta[name='og:description']", content)
	}
	if desc == "" {
		desc = getElementVal(doc, "meta[property='twitter:description']", content)
	}
	if desc == "" {
		desc = getElementVal(doc, "meta[name='twitter:description']", content)
	}
	if desc == "" {
		desc = doc.Find("div").Text()
	}
	if desc == "" {
		// 没有获取到,设置默认值
		desc = defaultDesc
		return
	}
	// UTF-8转换
	desc, err = transformUtf8(desc)
	if err != nil {
		return
	}
	desc = strings.ReplaceAll(desc, "\n", "")
	desc = strings.TrimSpace(desc)
	descRunes := []rune(desc)
	if len(descRunes) >= 150 {
		desc = string(descRunes[:150])
	}
	return
}

func getIcon(doc *goquery.Document, link string) (icon string, err error) {
	icon = getElementVal(doc, "link[rel='shortcut icon']", href)
	if icon == "" {
		icon = getElementVal(doc, "link[rel='SHORTCUT ICON']", href)
	}
	if icon == "" {
		icon = getElementVal(doc, "link[rel='icon']", href)
	}
	if icon == "" {
		// 当没有获取到图片地址时,设置默认图片
		icon = defaultIcon
		return
	}
	// http开头,直接退出
	if strings.HasPrefix(icon, "http") {
		return
	}
	// 如果以'//'开头,则拼接上'http:'前缀
	if strings.HasPrefix(icon, "//") {
		icon = "http:" + icon
	} else {
		// 既不是以'http'开头,也不是'//'开头,则需要拼接上网站的域名
		urlDetail, err := url.Parse(link)
		if err != nil {
			return icon, err
		}
		icon = urlDetail.Scheme + "://" + urlDetail.Hostname() + icon
	}
	return
}

func getElementVal(doc *goquery.Document, selector, key string) (val string) {
	elements := doc.Find(selector)
	if len(elements.Nodes) > 0 {
		attr := elements.Get(0).Attr
		for _, attribute := range attr {
			if attribute.Key == key {
				val = attribute.Val
				return
			}
		}
	}
	return
}

func transformUtf8(sourceStr string) (utf8Str string, err error) {
	descBytes := []byte(sourceStr)
	coding := GetStrCoding(descBytes)
	// 判断是否是utf-8
	if coding != UTF8 {
		// 将gbk转换为utf-8
		descBytes, err = simplifiedchinese.GBK.NewDecoder().Bytes(descBytes)
		if err != nil {
			return
		}
		utf8Str = string(descBytes)
	} else {
		utf8Str = sourceStr
	}
	return
}

func isGBK(data []byte) bool {
	length := len(data)
	var i = 0
	for i < length {
		if data[i] <= 0x7f {
			// 编码0~127,只有一个字节的编码,兼容ASCII码
			i++
			continue
		} else {
			// 大于127的使用双字节编码,落在gbk编码范围内的字符
			if data[i] >= 0x81 &&
				data[i] <= 0xfe &&
				data[i+1] >= 0x40 &&
				data[i+1] <= 0xfe &&
				data[i+1] != 0xf7 {
				i += 2
				continue
			} else {
				return false
			}
		}
	}
	return true
}

func isUtf8(data []byte) bool {
	i := 0
	for i < len(data) {
		if (data[i] & 0x80) == 0x00 {
			// 0XXX_XXXX
			i++
			continue
		} else if num := preNUm(data[i]); num > 2 {
			// 110X_XXXX 10XX_XXXX
			// 1110_XXXX 10XX_XXXX 10XX_XXXX
			// 1111_0XXX 10XX_XXXX 10XX_XXXX 10XX_XXXX
			// 1111_10XX 10XX_XXXX 10XX_XXXX 10XX_XXXX 10XX_XXXX
			// 1111_110X 10XX_XXXX 10XX_XXXX 10XX_XXXX 10XX_XXXX 10XX_XXXX
			// preNUm() 返回首个字节的8个bits中首个0bit前面1bit的个数,该数量也是该字符所使用的字节数
			i++
			for j := 0; j < num-1; j++ {
				// 判断后面的 num - 1 个字节是不是都是10开头
				if (data[i] & 0xc0) != 0x80 {
					return false
				}
				i++
			}
		} else {
			// 其他情况说明不是utf-8
			return false
		}
	}
	return true
}

func preNUm(data byte) int {
	var mask byte = 0x80
	var num = 0
	//8bit中首个0bit前有多少个1bits
	for i := 0; i < 8; i++ {
		if (data & mask) == mask {
			num++
			mask = mask >> 1
		} else {
			break
		}
	}
	return num
}

// 需要说明的是,isGBK()是通过双字节是否落在gbk的编码范围内实现的,
// 而utf-8编码格式的每个字节都是落在gbk的编码范围内,
// 所以只有先调用isUtf8()先判断不是utf-8编码,再调用isGBK()才有意义
func GetStrCoding(data []byte) string {
	if isUtf8(data) == true {
		return UTF8
	} else if isGBK(data) == true {
		return GBK
	} else {
		return UNKNOWN
	}
}

func main() {
	// go字符串编码为utf-8
	str := "月色真美,风也温柔,233333333,~!@#"
	// 打印转换前的字符串
	fmt.Println("before convert:", str)

	// 判断是否是utf-8
	fmt.Println("coding:", GetStrCoding([]byte(str)))

	// 使用官方库将utf-8转换为gbk
	gbkData, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(str))
	// 乱码字符串
	fmt.Println("gbk直接打印会出现乱码:", string(gbkData), err)
	// 判断是否是gbk
	fmt.Println("coding:", GetStrCoding(gbkData))

	// 将gbk再转换为utf-8
	utf8Data, err := simplifiedchinese.GBK.NewDecoder().Bytes(gbkData)
	// 判断是否是utf-8
	fmt.Println("coding:", GetStrCoding(utf8Data), err)
	// 打印转换后的字符串
	fmt.Println("after convert:", string(utf8Data))

	// 解析网址信息
	fmt.Println(UrlCrawler("https://www.baidu.com"))
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值