PHP를 이용한 심플한 게시판 파싱 방법(Guzzle + domcralwer) > 그누보드5 팁자료실

그누보드5 팁자료실

PHP를 이용한 심플한 게시판 파싱 방법(Guzzle + domcralwer) 정보

PHP를 이용한 심플한 게시판 파싱 방법(Guzzle + domcralwer)

첨부파일

scrap.zip (2.9K) 333회 다운로드 2018-03-06 14:38:40

본문

 

들어가기 전에

파싱과 스크랩핑, 크롤링은 의미가 좀 다르지만, 한국에서는 보통 파싱이라는 단어로 통용됩니다.

파싱을 위해선 정규식을 쓰는게 일반적이지만, 웹의 데이타를 추출할때는 어려운점이 있다.

웹의 데이타를 파싱하기 위해 가장 보편적이고, 심플한 방법은 DOM 에서 데이타를 추출하는 형태이다. 웹페이지를 설계할때 css를 이용하여  계층적으로  만들기 때문에, 데이타를 추출하기 위해서도 계층적 구조로 접근이 가능한것이 유리하다.

Dom 파싱을  위해 각 언어별로 적당한 모듈이 있는데, 자바에서는 jsoup, 파이썬에선 BeautifulSoup 이 있고, nodejs 는 JSDOM, cheerio 가 있다.

 

문제는 PHP인데, 언어의 인지도에 비해 크롤링(파싱)부분이 취약하다.  그나마 정규식을 통해 파싱처리를 하는데, 조금 복잡한 웹페이지를 파싱할려면, 계층별로 split처리와 여러 정규식을 써야한다.  정규식 자체도 복잡하게되어 직관성이 매우 떨어지게 된다.(그러나 일단 데이타가 파싱만 되면, db처리나 활용이 편리한 이점이 있다.)

최근 php 로 데이타 파싱을 해야 할 일이 있어(정규식으로 해볼려다가 암걸리는줄 알았다.), 보던중에  php dom 파싱이 가능한 라이브러리가 있어 정리를 해본다. (물론 전통적인  php dom 파서가 있다. 타 언어에 비해 느리고, 잘 정의된 문서가 아닌 경우 오류가 심하고, dom 에 접근하는 방식이 직관적이지 못하다.)

웹 스크래핑(파싱) 라이브러리는 쉬운 사용법과, 직관적인 selector 사용이 가능해야 하며, 태그 일부가 유실되어 깨진 문서라고 하더라도 오류없이 파싱되어야 한다. 속도도 어느정도 되어야 한다.

 

Guzzle, domCrawer, css-selector 설치

guzzle + domcrawler을 사용하기 위해서는  php 5.5 이상이어야 한다. (웹사이트 보안을 위해서라도 php 5.6 또는 php 7.x 를 사용하길 권장)

guzzle + domcralwer 등은 composer 로 설치한다.  php 에서 composer 를 사용하는 방법은 구글링으로 쉽게 구할수 있음으로 본글에서는 설명하지 않습니다. 

composer.json


{
 "require": {
   "php": ">=5.6.0",
   "guzzlehttp/guzzle": "~6.0",
   "symfony/dom-crawler": "3.4.*",
   "symfony/css-selector": "3.4.*",
   "fabpot/goutte" : "^3.1"
 }
}

Guzzle + Dom crawler + css selector 라이브러리를 사용했는데, 굳이 따로 쓸필요없이, Goutte 라이브러리만 써도 된다. 아래 예제에서 Sir class는 위의 방식대로 작업한것이고, 아래 Sir2 클래스 파일은 Goutte를 사용한 방식이다. 내부에서 쓰는 라이브러리가 동일함으로 그냥 Goutte 를 쓰는걸 추천한다. 

예제코드중 Sir1 클래스는 sir.co.kr 자유게시판 목록을 스크래핑(파싱) 하는 코드이며, Sir2 클래스는 로그인 인증처리후 쪽지함 목록을 스크래핑(파싱)하는 코드이다. 본인이 테스트한 서버는 https 접속시 ssl 인증서 오류가 발생하여, 옵션 코드를 추가하였다.  

아래코드는 예제입니다. 테스트로만 사용하세요. 지나치게 많은 request를 sir.co.kr 로 보내면 아이피가 차단될수 있습니다.

 

예제코드

Sir.php 파일


<?php
require __DIR__."/vendor/autoload.php";
use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;
class Sir
{
    private $protocol = "https";
    private $doamin = "sir.co.kr";

    public function __construct() {
    }
    public function __destruct() {
    }
    public function getList($page = 1) {
        $bo_table = "cm_free"; //자유게시판명
        $url = $this->protocol."://".$this->doamin."/{$bo_table}/p{$page}";
        $client = new GuzzleHttp\Client();
        //https 접속시 ssl 오류가 발생하면, ['verify' => false] 옵션을 추가한다.
        $result = $client->request('GET', $url, ['verify' => false]);
        $html = $result->getBody()->getContents();
        $dom = new Crawler($html);
        $items = $dom->filter('div.sir_ul01 ul')->children();
        $result = array();
        foreach($items as $item) {
            $row = array();
            $obj = new Crawler($item);
            try {
                $title =  $obj->filter('a.title_link')->text();
                $href = $obj->filter('a.title_link')->attr("href");
                if(strpos($href, "//") ===0) $href = $this->protocol.":".$href;
                $row['title'] = trim($title);
                $row['href'] = trim($href);
                $result[] = $row;
            } catch(Exception $e) {
                echo $e->getMessage();
            }
        }
        return $result;
    }
    /**
     * url 에 있는 파일을 다운받아 $filepath 에 저장한다.
     * $filepath 는 cheditor 경로
     * @param $url
     * @param $filepath
     * @param $is_dryrun true : 실제 파일다운로드를 실행하지 않음. default : false
     */
    public function downloadAndSaveImage($url, $filepath, $is_dryrun = false) {
        if(!$is_dryrun) {
            $file_handle = fopen($filepath, 'w');
            $client = new \GuzzleHttp\Client();
            $response = $client->get($url, ['save_to' => $file_handle]);
            return ['response_code'=>$response->getStatusCode(), 'url' => $url];
        } else {
            return ['response_code'=>200, 'url' => $url];
        }
    }
}

 

Sir2.php 파일


<?php
require __DIR__."/vendor/autoload.php";
use Goutte\Client;
use Symfony\Component\DomCrawler\Crawler;
class Sir2
{
    private $protocol = "https";
    private $doamin = "sir.co.kr";
    public function __construct() {
    }
    public function __destruct() {
    }
    public function getList($page = 1) {
        $bo_table = "cm_free"; //자유게시판 id
        $url = $this->protocol."://".$this->doamin."/{$bo_table}/p{$page}";
        $client = new \Goutte\Client();
        // Create and use a guzzle client instance that will time out after 90 seconds
        $guzzleClient = new \GuzzleHttp\Client(array(
            'timeout' => 90,
            'verify' => false,
        ));
        $client->setClient($guzzleClient);
        $crawler = $client->request('GET', $url);
        $items = $crawler->filter('div.sir_ul01 ul')->children();

        $result = array();
        foreach($items as $item) {
            $row = array();
            $obj = new Crawler($item);
            try {
                $title =  $obj->filter('a.title_link')->text();
                $href = $obj->filter('a.title_link')->attr("href");
                if(strpos($href, "//") ===0) $href = $this->protocol.":".$href;
                $row['title'] = trim($title);
                $row['href'] = trim($href);
                $result[] = $row;
            } catch(Exception $e) {
                echo $e->getMessage();
            }
        }
        print_r($result);
        return $result;
    }
    public function getMemoList($page = 1) {
        $login_url = $this->protocol."://".$this->doamin."/bbs/login.php";
        $memo_url = $this->protocol."://".$this->doamin."/bbs/memo.php";
        $client = new \Goutte\Client();
        // Create and use a guzzle client instance that will time out after 90 seconds
        $guzzleClient = new \GuzzleHttp\Client(array(
            'timeout' => 90,
            'verify' => false,
        ));
        $client->setClient($guzzleClient);
        $crawler = $client->request('GET', $login_url);
        //$crawler = $client->click($crawler->selectLink('로그인')->link());
        $form = $crawler->selectButton('로그인')->form();
        $crawler = $client->submit($form, array('mb_id' => 'your_id', 'mb_password' => 'your_password'));
        //todo 로그인 오류 체크
        $crawler = $client->request('GET', $memo_url);
        $items = $crawler->filter('div.tbl_wrap table tbody')->children();
        $result = array();
        foreach($items as $item) {
            $row = array();
            $obj = new Crawler($item);
            $sender_name =  $obj->filter("td")->eq(1)->filter("a")->text();
            $sender_link =  $obj->filter("td")->eq(1)->filter("a")->attr("href");
            $parts = parse_url($sender_link);
            parse_str($parts['query'], $query);
            $send_mb_id = $query['mb_id'];
            $send_datetime =  $obj->filter("td")->eq(2)->text();
            $read_datetime =  $obj->filter("td")->eq(3)->text();
            $row['send_mb_nick'] = $sender_name;
            $row['send_mb_id'] = $send_mb_id;
            $row['send_datetime'] = $send_datetime;
            $row['read_datetime'] = $read_datetime;
            $result[] = $row;
        }
        return $result;
    }
    /**
     * url 에 있는 파일을 다운받아 $filepath 에 저장한다.
     * $filepath 는 cheditor 경로
     * @param $url
     * @param $filepath
     * @param $is_dryrun true : 실제 파일다운로드를 실행하지 않음. default : false
     */
    public function downloadAndSaveImage($url, $filepath, $is_dryrun = false) {
        if(!$is_dryrun) {
            $file_handle = fopen($filepath, 'w');
            $client = new \GuzzleHttp\Client();
            $response = $client->get($url, ['save_to' => $file_handle]);
            return ['response_code'=>$response->getStatusCode(), 'url' => $url];
        } else {
            return ['response_code'=>200, 'url' => $url];
        }
    }
}

 

sir_scrap.php 파일



<?php
include_once('./_common.php');
ini_set('display_errors', 'On');
error_reporting(E_ALL & ~E_NOTICE);
include_once __DIR__."/Sir.php";
include_once __DIR__."/Sir2.php";
$sir = new Sir();
$sir2 = new Sir2();

for($i = 0; $i < 3; $i++) {
    $list = $sir->getList($i); //자유게시판 리스트를 스크래핑(파싱)한다.
    echo "<xmp>";
    print_r($list);
    echo "</xmp>";
}
echo "<h1>내쪽지함</h1><br>";
$list = $sir2->getMemoList(1); //로그인후 내쪽지함 목록을 파싱한다.
echo "<xmp>";
print_r($list);
echo "</xmp>";
?>

 

sir_scrap.php 실행 결과

The current node list is empty.
Array
(
    [0] => Array
        (
            [title] => 고용노동부 [일자리 안정자금] 사이트도 그누보드를... ㅎㅎ
            [href] => https://sir.kr/cm_free/1440716
        )

    [1] => Array
        (
            [title] => 비트코인을 10만원에 샀다한들.....
            [href] => https://sir.kr/cm_free/1440715
        )

    [2] => Array
        (
            [title] => 마우스로 쭉, 터치로 쭈욱~~~~~~ 4
            [href] => https://sir.kr/cm_free/1440675
        )

    [3] => Array
        (
            [title] => 서버 테스트.... 3
            [href] => https://sir.kr/cm_free/1440674
        )

    [4] => Array
        (
            [title] => 네이버 티비 연속재생 되게 하기 힘드네요..ㅠㅠ                         1
            [href] => https://sir.kr/cm_free/1440673
        )

      ......


Array
(
    [0] => Array
        (
            [send_mb_nick] => 리자
            [send_mb_id] => kagla
            [send_datetime] => 18-01-03 14:11
            [read_datetime] => 18-01-03 14:17
        )

    [1] => Array
        (
            [send_mb_nick] => 리자
            [send_mb_id] => kagla
            [send_datetime] => 18-01-03 14:05
            [read_datetime] => 18-01-03 14:06
        )

    [2] => Array
        (
            [send_mb_nick] => 리자
            [send_mb_id] => kagla
            [send_datetime] => 17-12-04 14:42
            [read_datetime] => 17-12-04 15:15
        )

    [3] => Array
        (
            [send_mb_nick] =>  나양
            [send_mb_id] => sea1231
            [send_datetime] => 17-10-30 09:46
            [read_datetime] => 17-10-30 17:02
        )

 

구현 코드를 보면, php에서 정규식을 사용하는것보다 심플하고 직관적으로 웹페이지 파싱이 가능하다는걸 알수 있습니다. php 에서 웹페이지에 대한 파싱이 필요한 경우 guzzle + dom crawler 는 좋은 선택이 될수 있습니다. 라이브러리 에 대한 자세한 사용법은 공식홈페이지와 아래 링크를 참고바랍니다.

 

참고

https://lamp-dev.com/scraping-products-from-walmart-with-php-guzzle-crawler-and-doctrine/958

http://vegibit.com/php-simple-html-dom-parser-vs-friendsofphp-goutte/

 

 

 

추천
10

댓글 26개

인터넷을 검색해보니 guzzle 관련된 한글 문서나 참고할 예제가 없어서 직접다 구현하였습니다.

위의 예제코드는 단순히 테스트정도 해볼수 있는 코드이고, 실제 웹페이지 본문과 본문에 첨부된 이미지까지 파싱 처리하는 코드는 플러그인으로 만들어서 올리도록 하겠습니다.
2년 전에 파싱 의뢰가 많았습니다. 이후에는 없는데요.
할 줄 몰라서 쳐다도 안 보았습니다. ^^
2년 걸립니다. 하고 받을 걸 그랬습니다. ㅎㅎ
존귀하신 자료 감사합니다!
감사합니다.  이리저리 수정해서  맞춤으로 하나 만들었네요.
composer 설치부터  하루 정도 소요  돼고 php 7.0 버젼에서는 domCrawer 설치가 안돼더군요

5.6 으로 변경하니 os가 잘못돼었는지 에러가 나고 또 다시 설치하고  ^^;;

해결하고 금방 소스 올리면 돼는지 알았는데  부딪치는 벽이 많이 있었네요^^

Guzzle, domCrawer, css-selector  국내에는  샘플이 별로 없어서  잘모르는 영문보면서 어렵게 진행되었습니다.  영문 질의문답 및 참고예제 문서 전부다 본것 같네요

가상 os로 했는데 composer 호스팅 업체도 국내에는 없는것 같습니다.

소스로 수정하면서 국내 참고서적 없이 하셨다니 대단하십니다.

다시한번 감사드립니다. ^^
안녕하세요. 올려주신 내용 잘 보고 있습니다.
파싱 테스트 도중에 로그인에 대한 오류가 나타나서 문의 드립니다.
$crawler = $client->submit($form, array('mb_id' => 'your_id', 'mb_password' => 'your_password'));
로그인 form을 넘길 시  Call to a member function submit() on null
해당 submit 함수가 없다고 표현이 되는데 어떻게 해야 하나요?
composer install 을 통해서 "fabpot/goutte"  클래스가 설치되어야 합니다.
위의 예제코드들은 기본적으로 composer 환경에서 동작되도록 코딩되어 있습니다.
답변감사합니다. composer install 통해서 설치 해주긴 했는데 안되네요. 버전의 문제인가.. 일단 다시 한번 시도해보도록 하겠습니다
전체 2,411 |RSS
그누보드5 팁자료실 내용 검색

회원로그인

(주)에스아이알소프트 / 대표:홍석명 / (06211) 서울특별시 강남구 역삼동 707-34 한신인터밸리24 서관 1404호 / E-Mail: admin@sir.kr
사업자등록번호: 217-81-36347 / 통신판매업신고번호:2014-서울강남-02098호 / 개인정보보호책임자:김민섭(minsup@sir.kr)
© SIRSOFT