利用动态VPS服务器建立自用专属proxy

需求:爬虫需要时不时换ip,但是又不需要那么频繁的切换,所以决定自己用一个动态服务器做自己的代理ip。这时需要准备至少两个服务器:

  1. 动态VPS支持拨号换ip
  2. 普通的服务器用来挂爬虫

(其实可以把爬虫也放在VPS上,但是一般VPS的内存CPU硬盘都很小。而性能够的普通服务器又不能换ip,用两个便宜服务器的这种方案是比较省钱的选择)

整个设置过程如下:

设置proxy服务器

首先配置VPS服务器,大流程参考如下链接:轻松获得海量稳定代理!ADSL拨号代理的搭建 (qq.com)

设置adsl

第一步需要先使用pppoe-setup, pppoe-start, pppoe-stop来控制拨号,先让机器连上外网才能正常联网下载安装app,但是可能具体命令要视不同服务器而定。参考:CentOS下配置ADSL 拨号上网CentOS中文站 - 专注Linux技术 (centoschina.cn)

使用pppoe-setup进行设置:第一步按回车,第二步填账号,第三步填网卡,第四步按回车,DNS也直接按回车,第五步填密码,第六步回车,第七步输入0,第八步输入yes,第九步输入y,设置完之后,pppoe-stop,先关闭拨号,然后pppoe-start启动拨号

参考:linux 在shell 下连接pppoe出现/usr/sbin/adsl-start: line 217:怎么办?

安装python3

在centos7上直接源码安装python3,按照以下方式安装

yum install -y zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make
wget https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tgz
tar -zxvf Python-3.6.4.tgz
cd Python-3.6.4
./configure && make &&make install

安装tinyproxy

使用源码安装新版本的,能开启密码认证,避免自己的vps沦为网上的免费代理

1:安装gcc(如果前面安装python时安装过,可跳过)

yum install gcc

2:安装TinyProxy,不使用yum安装,直接下载最新的源码安装

wget https://github.com/tinyproxy/tinyproxy/releases/download/1.11.0/tinyproxy-1.11.0.tar.gz
tar -zxvf tinyproxy-1.11.0.tar.gz
cd tinyproxy-1.11.0

3:编译安装

./configure && make &&make install
bash -r

4:创建系统服务

新建tinyproxy.service文件

vi /usr/lib/systemd/system/tinyproxy.service

贴入以下代码

[Unit]
Description=Startup script for the tinyproxy server
After=network.target
 
[Service]
Type=forking
PIDFile=/usr/local/var/run/tinyproxy/tinyproxy.pid
ExecStart=/bin/bash -c "/usr/local/bin/tinyproxy -c /usr/local/etc/tinyproxy/tinyproxy.conf &"
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
ExecStartPost=/bin/sleep 0.1
 
[Install]
WantedBy=multi-user.target

5:手动创建log和pid文件

mkdir -p /usr/local/var/run/tinyproxy
mkdir -p /usr/local/var/log/tinyproxy
touch /usr/local/var/log/tinyproxy/tinyproxy.log
touch /usr/local/var/run/tinyproxy/tinyproxy.pid

6:改配置文件/usr/local/etc/tinyproxy/tinyproxy.conf

#这里设置使用的用户名和组
User root
Group root

# 设置爬虫的ip,如果需要本地也连接的话就全部注释掉
# 只采用密码认证的方式,否则本地连本地会被拒绝
Allow xxx.xxx.xxx.xxx
 
# 设置用户名密码
BasicAuth username password
 
# 顺便将下面两行取消注释,后面有用到
PidFile "/usr/local/var/run/tinyproxy/tinyproxy.pid"
LogFile "/usr/local/var/log/tinyproxy/tinyproxy.log"

7:启动服务

sudo systemctl enable tinyproxy
service tinyproxy start
service tinyproxy status

参考:Linux安装最新版Tinyproxy

之后改一下防火墙设置

firewall-cmd --zone=public --add-port=8888/tcp --permanent
firewall-cmd --reload

这时候拿一台机器(本地或者用要做爬虫的那台机器)试以下命令

curl -x http://user_username:mypassword1234@xx.xx.xx.xx:8888 httpbin.org/get

这里xx的ip是用来拨号的网卡的ip地址,名字一般是ppp0的那个

设置ip地址提取方法

代码参考: Python网络爬虫开发实战,ADSL 拨号代理

1:在centos7上安装anaconda,流程和普通的Linux安装都是一样的。(直接用服务器python3环境的话可跳过)

2:安装redis、requests,直接pip安装即可,注意如果用conda安装redis库的话需要执行

conda install redis
conda install redis-py

3:之后建两个文件,第一个文件是db_setup.py,需要定制的地方就是REDIS_HOSTREDIS_PASSWORD,根据后面redis配置改。

import redis
import random

# Redis数据库安装在爬虫服务器上,他的ip
REDIS_HOST = '111.11.111.111'
# Redis数据库密码, 如无则填None
REDIS_PASSWORD = '11111111'
# Redis数据库端口
REDIS_PORT = 6379
# 代理池键名
PROXY_KEY = 'adsl'

class RedisClient(object):
    def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, proxy_key=PROXY_KEY):
        """
        初始化Redis连接
        :param host: Redis 地址
        :param port: Redis 端口
        :param password: Redis 密码
        :param proxy_key: Redis 散列表名
        """
        self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
        self.proxy_key = proxy_key

    def set(self, name, proxy):
        """
        设置代理
        :param name: 主机名称
        :param proxy: 代理
        :return: 设置结果
        """
        return self.db.hset(self.proxy_key, name, proxy)

    def get(self, name):
        """
        获取代理
        :param name: 主机名称
        :return: 代理
        """
        return self.db.hget(self.proxy_key, name)

    def count(self):
        """
        获取代理总数
        :return: 代理总数
        """
        return self.db.hlen(self.proxy_key)

    def remove(self, name):
        """
        删除代理
        :param name: 主机名称
        :return: 删除结果
        """
        return self.db.hdel(self.proxy_key, name)

    def names(self):
        """
        获取主机名称列表
        :return: 获取主机名称列表
        """
        return self.db.hkeys(self.proxy_key)

    def proxies(self):
        """
        获取代理列表
        :return: 代理列表
        """
        return self.db.hvals(self.proxy_key)

    def random(self):
        """
        随机获取代理
        :return:
        """
        proxies = self.proxies()
        if proxies:
            ip = random.choice(proxies)
        else:
            ip = None
        return ip

    def all(self):
        """
        获取字典
        :return:
        """
        return self.db.hgetall(self.proxy_key)

4:建一个refresh_ip.py,需要定制的地方是ADSL_CYCLE控制刷新ip的间隔。

另外在执行这个脚本之前也要确保机器联外网(执行pppoe-start),不然一直连不上爬虫服务器的redis

import re
import time
import requests
from requests.exceptions import ConnectionError, ReadTimeout
from db_setup import RedisClient
import subprocess
import logging

logging.basicConfig(level=logging.INFO, format='[%(asctime)s] - %(message)s')
logger = logging.getLogger(__name__)

# 拨号网卡
ADSL_IFNAME = 'ppp0'
# 测试URL
TEST_URL = 'http://httpbin.org/get'
# 测试超时时间
TEST_TIMEOUT = 30
# 拨号间隔
ADSL_CYCLE = 43200  # 12 hours
# 拨号出错重试间隔
ADSL_ERROR_CYCLE = 10
# 从数据库删掉旧ip到关闭旧ip之间的冷却时间
COOL_DOWN_CYCLE = 600
# ADSL命令
ADSL_BASH = 'pppoe-stop;pppoe-start'
# 代理运行端口
PROXY_PORT = 8888
# 客户端唯一标识
CLIENT_NAME = 'adsl1'

# proxy的用户名和密码
USERNAME = 'user_username'
PASSWORD = 'mypassword1234'

class Sender():
    def get_ip(self, ifname=ADSL_IFNAME):
        """
        获取本机IP
        :param ifname: 网卡名称
        :return:
        """
        (status, output) = subprocess.getstatusoutput('ifconfig')
        if status == 0:
            pattern = re.compile(ifname + '.*?inet.*?(\d+\.\d+\.\d+\.\d+).*?netmask', re.S)
            result = re.search(pattern, output)
            if result:
                ip = result.group(1)
                return ip

    def test_proxy(self, proxy):
        """
        测试代理
        :param proxy: 代理
        :return: 测试结果
        """
        try:
            response = requests.get(TEST_URL, proxies={
                'http': '{}:{}@{}'.format(USERNAME, PASSWORD, proxy)
            }, timeout=TEST_TIMEOUT)
            if response.status_code == 200:
                return True
        except (ConnectionError, ReadTimeout):
            return False

    def remove_proxy(self):
        """
        移除代理
        :return: None
        """
        self.redis = RedisClient()
        self.redis.remove(CLIENT_NAME)
        logger.info('Successfully Removed Proxy')

    def set_proxy(self, proxy):
        """
        设置代理
        :param proxy: 代理
        :return: None
        """
        self.redis = RedisClient()
        if self.redis.set(CLIENT_NAME, proxy):
            logger.info('Successfully Set Proxy {}'.format(proxy))

    def adsl(self):
        """
        拨号主进程
        :return: None
        """

        logger.info('Remove Proxy')
        self.remove_proxy()
        logger.info('Cooling Down')
        time.sleep(COOL_DOWN_CYCLE)

        while True:
            logger.info('ADSL Dialing Start')
            (status, output) = subprocess.getstatusoutput(ADSL_BASH)
            if status == 0:
                logger.info('Success')
                ip = self.get_ip()
                if ip:
                    logger.info('IP: {}'.format(ip))
                    logger.info('Testing Proxy')
                    proxy = '{ip}:{port}'.format(ip=ip, port=PROXY_PORT)
                    if self.test_proxy(proxy):
                        logger.info('Valid Proxy')
                        self.set_proxy(proxy)
                        logger.info('Waiting for Next Dialing')
                        time.sleep(ADSL_CYCLE)
                        logger.info('Remove Proxy')
                        self.remove_proxy()
                        logger.info('Cooling Down')
                        time.sleep(COOL_DOWN_CYCLE)
                    else:
                        logger.info('Invalid Proxy')
                        time.sleep(ADSL_ERROR_CYCLE)
                else:
                    logger.info('Get IP Failed, Re Dialing')
                    time.sleep(ADSL_ERROR_CYCLE)
            else:
                logger.info('ADSL Dialing Failed, Please Check')
                time.sleep(ADSL_ERROR_CYCLE)
                
if __name__ == '__main__':
    sender = Sender()
    sender.adsl()

5:但是偶尔有可能在拨号间隙时ip挂掉或者被ban,所以我又简化了一个'manual_refresh.py',可以手动控制切换ip

import re
import time
import requests
from requests.exceptions import ConnectionError, ReadTimeout
from db_setup import RedisClient
import subprocess
import logging

logging.basicConfig(level=logging.INFO, format='[%(asctime)s] - %(message)s')
logger = logging.getLogger(__name__)

# 拨号网卡
ADSL_IFNAME = 'ppp0'
# ADSL命令
ADSL_BASH = 'pppoe-stop;pppoe-start'
# 代理运行端口
PROXY_PORT = 8888
# 客户端唯一标识
CLIENT_NAME = 'adsl1'


class Sender():
    def get_ip(self, ifname=ADSL_IFNAME):
        """
        获取本机IP
        :param ifname: 网卡名称
        :return:
        """
        (status, output) = subprocess.getstatusoutput('ifconfig')
        if status == 0:
            pattern = re.compile(ifname + '.*?inet.*?(\d+\.\d+\.\d+\.\d+).*?netmask', re.S)
            result = re.search(pattern, output)
            if result:
                ip = result.group(1)
                return ip

    def remove_proxy(self):
        """
        移除代理
        :return: None
        """
        self.redis = RedisClient()
        self.redis.remove(CLIENT_NAME)
        logger.info('Successfully Removed Proxy')

    def set_proxy(self, proxy):
        """
        设置代理
        :param proxy: 代理
        :return: None
        """
        self.redis = RedisClient()
        if self.redis.set(CLIENT_NAME, proxy):
            logger.info('Successfully Set Proxy {}'.format(proxy))

    def adsl(self):
        logger.info('ADSL Start, Remove Proxy, Please wait')
        self.remove_proxy()
        (status, output) = subprocess.getstatusoutput(ADSL_BASH)
        ip = self.get_ip()
        logger.info('IP: {}'.format(ip))
        proxy = '{ip}:{port}'.format(ip=ip, port=PROXY_PORT)
        self.set_proxy(proxy)


if __name__ == '__main__':
    sender = Sender()
    sender.adsl()

设置爬虫服务器

爬虫服务器除了放爬虫代码以外,还要放一个redis数据库用来接proxy传来的ip,然后用web页面传进爬虫代码解析出来代理ip的地址。

安装redis

参考:如何在 Ubuntu 20.04 上安装和配置 Redis-阿里云开发者社区 (aliyun.com)

首先安装

apt install redis-server

之后可以利用下面指令查看状态

systemctl status redis-server

找到/etc/redis/redis.conf之后把下面这句的前面加个#号注释掉

bind 127.0.0.1 ::1

找到#requirepass foobared这句,取消前面的#号,并且把密码改成自己的

requirepass mypassword12345

之后重启服务

systemctl restart redis-server

然后上服务器的安全组或者防火墙什么的里面,把6379这个端口放开。

设置获取ip的web链接

1:安装redis,tornado,之后把上面那个db_setup.py照搬过来。

2:建立一个文件api.py,可以改的参数是def serve(port=8425)可以根据需要改。

import json
import tornado.ioloop
import tornado.web
from tornado.web import RequestHandler, Application
from db_setup import RedisClient
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class Server(RequestHandler):
    """
    服务器,对接 Redis 并提供 API
    """

    def initialize(self, redis):
        """
        初始化
        :param redis:
        :return:
        """
        self.redis = redis

    def get(self, api=''):
        """
        API 列表
        :param api:
        :return:
        """
        if not api:
            links = ['random', 'proxies', 'names', 'all', 'count']
            self.write('<h4>Welcome to ADSL Proxy API</h4>')
            for link in links:
                self.write('<a href=' + link + '>' + link + '</a><br>')

        if api == 'random':
            result = self.redis.random()
            if result:
                self.write(result)

        if api == 'names':
            result = self.redis.names()
            if result:
                self.write(json.dumps(result))

        if api == 'proxies':
            result = self.redis.proxies()
            if result:
                self.write(json.dumps(result))

        if api == 'all':
            result = self.redis.all()
            if result:
                self.write(json.dumps(result))

        if api == 'count':
            self.write(str(self.redis.count()))


def serve(port=8425, address='0.0.0.0'):
    redis = RedisClient()
    application = Application([
        (r'/', Server, dict(redis=redis)),
        (r'/(.*)', Server, dict(redis=redis)),
    ])
    application.listen(port, address=address)
    logger.info(f'API listening on http://{address}:{port}')
    tornado.ioloop.IOLoop.instance().start()


if __name__ == '__main__':
    serve()

3:可以通过在爬虫的脚本里面解析http://0.0.0.0:8425/random来直接获取到代理ip,然后传给爬虫就行。