Python EDA

[toc]

Python

Bash command execution

다음은 파일명에서 "CPU"라는 문자열을 포함한 파일만을 출력하여 준다.

$ ls | grep CPU

어떠한 대용량 데이터가 특정조건에 따라 파일로 작게 나뉘어 있는 경우가 있는데, 특정 조건으로만 파일을 불러들여 처리하는 경우가 종종 발생한다. 실제로 필자는 CPU 관련한 데이터를 기록한 xml을 개별로 취급할 때 사용하였다.

~/my_dev/ai_secu2020/src
$ ls ../data/NMS10_XMLs | grep CPU
FW_12348573_125.57-N_CPU.xml
FW_12348584_172.18-N_CPU.xml
  ...

sed 를 이용한 문자열 치환

필자는 데이터 전처리의 초기 단계에서 종종 bash command를 사용한다. 대표적으로 sed, cut, awk 등의 명령어가 있다. sed는 쉽고 빠르게 io만을 이용하여 문자열을 치환해 준다. 복잡하지 않은 단순 치환이라면 sed명령어는 매우 유용하다고 할 수 있다. 문자열을 취급하다보면, 개행문자(\n) 등의 command 문자나, 빈칸 (White Space)제거 또는 주석(Commet) 제거 등이 자주 사용된다.

파일명 등 bash 에서 취급하는 문자열의 처리에 유용할 뿐만 아니라, 실제 Contents 처리에도 유용하다.

$ cat sample.txt | sed 's/\n//g'

개행문자(\n)을 ''으로 치환한다. 즉, 개행문자를 삭제한다.

만약 Jupiter notebook 또는 IPython을 사용한다면, % 또는 !을 사용하여 bash command를 실행할 수 있다.

% cat sample.txt | sed 's/\n//g'

다음과 같이 Python 으로 python을 직접 파일을 읽어서, 문자열을 처리하는 방법도 있으나, 데이터의 양이 많거나, 파일이 여러개인 경우 bash 명령어로 처리한다면 쉽게 동일 작업을 수행할 수 있다.

print(open('sample.txt', 'r').read().replace('#',''))

sed를 사용하는 방법은 매우 간편할 뿐만 아니라, Bash 명령어를 사용하여 새롭게 치환된 파일을 생성하면, 실제 데이터를 처리하는 python 모듈의 메모리를 절약할 수 있다.

물론 파이썬으로 처리하면, 하나의 py파일에서 일관되게 처리를 할 수 있는 장정도 있다. 그러나, 적용해야하는 파일이 다수개인 경우 역시나 bash 명령어가 편하다.

ls | grep CPU | xargs sed 's/\n//g'

현재 working directory 에 존재하는 파일중, 파일명에 CPU가 포함되어 있는 파일을 대상으로, 모든 개행문자를 제거한다.

다음은 bash script의 for문을 활용하여, 모든 파일에 대하여 실행한 sed 결과를 새로운 파일(modify-<기존파일명>)에 저장한다.

for filename in $(ls | grep CPU); do
  sed 's/\n//g' $filename > modify-$filename
done

python으로 관리하는 프로젝트에서 bash script까지 별도로 진행되기 때문에 관리의 불편함을 느낄 수도 있다. 이라한 경우, bash script의 관리가 불편하다면, bash script 파일을 하나 만들어 두고, script 파일의 실행만을 python으로 진행하면 된다.

혹시, 이것도 관리가 번거롭다면, 다음과 같이 python 자체에서 bash script를 만들어 주고, 실행해 주면 된다.

import os

strDataPath = './data'
strScript ="""
#!/bin/bash
cd {}
for filename in $(ls | grep CPU); do
   sed 's/\n//g' $filename > modify-$filename
done
cd -
""".format(strDataPath, strMode)
with open('modifyScript.sh','wt') as fp:
    fp.write(strScript)

os.system('chmod +x modifyScript.sh')
os.system('./modifyScript.sh')

current directory 와 다른 path에서 작업을 하고 싶다면, cd 를 수행하여 directory를 변경하고, cd - 명령을 통해서 원래의 directory로 복귀할 수 있다.

Python의 이슈는 아니지만, 주의할 사항으로 스크립트파일(ex. script.sh)을 생성하고 난 후 실행 권한 ( +x)을 부여해 주는 것을 간과하기 쉬우므로 유의하기 바란다. 스크립트 파일을 생성하고, 실행권한을 부여 후 os.system()함수로 실제 스크립트를 구동해 준다.

Python CMD/프로세스 실행

Python에서 외부 프로세스 실행하는 방법은 크게 3가지 방법이 있다.

os.system()

python에서 shell 명령어를 실행하기에 가장 단순하고 편리한 방법은 system() 함수를 사용하는 방법이다.

os.system( '명령어' ) 형태로 실행할 수 있다. 간단한 스크립트를 실행하거나, 또다른 python을 구동할 때, 서비스를 구동 시킬 때 등 사용할 수 있다. 다음과 같은 예제를 들어 볼 수 있다.

os.system('chmod +x test.sh')

그러나, os.system()은 실행 결과에 대한 실행여부만 반환해 준다. 정상 실행은 0 , 비정상 실행은 에러코드값을 반환해 준다. 대표적으로, ls 명령어로 현재 디렉토리의 파일 리스트를 확인할 수 없다. 다음과 같이 실행은 할수 있다.

os.system('ls')
==> 결과값 : 0

실행은 해준다. 그러나 결과값을 확인 할 수 없다.

Ipython 또는 jupyter notebook 사용자라면, magic command(%명령어)로 단순하게 실행도 가능하다. ex. %ls

os.popen()

문제를 단순화 해서, python 내부에서는 어떻게 ls 명령어의 실행 결과를 확인하고, 결과값들을 저장할 수 있는가? os.system()은 결과값을 반환해 주지는 않기 때문에 다른 명령어가 필요하다.

os.popen()함수는 결과값을 반환해 준다. 좀 더 자세하게, 결과값보다는 실행하는 handle 또는 file descriptor를 반환해 준다. 따라서, os.system()함수처럼 호출하는 즉시 실행되는 것보다는, 실행구문을 지정하고 이후 read를 할 때 실행되는 구조이다. 함수의 이름에서 추측할 수 있듯이 open계열의 함수 이다. 파일을 오픈하는 것과 동일하게 Process를 open하는 개념이다. linux 계열의 kernel은 모든 것을 file로 보듯이, fd(file descriptor)로서 process를 오픈/실행하는 것으로 생각해 볼 수있다. 어찌 보면, lazy execution과 비슷한 방식이라고도 할 수 있다.

cmd = os.popen('ls')
val = cmd.readlines()

os.popen()을 실행하는 시점에 명령어가 수행되는 것은 아니다. 오히려 cmd.readlines() 시점에서 명령어가 실행된다.

다음은 필자가 실제 사용하고 있는 코드중 일부를 발췌하였다. 파일 들중에서 CPU 문자열을 포함한 파일만을 선별하여 파일명을 얻어오는 과정이다.

strDataPath = '../data/NMS10_XMLs/'
strCmd = "ls {} | grep CPU".format(strDataPath)
strFile = [x.strip() for x in os.popen(strCmd).readlines()]
print(strFile)

subprocess 실행 방법

os 모듈은 기본적으로 os에서 제공하는 기능을 사용하는 모듈이다. 좀 더 전문적으로 process자체를 다루기 위해서 subprocess 모듈을 제공하고 있다. 단순히 명령어를 실행해 준다는 개념보다는 process를 다루는 개념이며, 이중에서 process를 실행해서 결과를 받아오는 함수가 일부 포함되어 있는 것이다. 좀 더 하위 레벨에서 process와 thread 등을 다룰 때 사용할 수 있다.

os.system()는 동일 process에서 명령을 수행 후 종료하지만, subprocess 에 포함된 함수는 새로운 child process를 생성하여 실행한다. (fork) . 따라서, process 실행에 대한 상세 argument 들을 제어할 수 있다.

실행의 반환값은 CompletedProcess 개체이다. CompoletedProcess는 기본적으로 args, returncode, stdout, stderr를 포함하며, stdout과 stderr는 None을 기본값으로 한다.

import subprocess
r = subprocess.run('ls')
print(r)

결과는 실행코드이다. 성공:0, 에러:1

기본적으로 stdout, stderr는 capture하지 않는다. (capture_otput=False) stdout을 확인하기 위해서는 caputre=True로 변경해 준다. 다음을 실행해 보자.

import subprocess
r = subprocess.run('ls', capture_output=True )
print(r)
print(r.stdout)

subprocess는 low level function으로 실행결과가 bytes 라는 점에 유의가 필요하다. 이러한 이유로, utf-8의 bytes를 string으로 디코딩(decode)해줄 필요가 있다. 다음은 명령어 실행 결과로 얻은 최종 문자열의 리스트 이다.

fileList = [x.decode('utf-8') for x in r.stdout.splitlines()]

바이트 (utf-8로 인코딩된 byte)를 문자열로 변환하는 과정은 다음은 참조하기 바란다.

Byte to String
a = b'AAAA\n'
b = a.decode('utf-8')
print(a)
print(b)

IPython Magic Command

외부 명령어 실행까지는 아니지만 shell command 명령어로 한정한다면, Ipython 의 매직 커맨드 (magic command)를 활용할 수 있다. 앞서 잠깐 언급했듯이 ipython 또는 jupyter notebook 환경에서는 몇몇 매직커맨드를 제공하고 있다. 이중에서 shell command 실행을 지원하는 기능이 포함되어 있다. 다음과 같이 png 이미지 파일 리스트를 바로 확인 할 수 있다.

%ls | grep png

그러나 이와 같은 겨우, notebook의 cell에서 결과 확인은 가능하나, 여러개의 파일을 자동화 처리하기 위해서는 변수로 저장해야 한다. 이와 같은 경우 기본 매직커맨드 (%) 구문 대신 !를 사용하여 결과 같을 변수로 저장할 수 있다.

r = !ls | grep png
print(r)

기타 python os 모듈기능

앞서 설명한 방법들은 외부 명령어 실행에 대한 범용적인 방법이다. 그러나, python의 os모듈에는 자주 사용하는 shell 명령어 들을 이미 함수로 제공하고 있다. listdir(), makedir(), chdir() 등이 대표적인 예라 할 수 있다. 다음 명령어 라인을 참조하기 바란다.

os.listdir() # list dir
os.makedirs() # make a dir
os.rmdir()   # remove a dir
os.rename()  # rename a old one to  a new one

os.path
os.path.exists( <파일 또는 디렉토리명> )  # check if it exists
os.path.isdir()  # check if it is a directory
os.path.isfile() # check if it is a file
os.getcwd() # get current dir
os.chdir()  # change dir

경우에 따라서는, python에서 외부 명령어를 실행할 때, 환경변수가 필요한 경우가 있다. os.environ은 환경변수를 설정을 지원한다.

env = os.environ
env['PATH']
path2 = r'<add path dir>;' + env['PATH'] 
env['PATH'] = path2

환경변수의 설정은 현재 open된 창에만 적용된다. (메모리에만 존재한다. )

응용

디렉토리 부재 시 디렉토리 생성

개발을 진행하다 보면, 경우에 따라서 subdirectory를 사용하게 된다. 데이터를 참조하거나 저장할 때 sub-directory를 사용하는 경우도 많다. 로직의 흐름에 따라서 sub-directory가 존재하지 않는 경우도 발생하는데, 이런 경우 디렉토리 존재 여부를 확인하고 생성해 줄 필요가 있다.

다음은 현재 디렉토리를 기준으로 ../dev/img 디렉토리가 존재하는지 확인 후, 해당 디렉토리가 없다면 디렉토리를 생성한다.

# path check and make dir
strPath = '../dev'
rePath = "{}/img".format(strPath)
if not os.path.exists(rePath ):
    os.makedirs(rePath)

XML Parsing

우선 xml 파싱하는 기본 구문부터 살펴 보자.

import xml.etree.ElementTree as eTree 

xmlTree = eTree.parse('./test.xml')
data = xmlTree.findall('./rra')

xml.etree.ElementTree 모듈에 있는 parse함수를 사용하면, xml을 손쉽게 로딩할 수 있다. Xml을 파싱하여 얻은 개체에서 find / findall 등의 명령어를 사용하여 하위 tag를 개체로 가져올 수 있다. 좀 더 상세한 내용을 알아보기 전에, 분석해야하는 샘플 xml의 구조를 확인하고 진행하도록 하자.

파싱하고자 하는 XML의 파일 구조는 다음과 같은 구조를 가지고 있다. 데이터에 관련한 속성(property)를 가지고 있으며, 주요 데이터값은 RRA-> Database -> row -> v 에 데이터가 행으로 보유하고 있는 형식이다.

graph LR
classDef dotOutline fill:#f96,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5, 5
RRD --> version
RRD --> step
RRD --> lastUpdate
RRD --> RRA:::dotOutline

RRA --> cf
RRA --> pdp_per_row
RRA --> database:::dotOutline

database --> date
database --> time
database --> row:::dotOutline
row --> v :::dotOutline

다음은 파싱(Parsing) 하고자하는 실제 xml 파일의 일부를 발췌한 문서이다.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE rrd SYSTEM "http://oss.oetiker.ch/rrdtool/rrdtool.dtd">
<!-- Round Robin Database Dump -->
<rrd>
	<version>0003</version>
	<step>60</step> <!-- Seconds -->
	<lastupdate>1441253228</lastupdate> <!-- 2015-09-03 13:07:08 KST -->
	<!-- Round Robin Archives -->
	<rra>
		<cf>AVERAGE</cf>
		<pdp_per_row>1</pdp_per_row> <!-- 60 seconds -->
		<database>
			<date>2015-09-01 13:08:00</date><time>1441080480</time> <row><v>2.0000000000e+00</v></row>
			<date>2015-09-01 13:09:00</date><time>1441080540</time> <row><v>2.0000000000e+00</v></row>
			<date>2015-09-01 13:12:00</date><time>1441080720</time> <row><v>2.0000000000e+00</v></row>
    </database>
  </rra>
</rrd>
</

test 01

© 2021 S.J.Lee, Built with Gatsby