Scrapy 웹 크롤링 04 - 실습

Scrapy Practice

Daum 크롤링하기

  • 다음 디지털 뉴스 페이지에서 현재 URL, 기사 타이틀에 걸려있는 href URL, 기사 페이지로 이동한 후 기사 제목, 기사 내용을 크롤링하는 것을 목표로 크롤러를 만들것

items.py

  • 먼저, 크롤링 대상을 items를 활용하기 위해 items.py에 Field를 생성한다. 위에서 언급했던 사항뿐만 아니라 SQLite에 저장할때, 수집된 시간을 로그로 남겨 놓기 위한 Field도 생성시켜 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import scrapy

class PracticeItem(scrapy.Item):
# name = scrapy.Field()

# 기사 제목
headline = scrapy.Field()

# 기사 본문
contents = scrapy.Field()

# 요청 리스트 페이지
parent_link = scrapy.Field()

# 기사 페이지
article_link = scrapy.Field()

# 수집된 시간
crawled_time = scrapy.Field()

setting.py

Middlware

  • 개인이 만든 기능을 추가해서 사용가능하게 하는것, 즉, Pipeline은 Item이 Export되어 파일에 저장되기 직전에 작업을 수행한다면, Middleware는 요청하기 직전, 응답 후에, 어떤함수가 실행전에, 이런 중간에서 작업을 수행하는 것이라고 비교해볼 수 있다.

  • 다른 크롤링할때와 마찬가지로 동일한 User-agent를 가지고 한다면, 서버에 지속적인 부하를 주게되어서 벤을 당한다거나 할 수 있으므로 Fake User-agent를 활용하여 크롤링을 할 것이다. Middlware의 장점은 커스터마이징할 수 있으므로 다른 사람들이 이미 개발해 놓은 것들이 많다. 특히 Github에서 검색한다면 star가 많은 것을 사용하는 것이 좋다는 것은 누구나 알고 있는 사실!!

  • 필자가 사용한 Middleware의 사이트를 보면 scrapy 1.0이상과 1.0미만 버전에 따라 사용방법이 다른 것을 확인할 수 있다. 필자의 경우 1.8이므로 1.0이상의 방법을 사용할 것이다.

1
pip install scrapy-fake-useragent
  • 아래 주석 처리된 USER_AGENT 변수는 실제 자신의 user-agent를 사용해야하며, 주석처리한 이유는 추후에는 서버에 과부하주어 벤당하는 것을 방지하기 위해 fake-agent를 사용할 것이기 때문이다.
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
BOT_NAME = 'practice'

SPIDER_MODULES = ['practice.spiders']
NEWSPIDER_MODULE = 'practice.spiders'

# User-agent 설정(개발자도구에서 Network창에서 찾아서 자신의 정보를 복사
#USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 다운로드 간격
DOWNLOAD_DELAY = 2

# 쿠키사용
COOKIES_ENABLED = True

# Referer 삽입
# daum은 보안이 엄격하기에 referer속성을 주어야 한다.
DEFAULT_REQUEST_HEADERS = {'Referer' : 'https://news.daum.net/breakingnews/digital?page=2'}

# 재시도 횟수
RETRY_ENABLED = True
RETRY_TIME = 2


# 한글 쓰기(출력 인코딩)
FEED_EXPORT_ENCODING = 'utf-8'

# User-agent 미들웨어 사용
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware': 400,
'scrapy_fake_useragent.middleware.RetryUserAgentMiddleware': 401,
}


# 파이프 라인 활성화
# 숫자가 작을수록 우선순위 상위
ITEM_PIPELINES = {
'practice.pipelines.PracticePipeline': 300,
}


# 캐시 사용
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

pipelines.py

  • 크롤링으로 수집된 시간의 로그를 남기기 위해 datetime 라이브러리를, DB에 저장하기 위해 sqlite3, 마지막으로 예외적인 처리를 위해 DropItem 라이브러리를 사용할 것이다.
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
import datetime
import sqlite3
from scrapy.exceptions import DropItem

class PracticePipeline(object):
# 초기화 메소드
def __init__(self):
# DB 설정(자동 커밋)
# isolation_level=None => Auto Commit
self.conn = sqlite3.connect('저장할 위치에 대한 path/저장할 파일명.db', isolation_level=None)

# DB 연결
self.c = self.conn.cursor()

# 최초 1회 실행
def open_spider(self, spider):
spider.logger.info('NewsSpider Pipeline Started.')
self.c.execute("CREATE TABLE IF NOT EXISTS NEWS_DATA(id INTEGER PRIMARY KEY AUTOINCREMENT, headline text, contents text, parent_link text, article_link text, crawled_time text)"

# Item 건수 별 실행
def process_item(self, item, spider):
if not item.get('contents') is None:
# 삽입 시간
crawled_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

# 크롤링 시간 필드 추가
item['crawled_time'] = crawled_time

# 데이터 -> DB 삽입
# tuple(item[k] for k in item.keys()) 로 대신해도 된다.
self.c.execute('INSERT INTO NEWS_DATA(headline, contents, parent_link, article_link, crawled_time) VALUES(?, ?, ?, ?, ?);', (item.get('headline'), item.get('contents'), item.get('parent_link'), item.get('article_link'), item.get('crawled_time'))) # tuple(item[k] for k in item.keys())

# 로그
spider.logger.info('Item to DB inserted.')

# 결과 리턴
return item
else:
raise DropItem('Dropped Item. Because This Contents is Empty.')

# 마지막 1회 실행
def close_spider(self, spider):
spider.logger.info('NewsSpider Pipeline Stopped.')

# commit(auto commit으로 설정했지만 혹시 모르니)
self.conn.commit()

# 연결 해제
self.conn.close()

Spider.py

  • 먼저 도메인과 시작하는 URL을 정하고나서 페이지의 규칙을 찾아본다. 규칙을 정해주면 LinkExtractor로 반복되는 URL을 보내줄 수 있다. 단, 1자리수 이외의 2자리수부터의 반복은 Rule함수에 follow=True로 주어야한다. 또한 변수명 rules로 Rule객체를 받아야 사용가능함을 기억하자. parent page에서 해당 기사들에 대한 url을 parse_child 함수로 넘겨주는데, 이때 그냥 넘겨주지 않고 parent page에서 얻은 정보 또한 meta parameter를 통해 같이 넘겨 줄수 있다.
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
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import sys
sys.path.insert(0, 'items.py가 있는 절대 path')
from items import PracticeItem


class NewcralSpider(CrawlSpider):
name = 'newcral'
allowed_domains = ['news.daum.net']
start_urls = ['https://news.daum.net/breakingnews/digital']

# 링크 크롤링 규칙(정규표현식 사용 추천)
# page=\d$ : 1자리 수
# page=\d+ : 연속, follow=True
rules = [
Rule(LinkExtractor(allow=r'/breakingnews/digital\?page=\d+'), callback='parse_parent', follow=True),
]

def parse_parent(self, response):
# 부모 URL 로깅
self.logger.info('Parent Response URL : %s' % response.url)
for url in response.css('ul.list_news2.list_allnews > li > div'):
# URL 신문 기사 URL
article_link = url.css('strong > a::attr(href)').extract_first().strip()
yield scrapy.Request(article_link, self.parse_child, meta={'parent_url': response.url})

def parse_child(self, response):
# 부모, 자식 수신 정보 로깅
self.logger.info('----------------------------------------')
self.logger.info('Response From Parent URL : %s' % response.meta['parent_url'])
self.logger.info('Child Response URL : %s' % response.url)
self.logger.info('Child Response Status ; %s' % reponse.status)
self.logger.info('----------------------------------------')

# 요청 리스트 페이지
parent_link = response.meta['parent_url']

# 기사 페이지
article_link = response.url

# 헤드라인
headline = response.css('h3.tit_view::text').extract_first().strip()

# 본문
c_list = response.css('div.article_view > section > p::text').extract()
contents = ''.join(c_list).strip()

yield PracticeItem(headline=headline, contents=contents, article_link=article_link, parent_link=parent_link)

도움이 되는 학습

  • 비동기(asyncio), 병렬프로그래밍, 스레드, 멀티 프로세싱등 routine 개념을 학습해야 네트워크상의 블록 또는 논블럭 io로 인해 지연시간이 발생되는데, 제어권을 넘겨주면서 좀 더 성능을 올릴 수 있다.

  • scrapy twisted는 예를 들어, 위의 실습에서와 같이 데이터를 크롤링한 후 pipeline을 통해 DB에 insert 할 때 이 작업이 다 끝나지 않으면, 다음 데이터에 대한 작업으로 넘어 갈 수 없어 시간적으로 성능이 떨어질수 있는데, 이런 경우 비동기식으로 처리를 해줌으로써 성능을 올릴수 있게끔 하는 framework이다.