0%

들어가기

windows10에서 cmd에서 mvn -v를 입력하면 maven의 버전이 나와야 하지만, 설치가 안된 상태에서는 아래처럼 나오게 된다.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled.png

Maven 압축파일 다운받기

아래 사이트로 이동하자.

https://maven.apache.org/download.cgi#

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%201.png

접속한 페이지에서 Download에 들어가서 아래로 좀 내려보면 Files를 확인 할 수 있다.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%202.png

현재 2020-10-17 기준으로 3.6.3 버전을 제공하고 있다.

해당 압축 파일을 다운로드 하자.

아래와 같이 zip 파일이 다운로드가 될 것이다. 원하는 경로에 압축을 풀어주면 된다.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%203.png

내 경우 C드라이브 아래 maven이라는 폴더를 생성해서 아래처럼 압축을 풀어 놓았다.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%204.png

Windows10에서 Maven 환경변수 설정

windows10에서 maven의 환경변수를 설정해주자.

제어판 → 시스템 → 환경변수로 이동하자.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%205.png

아래처럼 입력해주자.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%206.png

그 다음으로 시스템 변수에서 Path를 찾아 Maven의 bin 경로를 추가해주자.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%207.png

새로만들기를 눌러서 %MAVEN_HOME%\bin 을 입력해주자.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%208.png

Maven의 설정이 완료 되었다.

새로운 cmd 창을 열어서 mvn -v 명령어를 입력해 보면, maven이 정상동작 하는 것을 확인 할 수 있다.

windows10%E1%84%8B%E1%85%A6%20maven%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8E%E1%85%B5%20%E1%84%86%E1%85%B5%E1%86%BE%20%E1%84%89%E1%85%A5%E1%86%AF%E1%84%8C%E1%85%A5%E1%86%BC%20d866af6b66984e5b9de1e491282be900/Untitled%209.png

들어가기

ubuntu 18.04가 설치된 virtual box에 elasticsearch7을 설치해보자.

아직 공부가 부족해서, 자세한 내용을 모른체 연습용 elasticsearch를 설치하는 내용이다.

클러스터, 샤드등 설정에 대한 내용은 포함하고 있지 않다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled.png

실습

jdk 설치

먼저 elasticsearch를 설치하기 위해서는 jdk 설치를 해야 한다.

1
javac -version

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%201.png

먼저 jdk가 설치 되어있는지 확인해보자. 난 위처럼 설치가 안되어 있다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%202.png

1
sudo apt install openjdk-11-jdk

난 위처럼 open jdk 11로 설치를 진행했다.

아래처럼 jdk 가 설치 되었다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%203.png

Elasticsearch를 설치하자

https://iruwl.github.io/catetan/install-elasticsearch-7.0.0-on-ubuntu-18.04/

위 자료를 참고하여 실습을 진행했다.

Elasticsearch리파지토리 GPG 공개키를 import 한다.

1
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

아래처럼 OK 가 나온다면 정상적으로 키를 import 한 것이다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%204.png

Elasticsearch 라파지토리를 등록한다. 아래처럼 현재 최신 버전인 7 버전 리파지토리를 등록했다.

1
sudo sh -c 'echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" > /etc/apt/sources.list.d/elastic-7.x.list'

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%205.png

다음 명령어로 elasticsearch를 설치하자.

1
2
sudo apt update
sudo apt install elasticsearch

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%206.png

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%207.png

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%208.png

위 처럼 설치가 정상적으로 진행 되었다.

엘라스틱서치를 동작시키기전에 간단한 설정파일을 집고 넘어가자.

  • /etc/elasticsearch/elasticsearch.yml
  • /etc/elasticsearch/jvm.options

elasticsearch.yml 파일에는 엘라스틱 서치의 전반적인 환경설정이 들어간다.

일단 아래처럼 클러스트명을 지정한다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%209.png

그리고 network 영역에서 network.host에서 해당 host의 ip를 등록한다.

network.host를 지정하지 않을경우, elasticsearch가 설치된 host pc에서 localhost로 접속이 가능하지만, 외부에서 elasticsearch에 접속 할 수 없다.

아래처럼 network.host에 명시적으로 host pc의 ip를 지정할경우 해당 ip를 사용해서 외부에서 elasticsearch에 접속 할 수 있다.

http.port 에서 포트를 지정한다.

discovery.seed_hosts를 아래 이미지 처럼 지정한다.(클러스트 노드 관련 설정인거 같다.)

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%2010.png

jvm.options 파일은 elasticsearch의 jvm heap 메모리 설정을 할 수 있다.

아래 이미지를 보면 기본으로 jvm heap이 1g 로 설정되어 있는 것을 확인 할 수 있다.

자신의 환경에 맞게 설정해주자.(초기 heap과 max heap의 사이즈를 동일하게 설정하는 것이 좋다고 한다.)

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%2011.png

기초적인 elasticsearch설정이 끝났다면, 서비스를 등록하고 실행해보자.

1
2
sudo systemctl enable elasticsearch.service
sudo systemctl start elasticsearch.service
1
sudo systemctl status elasticsearch.service

아래처럼 서비스가 정상 동작하는 것을 확인 할 수 있다.

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%2012.png

ubuntu의 포트 여는 것은 생략한다.

아래 명령어로 elasticsearch에 GET 요청을 날려보자.

1
curl -X GET '10.0.2.15:9200'

Ubuntu%2018%2004%20elasticsearch7%20install%20a053c074d29c440f9377bdbc67dbd811/Untitled%2013.png

정상적으로 elasticsearch가 응답하는 것을 확인 할 수 있다.

참고자료

https://iruwl.github.io/catetan/install-elasticsearch-7.0.0-on-ubuntu-18.04/

[https://kifarunix.com/install-elasticsearch-7-x-on-ubuntu-18-04-debian-9-8/](

들어가기

scrapy만으로는 크롤링 결과물에 동적인 이벤트를 발생해서 그 결과물을 크롤링 할 수 없다.

따라서 scrapy에서 이런 동적 크롤링을 지원하는 selenium을 연동해서 사용할 필요가 있다.

이 글은 scrapy로 간단한 프로젝트를 생성하고 seleinum을 연동하여 크롤링하는 예제를 만드는 과정을 기록한다.

scrapy의 모든 기능을 담고 있지 않다.(item, pipeline)

실습

실습환경

  • windows 10(64)
  • anaconda
  • python 3.7
  • scrapy
  • selenium
  • chrome dirver
  • pycham

scrapy 프로젝트 생성

anaconda 프롬프트에서 다음 명령으로 예제 scrapy 프로젝트를 생성하자.

1
scrapy startproject scrapy_with_selenium

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled.png

별 오류 없이 명령어가 실행되면, 명령어를 실행한 경로에서 해당 프로젝트라 폴더로 생성된다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%201.png

해당 폴더를 pycham으로 열어보자.

아래구조와 같이 프로젝트가 생성된다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%202.png

scrapy로 프로젝트를 생성했으니, 실습에 사용할 간단한 spider를 생성하자.

크롤링할 대상 페이지는 아래와 같다.

https://auto.naver.com/bike/mainList.nhn

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%203.png

네이버의 오토바이 검색 페이지를 크롤링 실습으로 사용해 보겠다.

spider 생성

1
scrapy genspider naverbike auto.naver.com/bike/mainList.nhn

위 명령어로 네이버 오토바이 페이지를 크롤링할 spider를 생성하자.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%204.png

명령어를 실행하면 아래처럼 spider 파일이 자동으로 생성된다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%205.png

여기서 기초적일 설정이 필요하다.

settings.py 파일에서 ROBOTSTXT_OBEY = False로 변경하자.

해당설정은 웹 서버가 크롤링을 거부해도 강제로 진행하는 옵션이다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%206.png

그리고 앞서 생성한 naverbike.py 라는 스파이더의코드를 아래처럼 수정해주자.

scrapy 명령으로 생성한 spider 코드의 start_urls 리스트를 보면 url 맨 뒤에 슬래시가 있는데 아래처럼 제거해주자. 아래 이미지를 처럼, xxxxxx/mainList.nhn 으로 맨 뒤에 슬래시가 없어야 정상적으로 크롤링이 될 것이다.

그리고 parse 메소드에서 reposen.text를 출력해서 크롤링이 재대로 되는지 확인해보자.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%207.png

터미널에서 아래 명령어로 해당 spider를 동작시켜 크롤링 결과가 출력되는지 확인해보자

1
scrapy crawl naverbike

아래처럼 크롤링 결과가 정상적으로 출력이 되는 것을 확인 할 수 있다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%208.png

지금 까지 한 작업은 순수하게 scrapy를 사용해서 크롤링을 해본 것이다.

이제 여기에 selenium을 연동시켜 보자.

scrapy에 seleinum연동하기

scrapy에 selenum을 연동하는 것은 생각 보다 간단하다.

scrapy 프로젝트를 생성할 때 같이 생성된 middleware 라는것에 적용하면 된다

요약

  • download middleware에 selenium설정과 selenium크롤링 코드를 작성한다.
  • spider에서 selenium을 설정한 download middleware를 사용한다고 옵션으로 지정해준다.
  • 해당 spider를 크롤링하게 되면, selenium으로 크롤링 하게 된다.

scrapy의 middleware란?

미들웨어란 보통 흐름, 처리 과정중 중간에 삽입되어 무언가를 처리하는 것을 말하는데, scrapy 의 middleware도 그런 역할이다.

앞서 생성한 scrapy 프로젝트의 디렉토리를 보면 아래처럼 middleware.py라는 파일이 자동으로 생성되어 있다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%209.png

파일 안을 보면 기본적으로 프로젝트명SpiderMiddleware라는 클래스가 있다.

그리고 좀 아래로 내려가다 보면 프로젝트명DownloadMiddleware라는 클래스가 있다.

이렇게 SpiderMiddleware, DownloadMiddleware라는 두개의 클래스가 디폴트로 생성된다.

일단 실습에 앞서 이 미들웨어들에 대해서 간단하게 알아보자.

scrapy의 미들웨어를 설명하기 전에 scrapy의 동작에 대해서 살짝 맛을 보자.

scrapy의 동작 과정설명

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2010.png

아래는 document의 원문이다. 내 짧은 영어로 발번역을 첨부한다.

  1. The Engine gets the initial Requests to crawl from the Spider.
    [우리가 spider에게 크롤링을 명령하면(ex:scpay crawl naverbike) spider는 Engine에게 request를 보낸다. ]
  2. The Engine schedules the Requests in the Scheduler and asks for the next Requests to crawl.
    [Engine은 spider로 부터 받은 최초의 request를 받아서 scheduler에 전달한다. scheduler는 일종의 크롤링 request를 저장하고 여유가 되면 동작시키는 일종의 이벤트큐와 같은 역할을 하는 것 같다. ]
  3. The Scheduler returns the next Requests to the Engine.
    [Scheduler가 다시 request를 Engine에게 전달한다.]
  4. The Engine sends the Requests to the Downloader, passing through the Downloader Middlewares (see [process_request()](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#scrapy.downloadermiddlewares.DownloaderMiddleware.process_request)).
    [Engine이 Request를 Downloader Middlewares를 거쳐, Downloader에게 전달한다. ]
  5. Once the page finishes downloading the Downloader generates a Response (with that page) and sends it to the Engine, passing through the Downloader Middlewares (see [process_response()](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#scrapy.downloadermiddlewares.DownloaderMiddleware.process_response)).
    [Downloader는 크롤링할 웹 페이지를 가져와서 그것을 Response을 생성한다. 그리고 이걸 다시 Downloader Middleware를 거쳐 Engine에게 전달한다.]
  6. The Engine receives the Response from the Downloader and sends it to the Spider for processing, passing through the Spider Middleware (see [process_spider_input()](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#scrapy.spidermiddlewares.SpiderMiddleware.process_spider_input)).
    [Engine이 Downloader로 부터 크롤링의 결과물을 담은 Response를 받는다. 그리고 이걸 다시 Spider Middleware를 거쳐서, Spider에게 보낸다.]
  7. The Spider processes the Response and returns scraped items and new Requests (to follow) to the Engine, passing through the Spider Middleware (see [process_spider_output()](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output)).
    [Spider는 engine으로 부터 받은 크롤링 결과물 Response를 처리하고, Engine에게 보낼 새로운 Request(새로운 크롤링을 위한)와 추출된 item(크롤링 결과물을 의미있는 데이터 단위로 추출)]
  8. The Engine sends processed items to Item Pipelines, then send processed Requests to the Scheduler and asks for possible next Requests to crawl.
    [Engine은 추출된 아이템(크롤링에서 추출한 데이터)를 item PipeLine에게 보낸다. Pipeline은 크롤링된 데이터를 후반 처리하는 녀석이다. 여기서 일단 크롤링 한번이 완료된 것이다.
    Engine은 spider에게 받은 Request를 다시 scheduler에제 전달하고 또 다른 크롤링의 절차가 시작된다.
    ]
  9. The process repeats (from step 1) until there are no more requests from the Scheduler.

위 scrapy의 크롤링 동작 과정 중 SpiderMiddleware와 DownloadMiddleware가 언급 되었다.

scrapy에 selenium을 연동하기 위해서는 크롤링할 웹 페이지에서 크롤링 결과를 받아올 때, 크롤링 방법에 개입을 해야 한다. 그 부분이 Download Middleware 이다.

아래 DownloadMiddleware에 selenium 을 적용해보자.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2011.png

middlewares.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from scrapy import signals
from scrapy.http import HtmlResponse
from scrapy.utils.python import to_bytes

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep

class ScrapyWithSeleniumSpiderMiddleware(object):

@classmethod
def from_crawler(cls, crawler):
print("this is ScrapyWithSeleniumSpiderMiddleware from_crawler ===================")
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s

def process_spider_input(self, response, spider):
return None

def process_spider_output(self, response, result, spider):
for i in result:
yield i

def process_spider_exception(self, response, exception, spider):
pass

def process_start_requests(self, start_requests, spider):
for r in start_requests:
yield r

def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)

class ScrapyWithSeleniumDownloaderMiddleware(object):

@classmethod
def from_crawler(cls, crawler):
print("this is ScrapyWithSeleniumSpiderMiddleware from_crawler +++++++++++++++++++++++")
middleware = cls()
crawler.signals.connect(middleware.spider_opened, signals.spider_opened)
crawler.signals.connect(middleware.spider_closed, signals.spider_closed)
return middleware

def spider_opened(self, spider):
CHROMEDRIVER_PATH = 'C:\dev_python\Webdriver\chromedriver.exe'
WINDOW_SIZE = "1920,1080"

chrome_options = Options()
# chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument(f"--window-size={WINDOW_SIZE}")

driver = webdriver.Chrome(executable_path=CHROMEDRIVER_PATH, chrome_options=chrome_options)
self.driver = driver

def spider_closed(self, spider):
self.driver.close()

def process_request(self, request, spider):
self.driver.get(request.url)

# scrapy에서 셀레니움을 연동해서 사용할경우. 셀레니움의 동적인 크롤링 코드는 여기 미들웨어에서 작성해야 할것 같다.
# headless 옵션을 끄고 아래 결과가 동작하는지 보자. 동작을 확인했다.
bikeCompanyAllBtn = self.driver.find_element_by_css_selector(
"#container > div.spot_main > div.spot_aside > div.tit > a")
bikeCompanyAllBtn.click()

body = to_bytes(text=self.driver.page_source)
sleep(5)
return HtmlResponse(url=request.url, body=body, encoding='utf-8', request=request)


def process_response(self, request, response, spider):
return response

def process_exception(self, request, exception, spider):
pass

위 코드에서 ScrapyWithSeleniumDownloaderMiddleware 클래스에 Selenium을 적용하는 코드들이 추가 되었다. ScrapyWithSeleniumSpiderMiddleware는 최초 생성된 상태로 수정하지 않았으니 신경쓰지 않아도 된다.

ScrapyWithSeleniumDownloaderMiddleware 에서 중점적으로 볼 부분은, spider_openedprocess_request 메소드이다.

  • spider_opened : Selenium의 기초 설정을 한다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2012.png

  • process_request: Downloader로 받은 크롤링의 결과물을 Selenuim으로 구체적인 크롤링 처리를 한다. 위 예제에서 특정 앵커 태그를 찾아서 click 이벤트를 발생 시켰다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2013.png

그리고 아래 부분은 selenium에서 크롤링한 결과물을 바이트로 변환해서 return 하게 된다. 이 결과물은 결과적으로 spider로 보내지게 된다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2014.png

spider 코드에서 해당 위의 download middleware를 사용하겠다고 설정을 해주자.

naverbike.py (spider)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import scrapy

class NaverbikeSpider(scrapy.Spider):
name = 'naverbike'
allowed_domains = ['auto.naver.com/bike/mainList.nhn']
start_urls = ['http://auto.naver.com/bike/mainList.nhn']
custom_settings = {
'DOWNLOADER_MIDDLEWARES': {
'scrapy_with_selenium.middlewares.ScrapyWithSeleniumDownloaderMiddleware': 100
}
}

def parse(self, response):
print(response.text)

custom_settings 부분이 DOWNLOADER_MIDDLEWARES를 어떤 것을 사용할지 지정하는 부분이다.

parse 부분에서는 간단하게 전달받은 respose의 내용물을 출력한다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2015.png

scrapy에 selenium을 연동하는 셋팅은 모두 끝이 났다.

이제 naverbike.py spider로 크롤링을 해보자.

위 download middleware에 selenium의 동작은 아래와 같다.

아래 페이지를 일단 크롤링 하고 전체 제조사 엘리멘트를 찾아서 클릭한다.

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2016.png

클릭한 다음에 나오는 페이지가 결론적으로 크롤링 되는 것이다.(아래화면)

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2017.png

scrapy프로젝트에서 터미널에서 아래 명령어를 실행하자.

1
scrapy crawl naverbike

아래처럼 selenium이 동작하면서, chrome web driver가 동작하고 크롤링이 진행되는 것을 확인 할 수 있다.(일부로 제대로 동작하는지 확인을 위해 selenium headless 옵션을 제거했다.)

Scrapy%20Selenium%20921481638b2f47b8addcc35b00f25926/Untitled%2018.png

재밌는 점은 크롤링이 종료 되면 자동으로 chrome web dirver창이 종료된다.(selenium에서는 별도의 quit 명령어를 호출하지만, scrapy에서는 그럴 필요가 없었다.)

끝!!!

마무리

scrapy를 정확히 모르는 입장에서, 단순히 scrapy에 selenium을 끼얹어 크롤링을 하는 예제를 만들어 보았다.

인터넷을 찾아보니 애시당초 scrapy와 selenium을 합쳐서 만든 프로젝트도 있는걸 보니, 그런것들을 활용해보는 것도 좋아 보인다.

위 예제 소스 github 주소

https://github.com/blog-examples/scrapy-with-selenium

참고자료

위 예제의 근간

https://heodolf.tistory.com/13

middelware 와 download middelware 의 차이점

https://stackoverflow.com/questions/17872753/what-is-the-difference-between-scrapys-spider-middleware-and-downloader-middlew

scrapy 와 selenium을 연동한 프로젝트가 존재한다.

https://github.com/clemfromspace/scrapy-selenium

scrapy 아키텍쳐 번역

http://sooyoung32.github.io/dev/2016/02/06/scrapy-architecture.html

scrapy structure

https://docs.scrapy.org/en/latest/topics/architecture.html

들어가기

저번에 anaconda에 scrapy를 설치 했었다. (windows와 호환성에 문제가 있는지 좀 고생을 했지만…

이번엔 scrapy로 간단한 프로젝트를 만들고 크롤링 실습을 해볼까 한다.

scrapy는 단순한 라이브러리가 아니라, 프레임 워크이다.

scrapy를 사용하기 위해서는 scrapy 기반 프로젝트를 생성해야 한다.

프레임워크 답게 cli 명령어로 프로젝트 생성, spider 생성, 크롤링 등을 지원한다.

실습

이 실습에서는 scrapy 프로젝트를 생성하고 간단한 spider를 생성해서 크롤링을 진행 하겠다.

실습환경

  • windows10(64)
  • anaconda(32)
  • conda 버전 4.8.3
  • scrapy 버전 1.6.0

scrapy로 프로젝트를 생성하자.

일단 conda 프롬프트를 열어서 아래 명령어로 scrapy 프로젝트를 생성한다.

1
2
scrapy startproject scrapy_example
문법: scrapy startproject 프로젝트명

주의 할점은 프로젝트 명으로 하이픈을 사용할 수 없다. (언더바는 사용이 가능)

scrapy%20952e11b67d354d7689155512461fde77/Untitled.png

위처럼 간단하게 scrapy 프로젝트를 생성 할 수 있다.

프로젝트가 생성되면 파이참으로 해당 프로젝트를 열어보자.

나는 개발도구로 파이참을 사용하겠다.

아래처럼 자동으로 프로젝트 폴더구조가 생성된 것을 확인 할 수 있다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%201.png

여기서 잠깐 파이참의 장점을 말하자면, 일반 cmd, gitbash에서는 conda 명령어가 path로 지정되지 않으면 conda 명령어를 사용 할 수 없지만, 파이참에서 인터프리터로 conda를 지정할 경우 파이참의 터미널에서 별도의 path 설정없이 conda 명령을 사용 할 수 있다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%202.png

다시 돌아와서, scrapy로 생성된 프로젝트의 폴더를 살펴보자.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%203.png

특의한 점은 프로젝트 루트 경로 아래, 프로젝트명과 동일한 폴더가 하나 더 추가로 생성되고, 그 아래 아래 디렉토리에 scrapy 프레임워크 디렉토리가 자동으로 생성된다.

  • spiders 폴더 : spider란 scrapy프레임워크에서 주체적으로 크롤링을 하는 일종의 봇과 같은 ㄴ녀석이다. 예를 들어서 내가 구글을 크롤링하고 싶으면 구글 스파이더를 생성해서 크롤링을 시키고, 또 네이버를 크롤링 하고 싶다면 네이버 스파이더를 생성해서 크롤링을 시키는 구조가 된다.
  • init.py : 엔트리 포인트
  • items.py : 크롤링한 데이터 어떻게 정형화 하고 저장할지에 대한 코드.
  • middelwares.py : node express 미들웨어처럼 크롤링 과정에서, 다양한 부가기능을 미들웨어라는 형태로 사용한다.
  • pipelines.py : 크롤링 이후 검색된 데이터를 어떻게 처리할지 코드.
  • settings.py : 프로젝트 설정 파일

Spider 를 만들자.

scrapy 에서 크롤링을 하는 실제 크롤러는 이 spider이다.

테스트로 https://auto.naver.com/bike/mainList.nhn 네이버 바이크 페이지를 크롤링 해도록 하겠다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%204.png

1
2
scrapy genspider naverbike auto.naver.com/bike/mainList.nhn
문법: scrapy genspider 스파이더이름 크롤링할url(http, https 프로토콜은 생략하자)

터미널에서 위 명령어롤 사용해서 naverbike라는 스파이더를 생성하자.

아래처럼 spiders 폴더에 naverbike.py 라는 파일이 자동으로 생겼다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%205.png

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
import scrapy

class NaverbikeSpider(scrapy.Spider):
name = 'naverbike'
allowed_domains = ['auto.naver.com/bike/mainList.nhn']
start_urls = ['http://auto.naver.com/bike/mainList.nhn/']

def parse(self, response):
print(response.text)
# pass

스파이더 소스를 보면 생성할때 입력된 url이 크롤링 대상으로 셋팅이 되어 있다.

parse 메소드가 실제 해당 스파이더가 동작하면서, 크롤링한 결과를 파싱해주는 메소드이다

위처럼 기존의 pass를 주석처리하고 print문으로 response.text를 출력하게 코드를 수정하자.

그리고 다음 명령어로 해당 스파이더를 동작 시키자.

1
2
scrapy crawl naverbike
문법: scrapy crawl 스파이이더명

내가 예상하기로는 해당 웹 페이지의 html 태그들이 주르륵 나와야 되는데 이상한, 로그 정보만 찍힌다.

내용을 보니 naver의 robot.txt에 의해서 응답을 못 받은것 같다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%206.png

scrapy crawl 명령어 리턴 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
2020-06-17 21:14:41 [scrapy.utils.log] INFO: Scrapy 1.6.0 started (bot: freerider_crawler)
2020-06-17 21:14:41 [scrapy.utils.log] INFO: Versions: lxml 4.5.0.0, libxml2 2.9.9, cssselect 1.1.0, parsel 1.5.2, w3lib 1.21.0, Twisted 20.3.0, Python 3.7.6 (default, Jan 8 2020, 16:
21:45) [MSC v.1916 32 bit (Intel)], pyOpenSSL 19.1.0 (OpenSSL 1.1.1d 10 Sep 2019), cryptography 2.8, Platform Windows-10-10.0.18362-SP0
2020-06-17 21:14:41 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'freerider_crawler', 'NEWSPIDER_MODULE': 'freerider_crawler.spiders', 'ROBOTSTXT_OBEY': True, 'SPIDER_MODUL
ES': ['freerider_crawler.spiders']}
2020-06-17 21:14:41 [scrapy.extensions.telnet] INFO: Telnet Password: 683edf7aaadefaed
2020-06-17 21:14:41 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
'scrapy.extensions.telnet.TelnetConsole',
'scrapy.extensions.logstats.LogStats']
2020-06-17 21:14:41 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
'scrapy.downloadermiddlewares.retry.RetryMiddleware',
'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
'scrapy.downloadermiddlewares.stats.DownloaderStats']
2020-06-17 21:14:41 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
'scrapy.spidermiddlewares.referer.RefererMiddleware',
'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
'scrapy.spidermiddlewares.depth.DepthMiddleware']
2020-06-17 21:14:41 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2020-06-17 21:14:41 [scrapy.core.engine] INFO: Spider opened
2020-06-17 21:14:41 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2020-06-17 21:14:41 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2020-06-17 21:14:42 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <GET https://auto.naver.com/robots.txt> from <GET http://auto.naver.com/robots.txt>
2020-06-17 21:14:42 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://auto.naver.com/robots.txt> (referer: None)
2020-06-17 21:14:42 [scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET http://auto.naver.com/bike/mainList.nhn/>
2020-06-17 21:14:42 [scrapy.core.engine] INFO: Closing spider (finished)
2020-06-17 21:14:42 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/exception_count': 1,
'downloader/exception_type_count/scrapy.exceptions.IgnoreRequest': 1,
'downloader/request_bytes': 446,
'downloader/request_count': 2,
'downloader/request_method_count/GET': 2,
'downloader/response_bytes': 500,
'downloader/response_count': 2,
'downloader/response_status_count/200': 1,
'downloader/response_status_count/301': 1,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2020, 6, 17, 12, 14, 42, 285564),
'log_count/DEBUG': 3,
'log_count/INFO': 9,
'response_received_count': 1,
'robotstxt/forbidden': 1,
'robotstxt/request_count': 1,
'robotstxt/response_count': 1,
'robotstxt/response_status_count/200': 1,
'scheduler/dequeued': 1,
'scheduler/dequeued/memory': 1,
'scheduler/enqueued': 1,
'scheduler/enqueued/memory': 1,
'start_time': datetime.datetime(2020, 6, 17, 12, 14, 41, 909219)}
2020-06-17 21:14:42 [scrapy.core.engine] INFO: Spider closed (finished)

추축하건데, 아마도 네이버에서 scrapy라는 크롤러의 크롤요청을 파악하고 거부 한듯 하다.

크롤링 하려는 아래 페이지의 robot.txt 를 확인해보자.

auto.naver.com/bike/mainList.nhn

역시 모든 경로가 Disallow 이다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%207.png

어떻게 이것을 무시하고 크롤링 할 수 있을까?

scrapy로 robot.txt를 무시하고 크롤링 하는 방법

방법1, 방법2 가 있다 원하는 방법을 골라서 설정 하면된다.

방법1. scrapy crawl 명령시 옵션 추가

1
scrapy crawl --set=ROBOTSTXT_OBEY='False' naverbike

방법2. settings.py 에서 ROBOTSTXT_OBEY 설정을 False로 변경

scrapy%20952e11b67d354d7689155512461fde77/Untitled%208.png

방법1을 사용하며 크롤링 할때마다 옵션을 넣어 주어야 하지만, 방법2를 사용하면 프로젝트 전체 설정으로 적용 되기 때문에 scrapy crawl naverbike 명령을 사용하면 된다.

나는 방법 1을 사용해서 다시 크롤링 해 보았다.

하지만 이상하게 역시 크롤링이 재대로 되지 않았다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%209.png

이 문제 때문에 한참 해맸었는데, 문제는 아래와 같다

start_urls 맨 뒤에 슬래시가 있는데, 저 슬래시가 문제였다. 슬래시를 지우고 다시 크롤링 해보자.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2010.png

아래처럼 spider가 정상적으로 크롤링을 하는 것을 확인 할 수 있다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2011.png

위처럼 html이 크롤링 되는 것을 터미널에서 확인 할 수 있다.

음 스파이더가 자동으로 생성해준 코드라서, 설마 start_url의 마지막 슬래시가 크롤링이 안되는 원인이 될 것이라고는 생각조차 하지 못해 한참을 헤맸다.

scrapy shell을 이용해서 프롬프트에서 scrapy 크롤링을 할 수도 있다.

직접 코딩을 하기전에 테스트용도로 활용하면 좋을 방법이다.

1
scrapy shell 'https://auto.naver.com/bike/mainList.nhn'

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2012.png

위처럼 크롤링이 진행되다가 아래처럼 프롬프트가 멈춘다. 해당 부분에서 크롤링한 response를 가지고 shell에서 주피터노트북 처럼 테스트 코드를 작성해서 돌려 볼 수 있다.

그리고 빨간부분을 보면 해당 scrapy shell에서 호출 가능한 명령어 들을 보여준다.

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2013.png

response 객체를 이용해서 크롤링 된 데이터에 접근 할 수 있다.

response 객체 사용법

1
2
3
response.css(css 셀렉터 양식).get()
response.css('head > title').get() # 하나만 가져오기
response.css('head > title').getall() # 일치하는 거 다 가져오기

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2014.png

1
response.css('head > title::text').get()     # 하나만 가져오기, 태그는 제외

scrapy%20952e11b67d354d7689155512461fde77/Untitled%2015.png

scrapy shell에서 위처럼 크롤링의 결과물인 response 객체를 이용해서 다양한 방법으로 크롤링 결과물을 간추리는 작업을 테스트 해 볼 수 있다.

네이버 자동차 바이크 페이지에서 전체 바이크 제조사 정보를 가져오고 싶은데, 마우스 클릭이 필요하다.

scrapy 만으로는 크롤링한 페이지에 마우스 클릭할 수 없다.

하지만 scrapy 크롤링 결과물에 셀레니움을 연동해서 처리가 가능해보인다.

https://stackoverflow.com/questions/36874494/simulating-a-javascript-button-click-with-scrapy

끝!!!

github 예제소스경로

https://github.com/blog-examples/python-scrapy-startproject/tree/master

들어가기

저번에 Selenium과 chrom web driver를 사용해서 naver bike에서 바이크 브랜드 정보를 크롤링 하는 예제를 진행 했다.

뭔가 아쉬운 점은, 크롤링을 할 때마다, web driver에 의해서 크롬 브라우저가 열린다는 것이다.

아래처럼 크롤링 스크립트를 돌리면 크롬 브라우저가 열린다.

selenium%20headless%203032c18b510e4d13bfb9c27587403f67/Untitled.png

물론 디버깅 코드를 작성하는 동안 재대로 동작하는 지 확인 하기에 좋고, 크롤링 마지막 코드에

driver.quit() 명령으로 해당 web driver 브라우저를 종료 시킬 수 있다.

하지만 내가 원하는 것은 아예 web driver 브라우저가 열리지 않고 크롤링이 되는 것을 원한다.

만약 실제 데몬서비스등으로 돌릴 때, 위 처럼 web driver 브라우저가 열릴 필요가 없기 때문이다.

아직 해보지 않아서 정확히는 모르겠지만, 만약 리눅스 서버에서 스크립트를 돌릴 경우 desktop환경이 아닌경우 오류가 나라 수도 있을 것 같다.(내 추축이다. 아닐수도 있다.)

Selenium에서는 headless 라는 기능을 통해 크롤링 할때 web driver의 브라우저가 화면에 직접 열리지 않고, 뒤에 숨어서 동작하게 할 수 있다고 한다.

한번 해보자.

Selenium headless 적용하기

일단 기존의 Selenium 예제 코드를 준비하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from selenium import webdriver
import time

chromedriver = 'C:\dev_python\Webdriver\chromedriver.exe'
driver = webdriver.Chrome(chromedriver)

driver.get('https://auto.naver.com/bike/mainList.nhn')

print("+" * 100)
print(driver.title)
print(driver.current_url)
print("바이크 브랜드 크롤링")
print("-" * 100)

# 바이크 제조사 전체 페이지 버튼 클릭
bikeCompanyAllBtn = driver.find_element_by_css_selector("#container > div.spot_main > div.spot_aside > div.tit > a")
bikeCompanyAllBtn.click()

time.sleep(3)

# 바이크 제조사 1번 페이지 진입해서 바이크 리스트 추출
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")

# 바이크 첫 페이지 크롤링
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

time.sleep(3)

# 바이크 제조사 리스트의 다음 페이지 버튼을 찾아서 클릭하자.

nextBtn = driver.find_element_by_css_selector(
"#_vendor_select_layer > div > div.maker_group > div.rolling_btn > button.next")
# 다음 바이크 제조사 페이지 버튼이 활성화 여부
isExistNextPage = nextBtn.is_enabled()

if (isExistNextPage == True):
print("다음 페이지 존재함=======================================>")
nextBtn.click()
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

# 크롤링이 끝나면 webdriver 브라우저를 종료한다.
driver.quit()

위 코드에 headless 기능을 적용해보자. 방법은 아주 간단하다.

headless를 기능을 사용하겠다고, web driver에게 알려주기만 하면 된다.

headless 기능을 활성화 하는 코드는 아래와 같다.

1
2
3
4
5
6
# webdirver옵션에서 headless기능을 사용하겠다 라는 내용
webdriver_options = webdriver.ChromeOptions()
webdriver_options .add_argument('headless')

chromedriver = 'C:/dev_python/Webdriver/chromedriver.exe'
driver = webdriver.Chrome(chromedriver, options=webdriver_options )

위 크롤링 코드에 적용 하면 아래처럼 된다.

headless가 적용된 예제코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from selenium import webdriver
import time

# webdirver옵션에서 headless기능을 사용하겠다 라는 내용
webdriver_options = webdriver.ChromeOptions()
webdriver_options .add_argument('headless')

chromedriver = 'C:\dev_python\Webdriver\chromedriver.exe'
driver = webdriver.Chrome(chromedriver, options=webdriver_options )

driver.get('https://auto.naver.com/bike/mainList.nhn')

print("+" * 100)
print(driver.title)
print(driver.current_url)
print("바이크 브랜드 크롤링")
print("-" * 100)

# 바이크 제조사 전체 페이지 버튼 클릭
bikeCompanyAllBtn = driver.find_element_by_css_selector("#container > div.spot_main > div.spot_aside > div.tit > a")
bikeCompanyAllBtn.click()

time.sleep(3)

# 바이크 제조사 1번 페이지 진입해서 바이크 리스트 추출
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")

# 바이크 첫 페이지 크롤링
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

time.sleep(3)

# 바이크 제조사 리스트의 다음 페이지 버튼을 찾아서 클릭하자.

nextBtn = driver.find_element_by_css_selector(
"#_vendor_select_layer > div > div.maker_group > div.rolling_btn > button.next")
# 다음 바이크 제조사 페이지 버튼이 활성화 여부
isExistNextPage = nextBtn.is_enabled()

if (isExistNextPage == True):
print("다음 페이지 존재함=======================================>")
nextBtn.click()
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

아래처럼 실행 하면, 이번에는 webdirver 브라우저 없이 크롤링이 진행되는 것을 확인 할 수 있다.

selenium%20headless%203032c18b510e4d13bfb9c27587403f67/Untitled%201.png

기타 webdirver 옵션

1
2
3
4
5
6
7
8
9
10
# 브라우저의 사이즈 지정(화면 사이즈에 따라서 동적으로 엘리멘트가 변하는 경우 필요할듯)
webdriver_options .add_argument('windows-size=1920x1080')
# 그래픽 카드 사용하지 않음
webdriver_options .add_argument('disable-gpu')
# http request header의 User-Agent 변조, 기본으로 크롤링 할 경우
# 이 정보는 크롬 헤드리스 웹드라이버로 넘어가므로 똑똑한 웹서버는
# 이 정보를 보고 응답을 안해줄수도 있는데 이걸 피하기 위해 변조할수있다.
webdriver_options .add_arguemnt('User-Agent: xxxxxxxxxxxxxxx')
# 사용자 언어
webdriver_options .add_arguemnt('lang=ko_KR')

마무리

seleinum과 webdriver의 조합 말고도, Phantomjs라는 것을 사용해서도 크롤링을 할 수 있고 위 처럼 headless또한 적용이 가능하다고 한다.

하지만 요즘 크롬이 워낙 대세라서, 유지보수가 될지 안될지 잘 모를 Phantomjs를 굳이 사용할 필요는 없어 보인다.

들어가기

python으로 크롤링을 할 때, 크롤링 대상인 웹 페이지에 동적인 동작을 곁들여서 크롤링을 할 수 있게 도와주는 라이브러리가 있는데, 그것이 selenium 이다.

selenium 장점

  • 웹 드라이버를 사용해서 웹 페이지를 동적크롤링 할 수 있다. 크롤링 결과의 특정 html 엘리먼트에 마우스 클릭를 발생시키거나 , input 엘리먼트에 텍스트를 채워넣기 등이 가능하다.

beautifulsoup, scrapy로는 특정 페이지에 동적인 이벤트를 가해서 크롤링을 할 수 없다.

예를 들어 10페이지 게시판을 크롤링 하려면, beautifulsoup에서는 페이지당 각각 url을 사용해서 크롤링 해야 하지만, selenium은 게시판 페이지를 크롤링 한 뒤, 다음 페이지 버튼을 찾아서 마우스 클릭하여 다음페이지로 이동해서 크롤링을 할 수 있다.

사실 selenium만으로 동적으로 크롤링을 하는 것은 아니다.

web driver라는 가상의 브라우저 프로그램(원래는 일종이 웹 테스트 도구라고 한다.)과 연동해서 위 기능을 구현 할 수 있다.

대충 아래와 비슷한 구조이다.

Selenium에 Chrom web driver를 연동해서 크롤링을 하면, Selenium는 가상 브라우저인 Chrom web driver를 통해 웹서버 응답을 받는다.

Selenium는 web driver를 제어 할 수 있으므로, 코드를 통해서 렌더링 결과물에 이벤트를 발생 시키고, 그에 대한 응답 결과를 받아 볼 수 있다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled.png

web driver종류가 여러개 있는것 같지만, 난 Chrome web driver를 사용할 예정이다.

Selenium을 windows10환경에 설치해보자.

실습환경

  • windows10(64bit)
  • anaconda(32bit)

Selenium 설치

셀레니움 설치 명령어

아래 두가지 방법 중 원하는 방법으로 설치하자.

1
2
pip install selenium     # pip 로 설치하는 방법
conda install selenium # conda로 설치하는 방법

난 conda를 사용해서 설치해보겠다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%201.png

selenium을 설치는 간단하게 진행 될 것이다.

이제 앞서 언급한 webdirver를 설치해보자.

Chrome web driver 설치

브라우저에서 아래 페이지를 열자.

1
https://sites.google.com/a/chromium.org/chromedriver

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%202.png

Current stable release링크를 클릭하자.

아래 페이지로 이동하는데, win32 버전 zip 파일을 클릭해서 다운 받자.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%203.png

압축파일을 다운로드 하고, 파일 압축을 풀어보면 아래처럼 chromedirver.exe라는 실행 파일이 보일 것이다.

크롤링을 할때 selenium에서 chromedirver.exe파일을 가져와서 사용하게 된다.

따라서 파이썬 스크립트에서 chromedirver.exe파일의 경로를 입력하게 된다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%204.png

위 파일을 스크립트에서 반복적으로 사용하므로 관리하기 쉽게, C 드라이브에 옮겨놓다.

1
C:\dev_python\Webdriver\chromedirver.exe

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%205.png

크롬 웹 드라이버 까지 셋팅을 마쳤다.

이제 간단한 예제를 이용해서 selenium으로 크롤링을 해보자.

Selenium 크롤링 실습(chromedriver사용)

일단 기본적인 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from selenium import webdriver

# selenium에서 사용할 웹 드라이버 절대 경로 정보
chromedriver = 'C:\dev_python\Webdriver\chromedriver.exe'
# selenum의 webdriver에 앞서 설치한 chromedirver를 연동한다.
driver = webdriver.Chrome(chromedriver)
# driver로 특정 페이지를 크롤링한다.
driver.get('https://auto.naver.com/bike/mainList.nhn')

print("+" * 100)
print(driver.title) # 크롤링한 페이지의 title 정보
print(driver.current_url) # 현재 크롤링된 페이지의 url
print("바이크 브랜드 크롤링")
print("-" * 100)

긱본적인 구조는 위와 같다. 파이썬 답게 아주 간결하고 이해하기 쉽다.

어떤 페이지를 크롤링 하더라도 위 코드는 반복적으로 사용하게 될 것이다.

실습으로 크롤링 할 페이지는 네이버의 바이크 페이지이다.

왼쪽 상단의 바이크 제조사 정보를 크롤링 한다고 가정해보자.

그런데 지금 화면에 보이는 것은 6개이다. 전체 제조사를 클릭해야 모든 바이크 정보를 추출 할 수 있다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%206.png

일단 전체 제조사 버튼을 클릭해보자.

아까 보다는 많은 제조사 리스트가 나오지만, 다음 페이지가 존재한다.

다음 페이지도 눌러보자.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%207.png

두번째 패이지도 둘러보자. 아래를 보니 두번째 페이지가 끝이다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%208.png

위의 페이지를 확인 했을 때, 모든 바이크 제조사 정보를 크롤링 하기 위해서는 다음과 같은 절차가 필요하다.

  1. 일단 https://auto.naver.com/bike/mainList.nhn 로 이동한다.
  2. 크롤링 결과에서 전체 제조사 엘리멘트를 찾아서 마우스 클릭하고 그 결과물을 받아온다.
  3. 바이크 제조사 정보를 크롤링 한다.
  4. 다음 페이지 버튼이 활성화 되어 있다면 클릭하고 그 결과물을 받아온다.(추가 제조사 정보 있음)
  5. 다음 페이지 버튼이 비 활성화 되어 있다면 크롤링을 중지한다.(더이상 제조사 정보 없음)

위와 같은 크롤링을 하기에는 beautifulsoup, scrapy 만으로는 무리가 있다.

selenium은 webdriver라는 일종의 테스트 가상 브라우저를 사용해서 위와 같은 일련의 프로그래밍 동작을 수행함으로, 동적으로 페이지를 크롤링 할 수 있다.

네이버 바이크에서 바이크 제조사 크롤링 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from selenium import webdriver
import time

chromedriver = 'C:\dev_python\Webdriver\chromedriver.exe'
driver = webdriver.Chrome(chromedriver)

driver.get('https://auto.naver.com/bike/mainList.nhn')

print("+" * 100)
print(driver.title)
print(driver.current_url)
print("바이크 브랜드 크롤링")
print("-" * 100)

# 바이크 제조사 전체 페이지 버튼 클릭
bikeCompanyAllBtn = driver.find_element_by_css_selector("#container > div.spot_main > div.spot_aside > div.tit > a")
bikeCompanyAllBtn.click()

time.sleep(3)

# 바이크 제조사 1번 페이지 진입해서 바이크 리스트 추출
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")

# 바이크 첫 페이지 크롤링
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

time.sleep(3)

# 바이크 제조사 리스트의 다음 페이지 버튼을 찾아서 클릭하자.
nextBtn = driver.find_element_by_css_selector(
"#_vendor_select_layer > div > div.maker_group > div.rolling_btn > button.next")
# 다음 바이크 제조사 페이지 버튼이 활성화 여부
isExistNextPage = nextBtn.is_enabled()

if (isExistNextPage == True):
print("다음 페이지 존재함=======================================>")
nextBtn.click()
allBikeCompanyElement = driver.find_elements_by_css_selector(
"#_vendor_select_layer > div > div.maker_group div.emblem_area > ul > li")
for item in allBikeCompanyElement:
bikeComName = item.find_element_by_tag_name("span").text
if (bikeComName != ''):
print("바이크 회사명:" + bikeComName)
ahref = item.find_element_by_tag_name("a").get_attribute("href")
print('네이버 자동차 바이크제조사 홈 sub url:', ahref)
imgUrl = item.find_element_by_tag_name("img").get_attribute("src")
print('바이크 회사 엠블럼:', imgUrl)

# 크롤링이 끝나면 webdriver 브라우저를 종료한다.
# driver.quit()

위 스크립트는 완벽하게 동작하는 전체 코드이다. 코드가 워낙 쉬어서 대충 보면 어떻게 동작하는지 이해가 될 것이다.

selenium api를 찾아보면 다양하게 엘리먼트를 추출하는 방법과, 추출한 엘리멘트에 어떠한 이벤트를 날릴수 있는 알 수 있다.(이글에서는 생략한다. 나도 잘 모르고, 구글링이 왕도이다)

위 코드의 실행 결과는 아래 와 같다.

아래처럼 크롤링 결과물이 출력된다.

특의 한 점은 webdriver라는 것이 일종의 웹 테스트 자동화 도구라고 말했듯이, webdriver가 제어하는 브라우저가 아래처럼 뜨고, 이벤트에 대한 동작이 해당 브라우저에 반영되어 실시간 관찰 할 수 있다. 위 코드 마지막 주석 driver.quit() 를 해제하면 크롤링이 끝난 뒤 해당 브라우저를 자동으로 종료 할 수 있다.

Selenium%20c103ada3648f4a389a9322d760ad1e9a/Untitled%209.png

마무리

간단하게 selenium, webdriver를 사용해서 동적으로 웹페이지를 크롤링 해보았다.

사실 위 실습에서 사용한 네이버 바이크 페이지를 개발자 도구로 보면, 위 처럼 버튼을 굳이 찾아서 클릭하고 크롤링 할 필요가 없다. (위 예제를 돌리고 나서 알게 되었다. 그냥 화면에 안보일 뿐 첫 페이지의 모든 제조사 정보가 있었음. 멍청한…)

위 예제를 돌려보면, webdriver에 의해 열리는 브라워창이 좀 애매하다.

만약 디버깅 용도라면, 괜찮지만 실제 크롤링 서비스를 돌릴 때 저렇게 브라우저가 열릴 필요가 없기 때문이다.

selenium에서는 headless 방식으로 브라우저가 실제 열리지 않고 크롤링이 가능하다고 한다.(phantomJS라는 비슷한 기술도 있다고 한다.)

이 것도 나중에 시간이 되면 한번 공부해 봐야 겠다.

별건 아니지만, 할 때마다 까먹고 짜증나서 정리한다.

기본적으로 windows10에 intelliJ를 설치하면 디폴트 terminal로 cmd.exe를 사용한다.

cmd가 짜증나는 부분은 아래처럼 git 명령어가 잘 안듣는다는 점이다.

또한 개인적으로 리눅스 sh에 더 익숙한 것도 cmd가 불편한 이유이다.

intelliJ%20terminal%20git%20bash%20d2b439b1765f448799653f4cbbc9de88/Untitled.png

자 이제 바꿔보자.

인텔리J에서 settings에 들어가자.(단축키 alt + ctrl + s)

왼쪽 상단에 terminal을 입력하면 쉽게 찾을 수 있다. 아래처럼 cmd.exe로 설정 되어있다.

intelliJ%20terminal%20git%20bash%20d2b439b1765f448799653f4cbbc9de88/Untitled%201.png

Shell path를 아래로 변경하자.

1
"C:\Program Files\Git\bin\sh.exe"  -login -i

만약 git을 기본경로로 설정 했다면 위 경로를 그대로 써도 될 것이다.

만약 커스텀한 경로에 설치 했다면, 위 경로를 찾아서 맞춰주면 된다.

intelliJ%20terminal%20git%20bash%20d2b439b1765f448799653f4cbbc9de88/Untitled%202.png

설정을 저장하고 인텔리J를 재시작 하자.

아래처럼 터미널이 git bash 로 변경된 것을 확인 할 수 있다.

intelliJ%20terminal%20git%20bash%20d2b439b1765f448799653f4cbbc9de88/Untitled%203.png

들어가기

springboot rest 서버에서 어떤 요청을 받으면, Shell command를 호출하는 기능을 구현해야 한다.

문제는 Shell command로 호출하는 호출하는 것이 Python 스크립트이고, 이 스크립트 동작이 몇분은 걸린다는 것이다.

대충 구조를 보면 아래와 같다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled.png

지금 고민하고 있는 문제는, spring 컨트롤러가 호출하는(서비스를 통해서) python 스크립트의 동작이 오래 걸린다는 것이다.

즉 다음과 같은 문제가 발생한다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%201.png

spring 컨트롤러가 호출 한 파이썬의 동작이 끝나지 않아 브라우저가 그 응답을 기다려야 하는 것이다.

그래서 지금 내가 원하는 컨트롤러의 동작은 다음과 같다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%202.png

구글링을 해보니 spring 컨트롤러가 호출하는 service의 메소드에 Async라는 어노테이션을 적용하면 위와 같은 동작을 구현 할 수 있다고 한다. 한번 간단하게 만들어보자.

@Async 어노테이션을 사용해서 비동기 서비스 만들기

일단 아주 간단한 springboot 웹 프로젝트를 생성했다.

spring에서 Async, 즉 비동기 기능을 사용하는 방법은 아주 간단하다.

  • @EnableAsync로 비동기 기능을 활성화
  • 비동기로 동작을 원하는 메소드(public 메소드)에 @Async 어노테이션을 붙여준다.

간단하게 위 두가지 행위로 spring에서 비동기 기능을 구현 할 수 있다.

springboot에서 Async 비동기 기능 활성화 및 thread pool 설정

AsyncConfig.java 라는 java config 파일을 추가했다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%203.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("hanumoka-async-");
executor.initialize();
return executor;
}
}
  • @EnableAsync : spring의 메소드의 비동기 기능을 활성화 해준다.
  • ThreadPoolTaskExecutor로 비동기로 호출하는 Thread 대한 설정을 한다.
    • corePoolSize: 기본적으로 실행을 대기하고 있는 Thread의 갯수
    • MaxPoolSise: 동시 동작하는, 최대 Thread 갯수
    • QueueCapacity : MaxPoolSize를 초과하는 요청이 Thread 생성 요청시 해당 내용을 Queue에 저장하게 되고, 사용할수 있는 Thread 여유 자리가 발생하면 하나씩 꺼내져서 동작하게 된다.
    • ThreadNamePrefix: spring이 생성하는 쓰레드의 접두사를 지정한다.

이제 비동기로 동작할 메소드를 갖는 Service를 생성해보자.

@Async를 적용한 service 만들기

AsyncService.java 소스코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

//비동기로 동작하는 메소드
@Async
public void onAsync() {
try {
Thread.sleep(5000);
logger.info("onAsync");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//동기로 동작하는 메소드
public void onSync() {
try {
Thread.sleep(5000);
logger.info("onSync");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

springboot%2042b5ceb5f304486093b4108772978651/Untitled%204.png

Service의 비동기로 호출할 메소드 위에 @Async 어노테이션을 달아주면 해당 메소드는 호출 시 비동기로 동작하게 된다. 따라서 해당 메소드를 호출한 Thread(예를 들어 Controller)는 논 블록킹으로 동작하게 된다.

주의 할 점은 private 메소드는 @Async 를 적용해도 비동기로 동작하지 않으며, 반드시 public 메소드에 @Async 를 적용해야 한다.

이제 위 서비스를 호출한 컨트롤러를 만들다.

컨트롤러 만들기

컨트롤러는 일반적인 컨트롤러와 다르지 않다.

AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {

Logger logger = LoggerFactory.getLogger(AsyncController.class);

@Autowired
AsyncService asyncService;

@Autowired
private AsyncService service;

@GetMapping("/async")
public String goAsync() {
service.onAsync();
String str = "Hello Spring Boot Async!!";
logger.info(str);
logger.info("==================================");
return str;
}

@GetMapping("/sync")
public String goSync() {
service.onSync();
String str = "Hello Spring Boot Sync!!";
logger.info(str);
logger.info("==================================");
return str;
}

}

비동기 요청을 컨트롤러에 보내보았다.

오른쪽 브라우저에서는 응답이 떨어졌으므로, 컨트롤러가 response 했다는 의미이다.

하지만 아직 스프링 console에서 비동기 메소드의 동작이 끝나지 않아있다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%205.png

몇초 뒤 아래처럼 비동기 서비스 동작이 끝나는 것을 확인 할 수 있다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%206.png

Q1.만약 QueueCapacity를 넘치는 비동기 요청이 있는 경우는 어떻게 될까?

아래 Async 설벙에서 QueueCapacity를 극단적으로 2로 줄여 놓았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(2);
executor.setThreadNamePrefix("hanumoka-async-");
executor.initialize();
return executor;
}
}

springboot%2042b5ceb5f304486093b4108772978651/Untitled%207.png

테스트 해 보니, 위처럼 RejectedExecutionException 가 발생한다.

예외내용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2020-07-02 12:34:15.419 ERROR 9588 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@918a620[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$483/564857467@35c2afb4] with root cause

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7df58ab rejected from java.util.concurrent.ThreadPoolExecutor@918a620[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) ~[na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) [na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) [na:1.8.0_232]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_232]
at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:341) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:290) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at com.example.demo.AsyncService$$EnhancerBySpringCGLIB$$1dc8b7ac.onAsync(<generated>) ~[classes/:na]
at com.example.demo.AsyncController.goAsync(AsyncController.java:23) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_232]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_232]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_232]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_232]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_232]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_232]

따라서 QueueCapacity를 넘치는 비동기 메소드 호출이 시도 될 때 방어하는 코드가 필요해 보인다.

예제소스 github 경로

https://github.com/blog-examples/springboot-async/tree/master

참고자료

https://heowc.dev/2018/02/10/spring-boot-async/

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/EnableAsync.html (EnableAsync docs)

https://jeong-pro.tistory.com/187 (spring async 주의사항)

http://blog.naver.com/PostView.nhn?blogId=goddes4&logNo=30125814709&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView (ThreadPoolTaskExecutor 설명)

들어가기

회사에서 springboot 백엔드 서버에서 파이썬 코드를 shell command로 호출하고 뒤 그 응답을 stdout으로 받아서 db에 저장해야 기능을 만들어야 하는 일이 생겼다.

욕심 같아선 파이썬 스크립트를 마이크로 서비스로 만드는 것이 가장 좋은 방법 처럼 보이지만, 매일 시간에 쫒기는 입장이다보니 그냥 java에서 shell command 로 파이썬을 호출하는 방향으로 진행하기로 했다.

이 글에서는 간단하게 java에서 shell command를 호출하고 호출결과(stdout)을 java에서 받아서 출력하는 예제를 정리한 글이다.

실행환경

  • 인텔리J
  • jdk 1.8
  • windows10

java 코드로 쉘 커멘드를 호출하는 방법을 알아보자.

java로 shell cmd를 호출하는 방법으로는 크게 두가지 방법이 있다고 한다.

  • Runtime 를 사용하는 방법
  • ProcessBuilder 를 사용하는 방법(이걸 더 추천하는듯 하다.)

하지만 위 두가지 방법을 구현하기 전에 java에서 쉘 커멘드를 호출하기 전에 고려해야 할 점이 있다.

JVM 실행 환경(OS) 구별하기

java에서 shell cmd를 호출할 때 os 마다 다르게 호출해 줘야 한다.

만약 windonws라면 cmd.exe 쉘을 호출해야 한다.

그리고 그외의 OS의 경우에는 sh라는 표준 쉘을 실행해야 해야 한다.

다음 코드로 JVM 실행환경 OS를 구별 할수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {
public static void main(String[] args) {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);
}
}

아래 처럼 간단한 예제 코드를 만들었다.

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled.png

windows 노트북에서 돌려보니 역시 windows라고 나온다.

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%201.png

또 한가지 고려해야 할 점이 있다.

앞서 말했듯이, 쉘 커멘드를 단순히 콜하고 끝나는 것이 아니다.

쉘 커멘드로 그 호출에 대한 응답을 stdout으로 받아와야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}

위 클래스는 별도의 스레드를 생성하는 클래스이다.

생성자로 InputStream과 Consumer를 주입받는다.

InputStream은 java에서 쉘 커멘드를 호출한 process로 부터 받아올 것이다.

shell command를 호출하는 process와 연결된 이 InputStream을 사용해서 shell command의 stdout을 받아 올 수 있다.

쓰레드의 실제 실행 로직인 run메소드를 보면 inputstream을 사용해서 한줄씩 BufferedReader로 읽어와서 consumer에 전달하는 것을 볼 수 있다.

예제코드에 아래처럼 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demo;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines()
.forEach(consumer);
}
}

public static void main(String[] args) {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);
}
}

이제 Runtime 클래스를 사용해서 java에서 Shell command 를 호출해보자.

Runtime 클래스를 사용해서 shell command 호출하기.

일단 Runtime 클래스가 뭔지 간단히 알아보자.

Java Runtime 클래스는 Java 런타임 환경과 상호 작용하는 데 사용된다. Java Runtime 클래스는 프로세스를 실행하고, GC를 호출하고, 총 메모리 및 여유 메모리를 얻는 방법을 제공한다. java.lang의 인스턴스는 하나뿐입니다.하나의 Java 응용프로그램에 대해 런타임 클래스를 사용할 수 있다.
Runtime.getRuntime() 메서드는 Runtime 클래스의 싱글톤 인스턴스를 반환한다.

뭔소린지 말 모를 땐 예제를 보자.

아래 예제는 운영체제에 따라서 현재 사용자의 디렉토리를 조회하는 쉘 커멘드를 호출하는 로직이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 간단하게 Runtime 클래스를 사용해서 현재 사용자 디렉토리 정보를 가져와 출력하는 쉘 커멘드를 호출하는 예제
*/
String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}

/**
* 쉘 프로세스를 실행하고 그 프로세스에 inputstream을 연결하고 컨슈머로 System.out::println를 넘긴다.
*/
String psRetMsg = "";
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), (item)->{
System.out.println(item);
});
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;

아래에서 빨간 줄 부분이 Runtime에게 실행을 부탁하는 쉘 커멘더 명령어이다.(String)

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%202.png

위 코드를 호출히면 Runtime 클래스는 자신이 동작하는 JVM외부 환경에 해당 빨간 명령어 실행을 요청하고, 그 명령어를 실행하는 process 인스턴스를 리턴 하게 된다.

이 프로세스에서 InputStream을 가져와서 앞서 생성한 StreamGobbler를 생성하면, StreamGobbler를 통해서 해당 process의 stdout을 가져올 수 있다.

아래는 최종 결과물이다.(한글이 깨져서 InputStreamReader의 파라미터가 추가 되었다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.demo;

import java.io.*;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
try {
new BufferedReader(new InputStreamReader(inputStream, "euc-kr")).lines()
.forEach(consumer);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws IOException, InterruptedException {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);

/**
* 간단하게 Runtime 클래스를 사용해서 현재 사용자 디렉토리 정보를 가져와 출력하는 쉘 커멘드를 호출하는 예제
*/
String homeDirectory = System.getProperty("user.home");
Process process;
if (isWindows) {
process = Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
process = Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}

/**
* 쉘 프로세스를 실행하고 그 프로세스에 inputstream을 연결하고 컨슈머로 System.out::println를 넘긴다.
*/
String psRetMsg = "";
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), (item)->{
System.out.println(item);
});
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;
}
}

실행결과

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%203.png

위 처럼 쉘커멘더가 동작하고 그 결과를 java에서 가져와서 출력하는 것을 확인 할 수 있다.

ProcessBuilder를 사용해서 shell command 호출하기

ProcessBuilder는 RunTime 보다 선호된다고 한다.

그 이유는 다음과 같다.

  • builder.directory()를 사용해서 shell command가 동작하는 working directory를 바꿀 수 있다.
  • builder.environment()를 사용해서 key-value map 형태의 사용자 정의 변수를 설정 할 수 있다.
  • inputstream, outputstream을 커스텀으로 교체할 수 있다.
  • inherit both of them to the streams of the current JVM process using builder.inheritIO() (이건 무슨말인지 모르겠다… 하위 프로세스의 IO를 현재 동작하는 JVM process와 동일하게 지정한다는 것 같은데…. 무슨 소리지?)

아래는 RunTime대신 ProcessBuilder를 사용한 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.demo;

import java.io.*;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

/**
* java에서 쉘 커멘드 호출 예제
*/
public class TestShellCmd {

private static class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;

public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
try {
new BufferedReader(new InputStreamReader(inputStream, "euc-kr")).lines()
.forEach(consumer);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws IOException, InterruptedException {
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");

System.out.println("실행환경이 윈도우인가? " + isWindows);

ProcessBuilder builder = new ProcessBuilder();
if (isWindows) {
builder.command("cmd.exe", "/c", "dir");
} else {
builder.command("sh", "-c", "ls");
}
builder.directory(new File(System.getProperty("user.home")));
Process process = builder.start();
StreamGobbler streamGobbler =
new StreamGobbler(process.getInputStream(), System.out::println);
Executors.newSingleThreadExecutor().submit(streamGobbler);
int exitCode = process.waitFor();
assert exitCode == 0;
}
}

ProcessBuilder 클래스는 command로 입력 받은 명령어를 사용해서 builter.start()를 호출해서 서브 해당 명령어를 실행하는 서브 프로세스를 생성한다.

실행결과

java%20shell%20command%20cd59c192a52849578dde8f6c36ee8eb9/Untitled%204.png

마무리

위 예제는 단순히 java에서 쉘 커멘드를 호출하는 예제이다.

java에서 shell command를 위처럼 단순히 호출하는 일은 거의 없을 것이다.

이런 다양한 요구 사항 또는 외부 전제 조건이 있을 수 있다.

  • java에서 shell command를 호출하고 그 응답으로 JSON String으로 받는다.
  • java에서 응답 받은 String을 객체화 하여 DB에 저장해야 한다.
  • java에서 호출하는 shell Command는 한번 호출하면 최소 5분 이상의 동작하고, 그 동작이 끝나야 응답을 한다.
  • 동작이 끝나지 않은 shell command가 존재하면, java에서 shell command를 호출 하면 안된다.
  • 동작이 끝나지 않은 shell command가 존재하더라도, java에서 shell command를 호출 할 수 있어야 한다.

대충 위와 같은 경우의 수를 처리 하려면, 얼핏봐도 비동기, 쓰레드 코딩이 필요해 보인다.

단순히 스크립트등을 shell command로 호출하고 결과를 받는 코딩을 하기에 너무 많은 의존성과 공수가 필요해 보인다.

이럴 바에는 차라리 shell cmd로 호출할 로직을 간단한 MSA로 구축해서 서비스로 만드는 것이 좋아 보인다.

날림으로 java를 공부해서 예제를 따라 만드는데도 이해가 잘 안가는 부분이 있다.

java를 다시 공부해야 겠다.

참고자료

https://www.baeldung.com/run-shell-command-in-java (java로 shell command 호출)

https://m.blog.naver.com/PostView.nhn?blogId=jhnyang&logNo=221407227360&proxyReferer=https:%2F%2Fwww.google.com%2F (java로 파이썬 실행)

https://d2.naver.com/helloworld/1113548 (java 에서 쉘 커멘드 호출 시 주의 사항)

https://www.javatpoint.com/java-runtime-class (java Runtime 클래스)

들어가기

토이프로젝트를 만드는 중, 웹 크롤링 기능이 필요했다.

자료를 찾아보니 python 기반의 beautifulsoup, selenium, scrapy 라는 것들이 주로 사용된 다는 것을 알게되었다.

beautifulsoup, selenium가 라이브러리 라면, scrapy 는 조금더 덩치가 큰 프레임워크라고 보면 될거 같다.

스크래피 (웹 프레임워크)

Scrapy(스크래피)(/ˈskreɪpi/ SKRAY-pee)[2] 는 Python으로 작성된 오픈소스 웹 크롤링 프레임워크이다. 웹 데이터를 수집하는 것을 목표로 설계되었다. 또한 API를 이용하여 데이터를 추출할 수 있고, 범용 웹 크롤러로 사용될 수 있다.[3] Scrapy는 웹 스크래핑 개발 및 서비스 회사 Scrapinghub Ltd. 에 의해 유지된다.
Scrapy 프로젝트는 “spiders” 를 중심으로 개발되었다. “spiders”는 여러 기능이 내장된 크롤러이다. 장고와 같은 철학인 중복배제를 따르고 있는 프레임워크이다.[4] Scrapy는 개발자들이 코드 재사용성을 높일 수 있도록 도와주어, 큰 규모의 크롤링 프로젝트 개발을 쉽게 할 수 있도록 해준다. 또한 Scrapy는 개발자들이 크롤링하려는 사이트의 동작을 테스트할 수 있도록 웹 크롤링 쉘을 제공한다.[5]
Scrapy는 Lyst,[6][7] Parse.ly,[8] Sayone Technologies[9], Sciences Po Medialab,[10]data.gov.uk’s World Government Data site.[11][1] 등등의 기업에서 사용되고 있다.

scrapy 단독으로 사용 시 일반적인 크롤링이 가능하지만, selenium의 기능이 필요하다면 연동이 가능한거 같다.

(selenium 경우 크롤링 대상결과를 브라우저로 가져와 상호작용을 통해 다이나믹하게 크롤링이 가능하다.

참고: https://stackoverflow.com/questions/6682503/click-a-button-in-scrapy

Python Scrapy 설치하기

일단 성공기와 실패기가 있는데 참고만 하길 바란다. 내가 한 작업의 시간순은 당연히 실패 → 성공이다.

요약하자면 pip로 설치를 실패했고, conda를 통해서 설치했다.

성공기

실습환경

  • windows10(64)
  • gitbash
  • anaconda 설치(32bit)
    • conda 4.8.2
    • python 3.8.0

현재 상태는 windows10 환경에서 anaconda를 설치한 상태이다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled.png

conda 명령어는 일반적인 윈도우 cmd에서 동작하지 않으며, 아래 anaconda prompt에서 실행해야 한다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%201.png

콘다 명령어로 scrapy 설치하자.

1
conda install -c scrapinghub scrapy

설치가 끝난 뒤 아래 명령어로 설치된 패키지를 찾아보면 scrapy가 보인다.

1
conda list

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%202.png

실패기(참고만 하길)

실습환경

  • windows10(64)
  • gitbash
  • anaconda 설치(64bit)
    • conda 4.8.2
    • python 3.8.0

현재 상태는 windows10 환경에서 anaconada 를 설치한 상태이다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled.png

아래 명령으로 Scrapy를 설치하자.

1
pip install scrapy

음 뭔가 안된다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%203.png

인터넷 강의에서 해보라는 대로 해보자.(windows에서 안되는 경우)

1
2
3
pip install --upgrade setuptools
pip install pypiwin32
pip install twisted[tls]

pip install –upgrade setuptools

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%204.png

pip install –upgrade setuptools 에러 발생

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%205.png

pip install twisted[tls] 역시 오류

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%206.png

음 이게 문제가 아닌가. 일단 pc 를 리부팅 해보자.

리부팅 후에도 여전히 문제가 있다. 오류나 경고 내용을 확인해보자.

1
2
3
4
5
6
7
8
9
10
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/setuptools/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/setuptools/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/setuptools/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/setuptools/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/setuptools/
Could not fetch URL https://pypi.org/simple/setuptools/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/setuptools/ (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.")) - skipping
Requirement already up-to-date: setuptools in c:\users\amagr\anaconda3\lib\site-packages (45.2.0.post20200210)
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
Could not fetch URL https://pypi.org/simple/pip/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/pip/ (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.")) - skipping

문제해결 방법

https://funfunit.tistory.com/170

pip install 아래를 추가하라고 한다. 뭔가 ssl 인증서쪽 문제 같은데… 일단 해보자

1
--trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org

다시 scrapy 설치해보기

1
2
3
4
5
pip install scrapy --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org

pip install --upgrade setuptools --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org
pip install pypiwin32 --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org
pip install twisted[tls] --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org

음 여전히 안된다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%207.png

아래 방법을 한번 써보자.

1
conda config --set ssl_verify false

역시 안됨.

홀리 씻…

git bash에서 하니 설치가 된다. 아마도 뭔가 openssl 등을 사용하는 것 같은데, windows 터미널에서는 안되는 듯하다.

아래처럼 git bash에서 설치가 잘된다. 아 윈도우 터미널 개똥같다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%208.png

결론으로

windows10 git bash에서 아래 명령으로 scrapy가 설치 되었다.

1
pip install scrapy

2.1.0 버전이 설치 되었다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%209.png

이제 설치된 scrapy를 확인해보고, scrapy를 이용해서 프로젝트를 만들어보자.

1
scrapy startproject test

gitbash , cmd 모두 오류가 발생한다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2010.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C:\Users\amagr\examples\scrapy_test>scrapy startproject test1
Traceback (most recent call last):
File "c:\users\amagr\anaconda3\lib\runpy.py", line 193, in _run_module_as_main
"__main__", mod_spec)
File "c:\users\amagr\anaconda3\lib\runpy.py", line 85, in _run_code
exec(code, run_globals)
File "C:\Users\amagr\anaconda3\Scripts\scrapy.exe\__main__.py", line 4, in <module>
File "c:\users\amagr\anaconda3\lib\site-packages\scrapy\__init__.py", line 34, in <module>
from scrapy.spiders import Spider
File "c:\users\amagr\anaconda3\lib\site-packages\scrapy\spiders\__init__.py", line 10, in <module>
from scrapy.http import Request
File "c:\users\amagr\anaconda3\lib\site-packages\scrapy\http\__init__.py", line 11, in <module>
from scrapy.http.request.form import FormRequest
File "c:\users\amagr\anaconda3\lib\site-packages\scrapy\http\request\form.py", line 10, in <module>
import lxml.html
File "c:\users\amagr\anaconda3\lib\site-packages\lxml\html\__init__.py", line 53, in <module>
from .. import etree
ImportError: DLL load failed: 지정된 모듈을 찾을 수 없습니다.

혹시나해서

scrapy가 안되면 설치하라는 패키지 3개를 설치 했다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2011.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
amagr@DESKTOP-77BUVLD MINGW64 ~/examples/scrapy_test
$ pip install --upgrade setuptools
Collecting setuptools
Downloading setuptools-47.3.0-py3-none-any.whl (583 kB)
Installing collected packages: setuptools
Attempting uninstall: setuptools
Found existing installation: setuptools 45.2.0.post20200210
Uninstalling setuptools-45.2.0.post20200210:
Successfully uninstalled setuptools-45.2.0.post20200210
Successfully installed setuptools-47.3.0

amagr@DESKTOP-77BUVLD MINGW64 ~/examples/scrapy_test
$ pip install pypiwin32
Collecting pypiwin32
Downloading pypiwin32-223-py3-none-any.whl (1.7 kB)
Requirement already satisfied: pywin32>=223 in c:\users\amagr\anaconda3\lib\site-packages (from pypiwin32) (227)
Installing collected packages: pypiwin32
Successfully installed pypiwin32-223

amagr@DESKTOP-77BUVLD MINGW64 ~/examples/scrapy_test
$ pip install twisted[tls]
Requirement already satisfied: twisted[tls] in c:\users\amagr\anaconda3\lib\site-packages (20.3.0)
Requirement already satisfied: incremental>=16.10.1 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (17.5.0)
Requirement already satisfied: zope.interface>=4.4.2 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (5.1.0)
Requirement already satisfied: attrs>=19.2.0 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (19.3.0)
Requirement already satisfied: Automat>=0.3.0 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (20.2.0)
Requirement already satisfied: hyperlink>=17.1.1 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (19.0.0)
Requirement already satisfied: PyHamcrest!=1.10.0,>=1.9.0 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (2.0.2)
Requirement already satisfied: constantly>=15.1 in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (15.1.0)
Requirement already satisfied: idna!=2.3,>=0.6; extra == "tls" in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (2.8)
Requirement already satisfied: service-identity>=18.1.0; extra == "tls" in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (18.1.0)
Requirement already satisfied: pyopenssl>=16.0.0; extra == "tls" in c:\users\amagr\anaconda3\lib\site-packages (from twisted[tls]) (19.1.0)
Requirement already satisfied: setuptools in c:\users\amagr\anaconda3\lib\site-packages (from zope.interface>=4.4.2->twisted[tls]) (47.3.0)
Requirement already satisfied: six in c:\users\amagr\anaconda3\lib\site-packages (from Automat>=0.3.0->twisted[tls]) (1.14.0)
Requirement already satisfied: pyasn1-modules in c:\users\amagr\anaconda3\lib\site-packages (from service-identity>=18.1.0; extra == "tls"->twisted[tls]) (0.2.8)
Requirement already satisfied: pyasn1 in c:\users\amagr\anaconda3\lib\site-packages (from service-identity>=18.1.0; extra == "tls"->twisted[tls]) (0.4.8)
Requirement already satisfied: cryptography in c:\users\amagr\anaconda3\lib\site-packages (from service-identity>=18.1.0; extra == "tls"->twisted[tls]) (2.8)
Requirement already satisfied: cffi!=1.11.3,>=1.8 in c:\users\amagr\anaconda3\lib\site-packages (from cryptography->service-identity>=18.1.0; extra == "tls"->twisted[tls]) (1.14.0)
Requirement already satisfied: pycparser in c:\users\amagr\anaconda3\lib\site-packages (from cffi!=1.11.3,>=1.8->cryptography->service-identity>=18.1.0; extra == "tls"->twisted[tls]) (2.19)

amagr@DESKTOP-77BUVLD MINGW64 ~/examples/scrapy_test

다시 스크래피로 프로젝트를 만들어보자.

음 역시 안된다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2012.png

conda를 업데이트 해보라는데 역시 안됨.

1
conda update -n base conda

일단 scrapy를 다시 지웠다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2013.png

https://wjdcjf0219.tistory.com/33

여길 보니 pip로 설치하지 말고 conda를 이용해서 설치하라고 한다.

1
conda install -c conda-forge scrapy

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2014.png

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2015.png

뭔가 한방에 많은 것들이 설치 되었다.

하지만 여전히 안된다?

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2016.png

원인을 모르겠다.

혹시나 하고 인터넷 강의에서 알려준대로 비주얼스튜디오커뮤니티를 설치해보자.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2017.png

어제 설치 완료함. 용량도 크고 그만큼 시간도 오래 걸림

다시 해보자.

역시 안된다.

scrapy.cmdline 을 못가져오는듯…. 뭐지

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2018.png

파이썬2 자료라 안될거 같긴한데, 한번 깔아보자.

https://www.python2.net/questions-80520.htm

1
pip install -I cryptography

역시 안깔린다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2019.png

conda를 업그레이드 해도 안된다.

1
conda update -n base conda

https://www.thetopsites.net/article/51813527.shtml

뭘 해도 안된다. anacoda를 지우고 다시 깔아보자.


기존에 64 bit를 깔아썼다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2020.png

다 지우고 리부트함….

혹시나 해서 이번에는 32 bit 아나콘다를 설치해보자.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2021.png

설치 완료후 다시 scrapy 설치해보자.

콘다 명령어로 설치하자. pip 는 뭔가 불안하다.

1
conda install -c scrapinghub scrapy

아나콘다 네비게이터를 키고 conda.exe 를 킨다.(이전에 conda 를 windows10 환경변수에 지정했는데, 뭔가 적절하지 못한 짓 같다. 이번에 anaconda 설치시 conda path 설정은 not recommended 였다.)

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2022.png

아 드디어 동작한다. 인터넷 강의에서 pip 로 설치해서 그런지 그냥 windows cmd 에서 실행되지만,

conda 로 설치한 경우 conda 전용 프롬프트에서 scrapy 명령어가 먹힌다.

windows10%20anaconda%20scrapy%20fdf71034c83b4250af25b84272075105/Untitled%2023.png

끝!!!

파이썬을 잘 몰라서 뭔가 내가 잘못해서 삽질을 한 것 같다.