네이버 뉴스, 구글 뉴스, 클리앙 게시판 등 특정 뉴스판이나 커뮤니티의 글을 자동으로 읽어서 메일로 보내거나, 메신저로 보내주는 것을 생각하는 개발자에게 우선 게시글을 읽는 것에 대해 설명하고자 한다.

사실 게시글(배열)을 읽은 후, 앞에서 배운 파일(csv, 엑셀등)로 생성하여 메일로 보내거나, 나중에 정리할 메신저(챗봇) 등으로 알림을 받을 수있다. 게시글을 읽는 방법은 보통 selenium이나 beautifulsoup 등을 활용하는데 이책은 selenium을 이용한다. 충분히 이용방법은 쉽다. 다만, 게시글을 분석하는 과정에서 요소(항목, 뉴스단위, 게시글 단위)를 어떻게 정의할지를 고민하고 익숙해 지는게 중요하다. 관련 방법에 대해서는 별도로 다시 정리하려고 한다.

#준비사항 #

1. "크롬브라우저(대부분 브라우저 지원) 에서 F12 를 누르면 개발자 도구가 나온다. 그상태에서 크롤링할 게시글의 게시물을 오른쪽 클릭하여 '검사'를 클릭하면, 자동으로 해당 elemenet를 표시해준다." 이정도의 개발자 도구 이용 지식.

2. 해당 element를 오른쪽 클릭하면, 'copy' 메뉴가 있고, 세부적으로 element, styles, Xpath, full XPath가 있는데, 우리는 selenium을 통해 위의 값을 이용해서 해당 elements(복수) 들을 지사하여 가져와 값(url, text, value 등)을 가져올 수 있다. 여기서는 가볍게 html 기본 문법과 Xpath(div/a/li 같은 값)에 대한 막연한 지식(값을 보면 이런거구나 정도 이해할 수준)

3. selenium 설치 (cmd 창에서 pip install selenium 으로 수행 필요시 특정 다운 버전 설치(pip install selenium ==3.1.9 등)
4. 크롬 드라이버 설치(https://chromedriver.chromium.org/downloads) 하고, 크롬브라우저에서 'chrome://settings/help' 로 접속하면 자동 업데이트도 됨.

#개념 설명 #

1. 브라우저에 옵션값을 설정한후, webdriver로 특정 게시물 주소를 driver.get(url) 접속한다.
2. 검색란에 검색어를 send_keys입력한 후, Enter를 친다. send_keys(Keys.RETURN)
2. 위 2번이 싫다면, 1번 수행시 검색어로 검색한 페이지로 접속한다.
3. 게시글(element)를 분석해서 항목을 배열로 읽는다.(selector, xpath 등을 이용)
4. for을 이용하여 게시글마다 date, title, content 등을 읽는다. (필요시 엑셀로 저장)
5. webdriver를 quit().
* 페이지 이동간에 시간이 소요될 수 있어서 sleep(3) 1~3초 쉬는 시간이 필요한다.

from selenium import webdriver                                     # 웹드라이버 호출 START
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager # 웹드라이버 호출  END

from selenium.webdriver.common.keys import Keys    # 특수키(ESC, ENTER 등) 이용
import time                                              #  시간
import datetime as d
from openpyxl import Workbook              # 엑셀

xlsx = Workbook()
sheet = xlsx.active
sheet.append(['Title', 'Link', 'Published date'])

try:

    options = Options()
    options.add_experimental_option("detach", True)
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    
    keyword = 'python'                                        # 검색어
    driver.get('https://search.naver.com/search.naver?sm=tab_hty.top&where=view&oquery=&tqi=h%2ByOzwp0YihssO60XsCssssssxN-078282&mode=normal')
    elem = driver.find_element(By.ID, 'nx_query') 
    elem.send_keys(keyword)                            # 검색어입력
    elem.send_keys(Keys.RETURN)                  

    first = driver.find_element(By.XPATH, "/html/body/div[3]/div[2]/div/div[1]/section/div/div[2]/panel-list/div[1]/more-contents/div/ul")
  
    divs = first.find_elements(By.CLASS_NAME, 'bx._svp_item .total_area')
 
    for div in divs[:10] :        
        title = div.find_elements(By.TAG_NAME, 'a')[5].text
        pub_date = div.find_element(By.CLASS_NAME, 'sub_time.sub_txt').text
        naver_link = div.find_elements(By.TAG_NAME, 'a')[5].get_attribute('href')
        if not pub_date :
            print('NULL')
            continue
        sheet.append([title, naver_link, pub_date])   
except Exception as e :
    print(e)
finally :
    driver.quit()

nowDate = d.datetime.now()
file_name = nowDate.strftime("%Y-%m-%d") + '.xlsx'  # '2023-03-05.xlsx' 형식으로 파일 생성.
xlsx.save(file_name)

위의 소스를 실행하다보면, 'list index out of range' 오류가 발생한다. 위의 소스에서 for div in divs[:10] : 이렇게 divs 10개까지만 읽는 이유는 요즘 포탈사이트 중 초기에 10개를 보여주고 스크롤을 내려야 10를 더 보여주고 있어서 오류가 발생하는데, 오류를 없애려면, 10개를 읽은 후에 스크롤을 내려면 된다. (driver.find_element_by_tag_name('body').send_keys(Keys.PAGE_DOWN) 또는 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
버전에따라 element를 찾는 함수 모양이 변경되었다. 오류 발생시 버전에 맞게 사용하면 되고, ID같이 유일한 값은 find_element로 검색해야 하지만, 복수개가 가능한 요소는> find_elements < 로 배열형태로 읽을 수 있다. 만약 css 클래스처럼 복수가 가능한 항목을 find_element로 읽으면 첫번째 항목이 반환된다.

driver.find_element_by_id('ID명') driver.find_element(By.ID, 'ID명') 유일한 값으로 검색
driver.find_elements_by_class_name('클래스명1.클래스명2') driver.find_elements(By.CLASS_NAME, 'C명.C2명') 복수, 단수 검색 가능
driver.find_element_by_xpath('Xpath') driver.find_element(By.XPATH, 'Xpath') 상대, 절대 모두 검색 가능

책을 보면 사이트들의 구조가 변경되었을 수도 있기에 그대로 실행이 되지않을 수 있고, 인터넷에 있는 것은 버전에 따라 함수값을 추가로 변경해줘야 할 수도 있다. 무엇보다. 이것은 정답이 없다.! 조건에 맞게 가져오기만 하면 된다.
귀찮아서 모두 Xpath로 사용해도 무방하고, css selector를 사용해도 무방하다. 아래의 예제에는 백그라운 실행을 통해 이용중인 PC에서 돌아가게 설정했으며
(options.add_argument('headless'), options.add_argument('window-size=1920,1080')

timedelta를 이용해서 120일 이내 글 중에서, 검색키워드가 포함된 게시글만 읽어서 개인 메일로 보내도록 했다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager

import time
from datetime import datetime, timedelta
from my_email import send_email
job_dir = 'C:\\workspace\\scheduler\\job'
  
try :
    options = Options()
    options.add_experimental_option("detach", True)    
    options.add_experimental_option("excludeSwitches", ["enable-logging"]) #불필요한 오류메시지 제거
    options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64")
    options.add_argument('headless') # 백그라운드
    options.add_argument('window-size=1920,1080')     ## 자동화 실행시, PC의 화면 밖에서 실행되도록(백그라운드) 조치, 백그라운드 시 화면 최소화 방지를 위해 사이즈 고정

    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    
    today = datetime.now()
    diff = timedelta(days=120)  # 최근 120일 이내 게시글 검색
    base_date = today - diff
    base_date = base_date.strftime('%Y.%m.%d.')                                    
    #keyword = open(job_dir + '\\keyword.txt', 'r').readlines() #키워드 파일로 저장할 경우
    keywords = ['디지털','환경','연구']
    matches =[]
    driver.get('https://www.msit.go.kr/bbs/list.do?sCode=user&mPid=224&mId=129')
    time.sleep(1)
    elems= driver.find_elements(By.XPATH, "//*[@id='result']/div/div")
    for elem in elems :        
        title_tag = elem.find_element(By.XPATH, ".//a/div[2]/p")          
        date_tag = elem.find_element(By.XPATH, ".//div[starts-with(@id,'td_REG_DT')]")
        print(title_tag.text, date_tag.text, sep='|')
        if date_tag.text > base_date :
            for k in keywords :
                if k.strip() in title_tag.text :    # strip() 공백제거.
                    matches.append(f'{date_tag.text} : {title_tag.text}')
                    break
    if matches :
        contents = '최근 올라온 공고가 있습니다. \n\n'
        contents += '\n'.join(matches)
        send_email('recv@mail.com', '***게시글 확인', contents )
    else :
        print('최근 공고가 없습니다')         
except Exception as e :
    print(e)     
finally :
    driver.quit()
Posted by 목표를 가지고 달린다
,

메일 발송 함수를 만들어, 다양한 자동화 프로그램에서 이용하기

* 참고로, 네이버 메일 발송 함수를 이해하면 Target을 구글 등으로 변경해서 이용하기 쉽고, 본인이 개발하거나 구상하는 여러 업무 자동화 프로젝트에 호출만 하면 되기에 활용도가 매우 높음.
'함수명 (변수로 수신받는자, 메일 제목, 메일 내용, 첨부파일)' 만 호출하면 됨
* 메일 발송은 실제 메일 서버를 구축해서 직접 발송하는게 아니라, 네이버에 메일을 보내주세요. 라고 요청하는 것입니다. 따라서 특정 규격(SMTP)에 만 맞게 설정하면 됩니다. 기본적으로 서버명, 포트(465), 이용자, 비밀번호 정도를 가지고 요청합니다. 포트 번호 등 확인이 원하시면, 네이버 메일에 접속하셔서 [네이버메일 > 환경 설정 > POP3/IMAP 설정 ] 화면에 서버명과 포트 정보를 확인하실 수 있습니다.

네이버 환경설정 > POP3/SMTP 설정 화면.

* 메일 발송은 2가지 타입을 만들어서 이용하는 것을 추천

  메일 제목 메일 내용 참조 숨은 참조 첨부파일 유무
간단 메일 발송 O O X X O
대량 메일 발송 O O O O O

* 첨부파일 유무에 따른 소스는 10줄 내외로 복잡하지 않으며, 메일 발송시에도 첨부파일이 없는 경우 함수 호출할 때 첨부파일명을 생략하면 기본값(False) 로 처리되어 추가 작업을 해줄 게 없다.
* 다만, 참조, 숨은 참조는 기능 등에 따라 구현이 필요 없을 수도 있다. CC(참조), TO(수신자)를 설정한 후 발송하면 TO, CC목록에 없는 이메일 주소는 BCC(숨은참조)가 되는 것이다.
* 다수에게 발송할 경우 기본적으로 TO, CC, BCC는 콤마(,)를 구분자로 하여 문자열로 던져주면, 규격에 맞게 함수에서 smtp요청시 배열로 변경하여 발송하고 있음.
아래의 샘플소스는 네이버메일, 비밀번호만 변경해서 저장한 후 바로 이용 가능하다.

# 간단 네이버 메일 발송(my_navermail.py)  #
from email.mime.text import MIMEText          # 여기서 부터
from email.mime.multipart import MIMEMultipart
from smtplib import SMTP_SSL
from email.mime.base import MIMEBase
from email import encoders                            # 여기까진 메일 발송을 위한 라이브러리

from os.path import basename                       # 파일 경로 확인 목적

SMTP_SERVER = "smtp.naver.com"             # 메일발송 요청서버
SMTP_PORT   = 465                                      # 공통
SMTP_USER   = "naverID@naver.com"        # 네이버 계정(메일)
SMTP_PASSWORD = "naver_password"      #비밀번호 기재로, 해당 파일 관리 잘 해야 함


def send_email(recvs, mail_title, contents, attachment=False) :
    msg = MIMEMultipart("alternative")            # 기본은 첨부파일이 없는 경우
    
    if attachment :                                             # 만약 첨부파일이 있다면,
        msg = MIMEMultipart('mixed')
        file_data = MIMEBase('application', 'octet-stream')        
        f = open(attachment, 'rb')
        file_contents = f.read()
        file_data.set_payload(file_contents)
        encoders.encode_base64(file_data)

        filename = basename( attachment )
        file_data.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', filename))
        msg.attach(file_data)

    msg['From'] = SMTP_USER
    msg['To']   = recvs
    msg['Subject'] = mail_title
     
    text=MIMEText(contents)
    msg.attach(text)

    smtp = SMTP_SSL(SMTP_SERVER, SMTP_PORT)
    smtp.login(SMTP_USER, SMTP_PASSWORD)
    smtp.sendmail(SMTP_USER, recvs.split(','), msg.as_string())
    smtp.close()

#호출은 다른 py파일에서 import하여 호출

from  my_navermail import send_email 
send_email('receiver@mail.com', '홍길동님, 메일이 왔습니다.', 'Welcome to Python')
send_email('receiver@mail.com', '홍길동님, 메일이 왔습니다.', 'Welcome to Python', 'auto.xlsx')

만약, 참조와 숨은 참조를 이용하려고 한다면, 위의 my_navermail.py 파일에 아래의 함수를 추가한다.

def send_email_with_cc(recvs, cc, hidden_cc, mail_title, contents, attachment=False) :
    msg = MIMEMultipart("alternative")

    if attachment :
        msg = MIMEMultipart('mixed')
        file_data = MIMEBase('application', 'octet-stream')
        f = open(attachment, 'rb')
        file_contents = f.read()
        file_data.set_payload(file_contents)
        encoders.encode_base64(file_data)

        filename = basename( attachment )
        file_data.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', filename))
        msg.attach(file_data)

    msg['From'] = SMTP_USER
    msg['To']   = recvs    
    msg['CC'] = cc
    msg['Subject'] = mail_title
    targets = ','.join((recvs, cc, hidden_cc))
     
    text=MIMEText(contents)
    msg.attach(text)

    smtp = SMTP_SSL(SMTP_SERVER, SMTP_PORT)
    smtp.login(SMTP_USER, SMTP_PASSWORD)
    smtp.sendmail(SMTP_USER, targets.split(','), msg.as_string())
    smtp.close()

#호출은 다른 py파일에서 import하여 호출하고, cc(참조)와 bcc(숨은참조)를 콤마(,) 로 구분하여 입력하여 발송.

from my_navermail import send_email, send_email_with_cc

send_email_with_cc('recv1@mail.com, recv2@mail.com', 'cc1@mail.com, cc2@mail.com', 'bcc1@mail.com', '홍길동님, 메일이 왔습니다.', 'Welcome to Python' )

send_email_with_cc('recv1@mail.com, recv2@mail.com', '', 'bcc1@mail.com', '홍길동님, 메일이 왔습니다.', 'Welcome to Python' )
send_email('recv1@mail.com, recv2@naver.com', '홍길동님, 메일이 왔습니다.', 'Welcome to Python' )
Posted by 목표를 가지고 달린다
,