python 코드로 작업한 프로그램을 container 형태로 배포할 일이 자주 생깁니다.

문제는 python 코드가 실행되기 위해서는 import된 python package들이 설치가 된 환경이 필요합니다.

 

 그러기 위해서 pc에서 작업할 때 설치된 package들은 아래 명령어를 통해 ' requirements.txt' 파일에 저장, 추출이 가능합니다

$ pip freeze > requirements.txt

하지만 위 명령어는 설치된 모든 패키지가 출력되므로 Dockerfile에서 아래와 같은 명령어가 실행될 때 불필요한 package들도 모드 설치되어 container의 용량과 설치시간이 늘어날 수 있는 비효율적인 상황이 발생하죠.

RUN pip3 install -r requirements.txt

 

그래서 'pipreqs'라는 명령어를 활용하면 위 문제를 해결할 수 있습니다.


pipreqs

간단한 웹페이지를 flask module로 제작하였습니다.

아래는 app.py라는 파일에 저장되어 있는 import 선언 부분입니다.

from flask import Flask, render_template, request, redirect, url_for, session
from werkzeug.utils import secure_filename
import os
import requests
import os
from datetime import datetime
import json
import pandas as pd
from PIL import Image, ImageDraw, ImageFont
from flask_caching import Cache

위에 해당하는 모듈들만 container에 설치하면 되는데 어떻게 추출을 할까요.

터미널을 켜서 'app.py'가 있는 경로로 이동한 후에 'pipreqs'라는 명령어만 입력해주면, 같은 경로에 아래처럼 'requirements.txt'이 생성됩니다.

 하지만 위 txt파일을 Dockerfile을 통해서 build하게 되면 문제가 발생합니다.

바로 중복되있는 패키지들 때문인데요.

 패키지들이 다른 버전마다 설치가 된 경우 발생하는 문제입니다.

위 처럼 버전을 체크한 후에 중복되있는 패키지들을 제거하고 docker build를 실행할 경우 문제가 발생하지 않게됩니다.

python 코드가 실행되는 ubuntu기반의 docker container를 생성할 때 발생하는 오류입니다.


ERROR: failed to solve: process "/bin/sh -c apt-get install -y python3 python3-pip" did not complete successfully: exit code: 100
# base image
FROM ubuntu:latest

# 필요한 패키지 설치
RUN apt-get install -y python3 python3-pip

RUN pip3 install -r requirements.txt

 위 처럼 Python이 설치된 Ubuntu환경의 container를 생성하고자 Dockerfile을 작성하였습니다.

별로 문제가 없는 것 처럼 보이지만 Dockerfile에 어떤 문제가 있을까요.

 해결책은 아주 간단합니다.

Ubunut는 패키지를 설치하기전에 아래 명령어가 먼저 실행되어야 합니다.

apt-get update

 

그래서 아래처럼 수정하면 이상없이 Dockerfile이 build 됩니다.

# base image
FROM ubuntu:latest

# 필요한 패키지 설치
RUN apt-get update
RUN apt-get install -y python3 python3-pip

(이하 생략)

url 호출에서 시작되는 일반적인 데이터 수집

실시간으로 변경되는 데이터이며, backfill 처리가 불가능한 수집 형태일 때.

 

1. url 호출 ➡️ 응답값(보통 json) ➡️ 전처리/가공 ➡️ Storage (DB, file ...)

 하나의 실행파일 안에서 자주 구축하던 형태였으나, 예외가 발생할 수 있는 좋지 않은 방식이다.

바로 '전처리' 단계에서 에러가 빈번하게 발생하게 된다.

 - (url의 host나 parameter들의 변경이 없다는 가정하에) respose 값이나 형태가 변경되는 경우

 - 전처리, 가공 후 storage에 저장하는 과정에서 발생하는 type 관련된 이슈

  위와 같은 상황은 꽤 흔하게 발생한다.

 문제는 바로 에러가 생길 때마다 강제로 형 변환(casting)을 해주거나 전처리나 가공하는 과정에서 변경된 'key:value'만 업데이트 해주었을 뿐 'raw data' 수준에서 생각하지 못하였던 것이었다.

 코드를 수정하는 과정에서 시간이 지체되면 수집 주기가 짧은 데이터의 경우 데이터를 수집하지 못하는 공백이 발생하게 되어 결국 데이터 손실이라는 치명적인 결과를 야기할 수 있다.

 

2. raw data를 저장하고 읽어와서 처리하는 파이프라인을 나눈다

1단계 : [url 호출 ➡️ 응답값(보통 json) ➡️ raw 데이터  ➡️ Storage (DB, file ...)] 
2단계 : [query or read file ➡️ 전처리/가공]

파이프 라인이 늘어났다고 볼 수 있으나 안정성은 훨씬 좋아진 결과를 낳았다.

 응답값을 저장하기 전에 별도로 처리하지 않고 바로 저장하게 됨으로써 안정성과 더불어 '데이터 공백'이라는 결정적인 문제가 해결되었다.

기존의 전처리/가공 후에 데이터를 저장하는 과정에서 가장 흔하게 발생하였던 데이터 형 변환 관련된 이슈를 해결하기 위한 시간을 여유롭게 확보할 수가 있으며, 이러한 시간동안에 채우지 못한 데이터 backfill이 가능한 환경을 만든 것이라 할 수 있다.

 

3. 왜 '전처리/가공 ➡️ Storage (DB, file ...)'에서 형 변환 관련된 이슈가 빈번하다고 하는가?

 Storage라 묶었지만, DB(Oracle, Mssql, mysql 등)과 NAS나 서버의 저장공간, 클라우드 서버의 storage 등에 데이터를 파일 형태로 저장하면서 데이터 형식에 관한 이슈가 자주 발생하게 된다.

가장 대표적인 것은 'str, object, datetime 🔁 int(int64, float....)'로 변환하는 과정이다.

 dataframe으로 가공한 데이터를 DB Table에 insert, update 할 때 'Attribute'의 type에 맞지 않는 문제, bigquery도 마찬가지이며 dataframe을 parquet로 변환하는 과정에서도 자주 발생한다. 이럴 때에는 parquet보다 csv로 저장하면 '일다 저장'하는데 문제가 발생하지 않는다.

 저장하려는 공간마다 형태가 제각각이기 때문이다.

 

URL을 호출하여 수집하는 데이터 파이프라인 구축에 대한 결론은

  • 먼저 Raw data를 '일단 수집'한다
  • 가공/데이터 처리 단계를 수집단계와 별도로 분리한다
  • Raw data는 가급적 txt나 json 형태 그대로 파일로 바로 저장하는 것이 좋으나, 수집주기가 짧고 많은 양의 데이터라면 Raw data를 바로 parquet, csv 등의 파일 형태로 변환하여 저장해준다.
  • 양에 따라 데이터가 많으면 parquet, 적을 경우 json, csv 파일로 저장하는 것이 좋다.

json이나 txt 형태의 raw data를 바로 parquet로 변환할 때 가끔 에러가 발생하게 된다.

parquet 압축방식에서 발생하는 문제로 이 이슈는 다음에 자세히 다뤄볼 것이다.

 

데이터 엔지니어링 업무를 맡은지 1년이 훌쩍 지났지만 데이터 수집 과정의 파이프라인을 구축할 때마다 너무 '개발적인'부분에 시간을 할애한게 아닌가 라는 생각이 번뜩 들게 되었다.

 

아래처럼 pattern 속성값을 추가해줍니다 (출처 : 출처: https://kcmschool.com/184 [web sprit])

<input type="number" pattern="\d*">

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Diagnostics;

namespace RunIE
{
    class Program
    {
        static void Main(string[] args)
        {

            Process.Start("microsoft-edge:www.mysite.com");

            //We need to find the most recent MicrosoftEdgeCP process that is active
            Process[] edgeProcessList = Process.GetProcessesByName("MicrosoftEdgeCP");
            Process newestEdgeProcess = null;

            foreach (Process theprocess in edgeProcessList)
            {
                if (newestEdgeProcess == null || theprocess.StartTime > newestEdgeProcess.StartTime)
                {
                    newestEdgeProcess = theprocess;
                }
            }

            //newestEdgeProcess.WaitForExit();

            System.Diagnostics.Process.Start("iexplore.exe", "http://naver.com");



            string museServerURL = "https://google.com";
            try
            {
                ProcessStartInfo startInfo = new ProcessStartInfo("microsoft-edge:" + museServerURL);
                startInfo.WindowStyle = ProcessWindowStyle.Maximized;
                startInfo.Arguments = museServerURL;
                Process.Start(startInfo);
            }
            catch (Exception)
            {
                System.Diagnostics.Process.Start("iexplore.exe", "http://naver.com");
                //WebBrowser webBrowser = new WebBrowser();
                //webBrowser.Navigate(museServerURL, true);
            }
        }
    }    
}

특정 도메인(url)을 브라우저에서 바로 실행시키고 싶을 때 사용하는 함수입니다.

아래 코드는 edge에서 실행이 안 될 경우 explorer에서 실행이 되게 하는 코드입니다.

서비스 이용자들 중에 여전히 IE유저가 많아서 아래와 같은 코드로 수정했었죠. ㅜ0ㅜ

 

 MSSQL 서버에 사용자 PASSWORD 데이터가 'SHA2_xxx'방식으로 암호화되어 저장되있습니다.

 

그럼 php로 입력받은 id와 password값을 처리하기 위해서 아래와 같은 쿼리를 날려 준 후 결과값이 있으면 로그인 허용을 해주면 될 것입니다.

select * from 사용자정보
where 유저id = $id
and 비밀번호 = $password

하지만 위처럼  $password 값이 암호화 처리되지 않은 값이라면 쿼리를 아래처럼 수정해야합니다.

select * from 사용자정보
where 유저id = $id
and 비밀번호 =  HASHBYTES('SHA2_256' , $password)

하지만 sql 수준의 hashbytes 함수의 return값은 binary형(0x.....)식 입니다.

만약 사용자정보 테이블의 유저id 컬럼이 binary형식으로 저장이 안 되어있는 경우라면 아래처럼 character 형태로 convert를 해줄 수도 있습니다.

select * from 사용자정보
where 유저id = $id
and 비밀번호 =  CONVERT(NVARCHAR(32),HASHBYTES('SHA2_256' , $password))

아니면 php 수준에서 아래와 같이 hash()함수로 처리한 뒤에 쿼리로 넘겨주는 방법도 활용할 수 있습니다.

$password_hash = hash("sha256", $password)

 

간혹 css를 아무리 변경해도 바로 적용이 안 되는 경우가 있습니다.

서버 스크립트 문제인가 싶어서 설정을 해봐도 적용이 되지 않았는데, 크롬에서 시크릿모드로 호출시 바로 적용이 되더라구요.

 여기서 브라우저 캐시문제라고 예상되어 해결책을 찾아봤는데 생각보다 간단했습니다.

    <link href="<?php echo base_url(); ?>static/css/sb-admin-2.css?after" rel="stylesheet">

 css link 경로에서 css끝에 아무 파라미터만 연결해주면 됩니다.

위에서는 경로에 '?after'라는 파라미터를 추가해 주니 바로 반영이 잘 되었습니다.

 

브라우저가 다른 css를 인식하게 해주는 방법 중 하나인데요, 아래 블로그 글에서 참고하여 잘 해결하였습니다.

감사합니다.

https://meaownworld.tistory.com/89

* 참고 : pytutbe 모듈은 python 3.6 버전 이상에서 작동합니다!

 

 유튜브 영상을 다운로드 받는 방법은 여러가지지만 써드파티 유틸리티를 다운받거나 사용하려니 번거로움이 있어서 파이썬으로 간단하게 구현해봤습니다.

 유튜브 플레이리스트에 있는 영상도 한 번에 다운로드 할 수 있게 구현한 코드이니 주석 참고해주시고 궁금하신 점은 무엇이든 남겨주세요 ㅎ


프로그램 순서는 대략적으로 아래와 같습니다.

1. 다운로드 받을 유튜브 플레이리스트 URL을 리스트로 생성해줌

2. 유튜브 플레이리스트 제목을 이름으로 갖는 폴더를 생성함

3. 생성한 폴더 안에 유튜브 플레이리스트 안에 있는 모든 영상을 다운로드 해줌

from pytube import YouTube
from pytube import Playlist
import os

# 참고 1) 폴더 생성 : https://data-make.tistory.com/170
# '폴더명'을 매개변수로 받는 함수, 파라미터로 받은 폴더명과 같은 폴더가 없으면 폴더를 새로 생성해줌
def createFolder(directory):
    try:
        if not os.path.exists(directory):
            os.makedirs(directory)
    except OSError:
        print ('Error: Creating directory. ' +  directory)


# 참고 2) pytube 기본 사용법 :  https://pytube.io/en/latest/user/playlist.html
# '플레이리스트 URL'을 매개변수로 받는 함수
def playlist_download(playlist_url):
    # 플레이리스트 제목을 폴더명으로 사용하여 폴더를 생성할 것 -> 플레이리스트 이름 가져오기
    p = Playlist(playlist_url)
    
    ############################################################################
    # 참고) 아래 방법으로 간다하게 구현할 수 있으나 고해상도 영상을 다운받을 수 없음!!    
    # for video in p.videos:
    #     video.streams.first().download(DOWNLOAD_FOLDER)
    ############################################################################
    
    print(p.title)
    DOWNLOAD_FOLDER = p.title
    
    # 플레이리스트 제목으로 폴더 생성
    createFolder(DOWNLOAD_FOLDER)

    # 플레이리스트 안의 각 영상마다 URL을 가져온 후 지정한한 경로에 다운로드를 해줌
    for url in p.video_urls:
        print(url)
        yt = YouTube(url)
        stream = yt.streams.get_highest_resolution()            
        stream.download(output_path=DOWNLOAD_FOLDER)
        
    print(p.title + " 다운로드 완료\n")

if __name__ == "__main__":
    
    # 다운로드 받을 플레이리스트 url을 리스트에 넣어줌
    playlist_url =  ['https://www.youtube.com/watch?v=~~~~~~', 
                 'https://youtube.com/playlist?list=~~~~~~~~~~~',
                 'https://youtube.com/playlist?list=~~~~~~~',
                 'https://youtube.com/playlist?list=~~~~~~~~~',
                 'https://youtube.com/playlist?list=~~~~~~~~-',
                 'https://youtube.com/playlist?list=~~~~~~-5mWbx0zdG0betdeoL']
    
    # 플레이리스트 url을 담고 있는 리스트를 반복문 안 에서 다운로드 함수 매개변수에 할당해주면서 실행
    for i in playlist_url:
        playlist_download(i)

추가로 단일 유튜브 영상을 받고 싶으시면 아래 처럼 간단하게 구현할 수 있습니다.

from pytube import YouTube
url = '유튜브 동영상 url (플레이리스트 url 아님!)'
DOWNLOAD_FOLDER = '다운로드 받을 폴더 경로' #빈값('')으로 두면 파이썬 소스파일이 있는 폴더에 다운로드 받습니다
yt = YouTube(url)
stream = yt.streams.get_highest_resolution()            
stream.download(output_path=DOWNLOAD_FOLDER)

https://youtu.be/JDh_lzHO_CA

 위 강의를 들으며 공부한 내용을 메모 한 포스팅입니다. 

네트워크 복습에 군더더기없이 깔끔한 강의입니다 ;)


* 개념(Abstaraction) VS 사실(Implementation)

= 사람 VS 김꾸꾸, 임백구..(사람이라는 개념의 실체 = 이상의 실체!)

 

* OSI 7 Layer는 '이상'에 해당 됨

김꾸꾸와 친해지려면? 김꾸꾸에 대해 실체를 알아야 하는 '사람'이란게 어떤건지 알아보는게 맞나..!

 

* OSI Layer는 구체적인 것들에 대한 개념적 표현이 잔뜩 있음

즉, 초기엔 OSI 계층에 초반부터 집중할 필요가 없다.


컴퓨터는 기본적으로 총 세 구성요소가 있음..

사용자 / 커널 / 하드웨어가 있으며 OSI 계층도 여기에 상응해서 표현할 수 있다.

최호성 강사님의 매우 중요한 설명!

tcp 소켓이란?

tcp를 user mode app 프로세스가 접근할 수 있도록 파일형식으로 추상화한 인터페이스를 소켓이라 함

OSI든 DoD구분 기준이든 어떻게 구현이 되는지가 중요함.

 

OSI가 무엇인지 너무 애쓸 필요업슴..TCP, HTTP 등을 공부하면 됨!


각 계층에 '식별자'가 있음

Access단에선 Mac주소, Network단 에서는 IP, Transport단에선 PORT..


* 식별자... (Prot, ip, mac...)

 각 식별자들은 '무엇에 대한' 식별자일까!!?

 

MAC : NIC(랜카드)에 대한 식별자, 호스트를 식별하는 것은 아님!

자주 변경되지 않음.. 하드웨어 주소지만 변경할 수 있다.

 

IP : HOST에 대한 식별자

- HOST가 뭔가요!

인터넷(네트워크)에 연결된 컴퓨터!, 이 컴퓨터에 부여되는 주소!

컴퓨터에 ip주소가 몇 개가 있을까?... n개가 있음

NIC이 하나면 IP주소도 한 개다? 아님! 여러 개를 '바인딩' 할 수 있음

결론적으로 한 컴퓨터에 여러 ip주소가 할당 될 수 있음

 

PORT : 여러 형태로 식별의 형태가 달라진다!

TOP, OFFSET 또는 FOR XML을 함께 지정하지 않으면 뷰, 인라인 함수, 파생 테이블, 하위 쿼리 및 공통 테이블 식에서 ORDER BY 절을 사용할 수 없습니다.

 프로시저 내에서 union 되는 쿼리를 임시테이블(with)로 작성하여 처리하던 중에 위와 같은 에러가 발생했습니다.

문제가 발생한 쿼리 구조는 아래와 같습니다.

;WITH '임시테이블' AS (
SELECT 
    --top 100 percent
    A.aaa
    A.bbb
    A.ccc
FROM	
	AAA AS A
WHERE	...			   

UNION ALL

SELECT
    --top 100 percent
    B.aaa
    B.bbb
    B.ccc
FROM	
	BBB AS B
WHERE	...

ORDER BY B.aaa DESC, B.ccc DESC
)
select * from '임시테이블'

 Union 한 두 select 쿼리 하단에 order by 구문으로 인해 발생한 에러입니다.

쿼리에 select 다음에 주석처리 되있는 'top 100 percent'를 작성해주면 위 에러는 해결이 됩니다.

 

 * 참고로 top N percent 퀴리는 예를 들어 select 쿼리 결과 row수가 400개 일 때 top 20 percent 쿼리를 작성해주면 400개의 20%인 80개의 결과. top 20으로 작성하면 20개의 결과만 출력됩니다

+ Recent posts