关于这个系列
这个项目实录系列是记录Mproxy项目的整个开发流程。项目最终的目标是开发一套代理服务器的API。这个系列中会记录项目的需求、设计、验证、实现、升级等等,包括设计决策的依据,开发过程中的各种坑。希望和大家共同交流,一起进步。
项目的源码我会同步更新到GitHub,项目地址:https://github.com/mrbcy/Mproxy。
系列地址:
今日计划
到目前为止,第一阶段的任务还剩下代理服务器的重新验证以及其他2个代理服务器爬虫的编写。今天准备完成代理服务器的定时验证以及1个爬虫的编写。
今天对验证器进行了测试,发现之前根本没有把头部信息携带上去。修改以后发现今天快代理的爬虫可用性变得很高,基本都能用。不知道是不是偶然的。持续验证几天应该就会有结果了,我们拭目以待。
代理服务器的定时验证
由于代理服务器可用的时间一般都不长,所以有必要对代理服务器进行定时的验证。实现方法就是调度器中新开一个线程,每隔3小时将超过6小时没有验证的代理服务器信息提交到Kafka的unchecked-server Topic中,则验证器会自动的进行验证。
从数据库中得到需要验证的代理服务器
首先,我们要先把数据库中超过6小时没有验证的代理服务器取出来。
先是从collector里面把dao和service拷过来了(不知道怎么消除重复代码了,后续再重构吧),然后添加了下面的代码。
proxydao(这)
具体代码看下边吧,有全的
proxyservice
def find_proxy_need_to_recheck(self):
now = datetime.datetime.now();
timestamp = now - datetime.timedelta(hours=6)
return self.proxy_dao.find_proxy_need_to_recheck(timestamp)
好的,到现在为止,我们已经可以把需要验证的代理服务器取出来了。
定时提交到Kafka集群
接下来,把快代理爬虫里面的提交工具类拷贝出来(笑)。然后改造Dispatcher。代码如下:
#-*- coding: utf-8 -*-
import json
import threading
import uuid
import time
from kafka import KafkaProducer
from conf.configloader import ConfigLoader
from service.proxyservice import ProxyService
class ProxyRechecker(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.conf_loader = ConfigLoader()
self.producer = KafkaProducer(value_serializer=lambda m: json.dumps(m).encode('utf-8'),
bootstrap_servers=self.conf_loader.get_kafka_bootstrap_servers())
self.proxy_service = ProxyService()
def run(self):
while True:
dao_items = self.proxy_service.find_proxy_need_to_recheck()
if dao_items is not None:
print "Proxy Rechecker 提交 %s 个代理服务器进行重新验证" % (len(dao_items))
for dao_item in dao_items:
# construct validate bean
validate_item = {'_values':{}}
validate_item['_values']['task_id'] = str(uuid.uuid4())
validate_item['_values']['ip'] = dao_item.proxy_addr.split(':')[0]
validate_item['_values']['port'] = dao_item.proxy_addr.split(':')[1]
validate_item['_values']['anonymity'] = dao_item.anonymity
validate_item['_values']['type'] = dao_item.type
validate_item['_values']['location'] = dao_item.location
validate_item['_values']['spider_name'] = "mproxy_dispatcher"
self.producer.send('unchecked-servers',validate_item)
time.sleep(60*60*3)
西刺代理爬虫的开发
这次要开发的代理服务器是西刺代理网站的爬虫。网址是:http://www.xicidaili.com/
搭建环境
使用下面的命令创建一个爬虫项目:
在spiders目录下面创建xicidaili_spider.py。一般来说爬虫的名字以域名或者网站名命名。
先写一个框架代码,像下面这样:
# -*- coding: utf-8 -*-
import scrapy
class XicidailiSpider(scrapy.spiders.Spider):
name = "xicidaili"
allowed_domains = ["xicidaili.com"]
download_delay = 1
start_urls = []
def __init__(self):
for x in range(20):
self.start_urls.append('http://www.xicidaili.com/nn/%d' % (x + 1))
def parse(self, response):
print(response.body)
获取网页内容
然后运行一下,看看我们能不能把内容打印出来。
如果想用PyCharm运行Scrapy,可以参照下图进行设置。首先是点击Run->Edit Configurations。
运行结果如下:
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.1.19</center>
</body>
</html>
很显然,我们又要用scrapy-splash了。顺便把User-Agent池也用上去,可以参考第二天的相关内容。http://blog.csdn.net/mrbcy/article/details/59110316
把scrapy-splash配上去之后我们就拿到数据了。
提取代理服务器数据
然后用Chrome的xpath把代理服务器数据提取出来。
# -*- coding: utf-8 -*-
import scrapy
from scrapy_splash import SplashRequest
class XicidailiSpider(scrapy.spiders.Spider):
name = "xicidaili"
allowed_domains = ["xicidaili.com"]
download_delay = 1
start_urls = []
def __init__(self):
for x in range(1):
self.start_urls.append('http://www.xicidaili.com/nn/%d' % (x + 1))
def start_requests(self):
for i, url in enumerate(self.start_urls):
yield SplashRequest(url, self.parse, args={'wait': 1})
def parse(self, response):
# print(response.body)
# //*[@id="ip_list"]/tbody/tr[2]/td[2]
trs = response.xpath('''//*[@id="ip_list"]/tbody/tr[@class='odd']''')
# print trs
for tr_selector in trs:
ip = tr_selector.xpath('''./td[2]/text()''').extract_first()
port = tr_selector.xpath('''./td[3]/text()''').extract_first()
location = tr_selector.xpath('''./td[4]/a/text()''').extract_first()
anonymity = tr_selector.xpath('''./td[5]/text()''').extract_first()
type = tr_selector.xpath('''./td[6]/text()''').extract_first()
print ip,port,location,anonymity,type
输出结果如下:
119.5.0.13 808 四川南充 高匿 HTTP
106.46.136.108 808 None 高匿 HTTP
106.46.136.16 808 None 高匿 HTTP
218.82.112.4 8118 上海 高匿 HTTP
175.155.24.46 808 四川德阳 高匿 HTTP
175.155.24.39 808 四川德阳 高匿 HTTP
106.46.136.177 808 None 高匿 HTTP
112.74.126.182 3128 广东佛山 高匿 HTTP
183.32.88.137 808 广东中山 高匿 HTTP
106.46.136.6 808 None 高匿 HTTP
171.38.181.129 8123 广西钦州 高匿 HTTP
106.46.136.85 808 None 高匿 HTTP
1.199.178.203 8998 河南三门峡 高匿 HTTP
106.46.136.107 808 None 高匿 HTTP
114.239.150.236 808 江苏宿迁市泗阳县 高匿 HTTP
222.22.61.51 8998 河南郑州 高匿 HTTP
106.46.136.103 808 None 高匿 HTTP
106.46.136.114 808 None 高匿 HTTP
119.5.0.61 808 四川南充 高匿 HTTP
119.5.1.88 808 四川南充 高匿 HTTP
106.46.136.84 808 None 高匿 HTTP
106.46.136.73 808 None 高匿 HTTP
106.46.136.161 808 None 高匿 HTTP
106.46.136.122 808 None 高匿 HTTP
125.118.69.130 808 浙江杭州 高匿 HTTP
119.5.1.19 808 四川南充 高匿 HTTP
106.46.136.149 808 None 高匿 HTTP
106.46.136.179 808 None 高匿 HTTP
122.6.151.66 8998 山东临沂 高匿 HTTP
106.46.136.145 808 None 高匿 HTTP
119.5.1.42 808 四川南充 高匿 HTTP
113.69.63.92 808 广东佛山 高匿 HTTP
111.72.126.167 808 江西吉安 高匿 HTTP
106.46.136.154 808 None 高匿 HTTP
106.46.136.178 808 None 高匿 HTTP
121.228.236.56 8998 江苏苏州 高匿 HTTP
175.155.152.172 808 四川德阳 高匿 HTTP
171.39.34.39 8123 广西百色 高匿 HTTP
110.18.181.236 80 内蒙古 高匿 HTTP
175.155.25.107 808 四川德阳 高匿 HTTP
114.239.1.75 808 江苏宿迁 高匿 HTTP
27.129.207.152 8118 河北 高匿 HTTP
183.32.88.182 808 广东中山 高匿 HTTP
106.46.136.139 808 None 高匿 HTTP
106.46.136.192 808 None 高匿 HTTP
114.239.2.191 808 江苏宿迁 高匿 HTTP
183.32.232.242 808 广东中山 高匿 HTTP
113.121.253.85 808 山东德州 高匿 HTTP
106.46.136.138 808 None 高匿 HTTP
111.72.122.171 8998 江西吉安 高匿 HTTP
封装数据到实体类
首先修改一下items.py文件的内容
import scrapy
class XicidailiItem(scrapy.Item):
task_id = scrapy.Field()
ip = scrapy.Field()
port = scrapy.Field()
anonymity = scrapy.Field()
type = scrapy.Field()
location = scrapy.Field()
spider_name = scrapy.Field()
然后修改xicidaili_spider中的parse方法:
def parse(self, response):
try:
# //*[@id="ip_list"]/tbody/tr[2]/td[2]
trs = response.xpath('''//*[@id="ip_list"]/tbody/tr[@class='odd']''')
# print trs
for tr_selector in trs:
item = XicidailiItem()
item['ip'] = tr_selector.xpath('''./td[2]/text()''').extract_first()
item['port'] = tr_selector.xpath('''./td[3]/text()''').extract_first()
item['location'] = tr_selector.xpath('''./td[4]/a/text()''').extract_first()
item['anonymity'] = tr_selector.xpath('''./td[5]/text()''').extract_first()
item['type'] = tr_selector.xpath('''./td[6]/text()''').extract_first()
item['task_id'] = str(uuid.uuid4())
item['spider_name'] = self.name
yield item
except Exception as e:
logging.exception("An Error Happens")
整合日志
这一部分参考第2天的相应内容。
将数据提交到Kafka集群中
这一部分也参考第2天的内容。
运行爬虫
完成了上述工作以后就可以开始爬取数据了。这次一共验证了3000+的代理服务器,基本都能用。我看我的博客是快要被封了(哭)
过程中出现了Kafka Customer不能消费消息的问题,参考http://www.lingdonge.com/coding/java/4035.html得到了解决。
现在也有4000+代理服务器了,好开心啊。
添加创建时间字段
突然发现还有一个问题,为了后来研究代理服务器的可用时间,需要在表里面添加一个创建时间字段。使用下面的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 '代理服务器类型',
`create_time` DATETIME 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
接下来修改dao
#-*- coding: utf-8 -*-
import traceback
from dao.proxystatus import ProxyStatus
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.create_time = data[4]
proxy_dao_item.last_validate_time = data[5]
proxy_dao_item.retry_count = data[6]
proxy_dao_item.last_available_time = data[7]
proxy_dao_item.status = data[8]
cur.close()
conn.close()
return proxy_dao_item
except Exception as e:
return None
def find_proxy_need_to_recheck(self,timestamp):
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "select * from proxy_list where last_validate_time < %s and status != %s limit 500"
count = cur.execute(sql,(timestamp,ProxyStatus.PERMANENT_UNAVAILABLE) )
proxy_dao_items = None
if count != 0:
proxy_dao_items = []
result = cur.fetchall()
for data in result:
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]
proxy_dao_items.append(proxy_dao_item)
cur.close()
conn.close()
return proxy_dao_items
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,create_time,last_validate_time,retry_count,last_available_time,status) " \
"values(%s,%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.create_time,
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,create_time = %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.create_time,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()
和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 find_proxy_by_addr(self,proxy_addr):
return self.proxy_dao.find_proxy_by_addr(proxy_addr)
def find_proxy_need_to_recheck(self):
now = datetime.datetime.now();
timestamp = now - datetime.timedelta(hours=6)
return self.proxy_dao.find_proxy_need_to_recheck(timestamp)
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.location
dao_item.type = validation_result_item.type
dao_item.create_time = datetime.datetime.now()
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)
重新把数据都清掉,然后运行了一次爬虫。最终拥有了3900+的代理服务器。
到现在为止,第一阶段的开发任务算是完成了。明天起研究部署,补文档。