8年多爬蟲經驗的人告訴你,國內ADSL是王道,多申請些線路,分佈在多個不一樣的電信機房,能跨省跨市更好,我這裏寫好的斷線重撥組件,你能夠直接使用。html
ADSL撥號上網使用動態IP地址,每一次撥號獲得的IP都不同,因此咱們能夠經過程序來自動進行從新撥號以得到新的IP地址,以達到突破反爬蟲封鎖的目的。java
那麼咱們如何進行自動從新撥號呢?node
假設有10個線程在跑,你們都正常的跑,跑着跑着達到限制了,WEB服務器提示你「很是抱歉,來自您ip的請求異常頻繁」,因而你們爭先恐後(幾乎是同時)請求撥號,這個時候同步的做用就顯示出來了,只會有一個線程能撥號,在他結束以前其餘線程都在等,等他撥號成功以後,其餘線程會被喚醒並返回git
算法描述:
一、假設總共有N個線程抓取網頁,發現被封鎖以後依次排隊請求鎖,注意:能夠想象成是同時請求。
二、線程1搶先得到鎖,而且設置isDialing = true後開始撥號,注意:線程1設置isDialing = true後其餘線程纔可能得到鎖。
三、其餘線程(2-N)依次得到鎖,發現isDialing = true,因而wait。注意:得到鎖並判斷一個布爾值,跟後面的撥號操做比起來,時間能夠忽略。
四、線程1撥號完畢isDialing = false。注意:這個時候能夠判定,其餘全部線程一定是處於wait狀態等待喚醒。
五、線程1喚醒其餘線程,其餘線程和線程1返回開始抓取網頁。
六、抓了一下子以後,又會被封鎖,因而回到步驟1。github
在本場景中,3和4的判定是沒問題的,就算是出現「不可能」的狀況,即線程1已經撥號完成了,可2-N還沒得到鎖(汗),也不會重複撥號的狀況,由於算法考慮了請求撥號時間和上一次成功撥號時間。算法
下面以騰達300M無線路由器,型號:N302 v2爲例子來講明。服務器
首先,設置路由器:上網設置 -》請根據須要選擇鏈接模式 -》手動鏈接,由用戶手動進行鏈接,以下圖所示。其餘的路由器使用方法相似,參照本方法替換相應的登陸地址、斷開鏈接及創建鏈接地址便可。
cookie
其次,利用Firefox的Firebug功能找到路由器的登陸路徑及參數、斷開鏈接路徑及參數、創建鏈接路徑及參數,以下圖所示。多線程
接着,參考以下代碼,替換本身相關的路徑和參數:app
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個線程在跑,你們都正常的跑,跑着跑着達到限制了, * 因而你們爭先恐後(幾乎是同時)請求撥號, * 這個時候同步的做用就顯示出來了,只會有一個線程能撥號, * 在他結束以前其餘線程都在等,等他撥號成功以後, * 其餘線程會被喚醒並返回 * * 算法描述: * 一、假設總共有N個線程抓取網頁,發現被封鎖以後依次排隊請求鎖,注意:能夠想象成是同時請求。 * 二、線程1搶先得到鎖,而且設置isDialing = true後開始撥號,注意:線程1設置isDialing = true後其餘線程纔可能得到鎖。 * 三、其餘線程(2-N)依次得到鎖,發現isDialing = true,因而wait。注意:得到鎖並判斷一個布爾值,跟後面的撥號操做比起來,時間能夠忽略。 * 四、線程1撥號完畢isDialing = false。注意:這個時候能夠判定,其餘全部線程一定是處於wait狀態等待喚醒。 * 五、線程1喚醒其餘線程,其餘線程和線程1返回開始抓取網頁。 * 六、抓了一下子以後,又會被封鎖,因而回到步驟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實現的英文單詞分析軟件,主要研究英語單詞音近形似轉化規律、前綴後綴規律、詞之間的類似性規律等等。
代碼連接: