[ 파이썬 3.6 ]에서도 한방에 scrapy가 설치 가능하다.

  1. scrapy 설치 ( 각종 lxml 등 패키지와 함께 설치된다)
    [ conda install -c conda-forge scrapy ]
    https://doc.scrapy.org/en/latest/intro/install.html#installing-scrapy

  2. selenium 설치
    [ pip install -U selenium ]
    https://pypi.python.org/pypi/selenium




동적 웹페이지 웹 스크래핑 하기(scrapy+selenium)

동적 웹페이지인  premierleague.com를, scrapy + selenium을 활용하여,
2015/16 최종 순위표 url  : https://www.premierleague.com/tables?co=1&se=42&mw=-1&ha=-1 를 웹크롤링 해보자.


준비하기

  • 먼저, Prompt(1) scrapy shell용Prompt(2) jupyter notebook을 켜놓는다.

  • 접속하려는 url을 띄우고, 어떤 정보를 가져올지 생각해놓는다.
    순위/이름/경기수/승/무승부/패 / 골 득점/실점/골득실차/점수를 가져올 것이다.
    image


시작하기

  • 크롤링폴더인 web_da로 가서 scrapy startproject 프로젝트명 을 통해 크롤러의 새 프로젝트를 생성한다.
    scrapy startproject epl_crawler
    생성된 크롤러프로젝트는 jupyter로 확인한다

  • items.py를 열어서 item클래스를 정의해주자. item클래스에다가 각종 필드를 입력한다.
    image

  • spider폴더에 새로운 text-> epl_spider.py를 생성하여, Spider를 정의해주자.
    scrapy 및 epl_crawler폴더의 items.py의 EPLItem 클래스를  import해서 쓴다.
    (1) EPLSpider를 정의한다. 이 때,  크롤러이름 / 전체도메인 / 시작url을 정의한다.
    image
    (2) parse()함수를 정의한다. 이 때, parse()함수를 정의하기 위해서는 [크롬개발자도구 + shell]을 먼저 이용한다.

  • 해당 웹사이트를 shell에서 실행시켜놓고, 크롬개발자도구로 해당 웹사이트를 살펴봐야한다.
    image
    총 20개의 리스트가 있고, 각 item(행)에 속한 요소들을 가져올 것이다.
    20개의 리스트 상의 규칙성을 찾아, 한 item(행) 전체를 가져오게 할 것이다.
    (이전시간에는 상세페이지에서, 제목, 평점, 장르 각각의 xpath를 따로 구했는데,
    여기서는 리스트의 한 item를 먼저 가져올 것이다.)

    (1) 총 20개 리스트에서 한 item(행)의 요소 아무거나 [ctrl+shft+c]로 클릭한 뒤 타고 올라가보자.
        <tr> 요소들 중에서, class이름이 expandable인 것만 제외한 것을 구하면 된다.
       먼저, 첫번째 줄의 <tr>요소의 xpath를 가져와보자. 역시나 빈칸으로 셀렉터가 안가져와진다.
    image

    (2) 뒤에서부터 계층별로 살펴보면 된다.
    이 때, 맨 마지막 <tr>계층에서는 실제값을 가진 item이 “tableDark”와 “tableMid” 2가지 값을 가진 반면에 item이 없는 <tr>계층은 “expandable”만 있었다.
    tr의 인덱싱[] 란에,  tr[ not(@class="expandable") ]을 통해, 실제 필요한 class를 가져올 수 있게 되었다.
    그리고 각 계층을 따라올라가면서 필요없는 계층은 삭제해주고, 틀린인덱싱도 순서대로 세서 바르게 인덱싱시켜줘야한다.
    response.xpath('//*[@id="mainContent"]/div[2]/div[3]/div/div/div/table/tbody/tr[not(@class="expandable")]  ')
    image
    (3) 파이썬리스트 형태로 얻어진 item의 셀렉터리스트들을 rows라는 변수에 저장한 뒤,
       rows[0] 인덱싱으로 하나의 row만 따본다.
    rows = response.xpath('//*[@id="mainContent"]/div[2]/div[3]/div/div/div/table/tbody/tr[not(@class="expandable")]')
    row = rows[0]
    이 때, row에는 하나의 <tr>요소의 xpath셀렉터가 담겨있고, 
    .xpath( ‘./ ~~~ ’)로 추가할 <tr>요소 밑의 몇번째 <td>요소를 가져오냐에 따라서, 각 데이터값이 정해진다.
    <전체 item의 셀렉터리스트이므로, parse함수의 for문의 인자로 들어갈 것이다.>
    image
  • 첫번 째 [순위]의 경우, tr요소 밑-> 2번째 td 밑 –> span요소의 text로서 숨겨져 있었다.
    row(첫번째 item의 <tr>까지의 셀렉터)에 해당자식들까지 xpath를 추가한 다음, [0].extract()로 셀렉터->만 뽑아내보자.
    row.xpath('./td[2]/span/text()')[0].extract()
    image

  • 이제 [경기수]를 찍어내보자.  전체리스트[rows]의 첫번째[0] item의 <tr>요소 셀렉터인 row변수에,
    4번째 td요소까지 간 다음, text를 뽑으면 될 것이다.
    row.xpath('./td[4]/text()')[0].extract()
    1위팀의 경기 수 이므로, 38경기가 나와야하는데, shell에서는 29경기가 찍힌다.
    이것이 바로, 기본 정적페이지를 띄워놓고 –> 동적으로 웹페이지를 수정하는 [동적웹페이지]의 특징이기 때문이다.
    찍힌 29경기수는, 아직 refresh되기전의 1위팀의 경기수이고,
    다 로딩이 되어 동적으로 수정된 것의 1위팀 경기수는 38이다.
    image
    <이러한 동적웹페이지 크롤링이 불가능한 것은 scrapy만 사용한 한계이다.>


Selenium을 이용하여,   scrapy의 동적 웹페이지 스크래핑의 한계 극복하기

현재 띄워놓은 scrapy shell을 [ctrl+d]로 종료하고, 다시 shell을 띄우자.

shell에서 사용했던, response안에는 [정적웹페이지]의 응답정보를 담고 있다. 그러므로, scrapy의 response 대신
selenium에서 제공하는 webdriver모듈을 import한 다음,
[ webdriver모듈 + 크롬드라이버의 절대경로(tab으로 자동완성) ]를 통해, 크롬브라우저를 생성하여 browser 변수에 할당한다.
새로운 크롬브라우저가 실행이 된다.
from selenium import webdriver
browser = webdriver.Chrome("C:/Users/cho/chromedriver.exe")
image

  • response.url 을 통해, shell에 띄운 크롤링할 웹페이지의 url만 얻는다(response의 정보 이용x  .xpath (x))
    image

  • 이제 browser에 .get(  url  )함수를 이용해서, 새롭게 띄워진 브라우저에 크롤링할 웹페이지를 띄우도록 만든다.
    browser.get(response.url)
    image

  • 이제 scrapy의 response.text와 같이,
    현재 browser에 띄워진 웹페이지전체 html소스코드를 String형태로 담기 위해 아래코드를 실행한다.
    html = browser.find_element_by_xpath('//*').get_attribute('outerHTML')
    image
    <브라우저.get()함수 호출 후 – 충분한 시간간격(5초)가 있어야지 – 동적으로 생성된 컨텐츠가 포함된 html코드가 얻어진다>

  • selenium을 통해 얻어진 동적웹사이트의 html코드는 스트링으로 밖에 얻을 수 없다.
    그러므로, String을 –> scrapy의 Selector로 변환하는 과정이 필요하다.
    (1) scrapy의 selector.py의  Selector클래스import해야한다.
    from scrapy.selector import Selector

    (2) Selector클래스의 객체를 생성하고, 인자 text=에다가 String형 html코드 변수 를 넣어준다.
    selector = Selector(text = html)

    이 때, 소문자 selector
    띄워진 html코드를 selector 형태로서 가지고 있게 되므로,
    srcapy의 웹사이트 응답정보 변수이면서, parse()함수의 인자로 들어가는 response와 완전히 동일한 것
    이다.

    (my : response는 웹사이트에 대한 응답으로서, 전체 html코드를 셀렉터형태로 가지고 있었고,
           selenium을 통해 얻은 동적웹페이지의 전체 html코드의 문자열을 –> Selector객체에 담게 되면, response와 같은 놈이 된다!)
    image

  • 이제 browser에 띄워진 웹사이트에서, 크롬개발자도구를 이용하여, 원하는 부분의 xpath를 얻고 selector.xpath로 추가하면 된다.
    (broswer에 띄워진 것을 크롬개발자도구로 선택하여, xpath를 추출한 뒤, 셀렉터에서 얻으니까 한방에 얻어진다..
    맨끝의 td의 인덱싱으로 각각 필요한 정보를 바로 뽑아낼 수 있다.
    대신 한 item(행)만을 추출은 안된다. for문에 들어갈 전체item 리스트는 뒤에서 따라올라가는 식으로 잡아야할듯)

    1위팀의 이름xpath셀렉터 )
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[3]/a/span[3]')
    1위팀의 이름text()추출)
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[3]/a/span[3]/text()')[0].extract()
    1위팀의 경기수text()바로 추출)
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[4]/text()')[0].extract()
    image


여기까지 정리하자면,
원래 spider.py 속의 parse(self, response)함수에서, response변수에 얻어진 <정적컨텐츠가 포함된 웹페이지 셀렉터형태의 html>는 사용하지 않을 것이고, selenium webdriver를 사용하여 해당 url의 웹페이지를 직접 불어들인 뒤, <동적컨텐츠까지 포함되어 로딩된 html코드>를 사용하여 selector객체를 생성하고, 이를 이용해서 웹 스크래핑을 할 것이다.

  • 이제 scrapy shell –> selenium webdriver –> scrapy Selector 객체에 담긴 데이터를 이용해서
    ( 기존 scrapy shell –> 직접 웹페이지띄움 –> scrapy response )
    epl_spider.py의 EPLSpider클래스의 parse()함수를 채워야한다.
    그전에, 먼저 [epl_spider.py]에서 selenium의 webdriver를 사용할 수 있도록 각종 설정을 해줘야한다. 
    (우리가 shell에서 selenium webdriver를 import하고 사용한 것은, xpath를 알아내기 위함이다.
    이 알아낸 것들을 이용해서 ---> 결국에는 [ epl_spider.py ]에서 다 작동시켜야한다.)

    (1)[ epl_spider.py ]에  selenium의 webdriver모듈을 import해준다.
        from selenium import webdriver
    (2) EPLSpider클래스의 생성자 __init__(self): 함수오버라이딩 해야한다.
    (기존 pipelines클래스만 생성자 오버라이딩해서, csv쓰고, 첫행 기입)
    - 먼저 생략되있던 scrapy의 Spider클래스를 초기화해준다.(오버라이딩 하기전에, 기존 내부적으로 자동호출되는 초기화함수코드)
        scrapy.Spider.__init__(self)
    - 이제 spider클래스가 호출될 때 작동할 초기화 부분에,  broswer변수 생성 및 크롬드라이브 호출이 되도록 한다.
        self.browser = webdriver.Chrome("C:\Users\cho\chromedrive")
    image

  • 이제 parse(self, response) : 를 채워보자.
    (1) self.browser변수에 동적 웹페이지를 불러올 것인데, get함수의 인자에는
          기존 scrapy의 response 이용해서, url주소만! 을 얻어온다.
    *** time.sleep(5)를 통해 웹페이지가 동적컨텐츠를 모두 로딩할때까지의 시간을 줘야한다!
        < 크롬broswer를 통해 get으로 웹사이트 다시한번 띄우기 < ---충분한시간필요(5초)-----> html코드를 담기>
    *** import time
        from scrapy.selector import Selector
        < Selector 클래스 및, time모듈을  spider에도 import해줄 것! 강의에서는 안해줬는데, 에러남!>
    (2) 이전에 shell에서 했던 것처럼,  browser변수를 이용해서, 전체 html코드를 문자열으로 불러와 html변수에 담고
         Selector객체를 생성해 html문자열 변수를 인자로 넣어서, selector형태로 담아준다.
    (3) selector에 .xpath(‘ 전체 item의 리스트를 담는 xpath’)를 넣어, for문에서 각 item으로 꺼내쓰도록 한다.
      (이 때, 각 item(행)이 webdriver에서 안찍히더라도, 타고 올라가서 확인할 수 있고, 바로 xpath가 뽑힌다
       주의 할점은, 각 item(행)을 의미하는 <tr>가  xpath에서 tr[1]로부터 차근차근 시작하는 것이 아니라,
       <tr>의 속성중 클래스 이름이 “expandable”이 아닌 것을 리스트로 가져와야한다는 것을 알아챈 뒤,
       /tr[  not (@class= “expandable” ) ] 으로 인덱싱할줄 알아야하는 것
    이다.)
    (4) for문에서는 전체item리스트를 담은 변수(rows)를 인자로 넣고, row로 하나씩 꺼내본다.
        for문안에서, [items.py]에 정의해둔 EPLItem객체를 생성하고,
        객체의 각 필드에 꺼낸 row에 .xpath경로를 추가해서 세세한 text()를 [0].extract()추출한 것을 담아준다.
    (5) spider의 parse함수에서 item객체에 데이터를 넣은 뒤, yield로 반환했다.
    image

다시한번 정리하자면)
EPLSpider클래스에서 다시한번 생성자를 오버라이딩해서, 기본코드 + 크롬브라우저를 띄우도록 했고,
parse함수에서 크롬브라우저를 이용해서  다시한번 해당 웹페이지url을 불러온다. (scrapy에서 기본적으로 shell에서 한번 불러오므로)

  • 이제 크롤링을 하기 위해서, shell은 종료하고, 크롤링 프로젝트 1번째 폴더로 이동해야한다.
    cd epl_crawler

  • 크롤링을 시작한다. –o를 옵션을 통해서 pl.csv로 저장하도록 해보자.
    scrapy crawl PremierLeague -o pl.csv

<시작시 각종에러>
1. chrome driver의 절대경로는 폴더할때 쓰이는 \(W) 가 아니라 반대방향 / (쉬프트옆 문자)로 표시할 것.
2. items.py 의 scrapy.Item를 item 소문자로 잘못적었음.
3. shell에서는 해줬었던 Selector 클래스 및, time모듈 을  spider에도 import해줄 것
import time
from scrapy.selector import Selector

4. 전체리스트의 경로 수정
5. parse함수대로 데이터가 나오지 않는다. 수정하고 싶다면 pipeline을 이용할 것!

image

image

+ Recent posts