这是 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 请求头来绕过豆瓣网的访问限制。如何让咱们的程序不被资源网站视为爬虫程序呢?须要作好如下三点:
但愿这篇文章对你有所帮助,下一篇是关于多线程爬虫的探索。若是你对爬虫感兴趣,不妨关注一波,相互学习,相互进步
文章不足之处,望你们多多指点,共同窗习,共同进步
打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一块儿进步吧。