继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

使用HttpClient和Jsoup快捷抓取和分析页面

Qing勇
关注TA
已关注
手记 13
粉丝 15
获赞 404

最近在写一个小爬虫,准备爬一部分网页数据,来做模型训练,在考虑如何抓取网页及分析网页时,参考了OSC站中一些项目,特别是@黄亿华写的《webmagic的设计机制及原理-如何开发一个Java爬虫》这篇文章给了不少启发,webmagic是一个垂直的爬虫,而我要写的是一个比较通用的爬虫,主要爬起中文的网站的内容,对于HTTP协议及报文的处理,没有比HttpClient组件更好的选择了,对于HTML代码的解析,在比较HTMLParser和Jsoup后,后者在API的使用上优势明显,简洁易懂。所使用的开源组件定下来后,接着开始思考如何抓取和解析这两个基本功能。

对于我的爬虫抓取这部分功能来说,只要根据网页的URL抓取HTML代码,再从HTML代码中的解析出链接和<p>标签的文本即可以,因此解析的结果可以用一个Page类来表示,这个类纯粹是一个POJO,那么如何使用HttpClient和Jsoup直接解析成Page对象?

在HttpClient 4.2中,提供了ResponseHandler<T>这个接口来负责处理HttpResponse,因此,借助实现这个接口,可以所返回的HTML代码解析成所需要的Page对象,主要的思路是先把HttpResponse中的数据读出来,变换成HTML代码,然后再用jsoup解析<p>标签和<a>标签。代码如下,

public class PageResponseHandler implements ResponseHandler<Page> {

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);
}

}
到目前为止,这个爬虫中的抓取和分析功能部分都工作的很好,对于中文,也能很好的解析,比较细心的读者会看到,这些代码中并没有设置字符集,也没有进行字符集进行转换,稍后会讲到HttpClient 4.2组件中字符集的处理。
先回顾一下关于HTTP协议的RFC规范中Content-Type作用,它指明发送给接收者Http Entity内容的媒介类型,对于文本类型的HttpEntity而言,它通常是下面的形式,指定HttpEntity的媒介类型,使用了何种字符集进行编码的,此外RFC规范还规定了,在Content-Type没有指定字符集的情况,默认使用ISO-8859-1字符集对Http Entity进行编码

1
Content-Type: text/html; charset=UTF-8
说到这里,大家应该能猜到HttpClient 4.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 Entity内进行编码,因此为了防止HttpClient在爬取中文网站出现乱码的问题,可以指定一个默认的GBK字符集,对原有的PageResponseHandler中的那行转换字符串的代码修改成

String html = EntityUtils.toString(entity, Charset.forName("gbk"));
至此,爬虫的爬取和解析功能都完成了,再也不怕没有正确设置Content-Type头的中文网站。希望这个篇文章能对读者有用。

打开App,阅读手记
11人推荐
发表评论
随时随地看视频慕课网APP

热门评论

这就可以了?不是吧?我蒙了。

查看全部评论