Scrapy 웹 크롤링 03 - Exports, Settings, pipeline

Exports

  • 우리가 실행후 크롤링한 데이터를 저장하는 path를 실행할때마다 지정하거나 실행했는데, 일종의 template같이 미리 만들어 놓을 수 있는 기능이 Exports이다.

  • Exports 참조 사이트 : https://docs.scrapy.org/en/latest/topics/feed-exports.html

1
2
3
4
# 아래 2가지 방법은 동일한 방법이다.
scrapy runspider using_items.py -o test.json -t json

scrapy runspider using_items.py --output test.json --output-format json
  • 위에서와 같이 크롤링을 할 경우에 명령어를 통해 결과의 형식과 파일 이름을 지정해주는 것과 다르게 Settings.py에서 미리 지정하여 사용할 수 있다. 커맨드라인에서도 가능하지만, 모든 테스트를 다 거친 후 확정적으로 사용할 것이라면, settings.py에서 변수설정을 하는 것이 더 좋다.

  • 우리가 크롤링할 사이트 https://globalvoices.org/ 사이트의 기사들의 제목만을 크롤링 할 것이다.

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
# -*- coding: utf-8 -*-
import scrapy

# Scrapy 환경설정
# 중요

# 실행방법
# 1.커맨드 라인 실행 -> scrapy crawl 크롤러명 -s(--set) <NAME>=<VALUE>

class ScrapyWithSettingsSpider(scrapy.Spider):
name = 'scrapy_with_settings'
allowed_domains = ['globalvoices.org']
start_urls = ['https://globalvoices.org/']

def parse(self, response):
# 아래 3가지는 동일한 결과를 보여주는 코드

# response.css('#main > div.post-archive-container > div#post-archive div.dategroup div.post-excerpt-container > h3 > a::text').getall()

# response.xpath('//*[@id="post-archive"]//div[@class="dategroup"]//div[@class="post-summary-content"]//div[@class="post-excerpt-container"]/h3/a/text()').getall()


# xpath + css 혼합
for i, v in enumerate(response.xpath('//div[@class="post-summary-content"]').css('div.post-excerpt-container > h3 > a::text').extract(),1):
# 인덱스 번호, 헤드라인
yield dict(num=i, headline=v)

최종 spider

settings.py에서 export하는 변수 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
# 출력(Exports)설정
# 파일이름 및 경로
# 만약 다른 특정 위치를 지정하고 싶다면 가능하다.
FEED_URI = 'result.json'

# 파일 형식
FEED_FORMAT = 'json'

# 출력 인코딩
FEED_EXPORT_ENCODING = 'utf-8'

# 기본 들여쓰기
FEED_EXPORT_INDENT = 2
  • 또한, 동일한 자원을 반복해서 크롤링할 경우 서버에 과부하를 주는 것을 막기 위해 cache를 사용할 수도 있다. 이 또한, setting.py에서 변수를 설정할 수 있다.

setting.py에서 설정가능한 변수들에 대한 설명 - 01

setting.py에서 설정가능한 변수들에 대한 설명 - 02

pipeline

  • 참고
  • pipeline은 item들이 최종적으로 나오는 파일을 만들기 전에 약간의 처리를 해주는 작업이라고 생각하면된다. 물론 spider에서도 가능하지만 장기적으로 코드 관리적인 면을 봤을 때 너무 좋지 않은 방식이다. 예를 들어, 크롤러를 만들었던 사이트의 구조가 바뀌었다면 한 python script에 모든 코드를 작성한다면 변경된 사이트의 구조에 맞춰 코드를 변경하려면 코드를 해석하는데 오랜시간을 투자해야 할 것이다.

    • Item pipeline의 전형적인 예시
      • HTML data 제거
      • 정확하지 않은 데이터(또는 동일 데이터)가 수집되었다면 출력 전 pipeline단계에서 validation을 할 수 있다.
      • 중복 체크
      • 데이터베이스에 저장

scrapy item pipeline

  • pipeline 사용을 위한 새로운 크롤링 사이트 : https://www.alexa.com/topsites
  • 자신의 사이트 방문 50위 사이트를 알 수 있는 웹사이트이다.
  • 사이트 순위, 사이트 명, 하루에 방문하는 평균 시간, 하루에 방문하는 평균 페이지뷰수를 크롤링할 것이다. items를 사용할 것이며, 모든 정보를 크롤링한후 pipeline을 통해 40위권안의 순위에 해당하는 데이터만 저장하는 방식으로 코드를 작성할 것이다.

새로운 크롤링을 위한 item.py 변경

  • setting.py에서 아래 그림과 같이 pipeline을 사용하기 위해 주석을 풀어주어서 사용할 것이다. 만약 아래에서와 다르게 여러개의 pipeline을 사용한다면 숫자가 낮을 수록 우선 순위를 갖는다는 점에 유의하자.

settings.py에서 pipeline을 사용하기위한 변경사항

  • spider를 다음과 같이 구성하였다.
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
# -*- coding: utf-8 -*-
import scrapy
import sys
# items.py에 대한 path 추가
sys.path.insert(0, '../project/chat_bot_project/section01_2/section01_2')
from items import SiteRankItems

class Pipeline01Spider(scrapy.Spider):
name = 'pipeline_01'
allowed_domains = ['alexa.com/topsites']
start_urls = ['https://www.alexa.com/topsites']

def parse(self, response):
"""
:param :response
: return : SiteRankItems
"""

for p in response.css('div.listings.table > div.tr.site-listing'):
# 아이템 객체 생성
item = SiteRankItems()

# 순위
item['rank_num'] = p.xpath('./div[1]/text()').get()

# 사이트명
item['site_name'] = p.xpath('./div[2]/p/a/text()').get()

# 평균 접속 시간
item['daily_time_site'] = p.xpath('./div[3]/p/text()').get()

# 평균 본 횟수
item['daily_page_view'] = p.xpath('./div[4]/p/text()').get()


yield item
~
  • 위의 코드를 통해 크롤링한 데이터를 이제 pipeline을 통해 처리해보자. 간단히 csv파일과 엑셀파일을 만들어 볼 것이다.
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
class Section012Pipeline(object):
# 초기화 method
# init method도 class가 초기화될 때 최초로 실행되므로 open_spider와 동일하게 사용가능
def __init__(self):
# 엑셀 처리 선언
self.workbook = xlsxwriter.Workbook("../chat_bot_project/section01_2/section01_2/spiders/result_excel.xlsx")

# CSV 처리 선언 (a, w 옵션 변경)
self.file_opener = open("../chat_bot_project/section01_2/section01_2/spiders/result_csv.csv", "w")
self.csv_writer = csv.DictWriter(self.file_opener, fieldnames=['rank_num','site_name','daily_time_site','daily_page_view','is_pass'])

# 워크시트
self.worksheet = self.workbook.add_worksheet()

# 삽입 수
self.rowcount = 1

# 최초 1회 실행
def open_spider(self, spider):
spider.logger.info("TestSpider Pipelines Started.")

# 데이터를 크롤링할때 매번실행
def process_item(self, item, spider):
# 현재 item은 spider에서 item을 활용해서 작성했으므로 dictionary로 되어있다.
# rank_num이 40위 안에 있는 사이트들만 저장하기 위한 코드
if int(item.get('rank_num')) < 41 :
item['is_pass'] = True

# 엑셀 저장
# item['rank_num']처럼 접근가능하지만 데이터가 없다면 에러가 발생하므로 아래에서 처럼 get method를 사용하는 것이 좋다.
self.worksheet.write('A%s' % self.rowcount, item.get('rank_num' ))
self.worksheet.write('B%s' % self.rowcount, item.get('site_name' ))
self.worksheet.write('C%s' % self.rowcount, item.get('daily_tiem_site' ))
self.worksheet.write('D%s' % self.rowcount, item.get('daily_page_view' ))
self.worksheet.write('E%s' % self.rowcount, item.get('is_pass' ))
self.rowcount += 1

# csv 저장
self.csv_writer.writerow(item)

return item
else:
#
raise DropItem(f'Dropped Item. Because This Site Rank is {item.get("rank_num")}')


# 마지막 1회 실행
def close_spider(self, spider):

# 엑셀 파일 닫기
self.workbook.close()

# CSV 파일 닫기
self.file_opener.close()

spider.logger.info("TestSpider Pipelines Finished")

결과파일