基于反爬策略的翻译词典demo | 青训营笔记

235 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

整个simpledic代码

其实这个小型词典的编写流程和基础的爬虫很像。 下面将以有道翻译为例,介绍整个爬取的过程。

1 爬虫基本流程

1.1 确定网址(接口)并对要提交的数据进行分析

首先需要确定我们发起请求的网址,在Network中,对我们想要的请求进行筛选,筛选顺序,分别按照从左到右的顺序,即 XHR、JS、CSS,最后为All。 其中XHR种为动态数据加载的信息。

在翻译时,因为我们要动态输入单词后才会得到结果,因此请求一般放在这个栏下。

xhr.png

1.2发送请求,爬取数据

请求一般分为get请求和post请求,在post请求中,我们需要在发起请求时提交一些数据,有时需要封装的数据过多,并且好多网站为了安全,许多需要封装的数据都通过js代码动态生成,因此我们需要对js代码进行分析,(js文件一般是请求后面的启动器中所指示的)得到这些要封装的数据。

image.png 发送请求,查看我们需要分析的数据

第一次请求

第一次请求.png

第二次请求

第二次请求.png

可以看出,在两次请求提交的数据中,salt、sign和lts不一样,因此,需要逐个分析他们的生成方式。 打开上述的js文件(小tips:点击左下角的{},即可看到正常的js代码显示,不然会是一行数据,很难看!) 查找salt字段,定位到以下代码:

流程1.png 逐步分析

var r = function(e) {
     var t = n.md5(navigator.appVersion) //t为对app版本号进行md5加密得到的
     , r = "" + (new Date).getTime() //r为时间戳
     , i = r + parseInt(10 * Math.random(), 10); //i为r与一个[0,10)以内的随机值的拼接
     return {
          ts: r,
          bv: t,
          salt: i,
          sign: n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")//sign为对字符串的md5加密得到的
      }
 };
  • ts值
  1. ts值为13位的时间戳,在go语言中,生成时间戳的代码为:(需要导入time包)
t := time.Now().UnixMilli() //以毫秒为单位,生成13位时间戳
b := time.Now().Unix()//以秒为单位,生成10位时间戳
  1. 再将得到的值转换成string型即可得到ts的值
r := strconv.FormatInt(t, 10)  

1)func FormatInt(i int64, base int) string

作用:将base进制的数字转为字符串

  • salt值 salt值为在ts基础上加上了一个[0,10)以内的随机数,在go语言中,随机数的生成代码为:(需要导入math/rand包)
rand.Seed(time.Now().UnixNano())     //生成随机种子,为生成随机数做准备
sufix := strconv.Itoa(rand.Intn(10)) //生成0-9之间的整数
i := r + sufix

1)func (r *Rand) Seed(seed int64)

作用:使用给定的seed来初始化生成器到一个确定的状态。

参数:传入一个值,如果想每次生成的随机数不同,则需要传入一个随机值

2)func Itoa(i int) string

作用:将十进制数字转为字符串,是FormatInt(i int64, 10)的简写

参数:传入一个十进制的数字

  • sign值
var r = function(e) {
    ...
    sign: n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")//sign为对字符串的md5加密得到的
}

e为我们传入的参数,即我们要翻译的单词。(可以通过打断点进行debug查看~)

因此,sign值为一个字符串的md5加密值,在go语言中,md5加密的代码为:(需要导入crypto/md5包)

var chuan = "fanyideskweb" + word + salt + "Ygy_4c=r#e#4EX^NUGUc5" //生成整个串
var arr = md5.Sum([]byte(chuan))//对字符串进行加密
sign := fmt.Sprintf("%x", arr)//加密之后返回的是byte数组,需转化为字符串

1)func Sum(data []byte) [Size]byte

作用:对byte数组进行md5加密操作,返回一个byte数组

2)func Sprintf(format string, a ...interface{}) string

作用:根据format参数生成格式化的字符串并返回该字符串。

参数:传入一个format参数和一个参数变量的列表。

----------------------------------------至此,所有的参数已经得到---------------------------------

上述即为获取post提交数据的过程,我们只需要封装数据,发送请求即可。

2 simpledict案例

2.1 利用工具生成代码

首先,有两个神奇的工具帮我们生成一些固定的代码

Convert curl commands to code (curlconverter.com)

JSON转Golang Struct - 在线工具 - OKTools

2.1.1 复制curl请求

复制curl.png

2.1.2 生成代码

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`i=花朵&from=AUTO&to=AUTO&smartresult=dict&client=fanyideskweb&salt=16521516931097&sign=77a461ace1b2ea9b01a028afad87e681&lts=1652151693109&bv=e70edeacd2efbca394a58b9e43a6ed2a&doctype=json&version=2.1&keyfrom=fanyi.web&action=FY_BY_REALTlME`)
	req, err := http.NewRequest("POST", "https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule", data)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"`)
	req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
	req.Header.Set("X-Requested-With", "XMLHttpRequest")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36")
	req.Header.Set("sec-ch-ua-platform", `"Windows"`)
	req.Header.Set("Origin", "https://fanyi.youdao.com")
	req.Header.Set("Sec-Fetch-Site", "same-origin")
	req.Header.Set("Sec-Fetch-Mode", "cors")
	req.Header.Set("Sec-Fetch-Dest", "empty")
	req.Header.Set("Referer", "https://fanyi.youdao.com/")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	req.Header.Set("Cookie", "OUTFOX_SEARCH_USER_ID=1709629224@10.108.160.100; OUTFOX_SEARCH_USER_ID_NCOO=178109166.30855334; _ntes_nnid=f6382c9da163857c60fd66ebca2e036d,1616070577146; fanyi-ad-id=305838; fanyi-ad-closed=1; JSESSIONID=aaa_JKyVTXwLT-XtG6Rcy; ___rl__test__cookies=1652151693107")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

2.1.3 测试代码运行情况

首先测试一下代码是否运行成功,再进行后续的更改。

运行成功1.png

2.1.4 包装自己的请求

这一步,我获取到每个数据的值,并对字符串进行了简单的拼接。

var data = strings.NewReader(`i=` + word + 
    `&from=AUTO
	&to=AUTO
	&smartresult=dict
	&client=fanyideskweb
	&salt=` + salt +
     `&sign=` + sign + 
	`&lts=` + ts + 
	`&bv=e70edeacd2efbca394a58b9e43a6ed2a
	&doctype=json&version=2.1
	&keyfrom=fanyi.web&action=FY_BY_REALTlME`)

2.1.5 创建response的结构体,通过复制网站生成的响应数据,用第二个工具生成。

复制响应.png

type DictResponse struct {
	TranslateResult [][]struct {
		Tgt string `json:"tgt"`
		Src string `json:"src"`
	} `json:"translateResult"`
	ErrorCode   int    `json:"errorCode"`
	Type        string `json:"type"`
	SmartResult struct {
		Entries []string `json:"entries"`
		Type    int      `json:"type"`
	} `json:"smartResult"`
}

2.1.6 筛选数据

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
		log.Fatal(err)
}
fmt.Print(word, " english: ")
for _, item := range dictResponse.TranslateResult {
	for _, coll := range item {
		fmt.Println(coll.Tgt)
	}
}

2.1.7 成功输出

成功.png