ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 활용 09 - 1 - 웹 스크래핑 (Headless/정리/프로젝트)
    파이썬/파이썬 활용 2021. 11. 9. 20:40

    유투버 '나도코딩'님 강의 참조

     

     

    01 Headless

    • 지금까지 Selenium을 써서 스크래핑을 하면 웹을 띄우고 원하는 작업을 하는 등 메모리 + 속도에 차이가 났는데 서버에서 스크래핑을 하게되면 이러한 작업이 필요가 없다. 
    • Headless 라는 것이 이를 가능하게 해준다. 속도가 원래의 작업보다 훨씬 빠르다

    • headless를 쓰는 방법은 간단 하다. 라이브러리를 불러오고 webdriver로 웹을 열때 options를 설정 해주면 된다.
    • 그리고 완료되면 스크린샷 기능으로 진행을 남길 수 있다
    • Headless를 쓸 때 몇몇의 웹사이트들은 headless를 감지 할 수 있다. 이때도 마찬가지로 UserAgent값을 바꿔서 사용해야 막힘없이 쓸 수 있다.
      from selenium import webdriver
      
      options = webdriver.ChromeOptions()
      options.headless = True
      options.add_argument("window-size=1920x1080")
      options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36")
      
      browser = webdriver.Chrome(options=options)
      browser.maximize_window()
      
      url = "https://www.whatismybrowser.com/detect/what-is-my-user-agent"
      browser.get(url)
      
      detected_value = browser.find_element_by_id("detected_value")
      print(detected_value.text)
      browser.quit()​

     

     

     

     

    02 정리

    1. HTML - 뼈대 / CSS - 예쁘게 / JS - 살아 있게
    2. XPath - element의 정확한 경로 (id/class/text)
    3. element 간의 관계도 중요 부모-자식
    4. Chrome을 이용해서 스크래핑/크롤링 해야 개발자 도구로 쉽게 가능
    5. 정규식 - 규칙을 가진 문자열을 표현하는 식
    6. User-Agent - 접속하는 유저의 상태를 나타냄 웹페이지는 이에 따라 보여주는 페이지가 다르거나 차단을 함
    7. Requests - 웹페이지를 읽어오기 위한 기능, 빠르지만 동적 웹페이지에는 적용 불가
    8. Selenium - 웹페이지 자동화를 위한 기능, 느리지만 동적 웹페이지에 적용 가능 
    9. BeautifulSoup - 위의 두 기능으로 HTML 데이터를 가져와서 원하는 데이터를 추출 가능 하게 함


    10. 파일 쓰기 및 이미지 다운로드 - with open("파일명", "wb") as f: f.write(res.content)
    11. CSV 저장 - import csv / f = open(filename, "w", encoding="utf-8-sig", newline="")
    12. headless - 브라우저를 띄우지 않고 동작

     

     

     

     

    03 미니 프로젝트

    • 날씨정보, 헤드라인 뉴스 3건, IT뉴스 3건, 영어 회화 지문 가져오기

     

     

    • 네이버 오늘의 날씨 가져 오기

    import requests
    from bs4 import BeautifulSoup
    
    def create_soup(url):
        res = requests.get(url)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, 'lxml')
        return soup
    
    def scrape_weather():
        print("[오늘의 날씨]")
        url = "https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query=%EC%84%9C%EC%9A%B8+%EB%82%A0%EC%94%A8"
        soup = create_soup(url)
        
        # 맑음, 어제보다 00 높아요
        weather = soup.find('span', class_="weather before_slash").get_text()
        cast = soup.find("p", attrs={'class':'summary'}).get_text().replace("맑음","")
        
        # 현재 00도 (최저 00 / 최고 00)
        curr_temp = soup.find('div', class_='temperature_text').get_text().strip()
        curr_temp = curr_temp.replace("현재 온도", "현재 온도: ")
        min_temp = soup.find('span', class_='lowest').get_text().replace("최저기온","최저 기온: ")
        max_temp = soup.find('span',class_='highest').get_text().replace("최고기온","최고 기온: ")
        
        # 강수 확률
        moring_rain_rate = soup.select_one('div > div.list_box > ul > li:nth-child(1) > div > div.cell_weather > span:nth-child(1) > span').get_text().strip()
        afternoon_rain_rate = soup.select_one('div > div.list_box > ul > li:nth-child(1) > div > div.cell_weather > span:nth-child(2) > span').get_text().strip()
        
        # 미세 먼지
        #똑같은 태그에 여러 클래스가 존재하면 리스트 형식으로 받으면 된다. 
        #클래스, 아이디, 속성 등으로 여러개 찾고 싶을때는 {'class':'', 'id':''} 식으로 찾음
        #특정 태그의 특성을 완벽히 찾고 싶으면 {'class':''}, text='' 이런식으로 구분자를 주면 됨
        dust = soup.find('ul', class_='today_chart_list')
        pm10 = dust.find_all('li')[0].get_text().strip().replace("미세먼지", "미세먼지: ")
        pm25 = dust.find_all('li')[1].get_text().strip().replace("초미세먼지", "초미세먼지: ")
        print(weather + ", "+cast)
        print(curr_temp)
        print(min_temp)
        print(max_temp)
        print("강수 확률: " + moring_rain_rate)
        print("강수 확률: " + afternoon_rain_rate)
        print(pm10)
        print(pm25)

     

     

     

    • 네이버 헤드라인 뉴스 들고 오기

    강의상의 네이버와 현재 네이버가 완전히 다르다. 그나마 똑같은 url에 있는 뉴스를 긁어와보자

    import requests
    from bs4 import BeautifulSoup
    
    def create_soup(url):
        res = requests.get(url)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, 'lxml')
        return soup
    
    def print_news(index, title, link):
        print("{}. {}".format(index+1, title))
        print("링크 : {}".format(link))
    
    def scrape_headline_news():
        print("[최신 뉴스]")
        url = "https://news.naver.com/"
        soup = create_soup(url)
        
        news = soup.find('div', class_='cjs_channel_card')
        news_list = news.find_all('div', class_='cjs_journal_wrap _item_contents', limit=3)
        
        for index, news in enumerate(news_list):
            title = news.find('div', class_='cjs_t').get_text().strip()
            link = news.find('a', class_='cjs_news_a').get('href')
            
            print_news(index, title, link)
    
        print()

     

     

    • IT 뉴스 스크래핑

    사이트가 약간 바뀌었다! 그래도 긁어보자!

    def create_soup(url):
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, 'lxml')
        return soup
    
    def print_news(index, title, link):
        print("{}. {}".format(index+1, title))
        print("링크 : {}".format(link))
        
    def scrape_it_news():
        print("[IT 뉴스]")
        url = "https://news.naver.com/main/list.naver?mode=LS2D&mid=shm&sid1=105&sid2=230"
        soup = create_soup(url)
        
        news_list = soup.find('ul', class_='type06_headline').find_all('li', limit=3)
        for index, news in enumerate(news_list):
            a = news.select('ul.type06_headline > li > dl > dt.photo > a > img')
            title = a[0].attrs['alt']
            link = news.find('a').get('href')
            print_news(index, title, link)
        
        print()

     

     

     

    • 영어회화 들고오기

    한글 영어 둘다 <div> 태그에 id값은 같으나 마지막 숫자가 바뀌는것을 볼 수 있다.

     

    def create_soup(url):
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, 'lxml')
        return soup
    
    def print_news(index, title, link):
        print("{}. {}".format(index+1, title))
        print("링크 : {}".format(link))
        
    def scrape_english():
        print("[오늘의 영어 회화]")
        print()
        url = "https://www.hackers.co.kr/?c=s_eng/eng_contents/I_others_english"
        soup = create_soup(url)
        
        sentences = soup.find_all('div', attrs={'id':re.compile('^conv_kor_t')})
        
        print('- 영어 지문 -')
        for sentence in sentences[len(sentences)//2:]: # 8문장이라고 가정, 4~7이 영어, 0~3이 한글
            print(sentence.get_text().strip())
            
        print()
        print('- 한글 지문 -')
        for sentence in sentences[:len(sentences)//2]:
            print(sentence.get_text().strip())
        print()
    • 이런 경우의 스크래핑을 꼭 알아 두어야 한다
    • 일단 애초에 <div> 태그로 한글 / 영어 부분으로 나뉘면서 똑같은 태그와 아이디를 쓰나 마지막 번호에서 갈리는 경우 정규식('^')을 써서 '^--' 으로 시작하는 모든것을 다 찾도록 함 (즉, ^conv_kor_t 로 시작하는 모든 문장을 찾음)
    • 이제 개수의 문제인데 어떤날은 짝수 개수의 문장, 어떤날은 홀수 개수의 문장들이 있을수 있으므로 거기에 맞게 for문을 돌려 줘야 한다. 
    • 즉 영어지문 반, 한글지문 반이라고 볼때 항상 몫으로 세팅을 해주면 몫의값은 항상 반으로 떨어지고 소수점도 '//' 인해 자동으로 처리되어서 7이라도 3개씩 떨어지도록 할 수 있다.
    • 영어문장이 반부터 끝까지, 한글문장이 처음부터 반까지 나오도록 돌려 주면 된다

     

     

    - 완성 코드 (한번에 돌려보기)

    더보기

    import requests
    from bs4 import BeautifulSoup
    import re

     

    def create_soup(url):
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        soup = BeautifulSoup(res.text, 'lxml')
        return soup

    def print_news(index, title, link):
        print("{}. {}".format(index+1, title))
        print("링크 : {}".format(link))
        
    def scrape_english():
        print("[오늘의 영어 회화]")
        print()
        url = "https://www.hackers.co.kr/?c=s_eng/eng_contents/I_others_english"
        soup = create_soup(url)
        
        sentences = soup.find_all('div', attrs={'id':re.compile('^conv_kor_t')})
        
    #     print(len(sentences)//2)
        
        print('- 영어 지문 -')
        for sentence in sentences[len(sentences)//2:]: # 8문장이라고 가정, 4~7이 영어, 0~3이 한글
            print(sentence.get_text().strip())
            
        print()
        print('- 한글 지문 -')
        for sentence in sentences[:len(sentences)//2]:
            print(sentence.get_text().strip())
        print()

    def scrape_headline_news():
        print("[최신 뉴스]")
        url = "https://news.naver.com/"
        soup = create_soup(url)
        
        news = soup.find('div', class_='cjs_channel_card')
        news_list = news.find_all('div', class_='cjs_journal_wrap _item_contents', limit=3)
        
        for index, news in enumerate(news_list):
            title = news.find('div', class_='cjs_t').get_text().strip()
            link = news.find('a', class_='cjs_news_a').get('href')
            
            print_news(index, title, link)

        print()
        
        
    def scrape_it_news():
        print("[IT 뉴스]")
        url = "https://news.naver.com/main/list.naver?mode=LS2D&mid=shm&sid1=105&sid2=230"
        soup = create_soup(url)
        
        news_list = soup.find('ul', class_='type06_headline').find_all('li', limit=3)
        for index, news in enumerate(news_list):
            a = news.select('ul.type06_headline > li > dl > dt.photo > a > img')
            title = a[0].attrs['alt']
            link = news.find('a').get('href')
            print_news(index, title, link)
        
        print()

    def scrape_weather():
        print("[오늘의 날씨]")
        url = "https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query=%EC%84%9C%EC%9A%B8+%EB%82%A0%EC%94%A8"
        soup = create_soup(url)
        
        # 맑음, 어제보다 00 높아요
        weather = soup.find('span', class_="weather before_slash").get_text()
        cast = soup.find("p", attrs={'class':'summary'}).get_text().replace("맑음","")
        
        # 현재 00도 (최저 00 / 최고 00)
        curr_temp = soup.find('div', class_='temperature_text').get_text().strip()
        curr_temp = curr_temp.replace("현재 온도", "현재 온도: ")
        min_temp = soup.find('span', class_='lowest').get_text().replace("최저기온","최저 기온: ")
        max_temp = soup.find('span',class_='highest').get_text().replace("최고기온","최고 기온: ")
        
        # 강수 확률
        moring_rain_rate = soup.select_one('div > div.list_box > ul > li:nth-child(1) > div > div.cell_weather > span:nth-child(1) > span').get_text().strip()
        afternoon_rain_rate = soup.select_one('div > div.list_box > ul > li:nth-child(1) > div > div.cell_weather > span:nth-child(2) > span').get_text().strip()
        
        # 미세 먼지
        #똑같은 태그에 여러 클래스가 존재하면 리스트 형식으로 받으면 된다. 
        #클래스, 아이디, 속성 등으로 여러개 찾고 싶을때는 {'class':'', 'id':''} 식으로 찾음
        #특정 태그의 특성을 완벽히 찾고 싶으면 {'class':''}, text='' 이런식으로 구분자를 주면 됨
        dust = soup.find('ul', class_='today_chart_list')
        pm10 = dust.find_all('li')[0].get_text().strip().replace("미세먼지", "미세먼지: ")
        pm25 = dust.find_all('li')[1].get_text().strip().replace("초미세먼지", "초미세먼지: ")
        print(weather + ", "+cast)
        print(curr_temp)
        print(min_temp)
        print(max_temp)
        print("강수 확률: " + moring_rain_rate)
        print("강수 확률: " + afternoon_rain_rate)
        print(pm10)
        print(pm25)
        
        

    if __name__ == "__main__":
        scrape_weather()
        print()
        scrape_headline_news()
        print()
        scrape_it_news()
        print()
        scrape_english()

     

Designed by Tistory.