记一个网站的爬虫,并思考爬虫与反爬虫(golang)

这篇具有很好参考价值的文章主要介绍了记一个网站的爬虫,并思考爬虫与反爬虫(golang)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最近在分析一个显示盗版小说的网站,其反爬虫思路绝对值得记上一笔.
该网站的地址为 : https://www.bravonovel.life .是一个展示英文小说的网站.

开始,发现这个网站没有登录权限.打开就能看到内容,查看网页源代码.没有内容加密.所以内容都明文的显示在网页中.(自信的以为,简单)

于是开干,自信的网络请求、翻页、内容抓取、过滤、写入文件.以为搞定了

接下来,对比文件中的内容与网页上的内容,咦,怎么多出些内容?

无奈,只得重新打开html 源代码开始分析问题出在哪里,通过仔细分析源代码.一个一个发现代码中存在大量的反爬虫措施.一遍遍的修改代码.花费了大量精力,写下下面这个复杂的爬虫代码.但最终在一个新发现的反爬虫措施下.失去继续做下去的兴趣. 这里记录一下这些反爬虫措施.以备将来使用

反爬虫措施:

  • css 设置 display:none 属性,用不可见的页面元素欺骗爬虫(解决办法:读取标签的css样式)
  • 动态生成css文件.css的文件地址和内容隔一会儿就会变,且没有规律(解决办法:脚本动态获取css地址,并加载)
  • 故意混杂大量错误语法css,例如 : display: block padding display: none!important; (解决方法: 利用更准确的正则表达式匹配规则准确获取正确的样式)
  • 利用css加载顺序等样式优先级规则,反复覆盖样式.(解决办法:按照加载优先级先低后高规则,一步步顺藤摸瓜,最终确定元素的display属性)
  • 在内联css中存在类的css 和标签的css.内联class css 优先级高.标签css优先级低.(解决办法:将标签css作为默认值.class 作为高优先级)
  • display: block!important 强制生效的css (解决办法:增加记录是否为强制生效css字段.后引入的强制生效覆盖先引入的)
  • 利用绝对定位,把页面元素定位到屏幕外面很远的地方去.以达到不显示的目的 (golang练手项目,搞到这里,不想搞了,实在是太麻烦了)(解决办法:找到元素定位的css ,判断优先级后,如果定位熟悉的值很大,当作不可见元素)
  • 元素的高度和宽度被设置为 0 ,且该熟悉被反复覆盖(解决办法:与display:none 一样,顺藤摸瓜)
    猜测还有一些别的反爬虫措施.
    总结: 网页的作用是给用户显示内容,利用css的各种设置,达到页面元素不可见的目的,再使用各种优先级反复修改这一属性,再使用几种不同的方式,加以组合.于是便得到了对正常用户十分友好,但对爬虫极其难以理解的反爬虫网页

本项目的目的是练习刚刚学到的golang语言语法与特性

下面做简要介绍:
1.首先打开开始页面.
2.抓取下一页的地址,这里用协程开启一个递归,调用函数本身,将新地址当作参数传入. 这里有一个用于同步的WaitGroup,需要传入,因为要保证是同一个 WaitGroup ,所以这个参数需要用到指针
3.在开启协程后,开始分析网页.首先获取css地址.开启协程以加快速度.这里因为css的加载顺序决定 css属性的优先级.故将其顺序作为参数传入.
4.分析css信息,获取最终生效熟悉分别为 display:none; display:block;
display:none !important ; display:block!important 的四个数组
5.解析网页内容,标签有多个css class ,一个个的遍历,通过判断他们在步骤4中的哪一个.确定标签是否可见

总结: golang这门语言的协程.简洁高效.文章来源地址https://www.toymoban.com/news/detail-612552.html

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
)

func main(){
	targetHost := "https://www.bravonovel.life"
	startHref := "/love-after-one-night-stand-with-ceo-2503335.html"
	chapterDir := "./chapter"
	createDir(chapterDir)
	var wg sync.WaitGroup
	wg.Add(1)
	go action(targetHost,startHref,chapterDir+"/",&wg)
	wg.Wait()
	//time.Sleep(30*time.Second)
}

// 抓取一个页面,获取到下一章地址后,递归调用这个函数,抓取下一个地址
// 使用协程递归,将waitGroup的地址传入,方便子协程计数,
// 然后获取章节内容,并将章节内容写入文件中
func action(targetHost string,currentHref string,chapterDir string,wg *sync.WaitGroup){
	defer wg.Done()
	html := getPage(targetHost+currentHref)
	nextUrl :=getNextPageUrl(html)
	fmt.Println("Next:",nextUrl,"\n")
	if nextUrl != ""{
		wg.Add(1)
		go action(targetHost,nextUrl,chapterDir,wg)
	}
	noneClassArray,blockClassArray,noneImportantClassNameArray,blockImportantClassNameArray,tagVisibilityMap := getCssVisibility(targetHost,html)
	// 获取章节内容
	chapterContent := getPageContent(html,noneClassArray,blockClassArray,noneImportantClassNameArray,blockImportantClassNameArray,tagVisibilityMap)
	// 获取章节序号
	chapterNumber := getChapterNumber(html)
	// 将内容写入文件中
	writeChapterToFile(chapterContent,chapterNumber,chapterDir)
	fmt.Println("结束一个过程")
}

// 获取下一章的 Url 地址
func getNextPageUrl(html string) string{
	compile := regexp.MustCompile("<a .*?href=\"(.*?)\" id=\"next_chapter\">")
	matchArr := compile.FindStringSubmatch(html)
	if len(matchArr) > 0{
		return matchArr[1]
	}
	return ""
}

// 获取页面中的章节内容
func getPageContent(html string,noneClassArray []string , blockClassArray []string,noneImportantClassNameArray []string,blockImportantClassNameArray []string,tagVisibilityMap map[string]string) string{
	// 先获取整个内容的字符串,包含html等多余字符
	compile1 := regexp.MustCompile("<div id=\"chapter-content\".*?>([\\s\\S]*?)If you find any errors ")
	matchArr := compile1.FindStringSubmatch(html)
	chapterBox1 := ""
	if len(matchArr) > 0{
		chapterBox1 = matchArr[1]
	}
	// 过滤不可显示的内容
	w3tagCompile := regexp.MustCompile("<(\\w{3}) .*?class=\"(.*?)\".*?>([\\s\\S]*?)</\\w{3}>")
	w3tagArray :=w3tagCompile.FindAllStringSubmatch(chapterBox1,-1)
	visibleContent := ""
	if len(w3tagArray) > 0{
		fmt.Println("\n\n**************************************************\n\n")
		fmt.Println(len(noneClassArray),"noneClassArray:",noneClassArray)
		fmt.Println(len(noneImportantClassNameArray),"noneImportantClassNameArray:",noneImportantClassNameArray)
		fmt.Println(len(blockClassArray),"blockClassArray:",blockClassArray)
		fmt.Println(len(blockImportantClassNameArray),"blockImportantClassNameArray:",blockImportantClassNameArray)
		for i:=0; i<len(w3tagArray); i++{
			fmt.Println("\n\n**************************************************\n\n")
			tagNameString := w3tagArray[i][1]
			classString := w3tagArray[i][2]
			contentString := w3tagArray[i][3]
			classArray := strings.Split(classString," ")
			w3tagVisibility := "block"
			// 标签本身的css可见性
			_,ok := tagVisibilityMap[tagNameString]
			if ok{
				// 当前标签在页面样式css中有默认是否显示
				if tagVisibilityMap[tagNameString] == "block" {
					w3tagVisibility = "block"
				}
				if tagVisibilityMap[tagNameString] == "block!important" {
					w3tagVisibility = "block!important"
				}
				if tagVisibilityMap[tagNameString] == "none" {
					w3tagVisibility = "none"
				}
				if tagVisibilityMap[tagNameString] == "none!important" {
					w3tagVisibility = "none!important"
				}
			}

			// 遍历当前内容的类列表,最终确定当前元素是否可见
			for j := 0; j < len(classArray); j++{
				fmt.Println("class:",classArray[j])
				inNoneClass := inArrayString(classArray[j],noneClassArray)
				fmt.Println("inNoneClass",inNoneClass)
				if inNoneClass{
					// 当前类 css 为不可见
					if w3tagVisibility != "none!important" && w3tagVisibility != "block!important"{
						w3tagVisibility = "none"
					}
				}
				inNoneImportantClass := inArrayString(classArray[j],noneImportantClassNameArray)
				fmt.Println("inNoneImportantClass",inNoneImportantClass)
				if inNoneImportantClass{
					// 当前类 css 为不可见
					w3tagVisibility = "none!important"
				}
				inBlockClass := inArrayString(classArray[j],blockClassArray)
				fmt.Println("inBlockClass",inBlockClass)
				if inBlockClass{
					// 当前类 css 为可见
					if w3tagVisibility != "none!important" && w3tagVisibility != "block!important"{
						w3tagVisibility = "block"
					}
				}
				inBlockImportantClass := inArrayString(classArray[j],blockImportantClassNameArray)
				fmt.Println("inBlockImportantClass",inBlockImportantClass)
				if inBlockImportantClass{
					// 当前类 css 为可见
					w3tagVisibility = "block!important"
				}

			}
			fmt.Println(classArray)
			if w3tagVisibility == "block" || w3tagVisibility == "block!important"{
				// 当前标签内的内容是页面可见的
				visibleContent = visibleContent + contentString
				fmt.Println("可见:",contentString)
			}else{
				fmt.Println("不可见:",contentString)
			}
		}
	}
	// 替换 <br> 标签为 \n 换行符
	brCompile := regexp.MustCompile("<br.*?>")
	chapterBox2 :=brCompile.ReplaceAllString(visibleContent,"\n")
	// 删除所有html 标签
	tagsCompile := regexp.MustCompile("<[\\s\\S]*?>")
	chapterBox3 := tagsCompile.ReplaceAllString(chapterBox2,"")
	// 连续多个空格或者制表符,只保留一个空格
	tabSpaceCompile := regexp.MustCompile("( +\\t*)+")
	chapterBox4 := tabSpaceCompile.ReplaceAllString(chapterBox3," ")
	// 换行符,统一为两个连续的换行
	wrapCompile := regexp.MustCompile("( *\\n *)+")
	chapterBox5 := wrapCompile.ReplaceAllString(chapterBox4,"\n\n")
	fmt.Println(chapterBox5)
	return chapterBox5
}

func getChapterNumber(html string) string{
	chapterNumberCompile := regexp.MustCompile(`class="chapter-title" .*?title=".*?Chapter (\d+).*?"`)
	chapterNumberArray :=chapterNumberCompile.FindStringSubmatch(html)
	if len(chapterNumberArray) > 0{
		chapterNumber := chapterNumberArray[1]
		return chapterNumber
	}
	return ""
}

func createDir(dirPath string){
	_,err := os.Stat(dirPath)
	if err != nil{
		if os.IsNotExist(err){
			err2 := os.Mkdir(dirPath,0755)
			if err2 != nil{ }
		}
	}
}

func writeChapterToFile(content string,chapterNum string,dirPath string) bool{
	filePath := dirPath + chapterNum + ".txt"
	err := ioutil.WriteFile(filePath,[]byte(content),0666)
	if err != nil{
		return false
	}
	return true
}

// 获取网页中的css 样式,分析获得这些css样式中的类,是否网页可见
func getCssVisibility(targetHost string,html string) ([] string,[] string,[] string,[] string,map[string]string){
	cssCompile := regexp.MustCompile("<link rel=\"stylesheet\" href=\"(/static/css/\\w{3}.css)\" />")
	cssPathArray := cssCompile.FindAllStringSubmatch(html,-1)
	noneClassNameArray := []string{}
	noneImportantClassNameArray := []string{}
	blockClassNameArray := []string{}
	blockImportantClassNameArray := []string{}
	tagVisibilityMap := map[string]string{}
	if len(cssPathArray) > 0{
		var (
				classMap map[string]map[string]string
				mutex sync.Mutex
			)
		mutex.Lock()
		classMap = make(map[string]map[string]string)
		mutex.Unlock()
		var cssWg sync.WaitGroup
		maxIndex := 0;
		for i:=0; i<len(cssPathArray); i++{
			maxIndex = i
			path := cssPathArray[i][1]
			currentUrl := targetHost + path
			cssWg.Add(1)
			go func(index int) {
			//func(index int) {
				// 该网页引入的css 中有相同 class ,按照后引入覆盖先引入的规则,i = index 的值越大 class 越生效
				// 所以拿到 class 后 ,没有值则写入,如果存在,判断优先级,如果当前优先级高,则覆盖,否则丢掉这一条
				defer cssWg.Done()
				// 1 删除所有换行
				print(currentUrl)
				cssPage := getPage(currentUrl)
				wrapCompile := regexp.MustCompile(`\r?\n`)
				tmpCssContent1 := wrapCompile.ReplaceAllString(cssPage,"")
				//tmpCssContent1 := strings.ReplaceAll(cssPage,"\n","")
				// 2 删除被注释掉的内容
				noteCompile := regexp.MustCompile(`/\*.*?\*/`)
				tmpCssContent2 := noteCompile.ReplaceAllString(tmpCssContent1,"")
				// 3 重新生成换行,每个class一行
				classCompile := regexp.MustCompile("(\\.\\w{3} *\\{.*?\\})")
				tmpCssContent3 := classCompile.ReplaceAllString(tmpCssContent2,"$1 \n")
				fmt.Println("\n\n####################################################\n\n")
				fmt.Println(currentUrl)
				fmt.Println("\n")
				fmt.Println(tmpCssContent3)
				fmt.Println("\n\n####################################################\n\n")
				displayCompile := regexp.MustCompile("\\.(\\w{3}) *.*?(?:\\{|;) *display: *(none|block|block *!important|none *!important) *;.*?}")
				displayClassArray := displayCompile.FindAllStringSubmatch(tmpCssContent3,-1)
				for j:=0; j<len(displayClassArray); j++{
					className := displayClassArray[j][1]
					classType := displayClassArray[j][2]
					classType = strings.ReplaceAll(classType," ","")
					mutex.Lock()
					_,ok := classMap[className]
					mutex.Unlock()
					if ok{
						// class 存在,比较index的大小
						oldIndex,err :=strconv.Atoi(classMap[className]["index"])
						if err != nil{
							oldIndex = 0
						}
						if classMap[className]["type"] != "none!important" && classMap[className]["type"] != "block!important"{
							if (index >= oldIndex) || (classType == "none!important" || classType == "block!important"){
								// 旧数据不是强制css的情况下,权重大或者是强制css时,覆盖
								mutex.Lock()
								classMap[className]["type"] = classType
								classMap[className]["index"] = strconv.Itoa(index)
								mutex.Unlock()
							}
						}else{
							if (classType == "none!important" || classType == "block!important") && (index >= oldIndex){
								// 旧数据已经是强制css,如果当前也是强制css,且权重比旧有的更大,覆盖
								mutex.Lock()
								classMap[className]["type"] = classType
								classMap[className]["index"] = strconv.Itoa(index)
								mutex.Unlock()
							}
						}
					}else{
						// class 不存在
						mutex.Lock()
						classMap[className] = map[string]string{"type":classType,"index":strconv.Itoa(index)}
						mutex.Unlock()
					}
				}
			}(i)
		}
		//cssWg.Wait()
		// 处理内嵌式css
		innerCssCompile := regexp.MustCompile("<style>([\\s\\S]*?)</style>")
		innerCssArray := innerCssCompile.FindAllStringSubmatch(html,-1)
		for i:=0; i<len(innerCssArray); i++{
			index := maxIndex + i + 1
			innerCss := innerCssArray[i][1]
			wrapCompile := regexp.MustCompile("\r?\n")
			tmpCssContent1 := wrapCompile.ReplaceAllString(innerCss,"")
			// 2 删除被注释掉的内容
			noteCompile := regexp.MustCompile("/\\*.*?\\*/")
			tmpCssContent2 := noteCompile.ReplaceAllString(tmpCssContent1,"")
			// 3 重新生成换行,每个class一行
			classCompile := regexp.MustCompile("(\\.*\\w{3} *\\{.*?\\})")
			tmpCssContent3 := classCompile.ReplaceAllString(tmpCssContent2,"$1 \n")
			fmt.Println("TT ###########################################################")
			fmt.Println(tmpCssContent3)
			fmt.Println("TT ###########################################################")
			displayCompile := regexp.MustCompile("\\.(\\w{3}) *.*?(?:\\{|;) *display: *(none|block|block *!important|none *!important) *;.*?}")
			displayClassArray := displayCompile.FindAllStringSubmatch(tmpCssContent3,-1)
			for j:=0; j<len(displayClassArray); j++{
				className := displayClassArray[j][1]
				classType := displayClassArray[j][2]
				classType = strings.ReplaceAll(classType," ","")
				_,ok := classMap[className]
				if ok{
					// class 存在,除非是强制css 否则内联css 的优先级一定大于链接式的
					if (classMap[className]["type"] != "none!important" && classMap[className]["type"] != "block!important")||(classType == "none!important" || classType == "block!important"){
						classMap[className]["type"] = classType
						classMap[className]["index"] = strconv.Itoa(index)
					}
				}else{
					// class 不存在
					classMap[className] = map[string]string{"type":classType,"index":strconv.Itoa(index)}
				}
			}
			// 处理标签 css 样式
			tagCssCompile := regexp.MustCompile(`[^ ](\w{3})(?:\{|\{.*?; *)display *: *(block|none|block *!important|none *!important) *;`)
			tagCssArray := tagCssCompile.FindAllStringSubmatch(tmpCssContent3,-1)
			fmt.Println(tagCssArray)
			for j:=0; j<len(tagCssArray);j++{
				tagName := tagCssArray[j][1]
				classType := tagCssArray[j][2]
				classType = strings.ReplaceAll(classType," ","")
				if classType == "none"{
					tagVisibilityMap[tagName] = "none"
				}
				if classType == "block"{
					tagVisibilityMap[tagName] = "block"
				}
				if classType == "none!important"{
					tagVisibilityMap[tagName] = "none!important"
				}
				if classType == "block!important"{
					tagVisibilityMap[tagName] = "block!important"
				}
			}
			fmt.Println("tagVisibilityMap",tagVisibilityMap)
		}
		fmt.Println(classMap)
		for key := range classMap{
			if classMap[key]["type"] == "block"{
				blockClassNameArray = append(blockClassNameArray,key)
			}
			if classMap[key]["type"] == "block!important"{
				blockImportantClassNameArray = append(blockImportantClassNameArray,key)
			}
			if classMap[key]["type"] == "none"{
				noneClassNameArray = append(noneClassNameArray,key)
			}
			if classMap[key]["type"] == "none!important"{
				noneImportantClassNameArray = append(noneImportantClassNameArray,key)
			}
		}
	}
	fmt.Println(noneClassNameArray,blockClassNameArray)
	return noneClassNameArray,blockClassNameArray,noneImportantClassNameArray,blockImportantClassNameArray,tagVisibilityMap
}

// 进行网络请求,获取网页内容
func getPage(url string) string{
	fmt.Println(url+"\n")
	response,err := http.Get(url)
	if err != nil{
		fmt.Println("请求错误:",err)
		return ""
	}
	defer response.Body.Close()
	body,err := ioutil.ReadAll(response.Body)
	if err != nil{
		fmt.Println("body读取错误:",err)
		return ""
	}
	bodyString := string(body)
	return bodyString
}

// 判断字符串是否在字符串数组中
func inArrayString(target string,strArray []string) bool{
	sort.Strings(strArray)
	index := sort.SearchStrings(strArray,target)
	if index < len(strArray) && strArray[index] == target{
		return true
	}
	return false
}

到了这里,关于记一个网站的爬虫,并思考爬虫与反爬虫(golang)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【爬虫与反爬虫】从技术手段与原理深度分析

    【作者主页】: 吴秋霖 【作者介绍】:Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作! 【作者推荐】:对JS逆向感兴趣的朋友可以关注《爬虫JS逆向实战》,对分布式爬虫平台感兴趣的朋友可以关注《分布式爬虫平台搭建

    2024年02月05日
    浏览(34)
  • C语言爬虫采集图书网站百万数据

    最近需要查阅一些资料,只给到相关项目名称以及,想通过图书文库找到对应书籍,那么怎么才能在百万数据库中找到自己需要的文献呢? 今天我依然用C语言写个爬虫程序,从百万数据库中查找到适合的文章,能节省很多事情。 下面是一个简单的C#爬虫程序,它使用

    2024年01月21日
    浏览(46)
  • 爬虫项目(10):白嫖抓第三方网站接口,基于Flask搭建搭建一个AI内容识别平台

    在数据驱动的时代,人工智能生成的内容变得越来越普遍。对于内容创作者和分析师来说,区分AI生成的内容与人类生成的内容变得尤为重要。在这篇文章中,我们将介绍一个项目,该项目使用 Flask 和 Requests 库来模拟对 writer.com 的 AI 内容检测功能的访问。 地址:https://nice

    2024年01月16日
    浏览(41)
  • R语言如何写一个爬虫代码模版

    R语言爬虫是利用R语言中的网络爬虫包,如XML、RCurl、rvest等,批量自动将网页的内容抓取下来。在进行R语言爬虫之前,需要了解HTML、XML、JSON等网页语言,因为正是通过这些语言我们才能在网页中提取数据。 在爬虫过程中,需要使用不同的函数来实现不同的功能,例如使用

    2024年02月06日
    浏览(39)
  • Golang不同平台编译的思考

    $GOOS可选值如下: darwin dragonfly freebsd linux netbsd openbsd plan9 solaris windows $GOARCH可选值如下 386 amd64 arm 在编译的时候我们可以根据实际需要对这两个参数进行组合。更详细的说明可以进官网看看 下面是实际使用。在Linux系统下跨平台编译。

    2024年02月09日
    浏览(36)
  • 一个开源的基于golang开发的企业级物联网平台

    SagooIOT是一个基于golang开发的开源的企业级物联网基础开发平台。负责设备管理和协议数据管理,支持跨平台的物联网接入及管理方案,平台实现了物联网开发相关的基础功能,基于该功能可以快速的搭建起一整套的IOT相关的业务系统。旨在通过可复用的组件,减少开发工作

    2024年02月07日
    浏览(76)
  • Scala语言用Selenium库写一个爬虫模版

    首先,我将使用Scala编写一个使用Selenium库下载yuanfudao内容的下载器程序。 然后我们需要在项目的build.sbt文件中添加selenium的依赖项。以下是添加Selenium依赖项的代码: 接下来,我们需要创建一个Selenium的WebDriver对象,以便我们可以使用它来控制浏览器。以下是如何创建WebDri

    2024年02月05日
    浏览(38)
  • Swift语言配合HTTP写的一个爬虫程序

    下段代码使用Embassy库编写一个Swift爬虫程序来爬取jshk的内容。我会使用proxy_host为duoip,proxy_port为8000的爬虫IP服务器。 使用Embassy库编写一个Swift爬虫程序可以实现从网页上抓取数据的功能。下面是一个简单的步骤: 1、首先,需要在Xcode中创建一个新的Swift项目。 2、然后,需

    2024年02月05日
    浏览(50)
  • 用java语言写一个网页爬虫 用于获取图片

    以下是一个简单的Java程序,用于爬取网站上的图片并下载到本地文件夹: 这个程序首先读取指定网址的HTML源码,然后从中提取出所有的图片URL。最后,程序利用 Java 的 IO 功能下载这些图片并保存到指定的本地文件夹中。 需要注意的是,该程序只是一个简单的演示,实际使

    2024年02月11日
    浏览(48)
  • R语言使用HTTP爬虫IP写一个程序

    R语言爬虫是指使用R语言编写程序,自动从互联网上获取数据的过程。在R语言中,可以使用三个主要的包(XML、RCurl、rvest)来实现爬虫功能。了解HTML等网页语言对于编写爬虫程序也非常重要,因为这些语言是从网页中提取数据的关键。网页语言通常是树形结构,只要理解了

    2024年02月06日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包