c httpclient抓取网页(如何开发一个Java爬虫(的设计机制及原理))
优采云 发布时间: 2021-10-18 13:11c httpclient抓取网页(如何开发一个Java爬虫(的设计机制及原理))
最近在写一个小爬虫,准备爬取一些网页数据进行模型训练。在考虑如何爬取和分析网页的时候,参考了OSC站的一些项目,特别是@黄亿华写的webmagic的设计机制和原理——如何开发一个Java爬虫”文章给了很多启发,webmagic是一个垂直爬虫,我想写的是一个更通用的爬虫,主要爬上中文网站内容,对于HTTP协议和消息处理,没有比HttpClient组件更好的选择了,对于HTML代码解析,经过对比HTMLParser和Jsoup后,后者在API的使用上优势明显,简洁易懂,使用的开源组件敲定后,
对于我的爬虫爬取部分功能,只要根据网页的url爬取html代码,然后从html代码中解析出link和link
标签的文字没问题,所以解析的结果可以用一个Page类来表示。这个类纯粹是一个POJO,那么如何使用HttpClient和Jsoup直接解析成一个Page对象呢?
在HttpClient4.2中,提供了ResponseHandler接口来处理HttpResponse。因此,通过实现该接口,可以将返回的 HTML 代码解析为所需的 Page 对象。主要思路是先把HttpResponse中的数据读出,转换成HTML代码,然后用jsoup解析
标签和标签。代码显示如下,
public class PageResponseHandler implements ResponseHandler {
private Page page;
public PageResponseHandler(Page page) {
this.page = page;
}
public void setPage(Page page) {
this.page = page;
}
public Page getPage() {
return page;
}
@Override
public Page handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
EntityUtils.consume(entity);
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
if (entity == null)
return null;
// 利用HTTPClient自带的EntityUtils把当前HttpResponse中的HttpEntity转化成HTML代码
String html = EntityUtils.toString(entity);
Document document = Jsoup.parse(html);
Elements links = document.getElementsByTag("a");
for (int i = 0; i < links.size(); i++) {
Element link = links.get(i);
page.addAnchor(link.attr("href"), link.text());
}
// parse context of plain text from HTML code,
Elements paragraphs = document.getElementsByTag("p");
StringBuffer plainText = new StringBuffer(html.length() / 2);
for (int i = 0; i < paragraphs.size(); i++) {
Element paragraph = paragraphs.get(i);
plainText.append(paragraph.text()).append("\n");
}
page.setPlainText(plainText.toString());
return page;
}
}
代码不超过40行,非常简单。现在您可以直接返回 Page 对象。编写一个测试类来测试这个 PageResponseHandler。测试这个类的功能不需要复杂的代码。
public class PageResponseHandlerTest {
HttpClient httpclient;
PageResponseHandler pageResponseHandler;
final String url = "http://news.163.com/13/0903/11/97RHS2NS0001121M.html";
Page page = new Page(url);
@Before
public void setUp() throws Exception {
httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet(url);
pageResponseHandler = new PageResponseHandler(page);
httpclient.execute(httpget, pageResponseHandler);
}
@After
public void tearDown() throws Exception {
httpclient.getConnectionManager().shutdown();
}
@Test
public void test() {
System.out.println(page.getPlainText());
assertTrue(page.getPlainText().length() > 0);
assertTrue(page.getAnchors().size() > 0);
}
}
到目前为止,该爬虫中的爬取和分析功能运行良好。对于中文,也可以很好的解析。更细心的读者会发现,这些代码中没有字符集,也没有设置字符集。转换字符集,后面会讨论HttpClient4.2组件中的字符集处理。
首先回顾一下Content-Type在HTTP协议的RFC规范中的作用。它指定发送给接收者的 Http 实体内容的媒体类型。对于文本类型HttpEntity,通常采用如下形式。指定 HttpEntity 的媒体类型。使用哪个字符集用于编码?另外,RFC规范还规定,如果Content-Type没有指定字符集,则默认使用ISO-8859-1字符集对Http实体进行编码
Content-Type: text/html; charset=UTF-8
说了这么多,大家应该都能猜到HttpClient4.2是如何正确编码的----就是使用Content-Type头中收录的字符集作为编码输入源。具体代码可以看EntityUtils类第212行开始的代码。EntityUtils 首先从 HttpEntity 对象获取 Content-Type。如果Content-Type的字符集不为空,则使用Content-Type对象中指定的字符集进行编码,否则使用开发者指定的字符集进行编码。字符集也没有指定,使用默认的字符集iso-8859-1进行编码。当然是实现了编码,还是调用了JDK的Reader类。
ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset();
if (charset == null) {
charset = defaultCharset;
}
if (charset == null) {
charset = HTTP.DEF_CONTENT_CHARSET;
}
Reader reader = new InputStreamReader(instream, charset);
CharArrayBuffer buffer = new CharArrayBuffer(i);
char[] tmp = new char[1024];
int l;
while((l = reader.read(tmp)) != -1) {
buffer.append(tmp, 0, l);
}
return buffer.toString();
开放的互联网在创造了繁荣的网站的同时,也为非HTML规范网站提供了一些机会。部分中文网站没有正确设置HTTP请求对应头部的Content-。Type 属性使 HttpClient 使用默认的 iso-8859-1 字符集对 Http 实体进行编码。所以为了防止HttpClient抓取中文网站,可以指定一个默认的GBK字符集。将原来的 PageResponseHandler 转换字符串的代码行修改为
String html = EntityUtils.toString(entity, Charset.forName("gbk"));
至此,爬虫的爬取解析功能已经完成,再也不怕Content-Type头的中文网站设置不正确了。希望这篇文章文章对读者有用。
@仪山湖