记一个网站的爬虫,并思考爬虫与反爬虫(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这门语言的协程.简洁高效.

go 复制代码
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
}
相关推荐
Hello.Reader5 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默16 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Code哈哈笑25 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
程序猿进阶29 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184431 分钟前
shell 编程(二)
开发语言·bash·shell
charlie1145141911 小时前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满1 小时前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程
ELI_He9991 小时前
PHP中替换某个包或某个类
开发语言·php
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
倔强的石头1061 小时前
【C++指南】类和对象(九):内部类
开发语言·c++