파이썬 Scrapy를 이용한 Layout 탐색기 구현.

[Scrapy]

스크래피(Scrapy)는 웹 사이트에서 필요한 데이터를 추출하기 위해
파이썬(Python)으로 작성된 오픈소스 프레임워크이다.

소스코드 : https://github.com/scrapy/scrapy
문서 : http://doc.scrapy.org/en/latest/

1. 설치
.paython 2.7
.pip, setuptools
.openssl

- 윈도우에서 설치
. Python 2.7 from https://www.python.org/downloads/
. Install pywin32 from http://sourceforge.net/projects/pywin32/
. Install pip from https://pip.pypa.io/en/latest/installing.html

설치가 완료 되면 pip를 이용해 scrapy를 인스톨한다.
> pip install Scrapy

2. 프로젝트 만들기

> scrapy startproject tutorial(프로젝트명)

# 프로젝트명(tutorial)으로 폴더가 만들어진다.

# 폴더 구조
tutorial/
    scrapy.cfg        # 설정파일이 있다. 프로젝트 자체에 관한 설정이다. 예를들어 설정파일은 어디있고 프로젝트 파일명은 어떤것이고..

    tutorial/         # 이 폴더에 프로젝트 관련 모듈이 들어간다. 일단 기본적으로 startproject 때 입력한 프로젝트 명으로 중복해서 폴더가 만들어진다.
                                          # 설정파일에 보면 기본적으로 이 파일명으로 세팅이 되어있다.
        __init__.py

        items.py            # item 즉 VO 라고 생각하면 된다.

        pipelines.py  # 파이프라인 관련 파일이다. 파이프라인이란 scrapy의 item을 가지고 디비에 데이터를 집어 넣거나 유효성체크를 하거나 다른아이템들과 가공을 하는데 사용한다.

        settings.py   # 설정관련 파일

        spiders/      # 스파이더라 부르는데 크롤링 하는 코드(비즈니스 록직)을 작성하면 된다.
            __init__.py
            ...


3. Spiders (http://doc.scrapy.org/en/latest/topics/spiders.html)
스크랩할 대상(사이트)의 스크랩 방법을 정의 클래스로,
크롤링 하는 코드(비즈니스 로직)을 작성하는 곳

진행순서
1. request를 초기화 하고, 크롤링을 시작한 url을 지정해 준다.
2. 파싱을 위한 콜백 함수를 지정한다.
3. 콜백함수는 selector를 이용해 데이터를 추출하고.
4. 추출된 데이터를 pipeline을 이용해 데이터 저장.

종류
.Spider
가장 기본적인 스파이더로 모든 스파이더는 이 스파이더를 상속해서 구현한다.

.CrawlSpider
일반적으로 웹사이트 크롤링시에 가장 많이 사용하는 스파이더.

.SitemapSpider
사이트 맵을 사용해 사이트를 크롤링 할 수 있다.
(robots.txt에에서 발견 사이트 맵 URL을 지원)

.initSpider
parse를 오버라이드해서 parse실행전에 다른 작업들을 할 수 있다.

4. Items
VO 라고 생각하면 된다.
scrapy.item을 이용해 크롤링할 항목들을 모델링 한다.

# items 선언
(Field 객체는 항목에 대한 메타데이터를 지정 하는데 사용된다.)
import scrapy
class DmozItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()
    desc = scrapy.Field()

5. Item Pipeline
spider에서 파싱이이 끝난 데이터를 순차적으로 전달한다.
Pipeline은 전달된 데이터를
1. 디비에 데이터를 저장,
2. 유효성체크(중복제거),
3. 데이터 가공 등을 처리 한다.
/*--삭제-- 구현 방법에 맞게 제공되는 메소드에 코드를 작성하면 된다.*/

Pipeline을 사용을 하려면 setting 파일에 등록해 준다.
(숫자값은 실행 순서를 결정하는데 0~1000 높을수록 우선순위)
ITEM_PIPELINES = {
    'myproject.pipelines.PricePipeline': 300,
    'myproject.pipelines.JsonWriterPipeline': 800,
}

6. Selectors
크롤링한 HTML 데이터 파싱하기.
response html을 분석하고 해당 tag나 id,class을 이용해 원하는 데이터를 파싱한다.
scrapy에서는 xpath와 css문법을 지원해준다.

response.xpath(), response.css()

>>> response.xpath('//title/text()')
[<Selector (text) xpath=//title/text()>]

>>> response.css('title::text')
[<Selector (text) xpath=//title/text()>]

>>> response.xpath('//title/text()').extract()
[u'Example website']

>>> response.css('img').xpath('@src').extract()
[u'image1_thumb.jpg',
 u'image2_thumb.jpg',
 u'image3_thumb.jpg',
 u'image4_thumb.jpg',
 u'image5_thumb.jpg']

 >>> response.xpath('//div[@id="images"]/a/text()').extract_first()
 u'Name: My image 1 '

7. 인증처리 (로그인)

로그인하는 폼을 찾아서 값을 대입 하거나, 세션, 쿠기값을 변경하는 방법, form request를 보내는 방법등이 있다.
scrapy에서는 scrapy.http를 이용해 이러한 처리들을 가능하게 해준다.

- 로그인 처리 방법
scrapy에 내장된 initSpider를 상속고, parse를 오버라이드해서
parse를 실행하기 전에 로그인을 처리할수 있다.

class TestSpider(InitSpider):

## initRequest 메소드가 맨 처음 시작 됨.
def init_request(self):
    ## 로그인 페이지와 callback 지정
    return Request(url=self.login_page, callback=self.login)

## FormRequest를 이용해서 해당 페이지에서 submit요청을 보낸다.
def login(self, response):
    return FormRequest.from_response(response,
                formdata={'id': 'com222', 'password': '90909090'},
                callback=self.check_login_response)

- 로그인 구현
Spider에서 InitSpider를 받는다.
크롤링을 시작하게 되면 initRequest 메소드가 가장 먼저 불린다.
여기서 로그인 할 페이지와 실행될 메소드를 지정해준다.
login 메소드에서는 FormRequest를 이용해서 해당 페이지에서 submit요청을 보내도록 한다.
요청이 끝나면 check_login_response가 불리는데 여기서 로그인이 제대로 되었는지 확인하면 된다.
response를 분석해 로그인 성공 여부를 판단한다.
여기서 response는 form request를 보내고 뜨는 페이지이다.
가장 쉬운 방법은 로그인해서 성공했을때의 페이지를 직접 보고 로그인에 성공했을 때 나타나는 엘리먼트가 있는지 보고 판단하는 것이다.
이 부분은 크롤링과 관계가 있어서 크롤링하는 방법은 바로 다음에서 살펴보도록 하자.
로그인이 제대로 되었다면 initialized에서 초기화 하고 parse_item이 불릴것이다.
이제 원하는 페이지에서 크롤링 할 수 있는 환경이 갖추어졌다.

/* --삭제--
8. 크롤링하기
response html을 분석하고 해당 tag나 id,class을 이용해 원하는 데이터를 파싱한다.
scrapy에서는 xpath와 css문법을 지원해준다.


8. 메일 보내기
scrapy.mail을 이용한다.

mailServer = smtplib.SMTP('smtp.gmail.com', 587)
mailServer.ehlo()
mailServer.starttls()
mailServer.ehlo()
mailServer.login(gmailUser, gmailPassword)
mailServer.sendmail(gmailUser, recipients, msg.as_string())
mailServer.close()
*/

8. 크롤링 실행

 > scrapy crawl 프로젝트명.


9. 코드 리뷰

## items.py #####################################

import scrapy
from scrapy.item import Item, Field

class SaraminWebItem(Item):
    site = Field()
    rank = Field()
    title = Field()
    req_url = Field()
    res_url = Field()
    doctype = Field()
    css = Field()
    js = Field()
    layout = Field()
    sidebar = Field()
    emulate = Field()
    embed_style_cnt = Field()
    embed_script_cnt = Field()
    etc = Field()
    reg_dt = Field()

## spider.py #####################################

from scrapy.spider import BaseSpider
from scrapy.contrib.spiders import Rule
from scrapy.contrib.spiders.init import InitSpider
from scrapy.http import Request, FormRequest

## Spider에서 InitSpider를 받는다.
class TestSpider(InitSpider):
    name = "test"
    allowed_domains = ["saramin.co.kr"]
    login_page = "http://mt.local.saramin.co.kr/zf_user/auth/login"
    start_urls = "http://mt.local.saramin.co.kr/zf_user"

  //Rule 객체를 이용해 크롤링 되는 사이트의 동작을 정의 한다.
    rules = (
        #Rule(SgmlLinkExtractor(allow=r'-\w+.html$'), callback='parse_item', follow=True),
        Rule(SgmlLinkExtractor(allow=("\mt\.local\.aramin\.co\.kr[^\s]*\/*$")), callback='parse_item', follow=True),
    )

  ## initRequest 메소드가 맨 처음 시작 됨.
    def init_request(self):
      ## 로그인 페이지와 callback 지정
        return Request(url=self.login_page, callback=self.login)

  ## FormRequest를 이용해서 해당 페이지에서 submit요청을 보낸다.
    def login(self, response):
        return FormRequest.from_response(response,
                    formdata={'id': 'com222', 'password': '90909090'},
                    callback=self.check_login_response)

  ## response된 html을 파싱해서 로그인 여부를 판단 한다.
    def check_login_response(self, response):
        //check login success
        if "/zf_user/auth/logout" in response.body:

          ## 로그인이 성공하면 initialized를 실행해 파싱을 시작한다.
            return self.initialized()
        else
            return self.error()

    def initialized(self):
        return Request(url=self.start_urls, callback=self.parse_item)

    def parse_item(self, response):

        ## 중복처리를 위해 수집된 url을 불러옴.
        if self.isFirstLoop :
            self.tempUrls = self.getUrlSet()
            self.isFirstLoop = 0;

        site = "saramin"
        rank = "0"
        title = response.xpath('//title/text()').extract()
        req_url = response.request.url.replace('http://'+host, '', 1)
        res_url = response.url
        s  = re.search("<(!\s*doctype\s*.*?)>", response.body, re.IGNORECASE)
        doctype = s.group(1) if s else ""
        css = response.xpath('//link/@href').extract()
        js = response.xpath('//script/@src').extract()
        layout = response.xpath('//div[@class="debug_layout"]/text()').extract()
        sidebar = response.xpath('//div[@class="debug_side_layout"]/text()').extract()
        emulate = response.xpath('//meta[contains(@content, "IE")]/@content').extract()
        embed_style_cnt = len(response.xpath('//style').extract())
        embed_script_cnt = len(response.xpath('//script').extract()) - len(response.xpath('//script/@src').extract())

        # 호스트부분은 제거해 준다.
        ckurl = req_url.replace("http://mt.local.saramin.co.kr", "")
        ckurl = req_url.replace("https://mt.local.saramin.co.kr", "")
        if ckurl.find('?') > -1 :
            ckurl = ckurl.split('?')[0]
        elif len(ckurl.split('/')) > 4 :
            piece = ckurl.split('/')
            ckurl = piece[0]+'/'+piece[1]+'/'+piece[2]+'/'+piece[3]+'/'+piece[4]

                # 중복 확인.
        if ckurl in self.tempUrls:
            print ">>>>>>>>>>>>>>>[DropItem]:" + ckurl
            raise #DropItem("Duplicate url found: %s" % ckurl)
        else :
            req_url = ckurl
            self.tempUrls.add(req_url)

            if len(layout) > 0 :
                layout = layout[-1]
            else :
                layout = ",".join(layout)

            if len(sidebar) > 0 :
                sidebar = sidebar[-1]
            else :
                sidebar = ",".join(sidebar)

            item = SaraminWebItem()
            item["site"] = site
            item["rank"] = rank
            item["title"] = ",".join(title)
            item["req_url"] = req_url
            item["res_url"] = res_url
            item["doctype"] = doctype
            item["css"] = ",".join(css)
            item["js"] = ",".join(js)
            item["layout"] = layout
            item["sidebar"] = sidebar
            item["emulate"] = ",".join(emulate)
            item["embed_style_cnt"] = embed_style_cnt
            item["embed_script_cnt"] = embed_script_cnt

            # print(item);
            yield item

## pipeline.py #####################################

import sys
import MySQLdb
import hashlib
from scrapy.exceptions import DropItem
from scrapy.http import Request

 # local
db_host = 'localhost'
db_user = 'root'
db_pass= ''
db_name = 'saramin_db_test'

 # dev2
db_host = '182.162.86.172'
db_user = 'sri_dev'
db_pass= 'tkfkadlsroqkfwk)^!*'
db_name = 'sri_layout_manage'

class SaveItem(object):
    def __init__(self):
        """ A """
        #self.conn = MySQLdb.connect(user='root', '', 'saramin_db_test', 'localhost', charset="utf8", use_unicode=True)
        #self.cursor = self.conn.cursor()

#    def open_spider(self, spider):
#        db = MySQLdb.connect(db_host, db_user, db_pass, db_name, use_unicode = True, charset = "utf8")
#        cursor = db.cursor()
#        cursor.execute("TRUNCATE saramin_layout_analytics")
#        db.commit()
#        cursor.close()
#        db.close()

    def process_item(self, item, spider):

        db = MySQLdb.connect(db_host, db_user, db_pass, db_name, use_unicode = True, charset = "utf8")
        cursor = db.cursor()

        query = (
            """ INSERT INTO saramin_layout_analytics
               (site_type, rank, title, request_url, response_url, doctype, css,
                js, layout, sidebar, emulate, embed_style_cnt, embed_script_cnt, reg_dt)
             VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, now()) """
        )
        bind = (
            item['site'].encode('utf-8'),
            item['rank'].encode('utf-8'),
            item['title'].encode('utf-8'),
            item['req_url'].encode('utf-8'),
            item['res_url'].encode('utf-8'),
            item['doctype'].encode('utf-8'),
            item['css'].encode('utf-8'),
            item['js'].encode('utf-8'),
            item['layout'].encode('utf-8'),
            item['sidebar'].encode('utf-8'),
            item['emulate'].encode('utf-8'),
            item['embed_style_cnt'],
            item['embed_script_cnt']
        )
        cursor.execute(query, bind)
        db.commit()

        # Fetch a single row using fetchone() method.
        #data = cursor.fetchone()

        cursor.close()
        db.close()

10. layout 탐색기

수집된 데이터를 사용하기 편리하도록 웹페이지 생성.

http://design-code.dev2.saramin.co.kr/zend/layout-inquiry/