Appearance
Java 和 HTTP
(一)模拟 HTTP 请求
一、使用 HttpURLConnection 发送 HTTP 请求
Java 自带的 java.net
这个包中包含了很多与网络请求相关的类,但是对于我们来说,最关心的应该是 HttpURLConnection
这个类了。
1.1 创建 HTTP 连接对象
要得到一个 HttpURLConnection
HTTP 连接对象,首先需要一个 URL
,代码如下:
java
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
1.2 添加 HTTP 请求头
得到 HTTP 连接对象之后,我们就可以进行 HTTP 操作了,我们可以添加任意的 HTTP 请求头,然后执行我们需要的 GET 或者 POST 请求。我们像下面这样,添加两个 HTTP 头(User-Agent 和 Accept-Language):
java
con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
con.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
对于有些爬虫来说,这个设置是必要的,譬如有很多网站会对请求头中的 Referer 进行检查,以此来防爬或者防盗链。又譬如有些网站还会对 User-Agent 进行检查,根据这个字段来过滤一些非浏览器的请求。如果请求头设置不对的话,很可能是爬不下正确的数据的。
1.3 HTTP GET
HTTP 协议中定义了很多种 HTTP 请求方法:GET、POST、PUT、DELETE、OPTIONS 等等,其中最常用到的就是 GET 和 POST,因为在浏览器中大多都是使用这两种请求方法。
使用 HttpURLConnection 来发送 GET 请求是非常简单的,通过上面的代码创建并初始化好一个 HTTP 连接之后,就可以直接来发送 GET 请求了。
java
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
String responseBody = readResponseBody(con.getInputStream());
可以看到,代码非常简洁,没有任何累赘的代码,甚至没有任何和发送请求相关的代码,请求就是在 getResponseCode() 函数中默默的执行了。其中 readResponseBody() 函数用于读取流并转换为字符串,具体的实现如下:
java
// 读取输入流中的数据
private String readResponseBody(InputStream inputStream) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(inputStream));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
}
1.4 HTTP POST
使用 HttpURLConnection 来模拟 POST 请求和 GET 请求基本上是一样的,但是有一点不同,由于 POST 请求一般都会向服务端发送一段数据,所以 HttpURLConnection 提供了一个方法 setDoOutput(true) 来表示有数据要输出给服务端,并可以通过 getOutputStream() 得到输出流,我们将要写的数据通过这个输出流 POST 到服务端。
java
con.setRequestMethod("POST");
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.writeBytes(parameter);
wr.flush();
wr.close();
POST 完成之后,和 GET 请求一样,我们通过 getInputStream() 函数来读取服务端返回的数据。
二、使用 HttpClient 发送 HTTP 请求
使用 Java 自带的 HttpURLConnection
类完全可以满足我们的一些日常需求,不过对于网络爬虫这种高度依赖于 HTTP 工具类的程序来说,它在有些方面还是显得略为不足(我们之后会讨论到),我们需要一种扩展性定制性更强的类。Apache 的 HttpClient 就是首选。
HttpClient
是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。它相比传统的 HttpURLConnection
,增加了易用性和灵活性,它不仅让客户端发送 HTTP 请求变得更容易,而且也方便了开发人员测试接口(基于 HTTP 协议的),即提高了开发的效率,也方便提高代码的健壮性。
好了,关于 HttpClient
介绍的大话空话套话结束,让我们来看一段使用 HttpClient
来模拟 HTTP GET 请求的代码片段:
java
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
CloseableHttpResponse response = httpclient.execute(request);
// read response
2.1 HTTP GET 与 HTTP POST
上面的示例代码展示了如何使用 HttpClient
来模拟 HTTP GET 请求,可以看出 HttpClient
对每一种 HTTP 方法都准备了一个类,GET 请求使用 HttpGet
类,POST 请求使用 HttpPost
类。
和上文中介绍的一样,在发送 POST 请求时,需要向服务端写入一段数据,我们这里使用 setEntity()
函数来写入数据:
java
String parameter = "key=value";
HttpPost request = new HttpPost(url);
request.setEntity(
new StringEntity(parameter, ContentType.create("application/x-www-form-urlencoded"))
);
Entity 是 HttpClient 中的一个特别的概念,有着各种的 Entity ,都实现自 HttpEntity 接口,输入是一个 Entity,输出也是一个 Entity 。要注意的是,在这里我采用一种取巧的方式,直接使用 StringEntity 来写入 POST 数据,然后将 Content-type 改成 application/x-www-form-urlencoded ,这样就和浏览器里的表单提交请求一致了。但是我们要知道的是,一般情况下,我们可能还会使用 UrlEncodedFormEntity 这个类,只是在写爬虫的时候比较繁琐,使用起来像下面这样:
java
List<NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("key", "value"));
request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
2.2 读取响应
正如上文所说,HttpClient 的输入是一个 Entity,输出也是一个 Entity 。这和 HttpURLConnection 的流有些不同,但是基本理念是相通的。对于 Entity ,HttpClient 提供给我们一个工具类 EntityUtils,使用它可以很方便的将其转换为字符串。
java
CloseableHttpResponse response = httpclient.execute(request);
String responseBody = EntityUtils.toString(response.getEntity());
2.3 HttpEntiy 接口
上面说到了 HttpClient
中的 HttpEntity
这个接口,这个接口在使用 HttpClient 的时候相当重要,这里对其略做补充。
大多数的 HTTP 请求和响应都会包含两个部分:头和体,譬如请求头请求体,响应头响应体, Entity 也就是这里的 “体” 部分,这里暂且称之为 “实体” 。一般情况下,请求包含实体的有 POST 和 PUT 方法,而绝大多数的响应都是包含实体的,除了 HEAD 请求的响应,还有 204 No Content、304 Not Modified 和 205 Reset Content 这些不包含实体。
HttpClient
将实体分为三种类型:
- 流类型(streamed):实体内容从流中读取的,通常只能读一次
- 自包含类型(self-contained):手工创建的,通常可重复读取
- 包装类型(wrapping):使用一种实体包装另一种实体
上面的例子中我们直接使用工具方法 EntityUtils.toString()
将一个 HttpEntity 转换为字符串,虽然使用起来非常方便,但是要特别注意的是这其实是不安全的做法,要确保返回内容的长度不能太长,如果太长的话,还是建议使用流的方式来读取:
java
CloseableHttpResponse response = httpclient.execute(request);
HttpEntity entity = response.getEntity();
if (entity != null) {
long length = entity.getContentLength();
if (length != -1 && length < 2048) {
String responseBody = EntityUtils.toString(entity);
}
else {
InputStream in = entity.getContent();
// read from the input stream ...
}
}
三、关于 HTTPS 请求
到这里为止,我们一直忽略了 HTTP 请求和 HTTPS 请求之间的差异,因为大多数情况下,我们确实不需要关心 URL 是 HTTP 的还是 HTTPS 的,上面给出的代码也都能很好的自动处理这两种不同类型的请求。
但是,我们还是应该注意下这两种请求的差异,后面我们在介绍 HTTP 代理时将会特别看到这两者之间的差异。另外还有一点,在调用 URL
的 openConnection()
方法时,如果 URL 是 HTTP 协议的,返回的是一个 HttpURLConnection 对象,而如果 URL 是 HTTPS 协议的,返回的将是一个 HttpsURLConnection 对象。
(二)使用代理
一、简单的 HTTP 代理
我们先从最简单的开始,网上有很多免费代理,直接上百度搜索 “免费代理” 或者 “HTTP 代理” 就能找到很多(虽然网上能找到大量的免费代理,但它们的安全性已经有很多文章讨论过了,也有专门的文章对此进行调研的,譬如这篇文章,我在这里就不多作说明,如果你的爬虫爬取的信息并没有什么特别的隐私问题,可以忽略之,如果你的爬虫涉及一些例如模拟登录之类的功能,考虑到安全性,我建议你还是不要使用网上公开的免费代理,而是搭建自己的代理服务器比较靠谱)。
1.1 HttpURLConnection 使用代理
HttpURLConnection 的 openConnection()
方法可以传入一个 Proxy 参数,如下:
java
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 9876));
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection(proxy);
OK 了,就这么简单!
不仅如此,我们注意到 Proxy 构造函数的第一个参数为枚举类型 Proxy.Type.HTTP
,那么很显然,如果将其修改为 Proxy.Type.SOCKS
即可以使用 SOCKS 代理。
1.2 HttpClient 使用代理
由于 HttpClient
非常灵活,使用 HttpClient 来连接代理有很多不同的方法。最简单的方法莫过于下面这样:
java
HttpHost proxy = new HttpHost("127.0.0.1", 9876, "HTTP");
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = httpclient.execute(proxy, request);
和上一篇中使用 HttpClient 发送请求的代码几乎一样,只是 httpclient.execute()
方法多加了一个参数,第一参数为 HttpHost
类型,我们这里设置成我们的代理即可。
这里要注意一点的是,虽然这里的 new HttpHost()
和上面的 new Proxy()
一样,也是可以指定协议类型的,但是遗憾的是 HttpClient 默认是不支持 SOCKS 协议的,如果我们使用下面的代码:
java
HttpHost proxy = new HttpHost("127.0.0.1", 1080, "SOCKS");
将会直接报协议不支持异常:
org.apache.http.conn.UnsupportedSchemeException: socks protocol is not supported
如果希望 HttpClient 支持 SOCKS 代理,可以参看这里:How to use Socks 5 proxy with Apache HTTP Client 4? 通过 HttpClient 提供的 ConnectionSocketFactory 类来实现。
虽然使用这种方式很简单,只需要加个参数就可以了,但是其实看 HttpClient 的代码注释,如下:
java
/*
* @param target the target host for the request.
* Implementations may accept <code>null</code>
* if they can still determine a route, for example
* to a default target or by inspecting the request.
* @param request the request to execute
*/
可以看到第一个参数 target 并不是代理,它的真实作用是 执行请求的目标主机,这个解释有点模糊,什么叫做 执行请求的目标主机?代理算不算执行请求的目标主机呢?因为按常理来讲,执行请求的目标主机 应该是要请求 URL 对应的站点才对。如果不算的话,为什么这里将 target 设置成代理也能正常工作?这个我也不清楚,还需要进一步研究下 HttpClient 的源码来深入了解下。
除了上面介绍的这种方式(自己写的,不推荐使用)来使用代理之外,HttpClient 官网还提供了几个示例,我将其作为推荐写法记录在此。
第一种写法是使用 RequestConfig 类,如下:
java
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
request.setConfig(
RequestConfig.custom()
.setProxy(new HttpHost("45.32.21.237", 8888, "HTTP"))
.build()
);
CloseableHttpResponse response = httpclient.execute(request);
第二种写法是使用 RoutePlanner 类,如下:
java
HttpHost proxy = new HttpHost("127.0.0.1", 9876, "HTTP");
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom()
.setRoutePlanner(routePlanner)
.build();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = httpclient.execute(request);
二、使用系统代理配置
我们在调试 HTTP 爬虫程序时,常常需要切换代理来测试,有时候直接使用系统自带的代理配置将是一种简单的方法。以前在做 .Net 项目时,程序默认使用 Internet 网络设置中配的代理,遗憾的是,我这里说的系统代理配置指的 JVM 系统,而不是操作系统,我还没找到简单的方法来让 Java 程序直接使用 Windows 系统下的代理配置。
尽管如此,系统代理使用起来还是很简单的。一般有下面两种方式可以设置 JVM 的代理配置:
2.1 System.setProperty
Java 中的 System
类不仅仅是用来给我们 System.out.println()
打印信息的,它其实还有很多静态方法和属性可以用。其中 System.setProperty()
就是比较常用的一个。
可以通过下面的方式来分别设置 HTTP 代理,HTTPS 代理和 SOCKS 代理:
java
// HTTP 代理,只能代理 HTTP 请求
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "9876");
// HTTPS 代理,只能代理 HTTPS 请求
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "9876");
// SOCKS 代理,支持 HTTP 和 HTTPS 请求
// 注意:如果设置了 SOCKS 代理就不要设 HTTP/HTTPS 代理
System.setProperty("socksProxyHost", "127.0.0.1");
System.setProperty("socksProxyPort", "1080");
这里有三点要说明:
- 系统默认先使用 HTTP/HTTPS 代理,如果既设置了 HTTP/HTTPS 代理,又设置了 SOCKS 代理,SOCKS 代理会起不到作用
- 由于历史原因,注意
socksProxyHost
和socksProxyPort
中间没有小数点 - HTTP 和 HTTPS 代理可以合起来缩写,如下:
java
// 同时支持代理 HTTP/HTTPS 请求
System.setProperty("proxyHost", "127.0.0.1");
System.setProperty("proxyPort", "9876");
2.2 JVM 命令行参数
可以使用 System.setProperty()
方法来设置系统代理,也可以直接将这些参数通过 JVM 的命令行参数来指定。如果你使用的是 Eclipse ,可以按下面的步骤来设置:
- 按顺序打开:Window -> Preferences -> Java -> Installed JREs -> Edit
- 在 Default VM arguments 中填写参数:
-DproxyHost=127.0.0.1 -DproxyPort=9876
2.3 使用系统代理
上面两种方法都可以设置系统,下面要怎么在程序中自动使用系统代理呢?
对于 HttpURLConnection
类来说,程序不用做任何变动,它会默认使用系统代理。但是 HttpClient
默认是不使用系统代理的,如果想让它默认使用系统代理,可以通过 SystemDefaultRoutePlanner
和 ProxySelector
来设置。示例代码如下:
java
SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
.setRoutePlanner(routePlanner)
.build();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = httpclient.execute(request);
(三) 代理认证
一、关于 HTTP 的身份认证
HTTP 协议最常见的认证方式有两种:基本认证(Basic authentication)和摘要认证(Digest authentication)。HTTP 的认证模型非常简单,就是所谓的质询/响应(challenge/response)框架:当用户向服务器发送一条 HTTP 请求报文时,服务器首先回复一个“认证质询”响应,要求用户提供身份信息,然后用户再一次发送 HTTP 请求报文,这次的请求头中附带上身份信息(用户名密码),如果身份匹配,服务器则正常响应,否则服务器会继续对用户进行质询或者直接拒绝请求。
摘要认证的实现比基本认证要复杂一点,在平时的使用中也并不多见,这里忽略,如果想详细了解它,可以查看维基百科上关于 HTTP 摘要认证 的解释。这里重点介绍下 HTTP 基本认证,因为无论是代理服务器对用户进行认证,还是 Web 服务器对用户进行认证,最常用的手段都是 HTTP 基本认证,它实现简单,容易理解,几乎所有的服务器都能支持它。
一个典型的 HTTP 基本认证,如下图所示,图片摘自《HTTP 权威指南》:
用户第一次向服务器发起请求时,服务器会返回一条 401 Unauthorized 响应,如果用户是使用浏览器访问的话,浏览器会弹出一个密码提示框,提醒用户输入用户名和密码,于是用户重新发起请求,在第二次请求中将在 Authorization 头部添加上身份信息,这个身份信息其实只是简单的对用户输入的用户名密码做了 Base64 编码 处理,服务器对用户的认证成功之后,返回 200 OK 。
二、使用基本认证
2.1 区分 Proxy 认证 和 WWW 认证
这篇博客本来是介绍代理认证的,但是代理认证其实只是 HTTP 身份认证中的一种而已,所以上面大部分内容对于代理认证来说是一样的,包括质询/响应框架以及身份认证的基本流程。不过要在代码里实现这两种认证,细节方面会有所不同,下面是两种认证的一个对比。
根本区别
- WWW 认证:指的是 Web 服务器对客户端发起的认证
- Proxy 认证:指的是代理服务器对客户端发起的认证
响应的状态码不同
- WWW 认证:第一次访问时响应 401 Unauthorized
- Proxy 认证:第一次访问时响应 407 Unauthorized
认证头部不同
- WWW 认证:WWW-Authenticate, Authorization, Authentication-Info
- Proxy 认证:Proxy-Authenticate, Proxy-Authorization, Proxy-Authentication-Info
2.2 手工设置认证头部
通过上面的介绍我们了解到,要实现 HTTP 身份认证,无论是 WWW 认证也好,Proxy 认证也罢,其实只需要在 HTTP 的请求头部添加一个认证的头部(Authorization
或者 Proxy-Authorization
)。认证头部的信息就是用户名和密码,将用户名和密码使用冒号分割,然后再对其进行 Base64 编码即可,我们使用 HttpURLConnection
来模拟这个过程,如下:
java
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
// 设置认证头部
final String userName = "username";
final String password = "password";
String nameAndPass = userName + ":" + password;
String encoding = new String(Base64.encodeBase64(nameAndPass.getBytes()));
con.setRequestProperty("Authorization", "Basic " + encoding);
con.setRequestMethod("GET");
String responseBody = readResponseBody(con.getInputStream());