关于这个系列
这个项目实录系列是记录Mproxy项目的整个开发流程。项目最终的目标是开发一套代理服务器的API。这个系列中会记录项目的需求、设计、验证、实现、升级等等,包括设计决策的依据,开发过程中的各种坑。希望和大家共同交流,一起进步。
项目的源码我会同步更新到GitHub,项目地址:https://github.com/mrbcy/Mproxy。
系列地址:
今日计划
到目前为止,我们已经拥有了一个代理服务器的爬虫,完善的验证器。还实现了调度器的部分功能以保证只有在所有的验证器都在线时才进行验证工作。今天我们将完成收集器部分的开发工作,将通过所有验证器验证的代理服务器保存到MySQL数据库中。并定时清理12小时后仍未收到所有验证器验证结果的任务。
从Kafka集群中读取验证结果
#-*- coding: utf-8 -*-
import json
from kafka import KafkaConsumer
def func():
consumer = KafkaConsumer('checked-servers',
group_id='mproxy_collector',
bootstrap_servers=['amaster:9092','anode1:9092','anode2:9092'],
auto_offset_reset='earliest', enable_auto_commit=False,
value_deserializer=lambda m: json.loads(m.decode('utf-8')))
for message in consumer:
v = message.value
print v
if __name__ == '__main__':
func()
获取验证器列表
验证器的列表信息写入配置文件中。配置文件的内容如下:
[validator]
validator_list = validator_huabei_test_1,validator_huabei_test_2
然后写一个读取配置文件的工具类。代码如下:
#-*- coding: utf-8 -*-
import ConfigParser
class ConfigLoader:
def __init__(self):
self.cp = ConfigParser.SafeConfigParser()
self.cp.read('collector.cfg')
def get_validator_list(self):
text = self.cp.get('validator','validator_list')
return text.split(',')
即可使用下面的代码获得验证器的列表:
from conf.configloader import ConfigLoader
def start_working():
config_loader = ConfigLoader()
validator_list = config_loader.get_validator_list()
print validator_list
if __name__ == '__main__':
start_working()
输出结果如下:
['validator_huabei_test_1', 'validator_huabei_test_2']
匹配验证结果
接下来我们就可以筛选哪些是通过了所有验证器的代理服务器。以task_id作为key,然后value可以是一个list。每当有新的value加入的时候就判断一下是否已经通过了所有验证器。如果通过了,就删除这个key,然后交给对应的函数进行处理。只要有一个代理服务器未通过,就表示这个代理服务器未通过,也交给对应的函数进行处理。
#-*- coding: utf-8 -*-
import json
import traceback
from kafka import KafkaConsumer
from conf.configloader import ConfigLoader
from validationresultitem import ValidationResultItem
validate_result = {}
validator_list = []
def print_validator_names():
global validator_list
config_loader = ConfigLoader()
validator_list = config_loader.get_validator_list()
print validator_list
def do_validation_match(proxy_task):
global validate_result
try:
if validate_result.has_key(proxy_task['task_id']) == False:
validate_result[proxy_task['task_id']] = [ValidationResultItem(proxy_task)]
else:
validate_result[proxy_task['task_id']].append(ValidationResultItem(proxy_task))
check_validation_result(proxy_task['task_id'])
except Exception as e:
# traceback.print_exc()
pass
def deal_unavailable(result_list):
if len(result_list) > 0:
print "代理服务器 %s:%s 不能用" %(result_list[0].ip,result_list[0].port)
def deal_available(result_list):
if len(result_list) > 0:
print "代理服务器 %s:%s 可用" % (result_list[0].ip, result_list[0].port)
def check_validation_result(task_id):
global validate_result
global validator_list
# print validate_result[task_id]
result_list = validate_result[task_id]
# iteratively check whether proxy passes all the validators
all_pass_flag = True
for validator in validator_list:
pass_flag = False
for result_item in result_list:
if validator == result_item.validator_name:
pass_flag = True
if result_item.validate_result == False:
# print "根据 %s 判定,代理服务器不能用" % result_item.validator_name
deal_unavailable(result_list)
return
break
if pass_flag == False:
all_pass_flag = False
if all_pass_flag == True:
deal_available(result_list)
def func():
consumer = KafkaConsumer('checked-servers',
group_id='mproxy_collector',
bootstrap_servers=['amaster:9092','anode1:9092','anode2:9092'],
auto_offset_reset='earliest', enable_auto_commit=False,
value_deserializer=lambda m: json.loads(m.decode('utf-8')))
for message in consumer:
v = message.value
do_validation_match(v)
if __name__ == '__main__':
print_validator_names()
func()
根据实验结果来看,快代理网站的代理服务器可用率非常低,大概3%上下。这个数据是我把响应时间放宽到20秒后得到的,因此可用率实际上更低。迫切的需要从其他的数据源获得代理服务器信息。
统一的数据结构
接下来我们来设计一下代理服务器信息的存储结构。在Kafka集群中,代理服务器的信息实际上包含在任务信息中。有下列字段组成。
- task_id
- ip
- port
- validator_name
- spider_name
- location
- validate_result
- anonymity
- type
而在数据库中,则包括下列字段,以支持后续的持续验证过程。
- proxy_addr 包括 ip:port
- location
- anonymity
- type
- last_validate_time
- retry_count
- last_available_time
- status
将可用的代理服务器保存到MySQL中
使用下面的SQL语句创建表:
CREATE TABLE `proxy_list` (
`proxy_addr` VARCHAR(255) NOT NULL COMMENT '代理服务器地址',
`location` VARCHAR(255) DEFAULT NULL COMMENT '位置信息',
`anonymity` VARCHAR(255) DEFAULT NULL COMMENT '匿名度',
`type` VARCHAR(255) DEFAULT NULL COMMENT '代理服务器类型',
`last_validate_time` DATETIME DEFAULT NULL COMMENT '最后一次验证时间',
`retry_count` INT(11) DEFAULT NULL COMMENT '重试次数',
`last_available_time` DATETIME DEFAULT NULL COMMENT '最后一次验证可用时间',
`status` VARCHAR(255) DEFAULT NULL COMMENT '代理服务器状态',
PRIMARY KEY (`proxy_addr`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
接下来就是传统的三层架构用起来。
service
#-*- coding: utf-8 -*-
import datetime
from dao.proxydao import ProxyDao
from dao.proxystatus import ProxyStatus
from domain.proxydaoitem import ProxyDaoItem
class ProxyService:
def __init__(self):
self.proxy_dao = ProxyDao()
def save_proxy(self,validation_result_item):
# Firstly, we look up the proxy_info in db
dao_item = self.proxy_dao.find_proxy_by_addr(validation_result_item.ip + ':'+validation_result_item.port)
insert_flag = False
if dao_item is None:
dao_item = ProxyDaoItem()
dao_item.proxy_addr = validation_result_item.ip + ':'+validation_result_item.port
dao_item.anonymity = validation_result_item.anonymity
dao_item.location = validation_result_item.anonymity
dao_item.type = validation_result_item.type
insert_flag = True
if validation_result_item.validate_result == True:
dao_item.last_validate_time = datetime.datetime.now()
dao_item.last_available_time = datetime.datetime.now()
dao_item.retry_count = 0
dao_item.status = ProxyStatus.AVAILABLE
else:
dao_item.last_validate_time = datetime.datetime.now()
dao_item.retry_count += 1
if dao_item.retry_count >= 3:
dao_item.status = ProxyStatus.PERMANENT_UNAVAILABLE
else:
dao_item.status = ProxyStatus.TEMP_UNAVAILABLE
if insert_flag == True:
self.proxy_dao.insert_proxy(dao_item)
else:
self.proxy_dao.update_proxy(dao_item)
dao
#-*- coding: utf-8 -*-
import traceback
from dbpool.poolutil import PoolUtil
from domain.proxydaoitem import ProxyDaoItem
class ProxyDao:
def find_proxy_by_addr(self, proxy_addr):
":param proxy_addr format like ip:port"
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "select * from proxy_list where proxy_addr=%s"
count = cur.execute(sql,(proxy_addr) )
proxy_dao_item = None
if count != 0:
data = cur.fetchone()
proxy_dao_item = ProxyDaoItem()
proxy_dao_item.proxy_addr = data[0]
proxy_dao_item.location = data[1]
proxy_dao_item.anonymity = data[2]
proxy_dao_item.type = data[3]
proxy_dao_item.last_validate_time = data[4]
proxy_dao_item.retry_count = data[5]
proxy_dao_item.last_available_time = data[6]
proxy_dao_item.status = data[7]
cur.close()
conn.close()
return proxy_dao_item
except Exception as e:
return None
def insert_proxy(self, dao_item):
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "insert into proxy_list(proxy_addr,location,anonymity,type,last_validate_time,retry_count,last_available_time,status) " \
"values(%s,%s,%s,%s,%s,%s,%s,%s)"
cur.execute(sql,(dao_item.proxy_addr,dao_item.location,dao_item.anonymity,dao_item.type,dao_item.last_validate_time,dao_item.retry_count,
dao_item.last_available_time,dao_item.status))
cur.close()
conn.commit()
conn.close()
except Exception as e:
traceback.print_exc()
traceback.print_exc()
def update_proxy(self, dao_item):
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "update proxy_list set location = %s,anonymity = %s,type = %s,last_validate_time = %s,retry_count = %s,last_available_time = %s,status = %s where proxy_addr=%s"
cur.execute(sql,(dao_item.location,dao_item.anonymity,dao_item.type,dao_item.last_validate_time,dao_item.retry_count,
dao_item.last_available_time,dao_item.status,dao_item.proxy_addr))
cur.close()
conn.commit()
conn.close()
except Exception as e:
traceback.print_exc()
单元测试
#-*- coding: utf-8 -*-
import datetime
from dao.proxydao import ProxyDao
from dao.proxystatus import ProxyStatus
from domain.proxydaoitem import ProxyDaoItem
proxy_dao = ProxyDao()
def test_insert_proxy():
global proxy_dao
dao_item = ProxyDaoItem()
dao_item.proxy_addr = "127.0.0.1:5002"
dao_item.location = "北京 海淀 移动"
dao_item.status = ProxyStatus.PERMANENT_UNAVAILABLE
dao_item.anonymity = "高匿名"
dao_item.last_available_time = datetime.datetime.now()
dao_item.last_validate_time = datetime.datetime.now()
dao_item.retry_count = 0
dao_item.type = "HTTP"
proxy_dao.insert_proxy(dao_item)
def test_find_one():
global proxy_dao
dao_item = proxy_dao.find_proxy_by_addr('127.0.0.1:5002')
print dao_item
def test_update():
global proxy_dao
dao_item = proxy_dao.find_proxy_by_addr('127.0.0.1:5002')
dao_item.location = "测试位置"
dao_item.type = "HTTP,HTTPS"
dao_item.anonymity = '透明'
dao_item.status = ProxyStatus.AVAILABLE
dao_item.retry_count = 3
dao_item.last_available_time = datetime.datetime.now()
dao_item.last_validate_time = datetime.datetime.now()
proxy_dao.update_proxy(dao_item)
if __name__ == '__main__':
test_update()
测试的结果还挺完美的,都顺利的实现了目标。
最后就是改造collector了,任务结果检查完毕以后就将结果更新到MySQL数据库里面去。
def deal_unavailable(result_list):
global proxy_service
if len(result_list) > 0:
print "代理服务器 %s:%s 不能用" %(result_list[0].ip,result_list[0].port)
dao_item = proxy_service.find_proxy_by_addr(result_list[0].ip + ':'+result_list[0].port)
if dao_item is not None:
proxy_service.save_proxy(result_list[0])
def deal_available(result_list):
global proxy_service
if len(result_list) > 0:
print "代理服务器 %s:%s 可用" % (result_list[0].ip, result_list[0].port)
proxy_service.save_proxy(result_list[0])
这边我做了一个检查,如果代理服务器不可用而且是第一次验证的话说明开始这个代理服务器就不能用,就不必保存到数据库了,抛弃即可,其他情况下则更新到数据库中。
把Kafka集群里面的数据清理干净,然后重新运行了一次,得到了6个代理服务器。感觉十分开心。