编码与加密在爬虫中经常涉及,常见的编码有base64, unicode, urlencode,常见的加密有MD5, SHA1, HMAC, DES, AES, RSA。
下面逐一介绍:
一,编码
1.1 常规编码
常规编码约定了字符集中字符与一定长度二进制的映射关系,字符集是指各国家的文字、标点符号、图形符号和数字等字符的集合,计算机要准确地处理不同字符集,就需对字符进行编码。这种编码包括ASCII,utf-8,GBK,unicode。
'lx'.encode() # 不指定编码格式时,会自动调用计算机的默认编码格式进行编码
'中国'.encode('utf-8').decode('GBK') # 编码与解码的格式不同,会导致报错
ASCII
ASCII是基于拉丁字母表的一套计算机编码系统,主要用于显示现代英语和其他西欧语言。ASCII编码实际上约定了字符和二进制的映射关系,如小写字母“a”对应的8位二进制数为01100001。因此,我们也可以将它看作二进制与拉丁字符的映射表。
ASCII的RFC文档编号为20(详见https://tools.ietf.org/html/rfc20),其中约定了ASCII的使用范围、标准码、字符表示和代码识别等内容。
ASCII码默认使用7位二进制数来表示所有的大写字母、小写字母、数字(0~9)、标点符号和特殊的控制符。
unicode
unicode被称为统一码、万国码,以\u、&#开头,python实现如下:
# str转为 Unicode 编码:
chinese_str = "四川省"
print(chinese_str.encode("unicode_escape")) # b'\\u56db\\u5ddd\\u7701'
# Unicode 编码转为str:
a = b'\\u56db\\u5ddd\\u7701'
print(a.decode('unicode_escape')) # 四川省
1.2 Base64
Base64在基于常规编码的基础上,将字符转为字节,然后将字节切分为6位的长度,并约定了6位字节与64个字符的对应关系。
Base64的出现是为了解决不可打印的字符(如非英文的字符)在网络传输过程中造成的乱码现象。Base64的RFC文档编号为4648,文档地址为https://tools.ietf.org/html/rfc4648。
将字符进行Base64编码时,首先要将字符转换成对应的ASCII码,然后得出8位二进制数,接着连接3个8位输入,形成字节数为24的输入组,再将24位输入组拆分成4组6位的二进制数,然后将6位二进制数转换为十进制数,最后找到十进制数在Base64编码表中对应的字符,并将这些字符组合成新的字符串,这个字符串就是编码结果。编码过程中用到的Base64编码表如图所示。
要注意的是,在编码过程中,如果字符位数少于24位,那么就需要进行特殊处理,也就是在编码结果的末尾用“=”符号填充。
我们可以通过一个例子来加深对Base64编码过程的理解。首先,我们将字符async转换成ASCII码,并找到对应的8位二进制数。字符、ASCII码和8位二进制数的对应值如图:
接着将3组8位二进制数连接成24位的输入组,再将24位输入组拆分成4组6位的二进制数。要注意的是,如果输入组的元素不足24位,那么就用0进行填充。24位输入组转换成6位二进制数的过程如图:
得到6位二进制数之后,我们还需要计算出对应的十进制。二进制转十进制其实是按权相加,将二进制数写成加权系数展开式,并按十进制加法规则求和。字符“a”对应的6位二进制数为011000,将其转换成十进制时,计算过程如图:
按照这个计算方法,计算其他的6位二进制数,最后得到字符“async”对应的十进制值:
24 23 13 57 27 38 12 65
补位字符“=”没有对应的值,本书约定其值为65。在得到所有的十进制值之后,就可以将其与RFC4648中的Base64编码表进行映射,从而得出编码后的字符串。映射过程如图:
最终得出字符“async”的Base64编码结果为“YXN5bmM=”,完整的编码过程如图:
Base64编码时所用的对照表是固定的,也就是说它的编码过程是可逆的。这意味着我们只需要将编码的流程倒置,就能够得解码的方法。Base64编码表中的 “+” 和 “/” 会影响文件编码和URI编码,我们在实际使用时,需要考虑到应用场景中是否包含文件编码或URI 。
如果在URI场景下使用Base64,就会引起错误,RFC4648文档中给出了一个解决办法:使用“-”和“_”替代“+”和“/”。
base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%。base64加密后的密文长度视原字符而定
base64加密后的密文大概率最后1位或2位是=,这就是它的特点;此外其他编码方法如果用16进制表示,字母要么是大写要么是小写,而bash64会有大小写同时出现的情况和+,/,且字母不仅仅是a-f
浏览器原生提供了base64的编码、解码方法:btoa atob
window.btoa('lx')
window.atob('bHg=')
Js实现如下:
<html>
<script type="text/javascript">
// 创建Base64对象
var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(e){var t="";var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f<e.length){n=e.charCodeAt(f++);r=e.charCodeAt(f++);i=e.charCodeAt(f++);s=n>>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t="";var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9+/=]/g,"");while(f<e.length){s=this._keyStr.indexOf(e.charAt(f++));o=this._keyStr.indexOf(e.charAt(f++));u=this._keyStr.indexOf(e.charAt(f++));a=this._keyStr.indexOf(e.charAt(f++));n=s<<2|o>>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/rn/g,"n");var t="";for(var n=0;n<e.length;n++){var r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r)}else if(r>127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t="";var n=0;var r=c1=c2=0;while(n<e.length){r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r);n++}else if(r>191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}}
// 定义字符串
var string = 'i am bobo!';
// 加密
var encodedString = Base64.encode(string);
alert(encodedString);
// 解密
var decodedString = Base64.decode(encodedString);
alert(decodedString);
</script>
</html>
nodejs实现如下:
var str1 = 'lx';
var str2 = 'bHg=';
var strToBase64 = new Buffer(str1).toString('base64');
var base64ToStr = new Buffer(str2, 'base64').toString();
python实现如下:
import base64
res = base64.b64encode('lx'.encode('utf-8'))
print(res) # b'bHg='
data = base64.b64decode('bHg='.encode())
origin = data.decode('utf-8')
print(origin) # 'lx'
爬虫工程师只需要按照Base64解码规则进行倒推,就能得到原字符。
爬虫工程师很轻松就拿到了原字符,这显然不是开发者想要见到的结果。其实,开发者还可以通过自定义编码规则的方式保护数据。只需要稍微改动一下Base64编码过程中用到的对照表,或者改动输入组的划分规则,就可以创造一个新的编码规则。
Base64编码和解码时都是将原本8位的二进制数转换成6位的二进制数。如果我们改动位数,将其设置为5位或者4位,那么就可以实现新的编码规则。
此时如果爬虫工程师使用Base64对该编码结果进行解码,那么他将无法得到正确的原字符。这不仅达到了保护数据的目的,还能够迷惑爬虫工程师,使其将时间花费在“Base64解码不成功”的问题上。
1.3 urlencode
urlencode 称为url编码、百分号编码;就是将字符串以URL编码,一种编码方式,主要为了解决url中中文乱码问题。
python实现如下:
- 传入参数类型:字典
- 功能:将存入的字典参数编码为URL查询字符串,即转换成以key1=value1&key2=value2的形式
例子1:url标准符号,数字字母
from urllib.parse import urlencode
base_url = "https://m.weibo.cn/api/container/getIndex?"
params1 = {"value": "english", "page": 1}
url1 = base_url + urlencode(params1)
print(urlencode(params1)) # value=english&page=1
print(url1) # https://m.weibo.cn/api/container/getIndex?value=english&page=1
例子2:汉字, /, &, =, URL编码转化为%xx的形式
from urllib.parse import urlencode
params2 = {
'name': "王二",
'extra': "/",
'special': '&',
'equal': '='}
base_url = "https://m.weibo.cn/api/container/getIndex?"
url2 = base_url + urlencode(params2)
print(urlencode(params2)) # name=%E7%8E%8B%E4%BA%8C&extra=%2F&special=%26&equal=%3D
print(url2) # https://m.weibo.cn/api/container/getIndex?name=name=%E7%8E%8B%E4%BA%8C&extra=%2F&special=%26&equal=%3D
例子3:以上两例子默认utf8编码,如果用gb2312编码,则需指定
from urllib.parse import urlencode
params2 = {
'name': "王二",
'extra': "/",
'special': '&',
'equal': '='}
base_url = "https://m.weibo.cn/api/container/getIndex?"
url2 = base_url + urlencode(params2, encoding='gb2312')
print(urlencode(params2, encoding='gb2312'))
print(url2)
# name=%CD%F5%B6%FE&extra=%2F&special=%26&equal=%3D
# https://m.weibo.cn/api/container/getIndex?name=%CD%F5%B6%FE&extra=%2F&special=%26&equal=%3D
二,加密
常见的加密算法基本分为这几类:
- 线性散列算法(签名算法)MD5 SHA
- 对称加密算法 AES DES
- 非对称加密算法 RSA
2.1 线性散列算法(签名算法)
2.1.1 MD5
Message-Digest Algorithm,消息摘要算法,一种被广泛使用的密码散列函数,它能够将任意长度的消息转换成128bit的消息摘要,这个特性被称为“压缩”。
相比Base64编码,MD5的运算过程要复杂很多。由于MD5在运算过程中使用了补位、追加和移位等操作,所以他人无法从输出结果倒推出输入字符,这个特性被称为**“不可逆”**,所以解密一般是通过暴力穷举,以及网站的接口实现解密。
MD5的RFC文档编号为1321,文档地址为https://tools.ietf.org/html/rfc1321。RFC1321约定了一些术语和符号,并描述了MD5算法的计算步骤和方法。
MD5的典型应用场景就是一致性验证,如文件一致性和信息一致性。在注册账号时的密码一般都是用的MD5加密。
md5加密后产生一个固定长度(大概率为32位)的数据,在线加密解密网址:https://md5jiami.bmcx.com/
字符串:123456 可加密成16位或32位
16位小写:49ba59abbe56e057
32位小写:e10adc39 49ba59abbe56e057 f20f883e 其实就是16位前后加了一些东西
MD系列的加密方式有很多,增加破解成本的方法举例:
- 使用一段无意义且随机的私匙进行MD5加密会生成一个加密串,称为串1,将要加密的的数据跟串1拼接,再进行一次MD5,这时会生成串2,将串2再次进行MD5加密,这时生成的串3就是加密后的数据
Js实现如下:
# Js可以通过crypto-js加密库进行算法实现
const CryptoJs = require('crypto-js');
let password = 'lx123';
let encPwd = CryptoJs.MD5(password).toString();
python实现如下:
import hashlib
obj = hashlib.md5() # 加盐:obj = hashlib.md5("salt".encode('utf-8'))
obj.update('lx'.encode('utf-8'))
v1 = obj.hexdigest() # fb0e22c79ac75679e9881e6ba183b354 十六进制
v2 = obj.digest() # b'\xfb\x0e"\xc7\x9a\xc7Vy\xe9\x88\x1ek\xa1\x83\xb3T' 二进制字节
2.1.2 SHA1
Secure Hash Algorithm,安全哈希算法,比MD5的安全性更强,对长度小于2^64位的消息,SHA1会产生一个160bit的消息摘要。一般在未高度混淆的Js代码中,SHA1加密的关键词就是sha1。
SHA在线加密解密网站:http://www.wetools.com/sha/
从代码上看,SHA1与MD5的实现很相似:
Js实现如下:
const CryptoJs = require('crypto-js');
let password = 'lx123';
let encPwd = CryptoJs.SHA1(password).toString();
python实现如下:
import hashlib
m = hashlib.sha1()
m.update("lx".encode("utf-8"))
m.hexdigest()
2.1.3 HMAC
Hash Message Authentication Code,散列消息鉴别码。实现原理是用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。
以HMAC中的SHA256加密为例:
Js实现如下:
const CryptoJs = require('crypto-js');
let key = 'key';
let text = 'lx';
let hash = CryptoJs.HmacSHA256(text, key);
let hashInHex = CryptoJs.enc.Hex.stringify(hash);
python实现如下:
import hmac
import hashlib
key = 'key'.encode()
text = 'lx'.encode()
mac = hmac.new(key, text, hashlib.sha256) # 或 hashlib.sha512
mac.digest()
mac.hexdigest()
2.2 对称加密算法
2.2.1 DES
Data Encryption Standard,数据加密标准,是使用密钥加密的算法。是一种对称加密方式,其加密、解密使用同样的密钥,密钥长度为56位。
破解方法:暴力破解。DES使用 56 位的密钥,则可能的密钥数量是 2 的 56 次方个,只要计算足够强大是可以被破解的。
DES算法的入口参数:Key、Data、Mode,padding
- Key 为7个字节共56位,是DES算法的工作密钥;
- Data 是要被加密或被解密的数据;
- Mode 为DES的工作方式;
- padding 为填充模式,如果加密后密文长度达不到指定整数倍(8个字节、16个字节),填充对应字符,padding的赋值固定为 CryptoJS.pad.Pkcs7 即可。
Js逆向时,DES加密的搜索关键词有DES,mode,padding等。
Js实现如下:(引用不同的包实现方式不同,这里举两例说明)
crypto-js.js:
var aseKey = "12345678" //定制秘钥,长度必须为:8/16/32B
var message = "i am bobo,who are you ?";
cfg = {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
asekey = CryptoJS.enc.Utf8.parse(aseKey)
//加密 DES/AES切换只需要修改 CryptoJS.DES <=> CryptoJS.AES
var encrypt = CryptoJS.DES.encrypt(message,asekey,cfg).toString();
// 0Gh9NGnwOpgmB525QS0JhVJlsn5Ev9cHbABgypzhGnM
//解密
var decrypt = CryptoJS.DES.decrypt(encrypt,asekey,cfg).toString(CryptoJS.enc.Utf8);
// i am bobo,who are you ?
aes.js:
<html>
<script src="static/plugins/aes/aes.js"></script>
<!-- 加密时需引入 pad-zeropadding.js -->
<script src="static/plugins/aes/pad-zeropadding-min.js"></script>
<script type="text/javascript">
// 加密
function encrypt(data,key,iv) { //key,iv:16B的字符串
var key = CryptoJS.enc.Latin1.parse(key);
var iv = CryptoJS.enc.Latin1.parse(iv);
return CryptoJS.AES.encrypt(data, key,{
iv : iv,
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.ZeroPadding
}).toString();
}
// 解密
function decrypt(data,key,iv){ //key,iv:16B的字符串
var key = CryptoJS.enc.Latin1.parse(key);
var iv = CryptoJS.enc.Latin1.parse(iv);
var decrypted=CryptoJS.AES.decrypt(data,key,{
iv : iv,
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.ZeroPadding
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
</script>
</html>
python实现:
pip install pycryptodome
当key与iv是普通字符串时:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def aes_encrypt(data_string):
key_string = "fd6b639dbcff0c2a1b03b389ec763c4b"
key = key_string.encode('utf-8')
iv_string = "77b07a672d57d64c"
iv = iv_string.encode('utf-8')
data = data_string.encode("utf-8")
aes = AES.new(
key=key,
mode=AES.MODE_CBC,
iv=iv
)
raw = pad(data, 16)
return aes.encrypt(raw)
data = "|878975262|d000035rirv|1631615607|mg3c3b04ba|1.3.5|ktjwlm89_to920weqpg|433070"
result = aes_encrypt(data)
print(result)
当key与iv是十六进制字符串时:
import binascii
v1 = "4E2918885FD98109869D14E0231A0BF4"
""" 自己实现
bs = bytearray() # []
for i in range(0, len(v1), 2):
item_hex = v1[i:i + 2]
item_int = int(item_hex, base=16)
bs.append(item_int)
v3 = bytes(bs)
print(v3) # b'N)\x18\x88_\xd9\x81\t\x86\x9d\x14\xe0#\x1a\x0b\xf4'
"""
# 调包实现
v3 = binascii.a2b_hex(v1)
print(v3) # b'N)\x18\x88_\xd9\x81\t\x86\x9d\x14\xe0#\x1a\x0b\xf4'
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii
def aes_encrypt(data_string):
key_string = "4E2918885FD98109869D14E0231A0BF4"
key = binascii.a2b_hex(key_string)
iv_string = "16B17E519DDD0CE5B79D7A63A4DD801C"
iv = binascii.a2b_hex(iv_string)
aes = AES.new(
key=key,
mode=AES.MODE_CBC,
iv=iv
)
raw = pad(data_string.encode('utf-8'), 16)
return aes.encrypt(raw)
data = "|878975262|d000035rirv|1631615607|mg3c3b04ba|1.3.5|ktjwlm89_to920weqpg"
result = aes_encrypt(data)
print(result)
2.2.2 AES
Advanced Encryption Standard,高级加密标准,这个标准用来替代原先的DES
AES和DES的区别:
-
加密后密文长度的不同:DES加密后密文长度是8的整数倍,AES加密后密文长度是16的整数倍
-
应用场景不同:企业级开发使用DES足够安全,如果要求高使用AES
AES的填充模式常用的有三种,分别是NoPadding,ZeroPadding,Pkcs7,默认为Pkcs7。
Js逆向时,AES加密的搜索关键词有AES,mode,padding等。
DES和AES切换只需要修改 CryptoJS.AES <=> CryptoJS.DES,故实现代码略。
2.3 非对称加密算法
2.3.1 RSA
RSA,全称Rivest-Shamir-Adleman,RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥),私有密钥(privatekey:简称私钥),公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法
注意:使用时都是使用公匙加密使用私匙解密。公匙可以公开,私匙自己保留。
Js代码中的RSA常见标志 setPublickey。
Js实现加密可以使用jsencrypt加密库,另外需要生成好公钥和私钥,可以到在线网站去生成:http://web.chacuo.net/netrsakeypair
<html>
<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js"></script>
<script type="text/javascript">
//公钥
var PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALyBJ6kZ/VFJYTV3vOC07jqWIqgyvHulv6us/8wzlSBqQ2+eOTX7s5zKfXY40yZWDoCaIGk+tP/sc0D6dQzjaxECAwEAAQ==-----END PUBLIC KEY-----';
//私钥
var PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAvIEnqRn9UUlhNXe84LTuOpYiqDK8e6W/q6z/zDOVIGpDb545NfuznMp9djjTJlYOgJogaT60/+xzQPp1DONrEQIDAQABAkEAu7DFsqQEDDnKJpiwYfUE9ySiIWNTNLJWZDN/Bu2dYIV4DO2A5aHZfMe48rga5BkoWq2LALlY3tqsOFTe3M6yoQIhAOSfSAU3H6jIOnlEiZabUrVGqiFLCb5Ut3Jz9NN+5p59AiEA0xQDMrxWBBJ9BYq6RRY4pXwa/MthX/8Hy+3GnvNw/yUCIG/3Ee578KVYakq5pih8KSVeVjO37C2qj60d3Ok3XPqBAiEAqGPvxTsAuBDz0kcBIPqASGzArumljkrLsoHHkakOfU0CIDuhxKQwHlXFDO79ppYAPcVO3bph672qGD84YUaHF+pQ-----END PRIVATE KEY-----';
//使用公钥加密
var encrypt = new JSEncrypt();//实例化加密对象
encrypt.setPublicKey(PUBLIC_KEY);//设置公钥
var encrypted = encrypt.encrypt('hello bobo!');//对指定数据进行加密
alert(encrypted)
//使用私钥解密
var decrypt = new JSEncrypt();
decrypt.setPrivateKey(PRIVATE_KEY);//设置私钥
var uncrypted = decrypt.decrypt(encrypted);//解密
alert(uncrypted);
</script>
</html>
python实现代码:实现代码较多,以后需要时自行搜索。