最近在分析一个显示盗版小说的网站,其反爬虫思路绝对值得记上一笔.
该网站的地址为 : 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
}