自己写的一个WEB爬虫程序,程序方面的优化我自己感觉做的不错了,各种对象的使用都想办法不让内存浪费。
但还是吃内存吃的很厉害
我想主要的消耗在于,对于每一个爬下来的网页,都要变成String在内存中分析超链接,这个估计很大。
另外对于每一个分析到的URL,都需要保存起来。
我当前使用的内存优化参数如下
-Xms1024M -Xmx1024M -Xmn512M -Xss128K -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:MaxPermSize=128M -XX:NewSize=128M -XX:MaxNewSize=128M -XX:SurvivorRatio=8
机器内存是1G。。上面那堆参数,除了最大最小堆内存我也是不求甚解,不是非常明白我设置了什么。网上搜答案也是五花八门,希望大家能推荐一个内存优化参数的文章
另外我上面设置的参数有哪些方面可以优化,或者哪些地方写的不对。
核心的代码我都贴到下面:(我是刚入门的菜鸟,大家别笑话我写的差哦)
这个是主类,负责控制,那个monitor大家就当做System.out.println,其实就是
package com.wood.core;
import java.util.concurrent.*;
import java.util.*;
import com.wood.core.monitor.*;
public class CrawlerController implements Runnable {
//一个类,用来管理URL,由各抓取线程共享
private URLMap urlmap;
//已成功处理URL总数
private int count;
//目标完成任务总数
private int total;
//管理所有的抓取线程,必要时进行关闭,由于抓取线程存在I/O读取,而I/0读取中的线程是不可中断的,需调用线程的cancel函数
private Crawler[] crawlers;
//线程工厂,可以监视产生的线程工作状态
private CrawlerFactory factory;
//线程数目
private int threadMax;
//当前活动线程
private int threadActive;
//线程执行器
private ExecutorService exec;
//当系统关闭时取消
private boolean canceled;
public CrawlerController(String seed)
{
this(seed,20,10000);
}
public CrawlerController(List<string> seeds)
{
this(seeds,20,10000);
}
public CrawlerController(String seed,int threadNum)
{
this(seed,threadNum,10000);
}
public CrawlerController(List<string> seeds,int threadNum)
{
this(seeds,threadNum,10000);
}
public CrawlerController(String seed,int threadNum,int total)
{
//初始化URLMap,保存抓取启始位置
urlmap=new URLMap(seed);
init(threadNum, total);
}
public CrawlerController(List<string> seeds,int threadNum,int total)
{
urlmap=new URLMap(seeds);
init(threadNum, total);
}
//构造
private void init(int threadNum,int total)
{
//初始完成URL总数为0
count=0;
//设置目标完成总数
this.total=total;
threadMax=threadNum;
threadActive=threadNum;
//初始化工作线程
crawlers=new Crawler[threadMax];
String id="Crawler ";
for(int i=0;i<threadmax;i++) crawlers[i]="new" crawler(id+(i+1),this,urlmap);="" factory="new" crawlerfactory();="" exec="Executors.newCachedThreadPool(factory);" canceled="false;" }="" 检查当前工作线程状态,并打印系统状态="" private="" boolean="" check()="" {="" if(canceled)="" return="" false;="" int="" count="getCount();" if(count="">total)
{
MonitorHolder.getMonitor().print("已抓取"+COUNT+"页面,完成目标任务"+total+"页面\n");
cancel();
return false;
}
threadActive=factory.getActive();
/*if(threadActive<=0)
{
MonitorHolder.getMonitor().print("无活动工作线程,抓取任务提前结束\n");
cancel();
return false;
}*/
MonitorHolder.getMonitor().print_status("统计信息:成功抓取"+COUNT+"页面,当前活动线程为"+threadActive+"个\n");
return true;
}
//结束抓取
public void cancel()
{
//调用每个抓取线程的离开方法
for(Crawler cw:crawlers)
cw.cancel();
//销毁工厂
factory.destory();
exec.shutdownNow();
MonitorHolder.getMonitor().print("成功结束抓取工作,共抓取"+getCount()+"页面\n");
this.canceled=true;
}
public synchronized void count()
{
count++;
}
public synchronized int getCount()
{
return count;
}
public int getTotal()
{
return total;
}
public URLMap getMap()
{
return urlmap;
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted() && !canceled)
{
try {
MonitorHolder.getMonitor().print("初始化完毕\n");
MonitorHolder.getMonitor().print("开始抓取工作\n");
for(Crawler cw:crawlers)
exec.execute(cw);
int check_count=0;
while (check()){
TimeUnit.SECONDS.sleep(5);
check_count++;
if(check_count==24)
{
//每2分钟把待处理URL队列打乱一次
urlmap.shuffle();
MonitorHolder.getMonitor().print("控制信息!!!待抓取URL顺序将打乱\n");
check_count=0;
System.gc();
}
}
} catch (InterruptedException e) {
MonitorHolder.getMonitor().print("抓取工作被中断\n");
cancel();
}
}
}
public static void main(String[] args)
{
List<string> seeds=new ArrayList<string>(15);
//问问的各个分类
seeds.add("http://wenwen.soso.com/");
seeds.add("http://wenwen.soso.com/z/c1879048192.htm");
seeds.add("http://wenwen.soso.com/z/c1090519040.htm");
seeds.add("http://wenwen.soso.com/z/c1627389952.htm");
seeds.add("http://wenwen.soso.com/z/c855638016.htm");
seeds.add("http://wenwen.soso.com/z/c1191182336.htm");
seeds.add("http://wenwen.soso.com/z/c1191182336.htm");
seeds.add("http://wenwen.soso.com/z/c620756992.htm");
seeds.add("http://wenwen.soso.com/z/c553648128.htm");
seeds.add("http://wenwen.soso.com/z/c385875968.htm");
seeds.add("http://wenwen.soso.com/z/c687865856.htm");
seeds.add("http://wenwen.soso.com/z/c16777216.htm");
seeds.add("http://wenwen.soso.com/z/c318767104.htm");
seeds.add("http://wenwen.soso.com/z/c150994944.htm");
seeds.add("http://wenwen.soso.com/z/c922746880.htm");
seeds.add("http://wenwen.soso.com/z/c83886080.htm");
CrawlerController controller=new CrawlerController(seeds,5,2000000);
List<string> format=new ArrayList<string>(10);
//问问回答问题的格式
format.add("http://wenwen.soso.com/z/q");
controller.getMap().addLimit(format);
Thread thread=new Thread(controller);
thread.start();
}
}
package com.wood.core;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.*;
import java.io.*;
import java.net.*;
import com.wood.core.monitor.*;
public class Crawler implements Runnable {
//抓取线程ID
private String id;
//控制器
private CrawlerController controller;
//URL,供各线程共享
private URLMap urlmap;
private boolean canceled;
//保存抓取的网页内容,用于分析超链接
private String content;
//字符集
private String charset;
//匹配超链接的正则表达式
private String link_reg="<a\\s+(.*?)href\\s*=\\s*\"?(.*?)[\"(\\s*?)](.*?)>";
private Pattern link_pattern=Pattern.compile(link_reg,Pattern.CASE_INSENSITIVE);
//匹配字符集的正则表达式
private String charset_reg="<meta\\s+http-equiv=\"content-type\"\\s+content=\"text html;="" charset="(.*?)\"(.*?)"> ";
private Pattern charset_pattern=Pattern.compile(charset_reg, Pattern.CASE_INSENSITIVE);
private Pattern dir_path_pattern=Pattern.compile("^\\w+$", Pattern.CASE_INSENSITIVE);
//保存当前抓取的URL的主机
private String host;
//生成待建立的文件夹名
private String host_path;
//抓取网页的根目录
private String root_path="E:\\web";
private File root_dir=new File(root_path);
//输入来自URL,输出到文件
private InputStreamReader in=null;
private OutputStreamWriter out=null;
//输入输出缓冲区
private char[] buf=new char[10240];
//网页内容缓冲区
private StringBuilder contentbuilder=new StringBuilder(1024*1024);
//通过一个URL,下载网页内容
private void download(URL url)
{
in=null;
out=null;
//建立抓取文件
String urlfile=url.getFile();
if(urlfile.endsWith("/"))
urlfile+="indexindex";
File file=new File(host_path,urlfile);
File file_dir=new File(file.getParent());
if(!file_dir.exists())
file_dir.mkdirs();
MonitorHolder.getMonitor().print(id+"开始准备下载"+url.toString()+"\n");
try {
//打开链接
URLConnection conn=url.openConnection();
//超时30秒
conn.setConnectTimeout(30000);
conn.setDoOutput(true);
//不是HTML不下载
/*String type=conn.getContentType();
if( (type==null)|| (!type.equals("text/html")) )
return;*/
//将网页内容的缓冲区清空
contentbuilder.setLength(0);
in=new InputStreamReader(conn.getInputStream(),charset);
out=new OutputStreamWriter(new FileOutputStream(file),charset);
int len;
//读取网页内容,并写入文件,保存到网页内容缓冲里面
while((len=in.read(buf, 0, 10240)) >0)
{
out.write(buf, 0, len);
//append可以减小系统损耗
contentbuilder.append(buf, 0, len);
}
out.flush();
//将网页内容缓冲区的内容读到content中,用于分析
content=null;
content=contentbuilder.toString();
} catch (IOException e) {
MonitorHolder.getMonitor().print("错误!!!"+id+"下载页面"+url.toString()+"错误\n");
}
finally{
try {
if(in!=null)
in.close();
if(out!=null)
out.close();
} catch (IOException e) {
}
in=null;
out=null;
}
}
public Crawler(String id,CrawlerController controller,URLMap urlmap)
{
this.id=id;
this.controller=controller;
this.urlmap=urlmap;
this.charset="utf-8";
canceled=false;
MonitorHolder.getMonitor().print(id+"就绪\n");
}
//设置字符集,通过正则表达式获取字符集格式
//为了性能,该功能并未使用
private void setCharset()
{
Matcher matcher=charset_pattern.matcher(content);
String CHARSET="GB2312";
if(matcher.find())
CHARSET=matcher.group(1).trim();
this.charset=CHARSET;
try {
content=new String(content.getBytes("GB2312"),CHARSET);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//从网页内容中分析超链接
private List<string> retrieveLinks(URL url)
{
List<string> urls=new LinkedList<string>();
if(content==null)
return urls;
Matcher matcher=link_pattern.matcher(content);
String link;
String host_reg="http://"+host;
String host_nowww;
if(host.contains("www"))
host_nowww=host.substring(host.lastIndexOf("w")+2);
else {
host_nowww=host;
}
while(matcher.find())
{
//通过抓取第1组的内容
link=matcher.group(2).trim().toLowerCase();
if (link.length() < 1) {
continue;
}
//网页内部链接,忽略
if (link.charAt(0) == '#') {
continue;
}
//发送邮件链接,忽略
if (link.indexOf("mailto:") != -1) {
continue;
}
if (link.toLowerCase().indexOf("javascript") != -1) {
continue;
}
//分析绝对地址或相对地址
if (link.indexOf("://") == -1){
if (link.charAt(0) == '/') //处理绝对地址
link = "http://" + host+ link;
else if(link.startsWith("./"))
link="http://" + host+ link.substring(1);
else {
String file = url.getFile();
String file_path=file.substring(0, file.lastIndexOf('/'));
while(link.startsWith("../"))
{
link=link.substring(link.indexOf("/")+1);
file_path=file_path.substring(0, file_path.lastIndexOf("/"));
}
link="http://"+host+file_path+"/"+link;
}
}
int index = link.indexOf('#');
if (index != -1) {
link = link.substring(0, index);
}
if(!urlmap.testHost(link))
continue;
if(!urlmap.testLimit(link))
continue;
urls.add(link);
}
return urls;
}
//设置主机并建立目录
private void setHost(String host)
{
this.host=host;
this.host_path=root_path+"\\"+host;
File host_dir=new File(host_path);
if(!host_dir.exists())
host_dir.mkdirs();
}
@Override
public void run() {
if(!root_dir.exists())
root_dir.mkdirs();
while(!Thread.currentThread().isInterrupted() && !canceled && (controller.getCount()<controller.gettotal()) )="" {="" try="" {="" 获得一个待抓取的url,如果没有可用url,则进入阻塞状态,该方法调用是线程安全的="" string="" urlstring="urlmap.getURL();" monitorholder.getmonitor().print(id+"开始抓取"+urlstring+"\n");="" 建立url="" url="" url="new" url(urlstring);="" 设置主机="" sethost(url.gethost());="" 清空网页内容,并下贼="" content="null;" download(url);="" 未下载到内容="" if(content="=null)" {="" continue;="" }="" monitorholder.getmonitor().print(id+"抓取"+urlstring+"完毕,进行解析\n");="" setcharset();="" 分析超链接="" list<string=""> urls=retrieveLinks(url);
//将分析到的超连接加入到待抓取的URL队列中,并将成功抓取数+1
urlmap.addAll(urls);
controller.count();
MonitorHolder.getMonitor().print(id+"解析"+urlstring+"完毕,共计"+urls.size()+"个超链接,开始新任务\n");
content=null;
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
MonitorHolder.getMonitor().print(id+"被中断\n");
canceled=true;
} catch (MalformedURLException e) {
MonitorHolder.getMonitor().print(id+"报告:URL格式错误\n");
}catch (Exception e) {
e.printStackTrace();
}
}
}
//退出,由控制器调用,关闭所有底层I/O资源
public void cancel()
{
try {
if(in!=null)
in.close();
} catch (IOException e) {
}
try {
if(out!=null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
MonitorHolder.getMonitor().print(id+"停止工作\n");
canceled=true;
}
}
package com.wood.core;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.*;
import java.util.*;
public class URLMap {
//待抓取的URL队列
private LinkedList<string> URLQueue;
//缓存解析出来的URL,使用Hash方便快速查找,<k,false>表示未抓取,<k,true>表示抓取结束
private HashMap<string,boolean> cachedURL;
private HashSet<string> hosts;
private HashSet<string> format_limit;
//默认构造器,初始化队列,集合
private URLMap()
{
URLQueue=new LinkedList<string>();
cachedURL=new HashMap<string,boolean>(50000);
hosts=new HashSet<string>(20);
format_limit=new HashSet<string>(20);
}
public URLMap(List<string> seeds)
{
this();
for(String s:seeds)
cachedURL.put(s, false);
URLQueue.addAll(seeds);
for (String string : seeds) {
try {
URL test=new URL(string);
String host=test.getHost();
hosts.add(host);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
public URLMap(String seed)
{
this();
cachedURL.put(seed, false);
URLQueue.add(seed);
try {
URL test=new URL(seed);
hosts.add(test.getHost());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
//将解析出来的URL添加到URLMap中,如果有重复则忽略,URL有效性由外部保证,该方法是线程安全的
public synchronized void addURL(String url) throws InterruptedException
{
//如果该URL已存在,忽略该URL
if(cachedURL.keySet().contains(url))
return;
cachedURL.put(url, false);
URLQueue.add(url);
//有可用URL,唤醒所有阻塞线程
notifyAll();
}
//将解析出来的URL添加到URLMap中,如果有重复则忽略,URL有效性由外部保证,该方法是同步的
public synchronized void addAll(List<string> urls) throws InterruptedException
{
for (String url : urls) {
if(cachedURL.keySet().contains(url))
continue;
cachedURL.put(url,false);
URLQueue.add(url);
}
notifyAll();
}
//从当前URL队列中获取一个URL,如果当前队列无可用URL,则该线程进入阻塞状态
public synchronized String getURL() throws InterruptedException
{
//该处进入阻塞
while(URLQueue.size()==0)
wait();
//将其从队列中删除
String url=URLQueue.remove();
cachedURL.put(url, true);
return url;
}
public boolean testHost(String host)
{
for(String host_allow:hosts)
{
if(host.contains(host_allow))
return true;
}
return false;
}
public void addHost(List<string> l)
{
hosts.addAll(l);
}
public void addLimit(List<string> l)
{
format_limit.addAll(l);
}
public boolean testLimit(String url)
{
if(format_limit.size()==0)
return true;
else
{
for(String s:format_limit)
if(url.contains(s))
return true;
}
return false;
}
//将待抓取队列打乱
private int swap_check=0;
public synchronized void shuffle()
{
swap_check++;
int size=URLQueue.size();
if(size<1000)
{
java.util.Collections.shuffle(URLQueue);
swap_check=0;
}
else {
if(size>=1000 && size<5000)
{
if(swap_check==5)
{
java.util.Collections.shuffle(URLQueue);
swap_check=0;
}
}
else {
if(swap_check==15)
{
java.util.Collections.shuffle(URLQueue);
swap_check=0;
}
}
}
}
}
附件里是内存使用情况
问题补充
主要占用内存的是char[]
我的策略是所有分析到的有效URL都存储在HashMap中
但是几万个URL不至于占用大多数内存吧?
我的工作线程只有5个,设置的多了,一会就崩了
哪位高人路过,帮我看眼吧
祝你新年好运~
附件里是内存占用,缓存的URL总数是20000个,一个大概占用80B,程序内存总数是150MB
我不知道到底是什么东西不停的吃内存,回收不掉,程序运行的时间越长,线程数越多,内存吃的越快