레이블이 http인 게시물을 표시합니다. 모든 게시물 표시
레이블이 http인 게시물을 표시합니다. 모든 게시물 표시

2019년 8월 11일 일요일

web page 크롤링(crawling) , 스크레이핑(scraping)시 python으로 다음 페이지 주소 접속하는 방법 / urlparse / urljoin / urllib.parse


설명이 일단 복잡합니다.
앞에서 crawling시 form에 대한 처리를 할때 처리에 관한 가장 기본적인 설명은 아래에 있습니다.
https://swlock.blogspot.com/2019/07/web-page-crawling-scraping-form-postget.html

form을 사용하면 action 항목이 있다고 했습니다.
action : 폼을 전송할 서버 쪽 스크립트 파일을 지정합니다.
즉, form에 전송할 다음 페이지 정보가 저장되어 있습니다. 앞선 예제에서 <form id="sform" name="sform" action="https://search.naver.com/search.naver" method="get"> https로 시작하는 전체 url 정보가 들어있다면 따로 추가 작업이 필요없지만, 일반적으로는 상대 주소를 포함한 page정보가 있습니다.

이것은 form의 action만이 아니라 A tag 및 모든 주소가 있는곳에서 동일하게 일어납니다.
아래는 daum기사 중 html 소스 한 곳입니다.

daum web page의 한 곳
<div id="kakaoGnb" role="navigation">
    <div class="inner_gnb">
        <h2 class="screen_out">뉴스 메인메뉴</h2>
        <ul class="gnb_comm #GNB#default">
            <li class="on"><a href="/" class="link_gnb link_gnb1 #media_home"><span class="screen_out">선택됨</span><span class="ir_wa"></span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/society/" class="link_gnb link_gnb2 #media_society"><span class="ir_wa">사회</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/politics/" class="link_gnb link_gnb3 #media_politics"><span class="ir_wa">정치</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/economic/" class="link_gnb link_gnb4 #media_economic"><span class="ir_wa">경제</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/foreign/" class="link_gnb link_gnb5 #media_foreign"><span class="ir_wa">국제</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/culture/" class="link_gnb link_gnb6 #media_culture"><span class="ir_wa">문화</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/digital/" class="link_gnb link_gnb7 #media_digital"><span class="ir_wa">IT</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/ranking/popular/" class="link_gnb link_gnb8 #media_ranking"><span class="ir_wa">랭킹</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/series/" class="link_gnb link_gnb9 #media_series"><span class="ir_wa">연재</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/photo/" class="link_gnb link_gnb10 #media_photo"><span class="ir_wa">포토</span><span class="bar_gnb"><span class="inner_bar"></span></span></a></li>
            <li><a href="/tv/" class="link_gnb link_gnb11 #media_tv"><span class="ir_wa">TV</span><span class="bar_gnb"><span class="inner_bar"></span></span><em class="ico_news ico_live">LIVE</em></a></li> 

a href 주소에 "/society/" 링크가 있습니다.
web crawling 을 하려면 한 페이지만 읽는것이 아니라 html 코드나 form코드에서 다음 주소를 찾아서 추가로 진행해야하는것이 필 수 입니다.
그러면 저 상대 주소는 내가 접속한 page+상대 주소 입니다.

구현 이론

즉, http://www.daum.net/ 으로 접속했다고 했고, 만약 거기 html응답을 분석했는데 link의 상대 주소가 위와 같이 나왔다고 한다면, http://www.daum.net/society/ 이런식으로 합쳐서 보내면 됩니다만, 실제 구현은 그렇게 단순 하지 않습니다.

우리가 접속하는 http://www.daum.net/ 접속은 실제 webpage 의 '/'를 의미 하지 않을 수 있습니다. 이것은 무슨 말이냐면 저 주소를 접속했지만 실제는 내부적으로  http://www.daum.net/abc/index.html 같은 위치의 html을 우리에게 넘겨 줄 가능성이 있다는 뜻입니다. 그래서 다음 주소는 http://www.daum.net/abc/society/ 이런 형태가 되어야 할 수도 있다는 의미입니다.(https://docs.python.org/ site의 경우)

또 다른 문제점은  http://www.daum.net/abc/index.html 이런 주소를 넘겨줬을때 html코드가 ../society/ 이런 형태가 될 수도 있다는 사실입니다. 그러면 두개 주소를 합쳐서 http://www.daum.net/abc/../society/ 이런 주소가 되며 http://www.daum.net/society/ 이런식으로 다음 접속 경로를 설정해야 할 수도 있습니다. 결론적으로 우리가 지금 접속한 web page의 정확한 경로를 알아야 하고 또한 다음 page link를 정확히 추출해서 다음 page를 생성 해내야합니다. 이때 ".."와 같은 상대 경로 url path를 절대 경로로 만들어 줘야 합니다. (urljoin 사용)

서론이 길었습니다. 위 내용을 python을 이용해 구현해 보았습니다.

Python3 예제
import requests
URL = "https://docs.python.org/"
res = requests.get(URL)
print("requested url:",res.url) # 접속한 url 정보

from bs4 import BeautifulSoup
bsObj = BeautifulSoup(res.text, "html.parser")

# 첫번째 a 태그 뽑아오기
first_a = bsObj.find("a")
print(first_a)

# 글자만 뽑아오기
print(first_a.text)
print("href",first_a.get("href"))

# 이 주소로 접속 하면 안됨
print(URL+first_a.get("href"))

# urljoin 사용해야함
from urllib.parse import urljoin
have_to_get_url = urljoin(res.url, first_a.get("href"))
print(have_to_get_url)

"""
requested url: https://docs.python.org/3/
<a accesskey="I" href="genindex.html" title="General Index">index</a>
index
href genindex.html
https://docs.python.org/genindex.html
https://docs.python.org/3/genindex.html
"""

모듈 설치 하기

pip install BeautifulSoup4
pip install requests

소스 설명


위 예제의 경우 https://docs.python.org/ 주소로 접속했는데 실제 접속하고 나니 https://docs.python.org/3/ 이런 주소로 접속했다고 나오게 되고 이때 A tag 주소를 잘못 사용하면 어뚱한곳으로 접속하게 됩니다.
urljoin을 이용하여 https://docs.python.org/3/genindex.html 과 같은 주소를 접속하는 예제 입니다.



2019년 7월 7일 일요일

web page 크롤링(crawling) , 스크레이핑(scraping)시 form post/get를 위한 기본 개념

크롤링(crawling) , 스크레이핑(scraping)시 form post/get를 위한 기본 개념

서론


Web page는 기본적으로 html 문법에 기초합니다. 크롤링을 하다 보면 login 처리를 해야할때도 있고 search 버튼에 text를 넣고 검색 버튼을 눌러야 할때도 있습니다. 이러한 처리를 하기 위해서는 기본적으로 html 문법중 form에 대해서 알아야 합니다.

기본 개념

form은 html의 form tag를 의미합니다.
form tag에는 몇가지 속성이 있습니다.
action : 폼을 전송할 서버 쪽 스크립트 파일을 지정합니다.
name : 폼을 식별하기 위한 이름을 지정합니다.
accept-charset : 폼 전송에 사용할 문자 인코딩을 지정합니다.
target : action에서 지정한 스크립트 파일을 현재 창이 아닌 다른 위치에 열도록 지정합니다.
method : 폼을 서버에 전송할 http 메소드를 정합니다. (GET 또는 POST)
예제)
        <form action = "form.jsp" accept-charset="utf-8" 
              name = "person_info" method = "get"> 
              <input type = "submit" value = "submit"/>
        </form>


POST 와 GET

전송할 http 메소드 종류인 GET과 POST는 브라우저에서 폼 데이터를 가져와 서버로 보내는 똑같은 기능을 수행하지만, 방식은 다릅니다. GET은 폼 데이터를 URL 끝에 붙여서 눈에 보이게 보내지만 POST 방식은 보이지 않게 보냅니다. 받는쪽에서는 POST, GET 방식이던 구별을 하지 않는 경우가 대다수 입니다.

예제 네이버 기본 페이지를 분석한 검색

naver의 기본 페이지중 form을 검색하면 아래 부분이 나옵니다.

<form id="sform" name="sform" action="https://search.naver.com/search.naver" method="get">

 <fieldset>
     <legend class="blind">검색</legend>
        <input type="hidden" id="sm" name="sm" value="top_hty" />
        <input type="hidden" id="fbm" name="fbm" value="0" />
        <input type="hidden" id="acr" name="acr" value="" disabled="disabled" />
        <input type="hidden" id="acq" name="acq" value="" disabled="disabled" />
        <input type="hidden" id="qdt" name="qdt" value="" disabled="disabled" />
        <input type="hidden" id="ie" name="ie" value="utf8" />
        <input type="hidden" id="acir" name="acir" value="" disabled="disabled" />
        <input type="hidden" id="os" name="os" value="" disabled="disabled" />
        <input type="hidden" id="bid" name="bid" value="" disabled="disabled" />
        <input type="hidden" id="pkid" name="pkid" value="" disabled="disabled" />
        <input type="hidden" id="eid" name="eid" value="" disabled="disabled" />
        <input type="hidden" id="mra" name="mra" value="" disabled="disabled" />
        <span class="green_window">
            <input id="query" name="query" type="text" title="검색어 입력" maxlength="255" class="input_text" tabindex="1" accesskey="s" style="ime-mode:active;" autocomplete="off" onclick="document.getElementById('fbm').value=1;" value="" />
        </span>
        <div id="nautocomplete" class="autocomplete">
            <!-- 자동완성 열린 경우 fold 클래스 추가, 딤드인 경우 dim 추가 -->
            <a href="javascript:;" role="button" tabindex="2" class="btn_arw _btn_arw fold"><span class="blind _text">자동완성 펼치기</span><span class="ico_arr"></span></a>
        </div>
        <button id="search_btn" type="submit" title="검색" tabindex="3" class="sch_smit" onmouseover="this.className='sch_smit over'" onmousedown="this.className='sch_smit down'" onmouseout="this.className='sch_smit'" onclick="clickcr(this,'sch.action','','',event);"><span class="blind">검색</span><span class="ico_search_submit"></span></button>
    </fieldset>
</form>

여기에서 form tag안에 type=submit이란건 사용자가 누르는 버튼을 의미하고 input tag는 넣어주는 값을 의미합니다.
input모든 항목을 name=value형태로 값을 넘겨주면 됩니다.
여기에 name=query항목이 검색 내용을 넣는 항목입니다 이 부분의 이름은 website마다 다르므로 시행착오를 해야하고,input의 type=hidden항목도 모두 채워줘야 합니다.

결론은 naver의 form은 get방식이고 input으로 된 항목들을 모두 채우고 그중에 query항목에 검색하고자 하는 문자열을 넣어서 https://search.naver.com/search.naver 여기 page를 get방식으로 호출 하면 됩니다.
실제 xcf라는 문자열을 호출하면 인터넷 창에 아래와 같은 형태가 됩니다.
https://search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=xcf

이것을 정리해보면 아래와 같습니다. 한글이라면 url에 직접쓸 수 없고 변환 해야 합니다.
https://search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=검색하고자하는 문자열



2018년 6월 17일 일요일

analysis http server (JLHTTP) source for upload in android (파일 upload를 위한 웹서버 소스 코드 분석) #2




앞에서 https://swlock.blogspot.com/2018/06/analysis-http-server-jlhttp-source-for.html JLHTTP를 실행시키는 방법에 대해서 알아보았습니다.

최초 이글을 쓰게되는 upload 관련해서 예제가 없어서 확인이 어려웠습니다. 예제는 구하기 어려웠지만 FAQ에서 중요한 단서를 찾았습니다.

아래 내용입니다.
How do I handle uploaded files?
When an HTML form is used to upload a file or other large content, the default application/x-www-form-urlencoded encoding is insufficient. In this case, the form is submitted as a multipart body of a POST request with the multipart/form-data content type instead. The parts are sent in the same order as the form fields.

You can use a MultipartIterator to parse the request body and iterate over its parts. String values can be conveniently retrieved as such, but the file can be read directly as a stream, thus preventing various issues that would arise from holding the entire file contents in memory at once.
Here's a basic example:
String comment;
String filename;
File file;
Iterator<Part> it = new MultipartIterator(request);
while (it.hasNext()) {
    Part part = it.next();
    if ("comment".equals(part.getName())) {
        comment = part.getString()
    } else if ("file".equals(part.getName())) {
        filename = part.getFilename();
        file = File.createTempFile(filename, ".uploaded");
        FileOutputStream out = new FileOutputStream(file);
        try {
            transfer(part.getBody(), out, -1);
        } finally {
            out.close();
        }
    }
}
// now do something with the comment, filename and file
Alternatively, you can use the lower-level MultipartInputStream directly.

그렇지만 어떻게 하는것인지 해당 코드를 어떻게 넣어야 하는것인지 몰라서 만들어 보았습니다.

main 코드에 위 내용을 추가 하여 보았습니다.
아래 예제는 파일을 upload하게되면 temp영역에 파일을 저장하는 내용입니다.

    public static void main(String[] args) {
        try {
            if (args.length == 0) {
                System.err.printf("Usage: java [-options] %s <directory> [port]%n" +
                    "To enable SSL: specify options -Djavax.net.ssl.keyStore, " +
                    "-Djavax.net.ssl.keyStorePassword, etc.%n", HTTPServer.class.getName());
                return;
            }
            File dir = new File(args[0]);
            if (!dir.canRead())
                throw new FileNotFoundException(dir.getAbsolutePath());
            int port = args.length < 2 ? 80 : Integer.parseInt(args[1]);
            // set up server
            for (File f : Arrays.asList(new File("/etc/mime.types"), new File(dir, ".mime.types")))
                if (f.exists())
                    addContentTypes(f);
            HTTPServer server = new HTTPServer(port);
            if (System.getProperty("javax.net.ssl.keyStore") != null) // enable SSL if configured
                server.setServerSocketFactory(SSLServerSocketFactory.getDefault());
            VirtualHost host = server.getVirtualHost(null); // default host
            host.setAllowGeneratedIndex(true); // with directory index pages
            host.addContext("/", new FileContextHandler(dir));
            host.addContext("/api/time", new ContextHandler() {
                public int serve(Request req, Response resp) throws IOException {
                    long now = System.currentTimeMillis();
                    resp.getHeaders().add("Content-Type", "text/plain");
                    resp.send(200, String.format("%tF %<tT", now));
                    return 0;
                }
            });
            host.addContext("/upload", new ContextHandler() {
                public int serve(Request req, Response resp)  {
                 System.out.println("upload");
                 String comment="";
                 String filename="";
                 File file;
                 try {
                  Iterator<MultipartIterator.Part> it = new MultipartIterator(req);
 
                  while (it.hasNext()) {
                   MultipartIterator.Part part = it.next();
                   System.out.println(part.getName());
                      if ("comment".equals(part.getName())) {
                          comment = part.getString();
                      } else if ("file1".equals(part.getName())) {
                          filename = part.getFilename();
                          file = File.createTempFile(filename, ".uploaded");
                          System.out.println(file.getAbsolutePath());
                          FileOutputStream out = new FileOutputStream(file);
                          try {
                              transfer(part.getBody(), out, -1);
                          } finally {
                              out.close();
                          }
                      }
                  }
                 } catch (Exception e ){
                  e.printStackTrace();
                 }
                    resp.getHeaders().add("Content-Type", "text/plain");
                    try {
      resp.send(200, "Hello, World!"+comment+" "+filename);
     } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
                    return 0;
                }
            },"POST");
            server.start();
            System.out.println("HTTPServer is listening on port " + port);
        } catch (Exception e) {
            System.err.println("error: " + e);
        }

살펴보아햐 하는 부분은 addContext /upload 하는 부분입니다.
host.addContext("/upload", new ContextHandler() {

제작자는 이것을 context handler 라고 부르며, http로 path 요청이 들어오면 그것을 처리하기 위한 context 라고 보면 됩니다.

html 페이지도 준비하였습니다. 이때 form의 action 부분을 "upload"로 한 이유는 context 처리 페이지가 upload이기 때문에 맞추어 주어야 합니다. 그리고 form은 다음과 같이 값을 맞추어야 합니다. method="POST" enctype="multipart/form-data"

index.html
<html>
<body>
<form action="upload" method="POST" enctype="multipart/form-data" name="">
 <div>
   <label for="file">Choose file to upload</label>
   <input type="file" id="file1" name="file1" >
 </div>
 <div>
   <label for="file">Choose file to upload</label>
   <input type="file" id="file2" name="file2" >
 </div>
 <div>
   <button>Submit</button>
 </div>
</form>
</body>
</html>

JLHTTP를 실행하고 localhost로 접속을 하면 아래와 같이 나옵니다.
(실행방법을 잘 모르겠으면 이전 링크를 참고하세요.)
1.png(임의의 파일)파일을 선택해서 submit 버튼을 누르면 다음과 같이 나옵니다.


HTTP 서버측 로그 화면
HTTPServer is listening on port 8080
upload
file1
C:\Users\xxx\AppData\Local\Temp\1.png3242384259546787045.uploaded
file2


전체 소스 링크 입니다.

소스는 파일 하나로 구성되어 있어 http server의 동작을 이해하는 좋은 참고 소스가 될것이라고 생각됩니다. 참고로 라이센스는 GPL 입니다.







2018년 6월 11일 월요일

analysis http server (JLHTTP) source for upload in android (파일 upload를 위한 웹서버 소스 코드 분석)


java로 웹서버를 간단하게 만드는 것은 어렵지 않습니다. 그러나 전체 기능이 제대로 구현된 소스는 많지 않고, 대부분 서블릿이나 JSP에 촛점이 맞추어져 있어서 원하는것을 찾기가 쉽지는 않습니다.

여기에서 알아보고자 하는것은 아래와 같습니다.

1. java로 구현된 http 서버(JLHTTP) 소스 동작 모습 (이번 내용)


2. 파일 upload를 하게되면 소스코드 어디 에서 처리를 하는지? (다음 내용)


이 글을 쓰는 목적은 웹방식의 응용프로그램을 만들기 위함입니다. 최종 사용자가 보면 local web server를 접속하게되고 브라우저를 오픈하여 특정 파일들을 선택해서 submit 을 하면 처리하여 web page로 결과를 돌려주는 동작입니다.
이것을 위해서 왜 web 방식을 해야하는지는 중요하지 않습니다. 아주 특별한 환경에서 처리를 위해서, 다만 이것을 위해서는 JSP 또는 서브릿을 사용해도 관계는 없지만, 구현 환경을 최대로 단순하게 처리하기 위해서 입니다.


소스 얻기

JLHTTP https://www.freeutils.net/source/jlhttp/
License 는 GPL 입니다. 코드는 공개되어 있지만 사용시 코드를 공개해야하는 라이센스입니다.

소스 폴더 구성

받은 소스는 java소스 하나와 jar 파일 하나로 구성되어있습니다. 아래와 같이 배치되도록 프로젝트를 만듭니다. (lib의 jar 파일은 필요가 없어도 실행하는데 문제가 없었습니다.)


실행하기

실행시키기 위해서는 인자가 2개가 필요합니다. 홈페이지 폴더와 서버 포트 번호입니다.


브라우저로 다음 주소를 접속합니다. http://localhost:8080/
실행하면 아래와 같이 볼 수 있습니다. (8080 서버 포트는 실행시 두번째 인자 입니다.)

이 소스는 html 만 지원되는 소스입니다. server side script 또는 cgi 등을 지원하지 않습니다.

















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월 어느날.....



2017년 1월 15일 일요일

Exception in thread "main" java.lang.RuntimeException: Stub! 발생이유

HttpClient 를 사용중 아래 와 같은 오류가 발생할때 대처법

Exception in thread "main" java.lang.RuntimeException: Stub!
at org.apache.http.impl.client.AbstractHttpClient.<init>(AbstractHttpClient.java:5)
at org.apache.http.impl.client.DefaultHttpClient.<init>(DefaultHttpClient.java:7)
at com.xxx.xxx.xxx.xxx.execute(xxx.java:76)
at com.xxx.xxx.xxx.xxx.main(xxx.java:126)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

코드는 아래와 같이 사용하였습니다.
발생 원인은 android studio에서 httpclient를 사용하였었는데요. 그리고는 android 단말에서 실행 시킨게 아니라 java main에 연결하여 java native형태로 실행시켜서 발생한 문제입니다. 아무래도 httpclient android용이 android없이 동작할때 제대로 동작이 안되도록 되어있어서 이러한 현상이 발생하였습니다.
해결책은 test code를 android가 정상적으로 돌 수있는 환경을 만들어서 앱 내에서 테스트 할수있도록 변경하였습니다.

샘플코드
public class GetHttpWithAHttpClient {
...

    public boolean execute(String addr) {
        boolean retval = true;
        errorCode = ERROR_CODE_NOERROR;

        //CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpClient httpclient = new DefaultHttpClient();
        try {
            HttpGet httpGet = new HttpGet(addr);
            HttpResponse response1 = httpclient.execute(httpGet);

            try {
                System.out.println(response1.getStatusLine());
                HttpEntity entity1 = response1.getEntity();
                // do something useful with the response body 
               // and ensure it is fully consumed
                entity1.getContent().read(htmlByte);
            } finally {
                //response1.close();            }

        } catch (ClientProtocolException e) {
            e.printStackTrace();
            errorCode = ERROR_CODE_HTTP_EXCEPTION;
            retval = false;
        } catch (IOException e) {
            e.printStackTrace();
            errorCode = ERROR_CODE_HTTP_EXCEPTION;
            retval = false;
        } finally {
            //try {
                //httpclient.close();
            //} catch (IOException e) {
            //    e.printStackTrace();
            //}
        }
        return retval;
    }

    public final static void main(String[] args) throws Exception {
        System.out.println("test");
        GetHttpWithAHttpClient http = new GetHttpWithAHttpClient();
        http.execute("http://www.daum.net");
    }
}