API를 문서를 읽지 않고, 도전하는 개발자의 실수

# 사전준비 #

가. 텔레그램 계정 : 기존 보유자도 Setting(환경설정)에서 '@' 로 시작하는 이름이 있는지 확인. 없다면 '편집'에서 Username에 입력하면, @홍길동 으로 등록
나. 챗 검색에서 'BotFather'를 검색한후, New Bot 을 생성하고, 아래 그림에서 가려진 부분이 토큰(Token)값임.

다. 챗 검색으로 나와, New Bot을 검색한 후, 대화 1~2개를 던짐
라. 텔레그램은 채팅방마다 Chat-ID를 부여하는데, 나와 Bot의 Chat-id를 찾아야 함
브라우저창에 'https://api.telegram.org/Bot생성시 받은 TOKEN값/getUpdates' 을 입력하면 결과 나옴
(아래의 id 가 Chat-ID 값임)

{"ok":true,"result":[{"update_id":770558652,
"message":{"message_id":3,"from":{"id":5113991,"is_bot":false,"first_name":"Kam","last_name":"dh","username":"teleno0","language_code":"ko"},"chat":{"id":5113991,"first_name":"Kam","last_name":"dh","username":"teleno0","type":"private"},"date":1677135418,"text":"Fjdjdjd"}}]}

여기서 잠깐 트러블 슈팅!!!
저 화면이 안나오는 경우는 2가지
하나. ok는 정상인데, 아래와 깉이 id 값이 없음. 이럴 때는 다시 Bot에게 대화를 한 후 브라우저에 동일하게 접속하면 나옴

{"ok":true,"result":{"id":603078,"is_bot":true,"first_name":"Tellm0","username":"Tellm0_bot","can_join_groups":true,"can_read_all_group_messages":false,"supports_inline_queries":false}}

둘. 토큰값 잘못 입력.
브라우저에 입력할 'https://api.telegram.org/Bot생성시 받은 TOKEN값/getUpdates' 에서 Token값에는 '603~~Xc0' 앞에 'bot'이라는 접두어를 붙어야 한다. 만약 그래도 오류가 나면, 값을 타이핑치지 말고 복사&붙여넣기로 입력..

# 대화시도 #

import telegram, asyncio             # 정책변경으로 async만 가능 

CHAT_ID = '5113996'                  # 상수는 대문자로 선언
TELE_TOKEN = '6030794********dXc0'   # 상수는 대문자로 선언

async def send_text(bot, text) :
    await bot.send_message(CHAT_ID, text)

msg = 'new is start yes' 
 
bot = telegram.Bot(token=TELE_TOKEN)
asyncio.run(send_text(bot, msg))

사전 확인한 CHAT_ID와 토큰으로 메시지를 보내면 된다. crontab 등으로 특정시간대가 되면, 웹크롤링을 수행하여 텔레그램으로 전달해주면 일일이 사이트를 조사할 필요없이 편리하게 이용 가능할 것 같다. 다른 예제들로는 Bot에게 특정어를 입력하면 해당 업무를 수행할 수 있도록 개발할 수도 있다.

# 트러블 슈팅 #

telegram.error.Forbidden: Forbidden: bot can't send messages to bots

Bot이 Bot에게 메시지를 전달할 수없다. 아마 여러분이 이용하고 입력한 CHAT_ID가 사용자나 사용자와 대화하고 있는 채팅방ID가 아닌 BotID일 수 있다. 다시 확인해보자.

RuntimeWarning: coroutine 'Bot.send_message' was never awaited

텔레그램 정책 변경으로 동기방식의 메시지 전송이 불가능하다. 인터넷의 대부분의 예제가
bot = telegram.Bot(token=TELE_TOKEN)
bot.send_message(CHAT_ID, msg)
이와 유사한데, 이제는 이렇게 보낼 수 없다. 위의 방식대로 asyncio.run()을 이용한 비동기 방식을 이용하자.

Posted by 목표를 가지고 달린다
,

웹크롤링 핵심인 웹element 식별. 그중에 Xpath에 대해 좀더 알아보자

XPATH 의미
./li 현재 태그의 바로 하위에 있는 li 태그
../li 바로 상위에 있는 태그의 하위에 있는 li 태그
//li 문서 전체 중 모든 li태그
//li//ul 문서 전체 중 모든 li 태그의 하위에 있는 모든 ul 태그
//li[@id='myid'] 문서 전체 중 id 속성이 'myid'인 li태그
//*[@id='myid'] 문서 전체 중 id 속성이 'myid'인 모든 태그
//input[@class!='myclass'] 문서 전체 중 class 속성이 'myclass'가 아닌 input 태그
//a[text() = '2'] 문서 전체 중 태그 내용이 '2' 인 a 태그
//a[contains(text(), '다음')] 문서 전체 중 태그 내용에 '다음' 이 포함되는 a 태그
//a[contains(@id,'this')] 문서 전체 중 id 속성에 'this'가 포함된 a 태그
//a[starts-with(@id, 'this')] 문서 전체 중 id 속성이 'this'로 시작되는 a 태그
//a[@class='myclass' and contains(text(), '제목')] 위의 조건을 2개 이상 요구할 경우 and 로 나열한다.
//a[@class='myclass' or contains(text(), '제목')] 위의 조건(2개 이상) 중 1개만 만족해도 된다면 or 로 나열한다

# Expected_conditions에 정의된 조건 목록(페이지 로딩 소요에 따른 이벤트 처리)

from selenium.webdriver.support import expected_conditions as EC
로 먼저 import 한다.

조건 클래스 의미 예시
title_is 웹사이트 제목이 특정 문자열과 일치 여부 확인 EC.title_is('네이버')
title_contains 웹사이트 제목에 특정 문자열 포함 확인 EC.title_contains('네이')
url_conditions 웹사이트 url(주소)에 특정 문자열 포함 확인 EC.url_contains('account')
visibility_of_element_located 특정 태그의 화면에 표시여부 확인 EC.visibility_of_element_located(By.ID, 'spec_id')
text_to_be_present_in_element 특정 태그 내용에 특정 문자열 포함 확인 EC.text_to_be_present_in_element_value(By.ID, 'spec_id'), '특정문자열')
element_to_be_clickable 특정태그 클릭가능 확인 EC.element_to_be_clickable(By.ID, 'spec_id')

# 파이썬에도 Thread(쓰레드)가 존재한다.#

try :
	t = Thread(target=함수명, args=(param1, param2..))
    t.start()
except Exception as e :
	pass #무시

위 처럼 except로 예외문에 대해서, print(e)를 출력해서 확인해도 내용 파악이 어렵다면, sys 모듈의 exc_info()함수를 호출해서 확인할 수도 있다.

import sys
try : 
	1/0
except :
	err = sys.exc_info()
    for e in err :
    	print(e)

# 자바처럼 생성자를 만들수 있다. __init()__.#

파이썬도 클래스를 생성할 수 있는데, 자바처럼 생성할때 인자를 받거나, 초기 작업을 하는 것은 선택적이다. 만약 초기작업이 필요하면, 아래와 같이, 클래스 안에 init함수를 선언하면 된다. 그리고 enter와 exit는 클래스 입장/퇴실할 때 호출되는 함수인데, 반복되는 내용을 입력하면 자동호출된다.
*클래스안에서 메소드 및 멤버 변수를 이용할 때, 반드시 self를 추가하지만, 이용할 때는 붙이지 않는다.

class Usedatabase :
	def __init__(self, config : dict) -> None :
    	self.configuration = config
        
    def __enter__(self) -> 'cursor' :
    	self.conn = mysql.connector.connect(**self.configuration)
        self.cursor = self.conn.cursor()
        return self.cursor
    
    def __exit__(self, exc_type, exc_value, exc_trace) -> None:
    	self.conn.commit()
        self.cursor.close()
        self.conn.close()

# 파일을 읽는 때는 with 절을 이용하세요. #

with open('vstart.log') as log :
	contents = log.read()

일반적으로 파일을 open('vstart.log', 'r') 로 읽어도 좋지만, with 문과 함께 쓴다면, 종료시 file.close()를 하지 않아도 자동으로 닫아준다. 큰 프로젝트가 아니라면 문제되지 않으나 개발자의 실수를 커버해주는 좋은 코딩 습관이다.

Posted by 목표를 가지고 달린다
,

selenium 이용 중 페이지 로딩을 기다리는 2가지 방법

#1. 항목을 가져와야 하는데, 오류 발생

object of type 'WebElement' has no len()

이런 경우, elems = driver.find_element(By.CLASS_NAME, '클래스명') 로 elems를 선언할 때, 함수를 단수형태로 가져왔기에 배열이 아니라, 단순(1개의) 항목이라 len(elems) 하면 오류가 발생한다. driver.find_elements(By.CLASS_NAME, '클래스명') 처럼 복수로 가져올 수 있도록 수정
#2. Xpath로 설정했는데, WebDriver(selenium)이 항목을 식별할 수 없다.

Message: no such element: Unable to locate element:

이런 경우, 문서 구조에서 iframe 을 포함하는 것인지 확인. 네이버 카페의 경우 "cafe_main" 이라는 iframe내에 게시글들이 들어가 있는 구조라서,
iframe = driver.find_element(By.ID, 'cafe_main')
driver.switch_to.frame(iframe)
이런식으로 driver를 switch해줘야 함. 만약 다시 원래 페이지 구조로 돌아오려면,
driver.switch_to_default_content()를 호출하면 됨.

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 webdriver_manager.chrome import ChromeDriverManager
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 = '유형자산'                      
    driver.get('https://cafe.naver.com/parksamaccount')

    elem = driver.find_element(By.ID, 'topLayerQueryInput') 
    elem.send_keys(keyword)
    elem.send_keys(Keys.RETURN)

    iframe = driver.find_element(By.ID, 'cafe_main')
    driver.switch_to.frame(iframe)

    articles = driver.find_elements(By.CLASS_NAME, 'td_article')
    print(len(articles))
    for article in articles :
        title = article.find_element(By.TAG_NAME, "a")
        print(title.text)    
except Exception as e :
    print(e)    
finally :
    #driver.quit()
    print('ee')

#3. 책의 소스 또는 블로그의 소스가 실행이 안되는 경우, 참고하는 자료의 버전을 확인하고 버전을 맞춰본다.
print(library.__verison__) 확인후 재설치

Posted by 목표를 가지고 달린다
,

selenium 이용 중 페이지 로딩을 기다리는 2가지 방법

사실 3가지다. 첫번째는 sleep(3) 이런식으로 특정 시간(3~5초 정도) 기다리게 하는 것이다.
두번째는 클릭할 수 있는 객체가 나타낼때까지 기다기는 것이다. expected_conditions 포함

from selenium.webdriver.support import expected_conditions as EC

   driver.get('https://ww.instagram.com/')
    wait = WebDriverWait(driver, 5)   #최대 5초 기다림.
    cond = EC.element_to_be_clickable((By.LINK_TEXT, '로그인'))
    btn = wait.until(cond)
    btn.click()

세번째는 객체가 보여질 때까지 기다리는 것이다. expected_conditions 포함

from selenium.webdriver.support import expected_conditions as EC

    WebDriverWait(driver, 20).until(
        EC.visibility_of_element_located(
          (By.CLASS_NAME, "_a9_1"))).click()

 

Posted by 목표를 가지고 달린다
,

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

사실 게시글(배열)을 읽은 후, 앞에서 배운 파일(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 목표를 가지고 달린다
,

shutil : 파일, 폴더와 관련된 함수와 클래스를 제공하는 라이브러리
os : 운영체제와 관련된 함수와 클래스를 제공하는 라이브러리

* listdir : 폴더의 파일 목록 조회
* makedirs : 폴더 생성
* isdir : 폴더가 이미 존재하는지 확인
* copyfile : 파일 복사

from os import listdir, makedirs
from os.path import isdir
from shutil import copyfile

input_dir = 'C:\\input\\'
output_dir = 'C:\\output\\'

file_list = listdir(input_dir)
for file in file_list :
    f_name = file[3:-4] + '.xlsx'
    if not isdir(output_dir) :
         makedirs(output_dir)
    copyfile(input_dir + f_name, output_dir + f_name)

위는 단순히, input_dir폴더에 있는 것을 output_dir폴더로 옮기는 것이다. 만약 특정 기준으로 폴더별로 옮기거나 , 파일명을 바꿀 수도 있다. 파일복사 참 쉬움...copyfile 끝.
* (폴더 복사) shutil.copytree : 원본 폴더가 파일이거나, 대상경로에 파일 또는 폴더가 존재할 경우 오류 발생
* (폴더 삭제) shutil.rmtree : 입력값이 파일인 경우 오류 발생
* (파일 삭제) shutil.unlink : 입력값이 폴더인 경우 오류 발생
ex) from shutil import copytree
copytree('c:\\input_dir' , 'c:\\output_dir')
rmtree('c:\\output_dir')
unlink('c:\\input_dir\\download.txt')
* (파일 삭제) shutil.unlink : 입력값이 폴더인 경우 오류 발생
* 파일열기 및 쓰기(CSV 파일 포함)
afile = open('test.csv' , 'w')
afile.write('년월,매출\n')
* 파일열기 및 읽기
ex) f = open(testfile , 'r')
while True :
row = f.readline()
if not row :
break


파이썬 엑셀다루기

* openpyxl 이용... (cmd 창에서 > 'pip install openpyxl' 로 설치)
회사 PC의 경우 파일 암호화 등으로 오류 발생이 가능하므로, try 문을 추가하여 트러블 슈팅에 참고...

from os import listdir
from openpyxl import load_workbook, Workbook

jobdir = "C:\\workspace\\org\\"
outdir = "C:\\workspace\\out\\"
files = listdir(jobdir)

files.sort(reverse=True)

result_xlsx = Workbook()
result_sheet = result_xlsx.active        #최근 활성화된 탭


try :
    for myfile in files :
        if myfile[-4:] != "xlsx" :                    # 폴더에서 엑셀파일이 아닌 경우 skip
            continue
        print(myfile)
        tg_xlsx = load_workbook(jobdir + myfile, read_only=True)
        tg_sheet = tg_xlsx["Sheet1"]
        
        for row in tg_sheet.iter_rows() :
            row_data = []
            for cell in row :
                row_data.append(cell.value)
            result_sheet.append(row_data)

     result_xlsx.save(outdir + "result.xlsx")
except Exception as e :
    print(e)
finally :
    print('end')

정말 단순

#특정셀, 열, 행을 읽는 샘플 코드#


from openpyxl import load_workbook

xlsx = load_workbook('sample.xlsx', 'read_only=True) #read_only로 읽으면, 필요시마다 행간을 메모리로 읽어 속도 향상
sheet = xlsx.active

rows = sheet['1:2']           # 1~2행 읽기
rows = sheet['1']              # 첫 번째 행 가져옴
for row in rows :              # A1, B1, C1 읽기
col    = sheet['A']              # 첫 번째 열을 가져옴
for cell in col :                  # A1, A2, A3 읽기..

wb.close()                        # 닫기.

 

Posted by 목표를 가지고 달린다
,

"내일을 바꾸는 업무 자동화" 프리렉(2020년)

기본 IT 소양이 있는 개발자라면, "Head First 파이썬"과 본 도서를 추천한다. 해당 도서가 2년이 지났기에 책에 나온대로 설치를 하다보면 라이브러리 버전 차이로 실행이 되지 않아 간혹 인터넷에서 새로운 버전에 따른 문법을 찾아야 할 때도 있지만, 전체적으로 "파이썬이 편리하다." 라는 생각과 자바처럼 "반복되는 코드없이 단순하게 호출하고 이용하는 것에 경이롭다."는 생각도 든다.

본 후기는 파이썬을 처음 접하는 개발자가 책을 읽지 않더라도 또는 내용이 기억나지 않은 파이썬 입문자를 위해 정리한 것이다. 개인적으로 책을 읽는 것을 추천하고, 책을 읽은 후 자기업무(생활)에 활용해 볼지 고민할 수 있는 시간을 갖기 바란다.


  1. 파이썬 설치 : https://www.python.org : 이하 설명 생략(easy)
  2. PyCharm, IDLE Shell, Visual Studio Code, Atom 등을 설치하면서 환경설정(설치과정에 포함됨)을 하면, CMD 창이 아니라 편집기에서 F5로 실행 가능.
  3. CMD 창에서 c:\> pip install library-name 을 수행하면 해당 라이브러리의 최신 버전을 설치하고, 유지보수, 참고자료의 버전 등 이유로 과거버전을 다운 받을려면, c:\> pip install library-name==3.4(원하는 버전) 을 수행하면 된다.
    cf) 설치삭제= c:\> pip uninstall library-name
    cf) 버전확인= c:\> python (enter)
    >>> import library-name
    >>> print(library-name.__version__) -- 버전 출력
  4. 자동화 프로그램 : 프로그램 개발 이후 crontab(리눅스 등), 시작프로그램 등록(윈도우) 등을 통해 실행
    ex) 네이버 메일 발송, 네이버 기사 크롤링(모두 100줄 이하로 간단)
    ex) 인스타그램 로그인 후, 좋아요 클릭 등
    ex) 특정사이트 게시판 읽어서 엑셀 파일로 정리한 후, 네이버 메일 발송
  5. 웹서비스 : 장고(큰 규모), 플라스크(작은 규모) 등을 이용하여 서비스
  6. BOT : 텔레그램(https://core.telegram.org/bots/api) 을 활용하여 챗봇 서비스

기본적인 X 곱하기 Y = X * Y 이런 내용은 생략한다.(^^, 생소하다면 블로그가 아닌 책을 읽으셔야 합니다)

가. x*y : print('*' * 7) > ******* : 문자열(*)을 숫자 Y번(7번) 반복하는 연산자다. 만약 x(문자) + y(숫자)인 경우에는 형변환이 발생하지 않아, TypeError 오류 발생. 즉, print('홍길동' + 100) 은 오류이다.

나. 인덱스가 잼이 있다. 인덱스 숫자가 양수, 음수 모두 가능하며 첫글자는 0으로 시작한다. 접근 편리함

문자열(data) Y o u C a n d
인덱스(양) 0 1 2 3 4 5 6
인덱스(음) -7 -6 -5 -4 -3 -2 -1

* 슬라이싱(문자열에 대한 접근 범위) : print(data[2:4]) > uCa, print(data[:4]) > YouCa, , print(data[:]) > YouCand, print(data[-4:-2]) > Can, print(data[-4:]) > Cand

다. 문자열 내장함수 :
* data.count('0') > 1 (문자열에 특정 문자(열)의 수 반환)
* data.find('C') > 3 (문자열에 특정 문자(열)의 가장 작은 위치 반환, 없으면 -1 반환)
* '#'.join(data) > Y#o#u#C#a#n#d (문자열 사이에 특정 문자를 추가한 결과를 반환)
* data.upper(), data.lower() > (문자를 대문자(소문자)로 변환하여 반환)
* data.split('C') > You and (특정 문자로 문자열을 쪼개서 반환)
* data.strip() > (문자 양 끝의 공백 문자를 제거하여 반환, 자바의 trim() 기능)
* data.isdigit() > false (숫자만으로 이루어졌는지 검증 true/false)
* f-string포매팅(문자열에 변수를 쉽게 포함할 수 있게 지원)
ex) name = '홍길동'
age = 18
res = f'My name is {name} and i am {age} old.'

라. 리스트 : data = ['data1','data2','data3']
* x + y : 리스트(x) 와 리스트(y) 연결(자바의 arraycopy() 기능 수행, 직관적인 프로그래밍ㅋ)
* x * y : 리스트(x) 를 y번 반복, 리스트(x) + 리스트(x) + 리스트(x) .. y 번 수행.
* 항목 삭제(위치, 값 기준) : del(data[2]), data.remove('data2')
* 항목 추가(맨 뒤) : data.append('data4')
* 정렬 : data.sort()
* 순서 역전 : data.reverse(), data.sort(reverse=True)
* 특정 위치에 항목 추가 : data.insert(1, 'data1.3')
* 특정값 갯수 반환 : data.count('data1')

마. 튜플 : 리스트와 동일. 주의해야 할 점은 값이 1개일 경우 변수명=(값1,) 과 같이 값 뒤에 ','를 표기해야 함. 그리고 상수처럼 수정이 불가능하다. 즉 처음 만든 이후로는 수정되지 않는데 개발자의 실수를 차단할 목적이라고 함. ex) data = (1,)
>>> print(data)
>>> (1,)

바. 딕셔너리 자료형 : key-value 형
* 변수명 = {'key1' : 'value1', 'key2':'value2', 'key3':'value3'}
* data.keys(), data.values(), data.items(), data.get('key1'), data.get('key3',0) 등이 가능하다. 마지막은 키값이 없을 경우 기본값을 지정할 수 있다.

사. if문 : 기본적으로 파이썬은 { } 가 없다. indent(탭)으로 구분하므로,
if 조건 :
코드1
elif 조건2 :
코드2
else :
코드3
형식이다. 이런 띄어쓰기 때문에 본인 보기 편한대로 편집하다 IndentationError : unexpected indent 오류가 발생할 수 있다. 문자열, 리스트, 튜플, 딕셔너리는 비어있으면(empty) false이고, 숫자인 경우 0이면 false 그외 모두 true 이다.

아. for, while : if문 과 동일하게 { } 대신 인덴트(탭, 띄어쓰기)를 이용하고, for, while 키워드 옆에 ':' 를 붙인다.
* for elem in elems : , while data-list :

자. 함수 정의 :
* def 함수명 (전달값_변수명, 전달값_변수명....) :
코드 1....
return data (반환값 없으면 return 생략)

차. range : 범위의 갯체를 반환
* range(A) : 0 ~A-1 까지 순번을 가진 range 객체 반환
* range(A, B) : A ~ B-1 까지 순번을 가진 range 객체 반환
* range(A, B, C) : A ~ B-1 까지 C간격으로 순번을 가진 range 객체 반환, C는 음수도 가능

카. class : 자바랑 동일한데 차이점은 함수, 변수 호출시 self 추가.(정의할때만 사용하고 이용할 때는 생략)
* class 클래스명 : << (형식)
변수명 = 값
def 함수명(self, 전달값_변수명, ..)
코드내용
* class Email : << (예제)
sender = 'dddd@gmail.com'
def send_mail(self, recv, subject, content) :
print(self.sender)
print('**', 20)
e = Email()
e.send_mail('hhhh@gmail.com', 'Welcome','Nice to Meet U')
* 클래스 활용 : from 파일명 import 클래스명, 클래스명...
cf) 만약 import한 여러 라이브러리들 간에 이름이 중첩이 된다면, 'as 별칭' 으로 구분
from my_email import send_email as SE
cf) 본인이 작성한 파일을 불러 이용할 때 from import 활용하며, import를 생략하면 from 파일명에 선언된 모든 함수와 클래스를 이용가능

타. 주석 : 한줄은 '#'을 이용하고, 여러줄은 ''' ''' 를 이용한다. 또는 편집기의 (un)comment 기능 활용.

이상 기본 문법적인 내용을 마치고, 메일발송(1), 크롤링(요소 검색법 포함), 인스타그램 자동로그인, 게시물 검색 에 대해 개별적으로 알아보자.

 

Posted by 목표를 가지고 달린다
,