Python爬虫实战——搭建自己的IP代理池

如今爬虫越来越多,一些网站网站加强反爬措施,其中最为常见的就是限制IP,对于爬虫爱好者来说,能有一个属于自己的IP代理池,在爬虫的道路上会减少很多麻烦

Bárbara Montavon

环境参数

工具 详情
服务器 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;">59</span><span style="display:inline-block;">.1</span><div style="display:inline-block;"></div><p style="display:none;">0</p><span>0</span><span style="display:inline-block;"></span><span style="display:inline-block;">8.</span><div style="display:inline-block;">12</div><span style="display:inline-block;">5</span><p style="display:none;"></p><span></span><p style="display:none;"></p><span></span><div style="display:inline-block;">.2</div><div style="display:inline-block;">41</div>:<span class="port GEGEA">8080</span></td>
<td><a title="高匿代理IP" style="color:red;" class="href">高匿</a></td>
<td><a title="http代理IP" class="href">http</a></td>
<td><a title="中国代理IP" class="href">中国</a>&nbsp;&nbsp;
<a title="北京代理IP" class="href">北京</a>&nbsp;&nbsp;
<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地址和端口号。
使用正则表达式的时候我们一般会有两种思路

  • 1.提取数字和点.
  • 2.过滤html标签,保留我们想要的数字和点

这里我们以第二种方法为例

soup = BeautifulSoup(html, 'html.parser')
data = soup.find('td', class_='ip')
res=re.compile('<p.*?/p>|<.*?>',re.S)
proxy=re.sub(res, '', str(data))
print(proxy)

# 59.108.125.241:8080

这个时候IP地址和端口号就提取出来了,当你把整个网页的代理地址都提取出来以后,你会发现没有一个可以使用的。

这是为什么呢?难道是代理商提供的免费代理都是垃圾,其实不然,细心的你可能会发现你匹配的端口和他们官网显示的端口号不一样,很显然他们的端口号是通过js动态加载的,遇到这种情况,我们一般也会想到2种解决方案

  • 使用selenium
  • 破解js
    如果加密方式复杂、js文件很多,无从下手时可以使用selenium,好在我们今天爬取的这个网站js文件不是很多,通过打断点,很容易定位到我们要解密的js文件,下面我就分享一下的我解决方法

破解js

  • 找到对应的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(c<a?\'\':e(1w(c/a)))+((c=c%a)>1s?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<d;g++){f[h$[11]](h$[12][h$[3]](c[g]))};$(t)[h$[2]](X[h$[13]](f[h$[14]](h$[15]))>>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文件

很显然上面这个文件是加密压缩过的,通过在线解密工具,两次解密以后我们得到这样一个方法,有点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存储,有以下几点原因:

  1. redis相比mysql、文件写入速度更快
  2. 使用redis里的集合特性,不用担心有重复数据
  3. 项目中使用了分布式爬虫,存到redis中方便多台服务器调用

IP代理池添加和维护

下面分为4个步骤来分享一下IP代理池的维护

  • 安装redis
    不同系统redis的安装方法不同,本文以Ubuntu为空
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(集合)

  • 添加IP代理地址到redis
# 导入模块
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')
  • 验证IP代理是否有效
    可以在存入的时候可以验证,也可以在取的时候验证代理是否有效,但是如果存入的时候就验证,取得时候有可能已经不能使用
# 导入模块
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代理

验证IP代理是否无效,如果代理地址无效,可以使用以下命令删除代理,这样可以保证我们代理池中的地址都是有效的

conn.redis.srem('proxy', '无效的IP代理地址')

最后把获取代理的步骤封装成一个方法,在需要代理的地方调用即可

到这里我们的代理池就搭建好了,如果感觉只有一个网站的数据不能我们使用,只需要多爬取几个免费代理及时维护就可以啦。

对于大多数爬虫初学者来说,其实爬取一个没有反爬的网站不是什么难事,无非就是把网站的源代码获取下来,然后使用bs4或者正则表达式来提取数据,这里我专门找来一个有反爬的网站,就是想让大家感受一下反爬的流程,当然这也是很简单的一个。

对于代理池的搭建记住三点即可:

  1. 添加IP代理
  2. 验证IP代理是否有效
  3. 及时删除无效代理

总结:本文用一半的篇幅再和大家分享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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,273评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,905评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,281评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,817评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,690评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,491评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,886评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,513评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,810评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,839评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,642评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,455评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,901评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,091评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,381评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,895评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,104评论 2 338

推荐阅读更多精彩内容