2017년 1월 20일 금요일

web crawler with java 설명

web crawler with java 에 대한 설명

이전에 만든 소스 코드는 아래 링크에 있습니다.
http://swlock.blogspot.com/2017/01/web-crawler-with-java.html
여기에서는 내용중 중요한 부분을 설명을 하도록 하겠습니다.

jericho htmlparser 에서 나오는 로그 막기

jericho 를 사용하다 보면 가끔씩 경고 형태의 로그들이 나오는데 안나오도록 막는 방법은 아래와 같습니다.

  Config.LoggerProvider=LoggerProvider.DISABLED;

구현 코드 main() 함수에 존재합니다.

apache httpclient 에서 나오는 로그 막기

http client에서도 불필요한 로그가 나오는 경우가 있습니다. 이럴때에는 아래와 같이 합니다.

System.setProperty("org.apache.commons.logging.Log",
 "org.apache.commons.logging.impl.NoOpLog"); 
 

apache.commons.cli 로 command line parameter 처리하기

jar 파일은 https://commons.apache.org/proper/commons-cli/ 여기에서 받을 수 있습니다.
이건 command line 툴 만들때 굉장히 편리한 도구 입니다.
전체 코드에서 아래 부분입니다.

Options options = new Options();

   Option savepath = new Option("s", "savepath", true, "input save folder file path");
   savepath.setRequired(true);
   options.addOption(savepath);

   Option url = new Option("u", "url", true, "url ex) http://www.daum.net");
   url.setRequired(true);
   options.addOption(url);

   Option depth = new Option("d", "depth", true, "max depth");
   depth.setRequired(false);
   options.addOption(depth);

   Option changehostdepth = new Option("c", "changehostdepth", true, "change host depth");
   changehostdepth.setRequired(false);
   options.addOption(changehostdepth);

   CommandLineParser parser = new DefaultParser();
   HelpFormatter formatter = new HelpFormatter();
   CommandLine cmd;

   try {
    cmd = parser.parse(options, args);
   } catch (ParseException e) {
    System.out.println(e.getMessage());
    formatter.printHelp("Webcrawler", options);
    System.exit(1);
    return;
   }

   String saveFilePath = cmd.getOptionValue("savepath");
   String urlPath = cmd.getOptionValue("url");
   String depthParam = cmd.getOptionValue("depth");
   if(depthParam==null || depthParam.isEmpty()) depthParam = "2";
   String changehostdepthdepthParam = cmd.getOptionValue("changehostdepth");

Options 라는 클래스가 있고 여기에 각각의 Option을 Options.addOption(Option) 메소드를 이용해서 추가하는 방법입니다.
Option의 생성자 인자는 아래와 같습니다.

org.apache.commons.cli.Option.Option(String opt, String longOpt, boolean hasArg, String description)
opt:옵션 이름
longOpt:긴 옵션 이름
hasArg:옵션 뒤에 인자를 가지는지 여부
description:옵션의 설명
그리고 아래 사용한 setRequired() 메소드는 필수 여부를 나타냅니다. 필수 옵션의 경우 인자를 입력하지 않으면 오류가 발생하게 됩니다.
savepath.setRequired(true);

옵션을 모두 만들었으면 파서를 해야 사용할 수 있습니다.
그리고 helpformatter를 이용해서 파싱시 오류가 발생하면 뭐가 문제인지 출력하도록 합니다.

CommandLineParser parser = new DefaultParser();
HelpFormatter formatter = new HelpFormatter();
CommandLine cmd;

try {
 cmd = parser.parse(options, args);
} catch (ParseException e) {
 System.out.println(e.getMessage());
 formatter.printHelp("Webcrawler", options);
 System.exit(1);
 return;
}

위의 내용을 정리해보자면 아래와 같습니다.

필수 : Option("s", "savepath", true, "input save folder file path");
필수 : Option("u", "url", true, "url ex) http://www.daum.net");
옵션 : Option("d", "depth", true, "max depth");
옵션 : Option("c", "changehostdepth", true, "change host depth");

그리고 그냥 실행시키면 아래와 같이 출력됩니다.

Missing required options: s, u
usage: Webcrawler
 -c,--changehostdepth <arg>   change host depth
 -d,--depth <arg>             max depth
 -s,--savepath <arg>          input save folder file path
 -u,--url <arg>               url ex) http://www.daum.net

필수 옵션을 안넣으면 빠졌다고 알려주고 사용법은 짧은 옵션은 -, 긴 옵션은 -- 를 추가해서 커멘드를 내리면 됩니다.

다음으로 만약 필수 옵션이 아닌경우 디폴트값을 처리해야 합니다.
getOptionValue() 메소드를 사용합니다. 그러면 입력한 데이터가 없으면 null 이 돌아오게 되되는데 그걸 처리하는 부분이 아래 코드 입니다.

String saveFilePath = cmd.getOptionValue("savepath");
String urlPath = cmd.getOptionValue("url");
String depthParam = cmd.getOptionValue("depth");
if(depthParam==null || depthParam.isEmpty()) depthParam = "2";
String changehostdepthdepthParam = cmd.getOptionValue("changehostdepth");
if(changehostdepthdepthParam==null || changehostdepthdepthParam.isEmpty()) changehostdepthdepthParam = "1";

아래는 실행 가능한 코드를 다시 만들었습니다.

package prj.dish;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

public class OptionsTest {
 public static void main(String[] args) {
  Options options = new Options();

  Option depth = new Option("d", "depth", true, "max depth");
  depth.setRequired(false);
  options.addOption(depth);

  Option changehostdepth = new Option("c", "changehostdepth", true, "change host depth");
  changehostdepth.setRequired(false);
  options.addOption(changehostdepth);

  CommandLineParser parser = new DefaultParser();
  HelpFormatter formatter = new HelpFormatter();
  CommandLine cmd;

  try {
   cmd = parser.parse(options, args);
  } catch (ParseException e) {
   System.out.println(e.getMessage());
   formatter.printHelp("Test", options);
   System.exit(1);
   return;
  }

  String depthParam = cmd.getOptionValue("depth");
  if(depthParam==null) depthParam = "ld empty";
  
  String shortdepthParam = cmd.getOptionValue("d");
  if(shortdepthParam==null) shortdepthParam = "sd empty";
  
  String changehostdepthdepthParam = cmd.getOptionValue("changehostdepth");
  if(changehostdepthdepthParam==null) changehostdepthdepthParam = "lc empty";
  
  System.out.println("depth:"+depthParam);
  System.out.println("d:"+shortdepthParam);
  System.out.println("changehostdepth:"+changehostdepthdepthParam);
 }

}

인자가 없을때 실행 결과 코드
depth:ld empty
d:sd empty
changehostdepth:lc empty



지금까지 main코드에 대한 설명은 대충 마무리가 된것 같습니다.

재귀 호출로 접속하기

main에서 남은 코드는 아래 내용입니다. 가장 중요한 run 메소드가 남아있습니다.
인자는 시작 하고자 하는 http의 url 주소가 됩니다.

   Webcrawler crawler;
   crawler = new Webcrawler();
   crawler.run(urlPath);

run은 내부적으로 connect 메소드를 호출하고 connect는 재귀 호출 방식으로 connect를 호출하도록 되어 있습니다.
다음은 코드를 극단적으로 정리한 코드입니다.

private void run(String string) {
 host = string;
 connect( host, "/", 0, 0);
}
private void connect(String lasturl, String addurl, int depth, int hostchange) {
 ...
 if( maxDepth <= depth ){
  return;
 }
 ...
 lasturl = calcNextUrl(lasturl, addurl);
 newurl = getHttp(lasturl);
 source=new Source(getString());

 ...
 List <Element> elements = source.getAllElements("a");

 for(int i = 0 ; i < elements.size(); i++){
  ...
  String href = ele.getAttributeValue("href");
  ...
  connect(newurl,href,depth+1,hostchange+hostchanged);
 }
}

calcNextUrl() 메소드를 이용해서 다음 접속해야하는 주소를 구한뒤 getHttp로 접속해서 page를 얻어옵니다. 그런후 Source로 파싱을 해서 이중에 A tag href 로 되어 있는 주소를 검색해서 다시 connect 메소드를 호출합니다.
connect->connect->connect->connect->... 이런 식으로 계속 들어가다보면 방문해야 하는 page가 너무 늘어나기 때문에 리턴하는 조건은 재귀로 들어갈때 마다 depth의 인자를 증가시켜서 정해놓은 depth만큼만 들어가도록 하였습니다.

jericho html parser 이용하기

new Source를 사용하는 부분은 jericho html parser를 사용하는데 아래 링크를 확인해보면 좀 더 쉽게 이해할 수 있으리라 생각됩니다.
http://swlock.blogspot.com/2017/01/jericho-htmlparser.html

다음 url 주소 계산하기

얻은 주소로부터 다음 접속해야하는 url 계산은 calcNextUrl메소드를 이용하게 되는데 아래 링크 설명으로 확인하면 됩니다.
http://swlock.blogspot.com/2016/12/httpclient-page.html

접속한 페이지 다시 접속하지 않기

간단한 방법은 방문한 페이지를 저장해놓고 이름이 같으면 접속하지 않게 구현도 가능한데 여기에서는 Set이라는 자료 구조를 사용하였습니다.
Java에는 HashSet이라고 있습니다. set은 집합이라고 생각하면 됩니다. 집합이라 넣기전에 들어있는 내용을 확인가능한데 contains로 들어있는지 확인하고 add로 넣으면 됩니다. 만약 방문한 페이지라면 해당 주소는 들어있을 겁니다.

HashSet<String> visited = new HashSet<String>();

if( !visited.contains(lasturl) ){
 visited.add(lasturl);
}else{
 System.out.println("visited !");
 return;
}

여기까지 간단하게나마 설명을 마치도록 하겠습니다.
2017.1월 어느날.....



댓글 없음:

댓글 쓰기