从今年的三月份,正式接手项目。主要工作涉及到网页抓取,一直想花时间总结一下。但是博主太懒了..........
要写一篇博客很难的....思路稍有不畅,就没有办法继续写不下去了。网站上高手有的是,总不能拿一篇狗屁不通的文章糊弄大家吧!
我擦咧,把自己心里想的话不小心说出来了!
反正我是鄙视那些花哨的空洞的东西!可以说深恶痛绝。......咳咳.......感觉压力越来越大,装逼的感觉很爽哒.......
看来我多虑了,写的文章不知道有没有人看呢。没人看也罢,权当备忘。(当然希望有人看,有人评论了),每每看到那些大牛写的文章,我都羡慕的口水直流。
围观(●>∀<●): 我擦咧,废话够没?滚粗....... 额,好吧 ....我还没有思路马上就来。
其实我想对网站的的抓取做一个类型区分<主要是根据我抓取的网站的经验>:
1 普通的网站,主要是要抓取的数据直接可以通过dom的解析获取。这类网站的抓取比较容易,关键点dom的解析。用正则获取想要的数据会很复杂,有更好的方式解决这个问题。这类可以用jsoup(注: jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提 供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操数据),一个简单易用的java解析框架。
2 用js封装数据的网站。这类网站如果直接获取html的话,你要的数据根本得不到。其实一般情况下在浏览器中看到的页面,是渲染过的页面。dom的部分的呈现用js控制的,数据是放js对象中的。针对这种网站,关键是js的解析。由于java本身在1.5版本的时候就已经集成了,一个Rhino的纯java实现的js引擎。可以利用Rhino解释javaScript脚本。又因为js的数据本身的封装的特点,我更喜欢把js对象转化为json处理。(用jackson这个框架和方便)。
3 反爬的网站。有些网站有反扒的策略,这种情况是最麻烦的。需要仔细的分析和琢磨。
要抓取网站上的有用数据,还有一个很重要,抓取的效率,这个问题的解决用多线程和任务队列的东西。
涉及到时候,我会慢慢讲。
基本的页面抓取----主要是jsoup,通常情况下接手到一个新的东西,我会直接在官网上看文档。这其中的原因
只有经历过的人才能明白。胡乱的在网上搜的话,会有各种问题。每个人理解的层次都不一样,这样就有可能曲解原作者的
意图。这不仅仅是因为不同的人的水平参差不齐,更重的要原因是不同的人面对情景不同.我花了很久才明白这个道理。
jsoup结构很简洁。在官网上下载的zip文件中由源码。
1 获取执行的url的document内容: http://www.open-open.com/jsoup/selector-syntax.htm 这里有详细的介绍。
Document doc = Jsoup.connect(url).get();
2 然后你就可以通过提供的一堆的选择器获取你想要的数据:
File input = new File("/tmp/input.html");
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");
Elements links = doc.select("a[href]"); //带有href属性的a元素
Elements pngs = doc.select("img[src$=.png]");
//扩展名为.png的图片
Element masthead = doc.select("div.masthead").first();
//class等于masthead的div标签
Elements resultLinks = doc.select("h3.r > a"); //在h3元素之后的a元素
js封装的页面抓取:
1 首先要获取js的内容。用httpClient读取js内容(代码)
String jsContent = null;
/*
//HttpClient httpClient = new HttpClient();
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
HttpClient httpClient = new HttpClient(connectionManager);
GetMethod getMethod= new GetMethod(url);
//是设置系统参数
HttpClientParams httpClientParams = new HttpClientParams();
// 设置读取内容时间超时
httpClientParams.setSoTimeout(10000);
// 设置系统提供的默认恢复策略,在发生异常时候将自动重试3次
httpClientParams.setParameter(
HttpMethodParams.RETRY_HANDLER, //重复操作控制
new DefaultHttpMethodRetryHandler()
);
*/
HttpClient httpClient = new HttpClient();
GetMethod getMethod = new GetMethod(url);
getMethod.addRequestHeader("Content-Type", "text/html; charset="+encode);
getMethod.addRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:28.0) Gecko/20100101 Firefox/28.0");
try {
int statusCode = httpClient.executeMethod(getMethod);
if(statusCode == 200){
byte[] responseBody = getMethod.getResponseBody();
jsContent = new String(responseBody,encode);
}
} catch (IOException e) {
throw e;
}finally{
getMethod.releaseConnection();
}
2 分析数据对象,加载引擎并执行js,获取数据对象json,jackson封装成java对象。
Context cx = Context.enter();
try {
Scriptable scope = cx.initStandardObjects();
Object rs1 = cx.evaluateString(scope, response, "arr", 1, null);//执行就脚本
Object arr = scope.get("arr", scope);
if (arr == Scriptable.NOT_FOUND) {
logger.error("js数据解析失败---"+url);
}else{
String groupData = (String)NativeJSON.stringify(cx, scope, arr, null, null);
JSONArray groups = JSONArray.fromObject(groupData);
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
cx.exit();
}
4 保存数据。
针对反扒网站的抓取:
1 限制单位时间内ip的访问次数,即是访问的频率。解决方案,可以用代理,在某些时候只需要简单的延时操作。
方式一 :网上有给出用拨号获取ip的方式解决,很多时候这种方式是不可行的。不过还是要贴一下代码:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.log4j.Logger;
/**
*
* ADSL拨号上网
* Windwos操作系统需要是GBK编码
* @author yijianfeng
*
*/
public class ConnectAdslNet {
static Logger logger = Logger.getLogger(ConnectAdslNet.class);
/**
* 执行CMD命令,并返回String字符串
*/
public static String executeCmd(String strCmd) throws Exception {
Process p = Runtime.getRuntime().exec("cmd /c " + strCmd);
StringBuilder sbCmd = new StringBuilder();
//注意编码 GBK
BufferedReader br = new BufferedReader(new InputStreamReader(p
.getInputStream(),"GBK"));
String line;
while ((line = br.readLine()) != null) {
sbCmd.append(line + "\n");
}
return sbCmd.toString();
}
/**
* 连接ADSL
*/
public static boolean connectAdsl(String adslTitle, String adslName, String adslPass) throws Exception {
System.out.println("正在建立连接.");
String adslCmd = "rasdial " + adslTitle + " " + adslName + " "
+ adslPass;
String tempCmd = executeCmd(adslCmd);
// 判断是否连接成功
if (tempCmd.indexOf("已连接") > 0) {
System.out.println("已成功建立连接.");
return true;
} else {
System.out.println(tempCmd);
System.out.println("建立连接失败");
return false;
}
}
/**
* 断开ADSL
*/
public static boolean disconnectAdsl(String adslTitle) throws Exception {
String disconnectAdsl = "rasdial " + adslTitle + " /disconnect";
String result = executeCmd(disconnectAdsl);
if (result.indexOf("没有连接")!=-1){
System.out.println(adslTitle + "连接不存在!");
return false;
} else {
System.out.println("连接已断开");
return true;
}
}
/**
* adsl重新拨号,支持失败不断重拨
* @param args
* @throws Exception
*/
public static boolean reconnectAdsl(String adslTitle, String adslName, String adslPass){
boolean bAdsl = false;
try {
disconnectAdsl(adslTitle);
Thread.sleep(3000);
bAdsl = connectAdsl(adslTitle,adslName,adslPass);
Thread.sleep(3000);
int i = 0;
while (!bAdsl){
disconnectAdsl(adslTitle);
Thread.sleep(3000);
bAdsl = connectAdsl(adslTitle,adslName,adslPass);
Thread.sleep(3000);
if(i>5){
break;
}
}
}catch(Exception e){
logger.error("ADSL拨号异常:", e);
}
return bAdsl;
}
public static void main(String[] args) throws Exception {
// reconnectAdsl("宽带","adsl账号","密码");
}
}
方式二:用代理
// 创建 HttpClient 的实例
HttpClient httpClient = new HttpClient();
// 代理的主机(实际上是一个代理的数组,网上有很多提供代理服务器的)
// 推荐:http://www.xici.net.co/ 动态爬去这个网站的代理ip,然后使用,也可以购买
// 我并没有试过,不过应该是可行的。主要是提供一种解决问题的思路
ProxyHost proxy = new ProxyHost("117.59.224.62", 80);
// 使用代理
httpClient.getHostConfiguration().setProxyHost(proxy);
// 创建Get连接方法的实例
HttpMethod getMethod = new GetMethod("http://vip.bet007.com/OverDown_n.aspx?id=279403");
// 使用系统提供的默认的恢复策略
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
try {
// 请求URI
System.out.println("executing request " + getMethod.getURI());
// 执行getMethod
int status = httpClient.executeMethod(getMethod);
System.out.println("status:" + status);
// 连接返回的状态码
if (HttpStatus.SC_OK == status) {
System.out.println("Connection to " + getMethod.getURI()
+ " Success!");
// 获取到的内容
//String responseBody = getMethod.getResponseBodyAsString();
byte[] responseBody = getMethod.getResponseBody();
String html=new String(responseBody,"gbk");
System.out.println("==="+html);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放连接
getMethod.abort();
}
多线程多任务并发执行的爬虫:
public class Grab_Team {
public static void main(String[] args){
// 构造一个线程池
/*
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5,//corePoolSize
10,//maxPoolSize
60l,//线程活跃时间 60s
TimeUnit.SECONDS, //活跃时间的度量
new LinkedBlockingDeque<Runnable>(),//无界队列 同步 ,优先执行,核心线程全部占用时增加新线程到最大值
//此时如果所有的线程被占用,加入执行队列(队列无界)。
new ThreadPoolExecutor.CallerRunsPolicy()
//由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
//此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
//如果该线程池尚未终止,则立即执行该队列的run方法
);*/
ThreadPoolExecutor threadPool =
new ThreadPoolExecutor(20, 40, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
//ExecutorService threadPool = Executors. newSingleThreadExecutor();//单线程调试
for(int i = 0;i < teamIds.size();i++){
threadPool.execute(new GrabTeam(teamIds.get(i)));
}
}
}
class GrabTeam implements Runnable, Serializable{
private Logger logger = Logger.getLogger(GrabTeam.class);
public void run() {
//doSomeThing
}
}