본문 바로가기
Python

[Python] lxml, request 조합으로 뉴스기사 크롤링하기 -2

by 슈퍼닷 2020. 1. 5.
반응형

코드 작성환경은 구름ide 우분투 18.0 버전에서 작성했고, python3 입니다.

2020/01/04 - [Python] - [Python] lxml, request 조합으로 뉴스기사 크롤링하기 -1

 

[Python] lxml, request 조합으로 뉴스기사 크롤링하기 -1

처음에 lxml으로 html파싱을 하면서 절대경로로 탐색하는 바람에 삽질을 많이했었다. // : 절대경로 , .// : 현재 Node에서 탐색 사용법을 기록해두기위해 간단한 뉴스기사 크롤러를 만들어봤다. https://news.joi..

jogamja.tistory.com

이번엔 1편에서 작성한 코드를 바탕으로 multiprocessing 을 통해 속도를 높여봅시다.

일단 앞서 작성한 코드들을  관리하기위해 함수단위로 분할해주었습니다.

 

time함수를 쓰기위해 time을 import합시다.

import time
def getParserPage(page):
    url = 'https://news.joins.com/sports/baseball/list/' + str(page) + '?filter=All'
    res = requests.get(url)
    parser = fromstring(res.text)
    return parser
def getArticles(parser):
    article_list = parser.xpath('//div[@class="list_basic"]')
    parsed_articles = article_list[0].xpath('.//li')
    return parsed_articles;

 

def getLinks(parsed_articles):
    links = []
    for article in parsed_articles:
        parsed_link = article.xpath('.//a[@href]')
        link = parsed_link[0].get('href')
        links.append(link)
    return links;
def processArticles(links):
    articles = [];
    
    for link in links:
        url = 'https://news.joins.com'
        new_url = url + link;
        res = requests.get(new_url)
        parser = fromstring(res.text)
        article_form = parser.xpath('//div[@id="body"]')[0]

        subject = article_form.xpath('.//div[@class="subject"]/h1/text()')[0]

        article_body = article_form.xpath('.//div[@id="article_body"]')[0].text_content()
        article_content = article_body.replace('\xa0','\n')
        article = {'title' : subject,'link' : new_url, 'content' : article_content}
        articles.append(article)
    return articles

그리고 이제 multiprocessing 에 사용할 함수를 만들어줍니다.

def crawlingNews(page):
    while(True):
        parser = getParserPage(page)
        p_arts = getArticles(parser)
        if (len(p_arts) == 0):
            break
        links = getLinks(p_arts)
        
        articles = processArticles2(links)
        print('page %d' % (page));
        page += 8; #process의 개수

main 에서 이제 실행해 봅시다. process의 개수는 대다수가 I/O 작업에서 많은 시간을 쓰기에 8개로 크게 잡았습니다.

 

if __name__ == '__main__':
    pages = [i+1 for i in range(8)];
    pool = Pool(8) #processes = 8 
    
    start = time.time()
    pool.map(crawlingNews,pages);
    end = time.time()
    print(end - start)
  
    pool.close()
    pool.join()

[실행결과]

4page * 20 + 1  =  81개의 기사를 파싱하는데 14.68초가 걸렸군요. 

0.18s/1개 정도의 속도입니다.

multiprocessing이 없었을경우는 속도가 어땠을까요?

[그냥 하나씩 받아올 경우]

단순히 처리부분을 제거하고 html을 요청하는데 걸리는 시간을 재봤습니다.

평균 0.5s/1개의 속도네요. 약 2.7배의 성능 향상이 있었습니다!

하지만 여기서 욕심을 더부려서 조금더 속도를 높일수 있을까요?

 

크롤링의 대부분의 시간은 I/O 작업에서 보냅니다. 코드를 살펴보면 processArticles 에서 가장많이 I/O에 시간을 보낸다는걸 알수있습니다. (  links의 길이만큼 request를 하기때문에 )

그러니 우리는 이부분만 threading을 통해 작업해봅시다.

파이썬에서 multithreading을 하면서 반환값을 어떻게 줘야되지 고민하고 있었는데 c++의 future같은 녀석을

지원해주고 있었습니다. 

import concurrent.futures

이제 link를 얻는 부분만 함수화 해봅시다.

def getTextUrl(link):
    url = 'https://news.joins.com'
    new_url = url + link;
    res = requests.get(new_url)
    return {'content' : res.text, 'link' : new_url}

processArticles도 바꿉시다.

def processArticles(infos):
    articles = [];
    
    for info in infos:
        
        parser = fromstring(info['content'])
        article_form = parser.xpath('//div[@id="body"]')[0]

        subject = article_form.xpath('.//div[@class="subject"]/h1/text()')[0]

        article_body = article_form.xpath('.//div[@id="article_body"]')[0].text_content()
        article_content = article_body.replace('\xa0','\n')
        article = {'title' : subject,'link' : info['link'], 'content' : article_content}
        articles.append(article)
    return articles

마지막으로 crawlingNews도 future를 다루는 코드를 추가합시다.

def crawlingNews(page):
    while(True):
        parser = getParserPage(page)
        p_arts = getArticles(parser)
        if (len(p_arts) == 0):
            break
        links = getLinks(p_arts)
        infos = []
        with concurrent.futures.ThreadPoolExecutor() as executor:
            futures = []
            for link in links:
                future = executor.submit(getTextUrl, link);
                futures.append(future)
            
            for future in futures:
                infos.append(future.result())
        
            
        articles = processArticles(infos)
        print('page %d' % (page));
        page += 8;

(찾아보니 async도 있던데 links 길이가 20이라 future보다 async가 더 효율적일수도 있습니다.)

[실행결과]

성공적이네요! 실행 시간이 14.68s -> 6.74s 로 줄어들었습니다.

속도는 0.08s/1개 로  약 2.25배, 총 2.25 * 2.7 = 6.07배  정도의 성능 향상이 생겼습니다.

 

다음글에서는 하나의 Crawler 객체로 만들고 db, excel 저장 기능을 부여해 봅시다.

 

댓글은 언제나 환영합니다.

반응형

댓글