3

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

 2 years ago
source link: https://divertingpan.github.io/post/7yzxwt8aX/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

需求:爬虫需要时不时换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:[email protected]: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,然后传给爬虫就行。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK