这篇博文将介绍一下我的这个单机爬虫作品,主要是给大家一个思路,如何做出一个比较有趣的单机爬虫,当然这个作品肯定会有很多改进的地方,欢迎大家提出建议。(注:代码仅供学习参考,须在法律允许范围内使用)
github: https://github.com/colabin/spider_demo
爬虫简介:
在部门做爬虫需求的时候,每次来一个需求就需要写一个爬虫程序然后打包部署到服务器,制定脚本定时运行,所以有了这个爬虫,目的是为了尽可能简化现有的流程,之前从网页源码获取,到网页解析,到持久化都需要重新在程序里面重新写一遍,分析了这个过程,我们抽取出了网页源码获取,持久化成为两个独立的类实现原有程序的解耦,然后剩下的也就是我们每次必须要自己手写的网页解析函数(这个过程是必不可少的,因为不同网页需要制定不同的解析规则),从而实现了每次爬虫需求只需要完成网页解析函数,就可以进行打包部署。另外这个爬虫最大的特点就是参数传递和动态加载功能,可以通过传递给程序的参数控制http请求获取网页源码的线程数,加快爬取效率,也可以通过参数控制持久化的方式(文件,数据库),提高程序的复用性。动态加载的功能是用来通过参数来运行对应的Spider实例,实现只需要服务器上部署一个脚本,以后新的需求来了只需要调用同一个脚本传递不同的参数就能运行对应的spider实例。关于多线程爬取返回网页源码的问题,因为不可能等待所有的线程爬取完后网页源码才返回给主线程解析,一是数据量大时内存可能会溢出,而是主线程会一直阻塞,效率太低,主线程采用了一个阻塞队列来达到一个非阻塞的效果,主线程会不断轮询这个阻塞队列,爬虫会不断往阻塞队列里面添加网页源码,主线程检测到队列连续10s为空则结束轮询。
关于代码的关键部分和说明我将在下面给出:
项目结构图如下:
common包:
SpiderTemple类: 所有爬虫实例的父类,规定了爬虫实例的入口函数run,规定爬虫实例需要实现的函数parseHtml
public class SpiderTemple {
protected Persistence persistence = new Persistence();
protected HttpRequest httpRequest = new HttpRequest();
public void run(Map map) throws Exception {
LinkedBlockingQueue contents = new LinkedBlockingQueue();
httpRequest.getContent(contents, map);
while(true){
String poll = contents.poll(5L, TimeUnit.SECONDS);
if(null!=poll){
System.err.println("poll获取成功,解析后"+parseHtml(poll));
persistence.save(map, parseHtml(poll) );
}
else{
System.out.println("队列中无待解析内容");
break ;
}
}
}
public String parseHtml(String url) throws Exception {
return null;
}
}
factory包:
factory类: 根据参数里的类名动态加载不同的爬虫实例进行调用
/**
* @author: by huizhong
* @date: 2016-9-3 上午11:07:14
*/
public class Factory {
private static URLClassLoader load;
/**
*
* @param className 执行的类名
* @param args 类所需要的参数
*/
private void loadClass(String className,Map args){
String currClasssPath = System.getProperty("java.class.path");
String sepStr = System.getProperty("path.separator");
String currClassPaths[] = currClasssPath.split(sepStr);
String libPath = ""; //lib文件路径
for ( int i = 0; i < currClassPaths.length; i++){
if( currClassPaths[i].indexOf("Factory") >= 0 && currClassPaths[i].indexOf(".jar") > 0 ){
// System.out.print(currClassPaths[i]);
libPath = currClassPaths[i].substring(0,currClassPaths[i].lastIndexOf("/"));
// System.out.print(libPath);
}
}
if ( libPath.equals("")){
libPath = ".";
}
File lib = new File(libPath + "/lib" );
File curr = new File(libPath);
File[] liblist = lib.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
boolean fileaccept = false;
if (name.endsWith("zip") || name.endsWith("jar")) {
fileaccept = true;
}
return fileaccept;
}
});
//check jar file
if(liblist == null || liblist.length < 0 ){
System.out.print("Can not find jar file!");
System.exit(1);
}
URL tmp[] = new URL[liblist.length + 1];
String classPathSeparator = System.getProperty("path.separator");
StringBuffer buffLib = new StringBuffer();
String path = null;
try {
for (int i = 0; i < liblist.length + 1; i++) {
if (i < liblist.length) {
path = liblist[i].getAbsolutePath();
} else {
path = curr.getAbsolutePath();
}
tmp[i] = new URL("file:/" + path);
buffLib.append(path + ";");
}
tmp[tmp.length - 1] = new URL("file:/" + path);
String paths = System.getProperty("java.class.path") + classPathSeparator
+ buffLib.toString();
// System.out.print("ClassPath : " + paths);
System.setProperty("java.class.path",paths);
load = new URLClassLoader(tmp, this.getClass().getClassLoader());
} catch (MalformedURLException e) {
System.out.print(e.toString());
}
Thread.currentThread().setContextClassLoader(load);
Class> tmp2;
try {
// System.out.print(System.getProperty("user.dir"));
tmp2 = load.loadClass(className);
Object obj = tmp2.newInstance();
Method m1 = tmp2.getMethod("run",Map.class);
m1.invoke(obj,args);
} catch ( ClassNotFoundException e) {
System.out.print(e.toString());
} catch (InstantiationException e) {
System.out.print(e.toString());
} catch (IllegalAccessException e) {
System.out.print(e.toString());
} catch (SecurityException e) {
System.out.print(e.toString());
} catch (NoSuchMethodException e) {
System.out.print(e.toString());
} catch (IllegalArgumentException e) {
System.out.print(e.toString());
} catch (InvocationTargetException e) {
System.out.print(e.toString());
}
}
public static OptionBuilder inputOption(String descrip,boolean require){
OptionBuilder.isRequired(require);
OptionBuilder.hasArg();
return OptionBuilder.withDescription(descrip);
}
/**
* 获取输入的参数
* @return CommandLine
*/
private CommandLine getInputArgs(String args[]){
Options opts = new Options();
opts.addOption("h", "help", false, "print help for the command.");
opts.addOption(Factory.inputOption("input className。 eg com.heme.taobaoSSQ",true).create("className"));
opts.addOption(Factory.inputOption("className require args. eg uri,saveType[,savePath,threadNum]",false).create("arg"));
BasicParser parser = new BasicParser();
String format_str = "java com.heme.httpWatch.httpRobot -className com.heme.** -arg ***";
HelpFormatter formatter = new HelpFormatter();
CommandLine cl = null;
try{
System.err.println("开始解析参数");
cl = parser.parse(opts, args);
if (cl.hasOption("h")){
formatter.printHelp(format_str, opts);
}
}catch (ParseException e) {
System.err.println("解析出错");
formatter.printHelp(format_str, opts);
System.exit(1);
}
return cl;
}
public static void main(String args[]) {
Factory hr = new Factory();
CommandLine cl = hr.getInputArgs(args);
String className = cl.getOptionValue("className"); //要执行的类
String classNameArg = cl.getOptionValue("arg"); //类所需要的参数
Map map = new HashMap();
if( classNameArg != null && !"".equals(classNameArg) ){
String[] classNameArgs = classNameArg.split(",");
for( int i = 0; i < classNameArgs.length; i++ ){
String[] darg = classNameArgs[i].split("=");
if ( darg.length == 2 ){
map.put(darg[0], darg[1]);
}else{
int pivot = classNameArgs[i].indexOf("=");
map.put(classNameArgs[i].substring(0, pivot),classNameArgs[i].substring(pivot+1));
}
}
}
hr.loadClass(className,map);
}
}
Rquest包:
httpRequest类:发送http请求获取网页源码,根据传递进来的参数决定是爬取一个url还是爬取文件里的一系列url
public class HttpRequest {
protected HttpUtil httpUtil = new HttpUtil();
protected ExecutorService executor = Executors.newFixedThreadPool(50); // 创建固定容量大小的缓冲池
List contents = new LinkedList();
public void getContent(final LinkedBlockingQueue contents,
final Map map) throws Exception {
String uri = map.get("uri");
if (null != uri && uri.contains("http")) { // uri代表网址
try {
String content = httpUtil.getUrlAsString(uri);
System.err.println("add content");
contents.add(content);
} catch (Exception e) {
System.err.println("爬起url出错,请检查网络问题");
}
}
else if (null != uri && !uri.contains("http")) { // uri代表文件
final List urlList = new ArrayList();
BufferedReader bufferedReader = new BufferedReader(new FileReader(uri));
String name = null;
while ((name = bufferedReader.readLine()) != null) {
urlList.add(name);
}
new Thread(new Runnable() { // 默认开启一个线程
@Override
public void run() {
// TODO Auto-generated method stub
for (final String url : urlList) {
if (null == map.get("thread")) {
try {
String content = httpUtil.getUrlAsString("http://" + url);
contents.add(content);
} catch (Exception e) {
System.err.println("爬起url出错,请检查网络问题");
}
} else if ("true".equals(map.get("thread"))) {
// 针对每个Ur均开启一个线程进行爬取
executor.execute(new Runnable() { // 缓冲池最多开启50个线程
@Override
public void run() {
String content;
try {
System.out.println(Thread.currentThread()+" is running");
content = httpUtil.getUrlAsString("http://"+ url);
contents.add(content);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}
}).start();
}
}
}
Spiders包:
SpiderDemo类:为Spider实例提供一个Demo,以后的Spider实例只需要按这个规范写即可以,
public class SpiderDemo extends SpiderTemple implements Entrance {
@Override
public String parseHtml(String content) throws Exception {
Document doc = Jsoup.parse(content);
String title = doc.select("meta[name=Description]").attr("content");
String time = doc.select("span[class=pubTime article-time]").text();
String comment = doc.select("a[id=cmtNum]").text();
String text = doc.select("div[id=Cnt-Main-Article-QQ] span").text()
+ doc.select("div[id=Cnt-Main-Article-QQ] p").text();
// 这里要和数据库除了主键以外的字段数目一致,字段分隔符&&&
return title + time + comment + text;
}
}
Util包:
一些用于发送http请求,下载到文件或者db的工具
这个爬虫可以改进的地方还有很多,比如一些应对反爬措施等等,但是结合业务需求来看暂时是没必要做的,有句话说得好,“如无必要,勿增实体”,一些应对反爬措施我也会在后面的分布式爬虫提到,大家可以去参考一下
如果大家有什么好的改进建议和疑问可以给我留言或者给我邮件,一起改进这个爬虫~