自動更改IP地址反爬蟲封鎖,支持多線程
ADSL撥號上網使用動態IP地址,每一次撥號得到的IP都不一樣,所以我們可以通過程序來自動進行重新撥號以獲得新的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實現的英文單詞分析軟件,主要研究英語單詞音近形似轉化規律、前綴后綴規律、詞之間的相似性規律等等。
代碼鏈接:
來自:http://my.oschina.net/apdplat/blog/391088