如今爬虫越来越多,一些网站网站加强反爬措施,其中最为常见的就是限制IP,对于爬虫爱好者来说,能有一个属于自己的IP代理池,在爬虫的道路上会减少很多麻烦
环境参数
工具 | 详情 |
---|---|
服务器 | Ubuntu |
编辑器 | Pycharm |
第三方库 | requests、bs4、redis |
搭建背景
之前用Scrapy写了个抓取新闻网站的项目,今天突然发现有一个网站的内容爬不下来了,通过查看日志发现是IP被封,于是就有了这篇文章。
思路
一般出售IP代理的都会提供一些免费代理,既然是免费的就不要浪费,我们只要把免费的代理爬下了,及时维护和更新就可以把免费的变成我们自己的代理池
编写爬虫
搜索免费代理会有很多结果,一般情况大部分都可以使用,这里以其中一家代理为例,打开代理网站以后,首先通过浏览器查看代码,然后分析代码开始编写爬虫
网站源代码
...
<tr class="success">
<td class="ip"><div style="display:inline-block;">div>
<span style="display:inline-block;">59span><span style="display:inline-block;">.1span><div style="display:inline-block;">div><p style="display:none;">0p><span>0span><span style="display:inline-block;">span><span style="display:inline-block;">8.span><div style="display:inline-block;">12div><span style="display:inline-block;">5span><p style="display:none;">p><span>span><p style="display:none;">p><span>span><div style="display:inline-block;">.2div><div style="display:inline-block;">41div>:<span class="port GEGEA">8080span>td>
<td><a title="高匿代理IP" style="color:red;" class="href">高匿a>td>
<td><a title="http代理IP" class="href">httpa>td>
<td><a title="中国代理IP" class="href">中国a>
<a title="北京代理IP" class="href">北京a>
<a title="北京代理IP" class="href">北京a> td><td><a title="方正宽带代理IP" class="href">方正宽带a>td>
<td>2.786 秒td><td>7分钟前td><td style="color: green; font-weight: bold;">11天td>tr>
....
通过上面一条数据可以看出,提供者为防止网站被爬取还是做了一些防范措施,但是我们可以使用正则表达式取出IP地址和端口号。
使用正则表达式的时候我们一般会有两种思路
这里我们以第二种方法为例
soup = BeautifulSoup(html, 'html.parser')
data = soup.find('td', class_='ip')
res=re.compile('|<.*?>' ,re.S)
proxy=re.sub(res, '', str(data))
print(proxy)
# 59.108.125.241:8080
这个时候IP地址和端口号就提取出来了,当你把整个网页的代理地址都提取出来以后,你会发现没有一个可以使用的。
这是为什么呢?难道是代理商提供的免费代理都是垃圾,其实不然,细心的你可能会发现你匹配的端口和他们官网显示的端口号不一样,很显然他们的端口号是通过js动态加载的,遇到这种情况,我们一般也会想到2种解决方案
破解js
可以给网站中的每个js文件打断点,一步步调试找出影响数据的js文件,通过调试我找到这样一个文件
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('1M(17(p,a,c,k,e,r){e=17(c){18(c1s?1b.1r(c+1q):c.1v(1u))};19(!\'\'.1a(/^/,1b)){1c(c--)r[e(c)]=k[c]||e(c);k=[17(e){18 r[e]}];e=17(){18\'\\\\w+\'};c=1};1c(c--)19(k[c])p=p.1a(1t 1y(\'\\\\b\'+e(c)+\'\\\\b\',\'g\'),k[c]);18 p}(\'i h$=[\\\'\\\\E\\\\n\\\\x\\\\s\\\\j\\\',"\\\\l\\\\m\\\\v\\\\o","\\\\o\\\\j\\\\G\\\\p","\\\\r\\\\q\\\\H\\\\l\\\\I\\\\J\\\\K",\\\'\\\\M\\\',"\\\\m\\\\j\\\\j\\\\s",\\\'\\\\v\\\\p\\\\m\\\\k\\\\k\\\',"\\\\k\\\\n\\\\p\\\\r\\\\j","\\\\O","","\\\\p\\\\l\\\\q\\\\Q\\\\j\\\\o","\\\\n\\\\R\\\\k\\\\o",\\\'\\\\S\\\\T\\\\V\\\\z\\\\A\\\\B\\\\C\\\\D\\\\u\\\\F\\\',"\\\\n\\\\m\\\\s\\\\k\\\\l\\\\u\\\\q\\\\j","\\\\16\\\\x\\\\r\\\\q",\\\'\\\'];$(y(){$(h$[0])[h$[1]](y(){i a=$(t)[h$[2]]();L(a[h$[3]](h$[4])!=-w){N};i b=$(t)[h$[5]](h$[6]);P{b=(b[h$[7]](h$[8]))[w];i c=b[h$[7]](h$[9]);i d=c[h$[10]];i f=[];U(i g=W;g>Y)}Z(e){}})})\',1A,1B,\'|||||||||||||||||1C|1x|1z|1p|1h|1i|1d|1e|1f|1m|1n|1o|1g|1k|1l|1j|1W|17|1X|1Y|1V|1S|1T|1U|1Z|23|25|24|20|21|19|22|18|1R|1H|1I|1J|1G|1D|1E|1F|1O|1P|1Q|1N|||||||1K\'.1L(\'|\'),0,{}))' ,62,130,'|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||function|return|if|replace|String|while|x70|x68|x6c|this|x65|x61|0x1|x49|x63|x6e|x69|x72|x73|29|fromCharCode|35|new|36|toString|parseInt|var|RegExp|x74|62|69|_|x42|for|x43|x41|try|x67|x75|x6a|split|eval|catch|0x0|window|0x3|x20|x47|x48|x2e|x46|x6f|x44|x45|x5a|x4f|x66|x2a|x6d|x78|x64'.split('|'),0,{}))
很显然上面这个文件是加密压缩过的,通过在线解密工具,两次解密以后我们得到这样一个方法,有点js基础的同学应该能看的懂,但是还是不够直观,因为这个方法首先定义了一个数组,每个变量都是用数组切片的方式代替,所以一眼很难看出加密方式
var _$ = ['\x2e\x70\x6f\x72\x74', "\x65\x61\x63\x68", "\x68\x74\x6d\x6c", "\x69\x6e\x64\x65\x78\x4f\x66", '\x2a', "\x61\x74\x74\x72", '\x63\x6c\x61\x73\x73', "\x73\x70\x6c\x69\x74", "\x20", "", "\x6c\x65\x6e\x67\x74\x68", "\x70\x75\x73\x68", '\x41\x42\x43\x44\x45\x46\x47\x48\x49\x5a', "\x70\x61\x72\x73\x65\x49\x6e\x74", "\x6a\x6f\x69\x6e", ''];
$(function() {
$(_$[0])[_$[1]](function() {
var a = $(this)[_$[2]]();
if (a[_$[3]](_$[4]) != -0x1) {
return
};
var b = $(this)[_$[5]](_$[6]);
try {
b = (b[_$[7]](_$[8]))[0x1];
var c = b[_$[7]](_$[9]);
var d = c[_$[10]];
var f = [];
for (var g = 0x0; g < d; g++) {
f[_$[11]](_$[12][_$[3]](c[g]))
};
$(this)[_$[2]](window[_$[13]](f[_$[14]](_$[15])) >> 0x3)
} catch (e) {}
})
})
通过对数组的拆分,你会发现上面方法的核心内容可以简化成这样
var f = [];
var c="GEGEA".split("");
for (var g = 0; g < c.length; g++) {
f.push('ABCDEFGHIZ'.indexOf(c[g]))
};
我来解释一下这个代码片段,首先"GEGEA"这个值是怎么来的? 这个值不是固定的,而是网页源码中class=‘port GEGEA’ port的同级class,获取到这个class以后,先把它转为数组,判断数组中的每个元素在’ABCDEFGHIZ’中的位置,会得到一个类似这样的数组[6, 4, 6, 4, 0],再把这个新数组转为字符串,然后位移,就可以得到真实的端口号,所以可以把解密函数简化成这样
// 定义一个数组,用于记录class在'ABCDEFGHIZ'出现的位置
var f = [];
// 把class转为一个数组
var c="GEGEA".split("");
// 根据数组的长度记录数组中每个元素在'ABCDEFGHIZ'出现的位置
for (var g = 0; g < c.length; g++) {
f.push('ABCDEFGHIZ'.indexOf(c[g]))
};
// 把数组转为字符串,再进行运算
var port=f.join('')>>0x3
// 得到真实的端口号
console.log(port)
如果上面的js解密步骤你已经理解,接下来用python重写一下这个解密步骤很会简单很多,具体代码如下所示
# port_class 是源代码port的同级class
def parse_port(self,port_class):
string = 'ABCDEFGHIZ'
arr = list(port_class)
lists = []
for x in range(0, len(arr)):
lists.append(string.find(arr[x]))
ports = ''.join(str(x) for x in lists)
return int(ports) >> 3
这段python代码和上面的js代码逻辑一致,效果也一样,只不过是用python翻译了一遍。
到这里爬虫的难点我们都解决了,现在要做的是把爬取下来的代理存储到redis里面。
至于为什么用redis存储,有以下几点原因:
IP代理池添加和维护
下面分为4个步骤来分享一下IP代理池的维护
apt-get install redis-server
redis 安装好以后会自动安装一个客户端redis-cli,我们可以通过redis-cli对数据的增删改查,比如:
# 进入客户端
redis-cli
# 添加一条数据
set name 'hello world'
# 获取name的值
get name
但是我们总不能把IP代理地址一个个手动添加到redis里,所以我们还要安装一个python操作redis的模块
这个模块名刚好也叫redis
pip install redis
模块安装好我们就可以通过python管理redis里的数据了
redis有5种数据类型分别为:string(字符串),hash(哈希),list(列表),set(集合)及zset(有序集合),我们这里主要使用set(集合)
# 导入模块
import redis
# 连接到Redis服务器
conn = redis.Redis(host='127.0.0.1', port=6379)
# 添加数据 key 可以更加自己的需求设置
conn.sadd('proxy','119.179.0.1:8083')
# 随机取出一条代理地址
conn.redis.srandmember('proxy')
# 导入模块
import redis
import requests
# 连接到Redis服务器
conn = redis.Redis(host='127.0.0.1', port=6379)
# 随机取出一条代理数据
ip=conn.redis.srandmember('proxy')
print(ip)
url='https://www.baidu.com'
proxies = {
"http": "http://" + ip.decode("utf-8")
}
# 使用IP代理访问百度,测试代理地址是否有效
try:
data = requests.get(url=url, proxies=proxies, timeout=5)
except:
# 代理地址无效
验证IP代理是否无效,如果代理地址无效,可以使用以下命令删除代理,这样可以保证我们代理池中的地址都是有效的
conn.redis.srem('proxy', '无效的IP代理地址')
最后把获取代理的步骤封装成一个方法,在需要代理的地方调用即可
到这里我们的代理池就搭建好了,如果感觉只有一个网站的数据不能我们使用,只需要多爬取几个免费代理及时维护就可以啦。
对于大多数爬虫初学者来说,其实爬取一个没有反爬的网站不是什么难事,无非就是把网站的源代码获取下来,然后使用bs4或者正则表达式来提取数据,这里我专门找来一个有反爬的网站,就是想让大家感受一下反爬的流程,当然这也是很简单的一个。
对于代理池的搭建记住三点即可:
总结:本文用一半的篇幅再和大家分享JS破解的步骤,对于没有JS基础的同学看起来会有点吃力,但是通过python的解密步骤,可能会让你对JS加密流程有个大致了解。reids的操作可以参考具体文档
JS在线解密工具
https://tool.lu/js/
参考文档
https://pypi.org/project/redis/
https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html#
爬取网站
http://www.goubanjia.com
源代码
https://github.com/iyuyoung/proxy_pool