zoukankan      html  css  js  c++  java
  • 自动更改IP地址反爬虫封锁,支持多线程(转)

    8年多爬虫经验的人告诉你,国内ADSL是王道,多申请些线路,分布在多个不同的电信机房,能跨省跨市更好,我这里写好的断线重拨组件,你可以直接使用。

    ADSL拨号上网使用动态IP地址,每一次拨号得到的IP都不一样,所以我们可以通过程序来自动进行重新拨号以获得新的IP地址,以达到突破反爬虫封锁的目的。

    那么我们如何进行自动重新拨号呢?

    假设有10个线程在跑,大家都正常的跑,跑着跑着达到限制了,WEB服务器提示你“非常抱歉,来自您ip的请求异常频繁”,于是大家争先恐后(几乎是同时)请求拨号,这个时候同步的作用就显示出来了,只会有一个线程能拨号,在他结束之前其他线程都在等,等他拨号成功之后,其他线程会被唤醒并返回

    算法描述: 1、假设总共有N个线程抓取网页,发现被封锁之后依次排队请求锁,注意:可以想象成是同时请求。 2、线程1抢先获得锁,并且设置isDialing = true后开始拨号,注意:线程1设置isDialing = true后其他线程才可能获得锁。 3、其他线程(2-N)依次获得锁,发现isDialing = true,于是wait。注意:获得锁并判断一个布尔值,跟后面的拨号操作比起来,时间可以忽略。 4、线程1拨号完毕isDialing = false。注意:这个时候可以断定,其他所有线程必定是处于wait状态等待唤醒。 5、线程1唤醒其他线程,其他线程和线程1返回开始抓取网页。 6、抓了一会儿之后,又会被封锁,于是回到步骤1。

    在本场景中,3和4的断定是没问题的,就算是出现“不可能”的情况,即线程1已经拨号完成了,可2-N还没获得锁(汗),也不会重复拨号的情况,因为算法考虑了请求拨号时间和上一次成功拨号时间。

    下面以腾达300M无线路由器,型号:N302 v2为例子来说明。

    首先,设置路由器:上网设置 -》请根据需要选择连接模式 -》手动连接,由用户手动进行连接,如下图所示。其他的路由器使用方法类似,参照本方法替换相应的登录地址、断开连接及建立连接地址即可。

    其次,利用Firefox的Firebug功能找到路由器的登录路径及参数、断开连接路径及参数、建立连接路径及参数,如下图所示。

    接着,参考如下代码,替换自己相关的路径和参数:

    import org.jsoup.Connection;
    import org.jsoup.Jsoup;
    import org.jsoup.nodes.Document;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import java.util.*;
    
    /**
     *
     * 自动更改IP地址反爬虫封锁,支持多线程
     *
     * ADSL拨号上网使用动态IP地址,每一次拨号得到的IP都不一样
     *
     * 使用腾达300M无线路由器,型号:N302 v2
     * 路由器设置中最好设置一下:上网设置 -》请根据需要选择连接模式 -》手动连接,由用户手动进行连接。
     * 其他的路由器使用方法类似,参照本类替换相应的登录地址、断开连接及建立连接地址即可
     *
     * @author 杨尚川
     */
    public class DynamicIp {
        private DynamicIp(){}
        private static final Logger LOGGER = LoggerFactory.getLogger(DynamicIp.class);
        private static final String ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
        private static final String ENCODING = "gzip, deflate";
        private static final String LANGUAGE = "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3";
        private static final String CONNECTION = "keep-alive";
        private static final String HOST = "192.168.0.1";
        private static final String REFERER = "http://192.168.0.1/login.asp";
        private static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:36.0) Gecko/20100101 Firefox/36.0";
        private static volatile boolean isDialing = false;
        private static volatile long lastDialTime = 0l;
    
        public static void main(String[] args) {
            toNewIp();
        }
    
        /**
         * 假设有10个线程在跑,大家都正常的跑,跑着跑着达到限制了,
         * 于是大家争先恐后(几乎是同时)请求拨号,
         * 这个时候同步的作用就显示出来了,只会有一个线程能拨号,
         * 在他结束之前其他线程都在等,等他拨号成功之后,
         * 其他线程会被唤醒并返回
         *
         * 算法描述:
         * 1、假设总共有N个线程抓取网页,发现被封锁之后依次排队请求锁,注意:可以想象成是同时请求。
         * 2、线程1抢先获得锁,并且设置isDialing = true后开始拨号,注意:线程1设置isDialing = true后其他线程才可能获得锁。
         * 3、其他线程(2-N)依次获得锁,发现isDialing = true,于是wait。注意:获得锁并判断一个布尔值,跟后面的拨号操作比起来,时间可以忽略。
         * 4、线程1拨号完毕isDialing = false。注意:这个时候可以断定,其他所有线程必定是处于wait状态等待唤醒。
         * 5、线程1唤醒其他线程,其他线程和线程1返回开始抓取网页。
         * 6、抓了一会儿之后,又会被封锁,于是回到步骤1。
         * 注意:在本场景中,3和4的断定是没问题的,就算是出现“不可能”的情况,
         * 即线程1已经拨号完成了,可2-N还没获得锁(汗),也不会重复拨号的情况,
         * 因为算法考虑了请求拨号时间和上一次成功拨号时间。
         * @return 更改IP是否成功
         */
        public static boolean toNewIp() {
            long requestDialTime = System.currentTimeMillis();
            LOGGER.info(Thread.currentThread()+"请求重新拨号");
            synchronized (DynamicIp.class) {
                if (isDialing) {
                    LOGGER.info(Thread.currentThread()+"已经有其他线程在进行拨号了,我睡觉等待吧,其他线程拨号完毕会叫醒我的");
                    try {
                        DynamicIp.class.wait();
                    } catch (InterruptedException e) {
                        LOGGER.error(e.getMessage(), e);
                    }
                    LOGGER.info(Thread.currentThread()+"其他线程已经拨完号了,我可以返回了");
                    return true;
                }
                isDialing = true;
            }
            //保险起见,这里再判断一下
            //如果请求拨号的时间小于上次成功拨号的时间,则说明这个请求来的【太迟了】,则返回。
            if(requestDialTime <= lastDialTime){
                LOGGER.info("请求来的太迟了");
                isDialing = true;
                return true;
            }
            LOGGER.info(Thread.currentThread()+"开始重新拨号");
            long start = System.currentTimeMillis();
            Map<String, String> cookies = login("username***", "password***", "phonenumber***");
            if("true".equals(cookies.get("success"))) {
                LOGGER.info(Thread.currentThread()+"登陆成功");
                cookies.remove("success");
                while (!disConnect(cookies)) {
                    LOGGER.info(Thread.currentThread()+"断开连接失败,重试!");
                }
                LOGGER.info(Thread.currentThread()+"断开连接成功");
                while (!connect(cookies)) {
                    LOGGER.info(Thread.currentThread()+"建立连接失败,重试!");
                }
                LOGGER.info(Thread.currentThread()+"建立连接成功");
                LOGGER.info(Thread.currentThread()+"自动更改IP地址成功!");
                LOGGER.info(Thread.currentThread()+"拨号耗时:"+(System.currentTimeMillis()-start)+"毫秒");
                //通知其他线程拨号成功
                synchronized (DynamicIp.class) {
                    DynamicIp.class.notifyAll();
                }
                isDialing = false;
                lastDialTime = System.currentTimeMillis();
                return true;
            }
            isDialing = false;
            return false;
        }
    
        public static boolean connect(Map<String, String> cookies){
            return execute(cookies, "3");
        }
        public static boolean disConnect(Map<String, String> cookies){
            return execute(cookies, "4");
        }
        public static boolean execute(Map<String, String> cookies, String action){
            String url = "http://192.168.0.1/goform/SysStatusHandle";
            Map<String, String> map = new HashMap<>();
            map.put("action", action);
            map.put("CMD", "WAN_CON");
            map.put("GO", "system_status.asp");
            Connection conn = Jsoup.connect(url)
                    .header("Accept", ACCEPT)
                    .header("Accept-Encoding", ENCODING)
                    .header("Accept-Language", LANGUAGE)
                    .header("Connection", CONNECTION)
                    .header("Host", HOST)
                    .header("Referer", REFERER)
                    .header("User-Agent", USER_AGENT)
                    .ignoreContentType(true)
                    .timeout(30000);
            for(String cookie : cookies.keySet()){
                conn.cookie(cookie, cookies.get(cookie));
            }
    
            String title = null;
            try {
                Connection.Response response = conn.method(Connection.Method.POST).data(map).execute();
                String html = response.body();
                Document doc = Jsoup.parse(html);
                title = doc.title();
                LOGGER.info("操作连接页面标题:"+title);
            }catch (Exception e){
                LOGGER.error(e.getMessage());
            }
            if("LAN | LAN Settings".equals(title)){
                if(("3".equals(action) && isConnected())
                        || ("4".equals(action) && !isConnected())){
                    return true;
                }
            }
            return false;
        }
        public static boolean isConnected(){
            try {
                Document doc = Jsoup.connect("http://www.baidu.com/s?wd=杨尚川&t=" + System.currentTimeMillis())
                        .header("Accept", ACCEPT)
                        .header("Accept-Encoding", ENCODING)
                        .header("Accept-Language", LANGUAGE)
                        .header("Connection", CONNECTION)
                        .header("Referer", "https://www.baidu.com")
                        .header("Host", "www.baidu.com")
                        .header("User-Agent", USER_AGENT)
                        .ignoreContentType(true)
                        .timeout(30000)
                        .get();
                LOGGER.info("搜索结果页面标题:"+doc.title());
                if(doc.title() != null && doc.title().contains("杨尚川")){
                    return true;
                }
            }catch (Exception e){
                if("Network is unreachable".equals(e.getMessage())){
                    return false;
                }else{
                    LOGGER.error("状态检查失败:"+e.getMessage());
                }
            }
            return false;
        }
        public static Map<String, String> login(String userName, String password, String verify){
            try {
                Map<String, String> map = new HashMap<>();
                map.put("Username", userName);
                map.put("Password", password);
                map.put("checkEn", "0");
                Connection conn = Jsoup.connect("http://192.168.0.1/LoginCheck")
                        .header("Accept", ACCEPT)
                        .header("Accept-Encoding", ENCODING)
                        .header("Accept-Language", LANGUAGE)
                        .header("Connection", CONNECTION)
                        .header("Referer", REFERER)
                        .header("Host", HOST)
                        .header("User-Agent", USER_AGENT)
                        .ignoreContentType(true)
                        .timeout(30000);
    
                Connection.Response response = conn.method(Connection.Method.POST).data(map).execute();
                String html = response.body();
                Document doc = Jsoup.parse(html);
                LOGGER.info("登陆页面标题:"+doc.title());
                Map<String, String> cookies = response.cookies();
                if(html.contains(verify)){
                    cookies.put("success", Boolean.TRUE.toString());
                }
                LOGGER.info("*******************************************************cookies start:");
                cookies.keySet().stream().forEach((cookie) -> {
                    LOGGER.info(cookie + ":" + cookies.get(cookie));
                });
                LOGGER.info("*******************************************************cookies end:");
                return cookies;
            }catch (Exception e){
                LOGGER.error(e.getMessage(), e);
            }
            return Collections.emptyMap();
        }
    }

    最后,就可以使用了,例子如下:

    public static void classify(Set<Word> words){
        LOGGER.debug("待处理词数目:"+words.size());
        AtomicInteger i = new AtomicInteger();
        Map<String, List<String>> data = new HashMap<>();
        words.forEach(word -> {
            if(i.get()%1000 == 999){
                save(data);
            }
            showStatus(data, i.incrementAndGet(), words.size(), word.getWord());
            String html = getContent(word.getWord());
            LOGGER.debug("获取到的HTML:" +html);
            while(html.contains("非常抱歉,来自您ip的请求异常频繁")){
                //使用新的IP地址
                DynamicIp.toNewIp();
                html = getContent(word.getWord());
            }
            if(StringUtils.isNotBlank(html)) {
                parse(word.getWord(), html, data);
            }else{
                NOT_FOUND_WORDS.add(word.getWord());
            }
    
        });
        //写入磁盘
        save(data);
        LOGGER.debug("处理完毕,总词数目:"+words.size());
    }

    本文讲述的方法和代码来源于本人的开源目superword,superword是一个Java实现的英文单词分析软件,主要研究英语单词音近形似转化规律、前缀后缀规律、词之间的相似性规律等等。

    代码链接:

    1、https://github.com/ysc/superword/blob/master/src/main/java/org/apdplat/superword/tools/DynamicIp.java 

    2、https://github.com/ysc/superword/blob/master/src/main/java/org/apdplat/superword/tools/WordClassifier.java

    http://yangshangchuan.iteye.com/blog/2195287

  • 相关阅读:
    ReentrantLock实现原理分析
    《亿级流量网站架构核心技术》概要
    Java日志框架:logback详解
    40个Java多线程问题总结
    使用Jenkins部署Spring Boot项目
    spring security 实践 + 源码分析
    Spring Boot使用过滤器和拦截器分别实现REST接口简易安全认证
    Spring Boot+redis存储session,满足集群部署、分布式系统的session共享
    maven-assembly-plugin的使用
    使用maven构建多模块项目,分块开发
  • 原文地址:https://www.cnblogs.com/softidea/p/4370302.html
Copyright © 2011-2022 走看看