├── PyV8破解JS加密Cookie ├── README.md ├── __init__.py └── code.py ├── README.md ├── bdtb_spdier └── 爬虫代理IP池 └── README.md /PyV8破解JS加密Cookie/README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 |   在GitHub上维护了一个[代理池](https://github.com/jhao104/proxy_pool)的项目,代理来源是抓取一些免费的代理发布网站。上午有个小哥告诉我说有个代理抓取接口不能用了,返回状态521。抱着帮人解决问题的心态去跑了一遍代码。发现果真是这样。 4 | 5 |   通过Fiddler抓包比较,基本可以确定是JavaScript生成加密Cookie导致原来的请求返回521。 6 | 7 | ## 发现问题 8 | 9 |   打开Fiddler软件,用浏览器打开目标站点(http://www.kuaidaili.com/proxylist/2/) 。可以发现浏览器对这个页面加载了两次,第一次返回521,第二次才正常返回数据。很多没有写过网站或是爬虫经验不足的童鞋,可能就会觉得奇怪为什么会这样?为什么浏览器可能正常返回数据而代码却不行? 10 | ![请求两次](http://ofcf9jxzt.bkt.clouddn.com/blog/python/%E7%A0%B4%E8%A7%A3JSp1.png "请求两次") 11 |   仔细观察两次返回的结果可以发现: 12 | ![第一次请求](http://ofcf9jxzt.bkt.clouddn.com/blog/python/%E7%A0%B4%E8%A7%A3JSp2.png "第一次请求") 13 | ![第二次请求](http://ofcf9jxzt.bkt.clouddn.com/blog/python/%E7%A0%B4%E8%A7%A3JSp3.png "第二次请求") 14 | 15 |   1、第二次请求比第一次请求的Cookie内容多了个这个`_ydclearance=0c316df6ea04c5281b421aa8-5570-47ae-9768-2510d9fe9107-1490254971` 16 | 17 |   2、第一次返回的内容一些复杂看不懂的JS代码,第二次返回的就是正确的内容 18 | 19 |   其实这是网站反爬虫的常用手段。大致过程是这样的:首次请求数据时,服务端返回动态的混淆加密过的JS,而这段JS的作用是给Cookie添加新的内容用于服务端验证,此时返回的状态码是521。浏览器带上新的Cookie再次请求,服务端验证Cookie通过返回数据(这也是为嘛代码不能返回数据的原因)。 20 | 21 | ## 解决问题 22 | 23 |   其实我第一次遇到这样的问题是,一开始想的就是既然你是用JS生成的Cookie, 那么我也可以将JS函数翻译成Python运行。但是最后还是发现我太傻太天真,因为现在的JS都流行混淆加密,原始的JS这样的: 24 | ```JavaScript 25 | function lq(VA) { 26 | var qo, mo = "", no = "", oo = [0x8c, 0xcd, 0x4c, 0xf9, 0xd7, 0x4d, 0x25, 0xba, 0x3c, 0x16, 0x96, 0x44, 0x8d, 0x0b, 0x90, 0x1e, 0xa3, 0x39, 0xc9, 0x86, 0x23, 0x61, 0x2f, 0xc8, 0x30, 0xdd, 0x57, 0xec, 0x92, 0x84, 0xc4, 0x6a, 0xeb, 0x99, 0x37, 0xeb, 0x25, 0x0e, 0xbb, 0xb0, 0x95, 0x76, 0x45, 0xde, 0x80, 0x59, 0xf6, 0x9c, 0x58, 0x39, 0x12, 0xc7, 0x9c, 0x8d, 0x18, 0xe0, 0xc5, 0x77, 0x50, 0x39, 0x01, 0xed, 0x93, 0x39, 0x02, 0x7e, 0x72, 0x4f, 0x24, 0x01, 0xe9, 0x66, 0x75, 0x4e, 0x2b, 0xd8, 0x6e, 0xe2, 0xfa, 0xc7, 0xa4, 0x85, 0x4e, 0xc2, 0xa5, 0x96, 0x6b, 0x58, 0x39, 0xd2, 0x7f, 0x44, 0xe5, 0x7b, 0x48, 0x2d, 0xf6, 0xdf, 0xbc, 0x31, 0x1e, 0xf6, 0xbf, 0x84, 0x6d, 0x5e, 0x33, 0x0c, 0x97, 0x5c, 0x39, 0x26, 0xf2, 0x9b, 0x77, 0x0d, 0xd6, 0xc0, 0x46, 0x38, 0x5f, 0xf4, 0xe2, 0x9f, 0xf1, 0x7b, 0xe8, 0xbe, 0x37, 0xdf, 0xd0, 0xbd, 0xb9, 0x36, 0x2c, 0xd1, 0xc3, 0x40, 0xe7, 0xcc, 0xa9, 0x52, 0x3b, 0x20, 0x40, 0x09, 0xe1, 0xd2, 0xa3, 0x80, 0x25, 0x0a, 0xb2, 0xd8, 0xce, 0x21, 0x69, 0x3e, 0xe6, 0x80, 0xfd, 0x73, 0xab, 0x51, 0xde, 0x60, 0x15, 0x95, 0x07, 0x94, 0x6a, 0x18, 0x9d, 0x37, 0x31, 0xde, 0x64, 0xdd, 0x63, 0xe3, 0x57, 0x05, 0x82, 0xff, 0xcc, 0x75, 0x79, 0x63, 0x09, 0xe2, 0x6c, 0x21, 0x5c, 0xe0, 0x7d, 0x4a, 0xf2, 0xd8, 0x9c, 0x22, 0xa3, 0x3d, 0xba, 0xa0, 0xaf, 0x30, 0xc1, 0x47, 0xf4, 0xca, 0xee, 0x64, 0xf9, 0x7b, 0x55, 0xd5, 0xd2, 0x4c, 0xc9, 0x7f, 0x25, 0xfe, 0x48, 0xcd, 0x4b, 0xcc, 0x81, 0x1b, 0x05, 0x82, 0x38, 0x0e, 0x83, 0x19, 0xe3, 0x65, 0x3f, 0xbf, 0x16, 0x88, 0x93, 0xdd, 0x3b]; 27 | qo = "qo=241; do{oo[qo]=(-oo[qo])&0xff; oo[qo]=(((oo[qo]>>3)|((oo[qo]<<5)&0xff))-70)&0xff;} while(--qo>=2);"; 28 | eval(qo); 29 | qo = 240; 30 | do { 31 | oo[qo] = (oo[qo] - oo[qo - 1]) & 0xff; 32 | } while (--qo >= 3); 33 | qo = 1; 34 | for (; ;) { 35 | if (qo > 240) break; 36 | oo[qo] = ((((((oo[qo] + 2) & 0xff) + 76) & 0xff) << 1) & 0xff) | (((((oo[qo] + 2) & 0xff) + 76) & 0xff) >> 7); 37 | qo++; 38 | } 39 | po = ""; 40 | for (qo = 1; qo < oo.length - 1; qo++) if (qo % 6) po += String.fromCharCode(oo[qo] ^ VA); 41 | eval("qo=eval;qo(po);"); 42 | } 43 | ``` 44 | 45 |   看到这样的JS代码,我只能说原谅我JS能力差,还原不了。。。 46 | 47 |   但是前端经验丰富的童鞋马上就能想到还有种方法可解,那就是利用浏览器的JS代码调试功能。这样一切就迎刃而解,新建一个html文件,将第一次返回的html原文复制进去,保存用浏览器打开,在eval之前打上断点,看到这样的输出: 48 | 49 | ![加密JS代码调试](http://ofcf9jxzt.bkt.clouddn.com/blog/python/%E7%A0%B4%E8%A7%A3JSp4.png "加密JS代码调试") 50 | 51 |   可以看到这个变量po为`document.cookie='_ydclearance=0c316df6ea04c5281b421aa8-5570-47ae-9768-2510d9fe9107-1490254971; expires=Thu, 23-Mar-17 07:42:51 GMT; domain=.kuaidaili.com; path=/'; window.document.location=document.URL`,下面还有个`eval("qo=eval;qo(po);")`。JS里面的eval和Python的差不多,第二句的意思就是将eval方法赋给qo。然后去eval字符串po。而字符串po的前半段的意思是给浏览器添加Cooklie,后半段`window.document.location=document.URL`是刷新当前页面。 52 | 53 |   这也印证了我上面的说法,首次请求没有Cookie,服务端回返回一段生成Cookie并自动刷新的JS代码。浏览器拿到代码能够成功执行,带着新的Cookie再次请求获取数据。而Python拿到这段代码就只能停留在第一步。 54 | 55 |   那么如何才能使Python也能执行这段JS呢,答案是PyV8。V8是Chromium中内嵌的javascript引擎,号称跑的最快。PyV8是用Python在V8的外部API包装了一个python壳,这样便可以使python可以直接与javascript操作。PyV8的安装大家可以自行百度。 56 | 57 | ## 代码 58 | 59 |   分析完成,下面切入正题撸代码。 60 | 61 |   首先是正常请求网页,返回带加密的JS函数的html: 62 | ```python 63 | import re 64 | import PyV8 65 | import requests 66 | 67 | TARGET_URL = "http://www.kuaidaili.com/proxylist/1/" 68 | 69 | 70 | def getHtml(url, cookie=None): 71 | header = { 72 | "Host": "www.kuaidaili.com", 73 | 'Connection': 'keep-alive', 74 | 'Cache-Control': 'max-age=0', 75 | 'Upgrade-Insecure-Requests': '1', 76 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36', 77 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 78 | 'Accept-Encoding': 'gzip, deflate, sdch', 79 | 'Accept-Language': 'zh-CN,zh;q=0.8', 80 | } 81 | html = requests.get(url=url, headers=header, timeout=30, cookies=cookie).content 82 | return html 83 | # 第一次访问获取动态加密的JS 84 | first_html = getHtml(TARGET_URL) 85 | 86 | ``` 87 |   由于返回的是html,并不单纯的JS函数,所以需要用正则提取JS函数的参数的参数。 88 | 89 | ![第一次返回内容](http://ofcf9jxzt.bkt.clouddn.com/blog/python/%E7%A0%B4%E8%A7%A3JSp5.png "第一次返回内容") 90 | 91 | ```python 92 | # 提取其中的JS加密函数 93 | js_func = ''.join(re.findall(r'(function .*?)', first_html)) 94 | 95 | print 'get js func:\n', js_func 96 | 97 | # 提取其中执行JS函数的参数 98 | js_arg = ''.join(re.findall(r'setTimeout\(\"\D+\((\d+)\)\"', first_html)) 99 | 100 | print 'get ja arg:\n', js_arg 101 | ``` 102 | 103 |   还有一点需要注意,在JS函数中并没有返回cookie,而是直接将cookie set到浏览器,所以我们需要将`eval("qo=eval;qo(po);")`替换成`return po`。这样就能成功返回po中的内容。 104 | ```python 105 | def executeJS(js_func_string, arg): 106 | ctxt = PyV8.JSContext() 107 | ctxt.enter() 108 | func = ctxt.eval("({js})".format(js=js_func_string)) 109 | return func(arg) 110 | 111 | # 修改JS函数,使其返回Cookie内容 112 | js_func = js_func.replace('eval("qo=eval;qo(po);")', 'return po') 113 | 114 | # 执行JS获取Cookie 115 | cookie_str = executeJS(js_func, js_arg) 116 | ``` 117 | 118 |   这样返回的cookie是字符串格式,但是用requests.get()需要字典形式,所以将其转换成字典: 119 | 120 | ```python 121 | def parseCookie(string): 122 | string = string.replace("document.cookie='", "") 123 | clearance = string.split(';')[0] 124 | return {clearance.split('=')[0]: clearance.split('=')[1]} 125 | 126 | # 将Cookie转换为字典格式 127 | cookie = parseCookie(cookie_str) 128 | ``` 129 |   最后带上解析出来的Cookie再次访问网页,成功获取数据: 130 | ``` 131 | # 带上Cookie再次访问url,获取正确数据 132 | print getHtml(TARGET_URL, cookie)[0:500] 133 | ``` 134 | 135 |   下面是完整[代码](https://github.com/jhao104/memory-notes/blob/master/Python/Python%E7%88%AC%E8%99%AB%E2%80%94%E7%A0%B4%E8%A7%A3JS%E5%8A%A0%E5%AF%86%E7%9A%84Cookie.md): 136 | ```python 137 | # -*- coding: utf-8 -*- 138 | """ 139 | ------------------------------------------------- 140 | File Name: demo_1.py.py 141 | Description : Python爬虫—破解JS加密的Cookie 快代理网站为例:http://www.kuaidaili.com/proxylist/1/ 142 | Document: 143 | Author : JHao 144 | date: 2017/3/23 145 | ------------------------------------------------- 146 | Change Activity: 147 | 2017/3/23: 破解JS加密的Cookie 148 | ------------------------------------------------- 149 | """ 150 | __author__ = 'JHao' 151 | 152 | import re 153 | import PyV8 154 | import requests 155 | 156 | TARGET_URL = "http://www.kuaidaili.com/proxylist/1/" 157 | 158 | 159 | def getHtml(url, cookie=None): 160 | header = { 161 | "Host": "www.kuaidaili.com", 162 | 'Connection': 'keep-alive', 163 | 'Cache-Control': 'max-age=0', 164 | 'Upgrade-Insecure-Requests': '1', 165 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36', 166 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 167 | 'Accept-Encoding': 'gzip, deflate, sdch', 168 | 'Accept-Language': 'zh-CN,zh;q=0.8', 169 | } 170 | html = requests.get(url=url, headers=header, timeout=30, cookies=cookie).content 171 | return html 172 | 173 | 174 | def executeJS(js_func_string, arg): 175 | ctxt = PyV8.JSContext() 176 | ctxt.enter() 177 | func = ctxt.eval("({js})".format(js=js_func_string)) 178 | return func(arg) 179 | 180 | 181 | def parseCookie(string): 182 | string = string.replace("document.cookie='", "") 183 | clearance = string.split(';')[0] 184 | return {clearance.split('=')[0]: clearance.split('=')[1]} 185 | 186 | 187 | # 第一次访问获取动态加密的JS 188 | first_html = getHtml(TARGET_URL) 189 | 190 | # first_html = """ 191 | # 192 | # """ 193 | 194 | # 提取其中的JS加密函数 195 | js_func = ''.join(re.findall(r'(function .*?)', first_html)) 196 | 197 | print 'get js func:\n', js_func 198 | 199 | # 提取其中执行JS函数的参数 200 | js_arg = ''.join(re.findall(r'setTimeout\(\"\D+\((\d+)\)\"', first_html)) 201 | 202 | print 'get ja arg:\n', js_arg 203 | 204 | # 修改JS函数,使其返回Cookie内容 205 | js_func = js_func.replace('eval("qo=eval;qo(po);")', 'return po') 206 | 207 | # 执行JS获取Cookie 208 | cookie_str = executeJS(js_func, js_arg) 209 | 210 | # 将Cookie转换为字典格式 211 | cookie = parseCookie(cookie_str) 212 | 213 | print cookie 214 | 215 | # 带上Cookie再次访问url,获取正确数据 216 | print getHtml(TARGET_URL, cookie)[0:500] 217 | 218 | ``` -------------------------------------------------------------------------------- /PyV8破解JS加密Cookie/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: __init__.py.py 5 | Description : 6 | Author : JHao 7 | date: 2017/3/24 8 | ------------------------------------------------- 9 | Change Activity: 10 | 2017/3/24: 11 | ------------------------------------------------- 12 | """ 13 | __author__ = 'JHao' 14 | 15 | import sys 16 | 17 | reload(sys) 18 | sys.setdefaultencoding('utf-8') 19 | 20 | if __name__ == '__main__': 21 | pass -------------------------------------------------------------------------------- /PyV8破解JS加密Cookie/code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ------------------------------------------------- 4 | File Name: demo_1.py.py 5 | Description : Python爬虫—破解JS加密的Cookie 快代理网站为例:http://www.kuaidaili.com/proxylist/1/ 6 | Document: 7 | Author : JHao 8 | date: 2017/3/23 9 | ------------------------------------------------- 10 | Change Activity: 11 | 2017/3/23: 破解JS加密的Cookie 12 | ------------------------------------------------- 13 | """ 14 | __author__ = 'JHao' 15 | 16 | import re 17 | import PyV8 18 | import requests 19 | 20 | TARGET_URL = "http://www.kuaidaili.com/proxylist/1/" 21 | 22 | 23 | def getHtml(url, cookie=None): 24 | header = { 25 | "Host": "www.kuaidaili.com", 26 | 'Connection': 'keep-alive', 27 | 'Cache-Control': 'max-age=0', 28 | 'Upgrade-Insecure-Requests': '1', 29 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36', 30 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 31 | 'Accept-Encoding': 'gzip, deflate, sdch', 32 | 'Accept-Language': 'zh-CN,zh;q=0.8', 33 | } 34 | html = requests.get(url=url, headers=header, timeout=30, cookies=cookie).content 35 | return html 36 | 37 | 38 | def executeJS(js_func_string, arg): 39 | ctxt = PyV8.JSContext() 40 | ctxt.enter() 41 | func = ctxt.eval("({js})".format(js=js_func_string)) 42 | return func(arg) 43 | 44 | 45 | def parseCookie(string): 46 | string = string.replace("document.cookie='", "") 47 | clearance = string.split(';')[0] 48 | return {clearance.split('=')[0]: clearance.split('=')[1]} 49 | 50 | 51 | # 第一次访问获取动态加密的JS 52 | first_html = getHtml(TARGET_URL) 53 | 54 | # first_html = """ 55 | # 56 | # """ 57 | 58 | # 提取其中的JS加密函数 59 | js_func = ''.join(re.findall(r'(function .*?)', first_html)) 60 | 61 | print 'get js func:\n', js_func 62 | 63 | # 提取其中执行JS函数的参数 64 | js_arg = ''.join(re.findall(r'setTimeout\(\"\D+\((\d+)\)\"', first_html)) 65 | 66 | print 'get ja arg:\n', js_arg 67 | 68 | # 修改JS函数,使其返回Cookie内容 69 | js_func = js_func.replace('eval("qo=eval;qo(po);")', 'return po') 70 | 71 | # 执行JS获取Cookie 72 | cookie_str = executeJS(js_func, js_arg) 73 | 74 | # 将Cookie转换为字典格式 75 | cookie = parseCookie(cookie_str) 76 | 77 | print cookie 78 | 79 | # 带上Cookie再次访问url,获取正确数据 80 | print getHtml(TARGET_URL, cookie)[0:500] 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spider 2 | 3 | > 爬虫相关的一些东西 4 | 5 | 欢迎大家贡献 6 | -------------------------------------------------------------------------------- /bdtb_spdier: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 百度贴吧帖子抓取 3 | """ 4 | import urllib2 5 | import json 6 | import os 7 | from lxml import etree 8 | from pymongo import MongoClient 9 | import sys 10 | reload(sys) 11 | sys.setdefaultencoding("utf-8") 12 | client = MongoClient('localhost', 27017) 13 | tb = u'四川大学' # 设置要抓取的贴吧 14 | 15 | 16 | def get_tz_id(tb_name, page_num): 17 | tz_id = [] 18 | for page in range(1, page_num+1): 19 | url = "http://tieba.baidu.com/f?kw=%s&pn=%s" % (tb_name, (page*50-50)) 20 | html = urllib2.urlopen(url).read() 21 | tree = etree.HTML(html) 22 | ul_li = tree.xpath('//*[@id="thread_list"]/li')[1:] 23 | for li in ul_li: 24 | data_field = li.xpath('./@data-field') # 滤掉百度推广部分 25 | if data_field: 26 | id_ = eval(data_field[0])['id'] 27 | tz_id.append(id_) 28 | return tz_id 29 | 30 | 31 | def save_img(path, img_id, url): 32 | try: 33 | picture = urllib2.urlopen(url).read() 34 | except urllib2.URLError, e: 35 | print e 36 | picture = False 37 | if picture: 38 | if not os.path.exists(path): # 创建文件路径 39 | os.makedirs(path) 40 | f = open('%s/%s.jpg' % (path, img_id), "wb") 41 | f.write(picture) 42 | f.flush() 43 | f.close() 44 | 45 | 46 | def store_mongodb(dic): 47 | database = client.bdtb 48 | return database[tb].insert(dic) 49 | 50 | 51 | def get_info(tz_id): 52 | tz_url = 'http://tieba.baidu.com/p/%s' % tz_id 53 | html = urllib2.urlopen(tz_url).read() 54 | tree = etree.HTML(html) 55 | fist_floor = tree.xpath('//div[@class="l_post j_l_post l_post_bright noborder "]') 56 | title = tree.xpath('//div[@class="core_title core_title_theme_bright"]/h1/@title') 57 | content = fist_floor[0].xpath('./div[3]/div[1]/cc/div')[0] 58 | info = {} 59 | 60 | if content.xpath('./img'): # 判断是否有图片,有图片为true 61 | text = fist_floor[0].xpath('./div[3]/div[1]/cc/div')[0].xpath('string(.)').strip() 62 | if len(text) == 0: 63 | return False # 滤掉没有文字的帖子 64 | images = fist_floor[0].xpath('./div[3]/div[1]/cc/div/img') # 获取图片 65 | number = 1 66 | image_li = [] 67 | for each in images: 68 | src = each.xpath('./@src')[0] 69 | if src.find('static')+1: # 滤掉贴吧表情图片 70 | pass 71 | else: 72 | img_id = '%s_%s' % (tz_id, number) 73 | save_img(tb, img_id, src) # 保存图片到本地 74 | image_li.append('%s/%s_%s' % (tb, tz_id, number)) 75 | number += 1 76 | info['content'] = text 77 | info['image'] = image_li 78 | else: 79 | info['content'] = content.text.strip() 80 | info['image'] = 'null' 81 | info['source'] = tb 82 | info['title'] = ''.join(title) 83 | data_field = fist_floor[0].xpath('./@data-field')[0] 84 | data_info = json.loads(data_field) 85 | info['dateline'] = data_info['content']['date'] # create time 86 | info['sex'] = data_info['author']['user_sex'] # sex 87 | info['author'] = data_info['author']['user_name'] 88 | reply_floor = tree.xpath('//div[@class="l_post j_l_post l_post_bright "]') 89 | reply_li = [] 90 | for each_floor in reply_floor: 91 | if not each_floor.xpath('./div[3]/div[1]/cc/div'): # 滤掉百度推广 92 | return False 93 | reply_content = each_floor.xpath('./div[3]/div[1]/cc/div')[0].xpath('string(.)').strip() 94 | reply_info = {} 95 | if len(reply_content) > 0: # 滤掉无文字的回复 96 | re_field = each_floor.xpath('./@data-field')[0] 97 | re_info = json.loads(re_field) 98 | reply_info['dateline'] = re_info['content']['date'] 99 | reply_info['author'] = re_info['author']['user_name'] 100 | reply_info['content'] = reply_content 101 | reply_li.append(reply_info) 102 | info['reply'] = reply_li 103 | store_mongodb(info) 104 | 105 | 106 | def main(): 107 | id_list = get_tz_id(tb, 1) 108 | for each in id_list: 109 | get_info(each) 110 | #break 111 | client.close() 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /爬虫代理IP池/README.md: -------------------------------------------------------------------------------- 1 | # [项目地址](https://github.com/jhao104/proxy_pool) 2 | 3 | > 在公司做分布式深网爬虫,搭建了一套稳定的代理池服务,为上千个爬虫提供有效的代理,保证各个爬虫拿到的都是对应网站有效的代理IP,从而保证爬虫快速稳定的运行,当然在公司做的东西不能开源出来。不过呢,闲暇时间手痒,所以就想利用一些免费的资源搞一个简单的代理池服务。 4 | 5 | 6 | ### 1、问题 7 | 8 | * 代理IP从何而来? 9 | 10 |   刚自学爬虫的时候没有代理IP就去西刺、快代理之类有免费代理的网站去爬,还是有个别代理能用。当然,如果你有更好的代理接口也可以自己接入。 11 |   免费代理的采集也很简单,无非就是:访问页面页面 —> 正则/xpath提取 —> 保存 12 | 13 | * 如何保证代理质量? 14 | 15 |   可以肯定免费的代理IP大部分都是不能用的,不然别人为什么还提供付费的(不过事实是很多代理商的付费IP也不稳定,也有很多是不能用)。所以采集回来的代理IP不能直接使用,可以写检测程序不断的去用这些代理访问一个稳定的网站,看是否可以正常使用。这个过程可以使用多线程或异步的方式,因为检测代理是个很慢的过程。 16 | 17 | * 采集回来的代理如何存储? 18 | 19 |   这里不得不推荐一个高性能支持多种数据结构的NoSQL数据库[SSDB](http://ssdb.io/docs/zh_cn/),用于代理Redis。支持队列、hash、set、k-v对,支持T级别数据。是做分布式爬虫很好中间存储工具。 20 | 21 | * 如何让爬虫更简单的使用这些代理? 22 | 23 |   答案肯定是做成服务咯,python有这么多的web框架,随便拿一个来写个api供爬虫调用。这样有很多好处,比如:当爬虫发现代理不能使用可以主动通过api去delete代理IP,当爬虫发现代理池IP不够用时可以主动去refresh代理池。这样比检测程序更加靠谱。 24 | 25 | ### 2、代理池设计 26 | 27 |   代理池由四部分组成: 28 | 29 | * ProxyGetter: 30 | 31 |   代理获取接口,目前有5个免费代理源,每调用一次就会抓取这个5个网站的最新代理放入DB,可自行添加额外的代理获取接口; 32 | 33 | * DB: 34 | 35 |   用于存放代理IP,现在暂时只支持SSDB。至于为什么选择SSDB,大家可以参考这篇[文章](https://www.sdk.cn/news/2684),个人觉得SSDB是个不错的Redis替代方案,如果你没有用过SSDB,安装起来也很简单,可以参考[这里](https://github.com/jhao104/memory-notes/blob/master/SSDB/SSDB%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE%E8%AE%B0%E5%BD%95.md); 36 | 37 | * Schedule: 38 | 39 |   计划任务用户定时去检测DB中的代理可用性,删除不可用的代理。同时也会主动通过ProxyGetter去获取最新代理放入DB; 40 | 41 | * ProxyApi: 42 | 43 |   代理池的外部接口,由于现在这么代理池功能比较简单,花两个小时看了下[Flask](http://flask.pocoo.org/),愉快的决定用Flask搞定。功能是给爬虫提供get/delete/refresh等接口,方便爬虫直接使用。 44 | 45 | ![设计](https://pic2.zhimg.com/v2-f2756da2986aa8a8cab1f9562a115b55_b.png) 46 | 47 | ### 3、代码模块 48 | 49 |   Python中高层次的数据结构,动态类型和动态绑定,使得它非常适合于快速应用开发,也适合于作为胶水语言连接已有的软件部件。用Python来搞这个代理IP池也很简单,代码分为6个模块: 50 | 51 | * Api: 52 |   api接口相关代码,目前api是由Flask实现,代码也非常简单。客户端请求传给Flask,Flask调用ProxyManager中的实现,包括`get/delete/refresh/get_all`; 53 | 54 | * DB: 55 |   数据库相关代码,目前数据库是采用SSDB。代码用工厂模式实现,方便日后扩展其他类型数据库; 56 | 57 | * Manager: 58 |   `get/delete/refresh/get_all`等接口的具体实现类,目前代理池只负责管理proxy,日后可能会有更多功能,比如代理和爬虫的绑定,代理和账号的绑定等等; 59 | 60 | * ProxyGetter: 61 |   代理获取的相关代码,目前抓取了[快代理](http://www.kuaidaili.com)、[代理66](http://www.66ip.cn/)、[有代理](http://www.youdaili.net/Daili/http/)、[西刺代理](http://api.xicidaili.com/free2016.txt)、[guobanjia](http://www.goubanjia.com/free/gngn/index.shtml)这个五个网站的免费代理,经测试这个5个网站每天更新的可用代理只有六七十个,当然也支持自己扩展代理接口; 62 | 63 | * Schedule: 64 |   定时任务相关代码,现在只是实现定时去刷新代码,并验证可用代理,采用多进程方式; 65 | 66 | * Util: 67 |   存放一些公共的模块方法或函数,包含`GetConfig`:读取配置文件config.ini的类,`ConfigParse`: 集成重写ConfigParser的类,使其对大小写敏感, `Singleton`:实现单例,`LazyProperty`:实现类属性惰性计算。等等; 68 | 69 | * 其他文件: 70 |   配置文件:Config.ini,数据库配置和代理获取接口配置,可以在GetFreeProxy中添加新的代理获取方法,并在Config.ini中注册即可使用; 71 | 72 | ### 4、安装 73 | 74 | 下载代码: 75 | ``` 76 | git clone git@github.com:jhao104/proxy_pool.git 77 | 78 | 或者直接到https://github.com/jhao104/proxy_pool 下载zip文件 79 | ``` 80 | 81 | 安装依赖: 82 | ``` 83 | pip install -r requirements.txt 84 | ``` 85 | 86 | 启动: 87 | 88 | ``` 89 | 需要分别启动定时任务和api 90 | 到Config.ini中配置你的SSDB 91 | 92 | 到Schedule目录下: 93 | >>>python ProxyRefreshSchedule.py 94 | 95 | 到Api目录下: 96 | >>>python ProxyApi.py 97 | ``` 98 | 99 | ### 5、使用 100 |   定时任务启动后,会通过代理获取方法fetch所有代理放入数据库并验证。此后默认每20分钟会重复执行一次。定时任务启动大概一两分钟后,便可在SSDB中看到刷新出来的可用的代理: 101 | 102 | ![useful_proxy](https://pic2.zhimg.com/v2-12f9b7eb72f60663212f317535a113d1_b.png) 103 | 104 |   启动ProxyApi.py后即可在浏览器中使用接口获取代理,一下是浏览器中的截图: 105 | 106 |   index页面: 107 | 108 | ![index](https://pic3.zhimg.com/v2-a867aa3db1d413fea8aeeb4c693f004a_b.png) 109 | 110 |   get: 111 | 112 | ![get](https://pic1.zhimg.com/v2-f54b876b428893235533de20f2edbfe0_b.png) 113 | 114 |   get_all: 115 | 116 | ![get_all](https://pic3.zhimg.com/v2-5c79f8c07e04f9ef655b9bea406d0306_b.png) 117 | 118 | 119 |   爬虫中使用,如果要在爬虫代码中使用的话, 可以将此api封装成函数直接使用,例如: 120 | ``` 121 | import requests 122 | 123 | def get_proxy(): 124 | return requests.get("http://127.0.0.1:5000/get/").content 125 | 126 | def delete_proxy(proxy): 127 | requests.get("http://127.0.0.1:5000/delete/?proxy={}".format(proxy)) 128 | 129 | # your spider code 130 | 131 | def spider(): 132 | # .... 133 | requests.get('https://www.example.com', proxies={"http": "http://{}".format(get_proxy)}) 134 | # .... 135 | 136 | ``` 137 | 138 | ### 6、最后 139 |   时间仓促,功能和代码都比较简陋,以后有时间再改进。喜欢的在github上给个star。感谢! --------------------------------------------------------------------------------