這是 Java 爬蟲系列博文的第四篇,在上一篇 Java 爬蟲趕上數據異步加載,試試這兩種辦法! 中,咱們從內置瀏覽器內核和反向解析法兩個角度簡單的聊了聊關於處理數據異步加載問題。在這篇文章中,咱們簡單的來聊一聊爬蟲時,資源網站根據用戶訪問行爲屏蔽掉爬蟲程序及其對應的解決辦法。java
屏蔽爬蟲程序是資源網站的一種保護措施,最經常使用的反爬蟲策略應該是基於用戶的訪問行爲。好比限制每臺服務器在必定的時間內只能訪問 X 次,超過該次數就認爲這是爬蟲程序進行的訪問,基於用戶訪問行爲判斷是不是爬蟲程序也不止是根據訪問次數,還會根據每次請求的User Agent 請求頭、每次訪問的間隔時間等。總的來講是由多個因數決定的,其中以訪問次數爲主。python
反爬蟲是每一個資源網站自保的措施,旨在保護資源不被爬蟲程序佔用。例如咱們前面使用到的豆瓣網,它會根據用戶訪問行爲來屏蔽掉爬蟲程序,每一個 IP 在每分鐘訪問次數達到必定次數後,後面一段時間內的請求返回直接返回 403 錯誤,覺得着你沒有權限訪問該頁面。因此咱們今天再次拿豆瓣網爲例,咱們用程序模擬出這個現象,下面是我編寫的一個採集豆瓣電影的程序git
/** * 採集豆瓣電影 */
public class CrawlerMovie {
public static void main(String[] args) {
try {
CrawlerMovie crawlerMovie = new CrawlerMovie();
// 豆瓣電影連接
List<String> movies = crawlerMovie.movieList();
//建立10個線程的線程池
ExecutorService exec = Executors.newFixedThreadPool(10);
for (String url : movies) {
//執行線程
exec.execute(new CrawlMovieThread(url));
}
//線程關閉
exec.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 豆瓣電影列表連接 * 採用反向解析法 * * @return */
public List<String> movieList() throws Exception {
// 獲取100條電影連接
String url = "https://movie.douban.com/j/search_subjects?type=movie&tag=熱門&sort=recommend&page_limit=200&page_start=0";
CloseableHttpClient client = HttpClients.createDefault();
List<String> movies = new ArrayList<>(100);
try {
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = client.execute(httpGet);
System.out.println("獲取豆瓣電影列表,返回驗證碼:" + response.getStatusLine().getStatusCode());
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity, "utf-8");
// 將請求結果格式化成json
JSONObject jsonObject = JSON.parseObject(body);
JSONArray data = jsonObject.getJSONArray("subjects");
for (int i = 0; i < data.size(); i++) {
JSONObject movie = data.getJSONObject(i);
movies.add(movie.getString("url"));
}
}
response.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
client.close();
}
return movies;
}
}
/** * 採集豆瓣電影線程 */
class CrawlMovieThread extends Thread {
// 待採集連接
String url;
public CrawlMovieThread(String url) {
this.url = url;
}
public void run() {
try {
Connection connection = Jsoup.connect(url)
.method(Connection.Method.GET)
.timeout(50000);
Connection.Response Response = connection.execute();
System.out.println("採集豆瓣電影,返回狀態碼:" + Response.statusCode());
} catch (Exception e) {
System.out.println("採集豆瓣電影,採集出異常:" + e.getMessage());
}
}
}
複製代碼
這段程序的邏輯比較簡單,先採集到豆瓣熱門電影,這裏使用直接訪問 Ajax 獲取豆瓣熱門電影的連接,而後解析出電影的詳情頁連接,多線程訪問詳情頁連接,由於只有在多線程的狀況下才能達到豆瓣的訪問要求。豆瓣熱門電影頁面以下:程序員
屢次運行上面的程序,你最後會獲得下圖的結果github
從上圖中咱們能夠看出,httpclient 訪問返回的狀態碼爲 403 ,說明咱們已經沒有權限訪問該頁面了,也就是說豆瓣網已經認爲咱們是爬蟲程序啦,拒接掉了咱們的訪問請求。咱們來分析一下咱們如今的訪問架構,由於咱們是直接訪問豆瓣網的,因此此時的訪問架構以下圖所示:docker
咱們想要突破這層限制的話,咱們就不能直接訪問豆瓣網的服務器,咱們須要拉入第三方,讓別人代替咱們去訪問,咱們每次訪問都找不一樣的人,這樣就不會被限制了,這個也就是所謂的 IP代理。 此時的訪問架構就變成了下面這張圖:數據庫
咱們使用的 IP代理,咱們就須要有 IP代理池,接下來咱們就來聊一聊 IP 代理池json
代理服務器有不少廠商在作這一塊,具體的我就不說了,本身百度 IP 代理能夠搜出一大堆,這些 IP代理商都有提供收費和免費的代理 IP,收費的代理 IP可用性高,速度快,在線上環境若是須要使用代理的話,建議使用收費的代理 IP。若是隻是本身研究的話,咱們就能夠去採集這些廠商的免費公開代理 IP,這些 IP 的性能和可用性都比較差,可是不影響咱們使用。瀏覽器
由於咱們是 Demo 項目,因此咱們就本身搭建 IP代理池。咱們該怎麼設計一個 IP代理池呢?下圖是我畫的簡單 IP代理池架構圖服務器
從上面的架構圖中,能夠看出一個 IP 代理系統會涉及到 4 個模塊,分別爲 IP 採集模塊、 IP 存儲模塊、IP 檢測模塊和 API 接口模塊。
負責從各大 IP代理廠商採集代理 IP,採集的網站越多,代理 IP 的可用性就越高
存儲採集回來的代理 IP,比較經常使用的是 Redis 這樣的高性能的數據庫,在存儲方面咱們須要存儲兩種數據,一種是檢測可用的代理 IP,另外一種是採集回來還未檢測的代理 IP。
檢測採集回來的 IP 是否可用,這樣可以讓咱們提供的 IP 可用性變高,咱們先過濾掉不可用的 IP。
以接口的形式對外提供可用代理 IP
上面就是關於 IP代理池的相關設計,對於這些咱們只須要簡單瞭解一下就好了,由於如今基本上不須要咱們去編寫 IP代理池服務啦,在 GitHub 上已經有大量優秀的開源項目,不必重複造輪子啦。我爲你們選取了在 GitHub 上有 8K star 的開源 IP代理池項目 proxy_pool ,咱們將使用它做爲咱們 IP 代理池。關於 proxy_pool 請訪問:https://github.com/jhao104/proxy_pool
proxy_pool 是用 python 語言寫的,不過這也沒什麼關係,由於如今均可以容器化部署,使用容器化部署能夠屏蔽掉一些環境的安裝,只須要運行鏡像就能夠運行服務了,並不須要知道它裏面的具體實現,因此這個項目不懂 Python 的 Java 程序員也是可使用的。proxy_pool 使用的是 Redis 來存儲採集的 IP,因此在啓動 proxy_pool 前,你須要先啓動 Redis 服務。下面是 proxy_pool docker啓動步驟。
docker pull jhao104/proxy_pool
docker run --env db_type=REDIS --env db_host=127.0.0.1 --env db_port=6379 --env db_password=pwd_str -p 5010:5010 jhao104/proxy_pool
運行鏡像後,咱們等待一段時間,由於第一次啓動採集數據和處理數據須要一段時間。等待以後訪問 http://{your_host}:5010/get_all/
,若是你獲得下圖所示的結果,說明 proxy_pool 項目你已經部署成功。
搭建好 IP代理池以後,咱們就可使用代理 IP 來採集豆瓣電影啦,咱們已經知道了除了 IP 以外,User Agent 請求頭也會是豆瓣網判斷訪問是不是爬蟲程序的一個因素,因此咱們也對 User Agent 請求頭進行僞造,咱們每次訪問使用不一樣的 User Agent 請求頭。
咱們爲豆瓣電影採集程序引入 IP代理和 隨機 User Agent 請求頭,具體代碼以下:
public class CrawlerMovieProxy {
/** * 經常使用 user agent 列表 */
static List<String> USER_AGENT = new ArrayList<String>(10) {
{
add("Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19");
add("Mozilla/5.0 (Linux; U; Android 4.0.4; en-gb; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30");
add("Mozilla/5.0 (Linux; U; Android 2.2; en-gb; GT-P1000 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1");
add("Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
add("Mozilla/5.0 (Android; Mobile; rv:14.0) Gecko/14.0 Firefox/14.0");
add("Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36");
add("Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19");
add("Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3");
add("Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A101a Safari/419.3");
}
};
/** * 隨機獲取 user agent * * @return */
public String randomUserAgent() {
Random random = new Random();
int num = random.nextInt(USER_AGENT.size());
return USER_AGENT.get(num);
}
/** * 設置代理ip池 * * @param queue 隊列 * @throws IOException */
public void proxyIpPool(LinkedBlockingQueue<String> queue) throws IOException {
// 每次能隨機獲取一個代理ip
String proxyUrl = "http://192.168.99.100:5010/get_all/";
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(proxyUrl);
CloseableHttpResponse response = httpclient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity, "utf-8");
JSONArray jsonArray = JSON.parseArray(body);
int size = Math.min(100, jsonArray.size());
for (int i = 0; i < size; i++) {
// 將請求結果格式化成json
JSONObject data = jsonArray.getJSONObject(i);
String proxy = data.getString("proxy");
queue.add(proxy);
}
}
response.close();
httpclient.close();
return;
}
/** * 隨機獲取一個代理ip * * @return * @throws IOException */
public String randomProxyIp() throws IOException {
// 每次能隨機獲取一個代理ip
String proxyUrl = "http://192.168.99.100:5010/get/";
String proxy = "";
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(proxyUrl);
CloseableHttpResponse response = httpclient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity, "utf-8");
// 將請求結果格式化成json
JSONObject data = JSON.parseObject(body);
proxy = data.getString("proxy");
}
return proxy;
}
/** * 豆瓣電影連接列表 * * @return */
public List<String> movieList(LinkedBlockingQueue<String> queue) {
// 獲取60條電影連接
String url = "https://movie.douban.com/j/search_subjects?type=movie&tag=熱門&sort=recommend&page_limit=40&page_start=0";
List<String> movies = new ArrayList<>(40);
try {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
// 設置 ip 代理
HttpHost proxy = null;
// 隨機獲取一個代理IP
String proxy_ip = randomProxyIp();
if (StringUtils.isNotBlank(proxy_ip)) {
String[] proxyList = proxy_ip.split(":");
System.out.println(proxyList[0]);
proxy = new HttpHost(proxyList[0], Integer.parseInt(proxyList[1]));
}
// 隨機獲取一個請求頭
httpGet.setHeader("User-Agent", randomUserAgent());
RequestConfig requestConfig = RequestConfig.custom()
.setProxy(proxy)
.setConnectTimeout(10000)
.setSocketTimeout(10000)
.setConnectionRequestTimeout(3000)
.build();
httpGet.setConfig(requestConfig);
CloseableHttpResponse response = client.execute(httpGet);
System.out.println("獲取豆瓣電影列表,返回驗證碼:" + response.getStatusLine().getStatusCode());
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity, "utf-8");
// 將請求結果格式化成json
JSONObject jsonObject = JSON.parseObject(body);
JSONArray data = jsonObject.getJSONArray("subjects");
for (int i = 0; i < data.size(); i++) {
JSONObject movie = data.getJSONObject(i);
movies.add(movie.getString("url"));
}
}
response.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
}
return movies;
}
public static void main(String[] args) {
// 存放代理ip的隊列
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue(100);
try {
CrawlerMovieProxy crawlerProxy = new CrawlerMovieProxy();
// 初始化ip代理隊列
crawlerProxy.proxyIpPool(queue);
// 獲取豆瓣電影列表
List<String> movies = crawlerProxy.movieList(queue);
//建立固定大小的線程池
ExecutorService exec = Executors.newFixedThreadPool(5);
for (String url : movies) {
//執行線程
exec.execute(new CrawlMovieProxyThread(url, queue, crawlerProxy));
}
//線程關閉
exec.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/** * 採集豆瓣電影線程 */
class CrawlMovieProxyThread extends Thread {
// 待採集連接
String url;
// 代理ip隊列
LinkedBlockingQueue<String> queue;
// 代理類
CrawlerMovieProxy crawlerProxy;
public CrawlMovieProxyThread(String url, LinkedBlockingQueue<String> queue, CrawlerMovieProxy crawlerProxy) {
this.url = url;
this.queue = queue;
this.crawlerProxy = crawlerProxy;
}
public void run() {
String proxy;
String[] proxys = new String[2];
try {
Connection connection = Jsoup.connect(url)
.method(Connection.Method.GET)
.timeout(50000);
// 若是代理ip隊列爲空,則從新獲取ip代理
if (queue.size() == 0) crawlerProxy.proxyIpPool(queue);
// 從隊列中獲取代理ip
proxy = queue.poll();
// 解析代理ip
proxys = proxy.split(":");
// 設置代理ip
connection.proxy(proxys[0], Integer.parseInt(proxys[1]));
// 設置 user agent
connection.header("User-Agent", crawlerProxy.randomUserAgent());
Connection.Response Response = connection.execute();
System.out.println("採集豆瓣電影,返回狀態碼:" + Response.statusCode() + " ,請求ip:" + proxys[0]);
} catch (Exception e) {
System.out.println("採集豆瓣電影,採集出異常:" + e.getMessage() + " ,請求ip:" + proxys[0]);
}
}
}
複製代碼
運行修改後的採集程序,可能須要屢次運行,由於你的代理 IP 不必定每次都有效。代理 IP 有效的話,你將獲得以下結果
結果中咱們能夠看出,40 次的電影詳情頁訪問,有大量的代理 IP 是無效的,只有一小部分的代理 IP 有效。結果直接證實了免費的代理 IP 可用性不高,因此若是線上須要使用代理 IP 的話,最好使用收費的代理 IP。儘管咱們本身搭建的 IP代理池可用性不是過高,可是咱們設置的 IP 代理訪問豆瓣電影已經成功了,使用 IP 代理成功繞過了豆瓣網的限制。
關於爬蟲服務器被屏蔽,緣由有不少,咱們這篇文章主要介紹的是經過 設置 IP 代理和僞造 User Agent 請求頭來繞過豆瓣網的訪問限制。如何讓咱們的程序不被資源網站視爲爬蟲程序呢?須要作好如下三點:
但願這篇文章對你有所幫助,下一篇是關於多線程爬蟲的探索。若是你對爬蟲感興趣,不妨關注一波,相互學習,相互進步
文章不足之處,望你們多多指點,共同窗習,共同進步
打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。