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

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

Spring Boot 공부하면서, swagger 연동 오류날 경우

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException

이런 에러가 발생했습니다

현제 사용하는 버전은

springboot : 2.6.5
swagger : 3.0.0

Spring boot 2.6버전 이후에 spring.mvc.pathmatch.matching-strategy 값이 ant_apth_matcher에서 path_pattern_parser로 변경되면서 몇몇 라이브러리(swagger포함)에 오류가 발생할 수 있다고 합니다.

[해결방안]
resource폴더안에 application.yml 에 아래 설정을 추가하면 오류가 발생 안합니다.

spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher


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

프로프레임은 현재는 AIX 서버에서만 돌아가는 프레임워크다.

실제 정상적인 비즈니스 오류, 디비 오류는 모두 일반적인 ulog 폴더에서 확인 가능하지만,

예상치 못한 오류(참조 오류 등)는 core로 떨어지면서 해당 TP서버는 재기동 된다.

core dump는 log 파일에 생성되지 않아 일반적으로 dbx를 사용해서 확인해야 하는데, 확인하기가 쉽지 않다. 

일반적으로 core 파일은 core를 발생시킨 바이너리와 동일한 폴더에 생성되지만 프로프레임은 보통 $PFM_CORE_DIR/{코어발생날짜}/{코어발생모듈} 폴더에 core 파일을 생성한다. ( 보통 ulog와 동일한 위치에 존재)


file 명령어를 사용하면 core 가 발생한 프로그램 바이너리와 bit 수를 출력해준다.

pfm5c@tmax:/pfm5c/proframe/logging/core/20181026/STEST00004]file core
core: AIX core file 64-bit, TPFM01(서버명 또는 서비스명)

dbx 명령어로 프로그램 바이너리 경로와 core 파일을 실행한다. 위의 코어는 티맥스 tcs 서버에서 호출한 서비스 모듈에서 발생한 core 이므로 서버 바이너리 경로는 $TMAXDIR/appbin 밑에 있는 서버 바이너리를 넣어준다.

ex> dbx [바이너리경로] [코어파일명]

pfm5c@tmax:/pfm5c/proframe/logging/core/20181026/STEST00004]dbx $TMAXDIR/appbin/TPFM01 core

경고: 코어 파일이 전체 코어(fullcore)가 아닙니다. 일부 정보가 사용 가능하지 않을 수 있습니다.

[core에서 메모리 이미지 사용]

기호 정보를 읽는 중 ...

Segmentation fault in pfmKillSelfProcess at line 1056 in file ""

"pfmOsWrapper.c"을(를) 읽을 수 없습니다.

(dbx)

dbx는 많은 명령어와 다양한 분석기법 사용이 가능하지만 가장 간단하고 흔히 쓰는 명령어는 [ where ] 명령어다. where 명령어로 function call stack을 읽어서 어떤 파일의 어떤 function이 호출될 때 core가 발생했는지 확인이 가능하다.

결론적으로, core 파일이 저장된 폴더로 가서 [dbx $TMAXDIR/appbin/TPFM01(서버 또는 서비스명) core ] 를 친 후, [ where ] 검색을 통해 참조 과정에서 오류나는 것을 찾으면 된다. 

통상 자주 발생하는 core는 PFM_ERR, PFM_DBG, PMF_LOG 시 문자열을 출력하는데, [%ld]를 사용하거나, 숫자를 출력하는데, [%s]를 사용하는 등 자료형을 잘못 이용할 때 발생한다. 

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

인텐트(intent)를 이용한 액티비티간 전환(화면 변경)

[MainActivity]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView textView = findViewById(R.id.hello_text);
textView.setText("안녕하세요");

findViewById(R.id.button).setOnClickListener( new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(this, SubActivity.class); // 이동할 액티비티를 입력변수로 사용
EditText editText = (EditText) findViewById(R.id.edit_message);
String message = editText.getText().toString();
intent.putExtra("EXTRA_MESSAGE", message); // Key-Value 형태로 데이터 저장하기
startActivity(intent); // 액티비티 이동
}
});
[SubActivity]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Intent intent = getIntent(); // 데이터 수신

// 변수 저장하기(From intent)
String message = intent.getExtras.getString("EXTRA_MESSAGE"); // 문자열
// intent.getExtras().getInt("EXTRA_MESSAGE"); // 숫자형
// intent.getExtras().getInt("EXTRA_MESSAGE"); // 숫자형
// intent.getExtras().getStringArray("EXTRA_MESSAGE"); // 배열형
// intent.getSerializableExtra("EXTRA_MESSAGE"); // 클래스형
});

버튼을 클릭하면 다른 화면이 표시되게 하고 싶을 때는 인텐트(Intent)객체를 사용해야 한다. 안드로이드를 구성하는 4대컴포넌트간(액티비티, 리시버, 프로바이더, 서비스)에 각종 의사전달에 사용되는 객체가 인텐트이다.

이번에는 액티비티간의 자료를 전달하는 것을 설명하고, 바로 ABA 이렇게 액티비티의 결과를 받은 경우에 사용되는 메소드에 대해서 알아보자.

우선 간단한 액티비티 전환(화면 변경)의 예제 소스이다. 전달한 값을 intent에 putExtra() 함수로 저장한 후, startActivity(intent) 호출하고, 호출되는 액티비티에서는 getIntent()로 intent를 받아 getExtras()로 자료를 추출한다.

 

- 화면을 구성하다보면, 액티비티간 이동을 ABA 이렇게 할 때가 있다. 위의 startActivity()을 할 경우, 사용자가 '뒤로가기' 버튼을 누르는 거나, 앱을 내렸다가 다시 화면을 띄우는 과정에서 예상치 못한 상황으로 결과를 잘못 받을 경우가 있어, startActivity() 대신 startActivityForResult()메소드 이용하고, BsetResult()로 결과 전송하고, AonActivityResult() 콜백 메소드가 호출되어, 전달받은 데이터를 받도록 처리해야 한다.

[Activity A]
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

public static final int REQUEST_CODE = 1000;
private EditText mNameEditText;
private EditText mAgeEditText;
private EditText mPhoneEditText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mNameEditText = (EditText) findViewById(R.id.edit_name);
mAgeEditText = (EditText) findViewById(R.id.edit_age);
mPhoneEditText =(EditText) findViewById(R.id.edit_phoneNum);
findViewById(R.id.btn_call).setOnClickListener(this);
}

@Override
public void onClick(View v) {
Intent intent = new Intent(this, ActivityB.class);
intent.putExtra("name", mNameEditText.getText().toString());
intent.putExtra("age", mAgeEditText.getText().toString());
startActivityForResult(intent, REQUEST_CODE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_CODE
&& resultCode == RESULT_OK
&& data != null) {
String result = data.getStringExtra("result");
Toast.makeText(ActivityB.this, result, Toast.LENGTH_SHORT).show();
}
}
}
[Activity B]
public class SecondActivity extends AppCompatActivity implements View.OnClickListener {

private TextView mMessageTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);

Intent intent = getIntent(); // Activity A의 전달값 수신
String name = intent.getStringExtra("name");
String age = intent.getStringExtra("age");

mMessageTextView= findViewById(R.id.text_message);
mMessageTextView.setText(age + "살" + name);

findViewById(R.id.btn_result).setOnClickListener(this);
}

public void onClick(View v) {
Intent intent = new Intent(); // 결과를 전달할 준비
intent.putExtra("result", mMessageTextView.getText().toString()); // 결과값 저장
setResult(RESULT_OK, intent); // 결과값을 전달(Activity A)
finish();
}

}

다음에는 액티비티를 지정하지 않고, 암시적으로 Action 기반의 인텐트 이용방법에 대해 알아보고자 한다. 해당하는 Action 방식은 모바일의 연락처 이용, 전화걸기, 공유(카카오톡, 파일 이용) 등 다양하게 사용된다.

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

Margin과 Padding의 차이

마진과 패딩의 정의

Margin(마진) : 테두리를 기준으로 바깥부분에 차지하는 영역
Padding(패딩): 테두리를 기준으로 안쪽에 차지하는 영역

Margin과 Padding은 보통 테이블을 작성할 때, View 객체들이 테이블 표테두리에 바짝 붙지 않고, 약간의 공백을 두기 위한 것으로, layout 을 정의할 때 layout_marginTop, layout_marginBottom등 특정면에만 공백을 줄 수 있고, layout_margin, layout_padding 으로 전체4면을 한꺼번에 줄 수도 있다.

안드로이드의 margin의 최소(기본)값은 8dp이며, 구글은 8의 배수(16, 24, ..)로 주는 것을 권고한다.

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