애들센스


곧 나올 책에 사용할 커버 이미지를 파이썬으로 만들어 보다 삽질의 추억

조만간 출간할 책에 사용할 커버이미지를 만들어 보았다. 책의 내용이 포트폴리오 이론을 파이썬으로 구현하면서 그 이론들을 다루는 만큼 효율적 포트폴리오 그림을 그 그림을 만드는 파이썬 코드로 채운 그림이다(이것도 일종의 recursive?)

이것은 ASCII Art 라고 불리우는 데, https://manytools.org/hacker-tools/convert-images-to-ascii-art/의 도움을 받았다. 그리고 텍스트파일을 다운받아 텍스트의 각 글자를 파이썬의 소스코드로 변환하는 것이다.
ascii_txt = r'C:\Users\Jim\Documents\portfolio.txt'
cover_txt = r'C:\Users\Jim\coverconda.py'
new_txt = r'C:\Users\Jim\Documents\bookportfolio.txt'

file_asc = open(ascii_txt, 'rt', encoding="utf-8") # ASCII Art 텍스트파일
file_cov = open(cover_txt, 'rt', encoding="utf-8") # 치환할 글자가 담긴 파일
file_new = open(new_txt, 'wt', encoding="utf-8") # 치환하여 새로 만들어질 파일

while 1:
    char = file_asc.read(1) # 한 글자를 읽는다
    if not char: # 다 읽었다면 종료
        break
    if not char.isspace(): #  치환대상이 공백문자가 아니라면
        code = file_cov.read(1) # 치환할 글자 읽기
        while code.isspace() or code=='\t' : # 치환할 글자가 공백 또는 탭문자라면
            code = file_cov.read(1)   #공백이 아닐때까지 계속 치환용 글자 읽기    
        char = code # 글자 바꿔치기
    file_new.write(char) # 새 파일에 쓰기

file_new.close()
file_cov.close()
file_asc.close()
소스코드는 아직 더 다듬을 여지가 있는 데, 캐리지-리턴을 공백으로 바꾸어 치환할 내용이 서로 달라붙지 않고 공백으로 구분되게 만들 필요가 있다. 그리고 소스코드 길이를 더 줄이는 최적화도 생각해볼 필요도 있다. 그리고 ASCII Art를 외부 서비스를 이용하지 않고 직접 만들어 작업 절차를 간단히 하면 더 좋을 듯한데...굳이 이런 삽질에 시간을 낭비할 필요는 없을 것 같다.

같은 내용을 c언어로 만들면:
#include 
#include

/* run this program using the console pauser or add your own getch, system("pause") or input loop */

int main(int argc, char *argv[]) {
char c1, c2;
FILE *f1, *f2, *f3;
f1 = fopen("ddangul.txt","rt"); if(f1==NULL) { perror("Ascii art file not found\n"); return 1; }
f2 = fopen("fin_recipes.h","rt"); if(f2==NULL) { perror("Source file not found\n"); return 2; }
f3 = fopen("source-art.txt","wt"); if(f3==NULL) { perror("Can not create target file\n"); return 3; }

for(;;)
{
c1 = fgetc(f1);
if(feof(f1)) break;

if(!(c1==' '||c1=='\n'||c1=='\t')) {
while(1)
{
c2 = fgetc(f2);
if(c2==' '||c2=='\n'||c2=='\t')
fseek(f2, 2, SEEK_CUR);
else
break;
}
c1 = c2;
}
fputc(c1, f3);
}

fclose(f1);
fclose(f2);
fclose(f3);
return 0;
}

어쩌다보니 파이썬으로 우연히 엑셀의 셀 주소 문자열을 만들다 파이썬/쟁고

두 개의 리스트내 각 원소를 뽑아서 합치다 보니 엑셀의 셀 주소처럼 보인다(이 정도면 중증이다)
x = [ "A", "B", "C", "D", "D" ]
y = [ 1, 2, 3, 4, 5 ]

z = ['A1', 'B2', 'C3', 'D4', 'D5']

위의 z처럼 값을 만들려고 하고 있습니다.

x = [ "A", "B", "C", "D", "D" ]
y = [ 1, 2, 3, 4, 5 ]
z = map( lambda x,y : ( "%s%d" % (x, y) ), x, y )
print(list(z))

하나 건너 또는 둘 건너 루프 돌기 오피스/VBA/Office.JS

For~Next 루프를 배울 때 흥미로운 것중 하나는 Step이다.
For counter [ As datatype ] = start To end [ Step step ]
    [ statements ]
    [ Continue For ]
    [ statements ]
    [ Exit For ]
    [ statements ]
Next [ counter ]

기본적으로 1씩 증가하면서 루프를 돌지만,
For index As Integer = 1 To 5
    Debug.Write(index.ToString & " ")
Next
Debug.WriteLine("")
' Output: 1 2 3 4 5

Step 을 사용하면 주어진 값만큼 건너 뛰면서 루프를 돌게 된다. Step 2라고 하면 카운터 변수가 2씩 증가하면서 루프가 실행되고 Step -1하면 역방향으로 1씩 감소하면서 루프를 돌게 된다.
For number As Double = 2 To 0 Step -0.25
    Debug.Write(number.ToString & " ")
Next
Debug.WriteLine("")
' Output: 2 1.75 1.5 1.25 1 0.75 0.5 0.25 0

그런데, 평소 가지는 불만중 하나는 For Each구문이다. 이넘은 Step이라는 개념이 없다. 물론 개체를 루핑하는 것인데, 개체를 서수화하는 기준이 없으니 For Each가 할 수 없는 영역이다. 하지만 For Each 대신 Do Loop와 Offset 메서드를 가지고 이런 루프를 만들 수 있다. 다음은 A6~ A22 사이를 반복하면서 짝수 행의 주소만 찍는 것이다.
  Set rngBegin = Sheet1.Range("A6")
  Set rngEnd = Sheet1.Range("A22")
  Set rng = rngBegin
 
  Debug.Print rngBegin.Address
  Debug.Print rngEnd.Address
  step = 2
  Do
    Debug.Print rng.Address
    Set rng = rng.Offset(step, 0)
    If rng.Address = rngEnd.Address Then Exit Do
  Loop

앞서의 주제와 별개의 얘기이지만 예제를 만들면서 If rng.Address = rngEnd.Address Then Exit Do을 처음에는 If rng Is rngEnd Then Exit Do으로 사용하였다. 런타임에러는 없지만 예상과는 달리 rngEnd, 즉 A22에서 종료되지 않는 문제가 있었다.

가만히 생각해보니 If rng Is rngEnd 는 문제가 있다. A22를 둘 다 가리키지만 사실 이 개체변수는 같은 것은 아니다. 개체변수를 선언할 때
  Dim rng As Range
  Dim rngEnd As Range
 이 개체변수들의 메모리 주소는 스택에 있을 거구, 각각 다른 메모리 주소를 가질 것이다. 그러므로 A22라는 같은 값을 가질 수 있지만 , 주소는 달라서 같은 개체일 수는 없을 것이다.

다운로드후 폴더를 만들고 그안에 압축풀기 파이썬/쟁고

분석을 하는 도구로는 주피터 노트북이 가장 애용하는 도구인데, 다음은 압축된 분석 데이터를 다운로드하고 폴더를 만들고 그 안에 압축을 풀어 놓은 예이다.
import os
import tarfile
import urllib.request

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"

def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()

fetch_housing_data()
개인적으론 PC에 설치한 주피터 노트북보단 구글 코랩을 선호하는 편이다. 구글 코랩에서 실행하면 다음과 같다.

정확한 폴더위치는 구글드라이브의 /content/폴더 밑에 설치된다.
import pandas as pd

housing=pd.read_csv('/content/datasets/housing/housing.csv')

housing.head()



루프를 없애는 또 하나의 잡설

워크시트에 테이블 형태의 자료를 만드는 경우 , 미리 컬럼명을 만들어둔 템플릿 시트를 사용할 수 있는데, 때론 컬럼명을 동적으로 만들어 생성하는 경우도 있다. 컬럼명을 VBA코드로 출력하는 경우 , 각각의 셀 마다 컬럼명을 일일이 찍어줄 수도 있고, 코드가 너무 많아 스마트하게 배열에 미리 넣어두고 루프를 돌려 출력하기도 한다.

다음은 루프를 사용하지 않고 셀 범위에 여러 개의 값을 출력하는 예인데, 하나의 값을 출력하는 경우, VBA배열을 사용하는 경우, 엑셀 배열리터럴을 사용하는 경우, 두 배열을 혼합하고, 워크시트 함수 TODAY(), NOW()를 사용하는 경우를 보여준다.
' http://timebird.egloos.com/7455699
' Resize로 셀 영역을 확장하고 값을 출력
Dim myStr As String

myStr = "a string from vba"
With Sheet1.Range("D1")
.Offset(0, 1).Resize(1, 2) = "단일값"
.Offset(1, 1).Resize(1, 2) = Array("Price", "Delta")
.Offset(2, 1).Resize(3, 2) = [{"Zip", "22150";"City", "Springfield"; "State", "VA"}]
.Offset(5, 1).Resize(1, 3) = Array([Today()], [Now()], myStr)
End With

루프없애는 게 대세라던데... 오피스/VBA/Office.JS

파이썬이나 함수형 프로그래밍 덕분에 당연히 사용하던 루프를 쓰면 웬지 촌스러운 느낌이 든다. 루프없이 쓴다면 웬지 멋있어 보인다. 얼마 전 셀 영역을 대상으로 조건부 최소값, 조건부 최대값, 조건부 표준편차 등이 필요했었다. 당연히 이를 계산하려면 필터함수 또는 루프를 사용해야 한다. 루프를 사용하지 않으려면 엑셀의 워크시트함수를 이용해야 한다.

엑셀의 워크시트함수중 조건관련함수로는 AVERAGEIF, AVERAGEIFS, SUMIF, SUMIFS 등이 있다. 그러나 MINIF, MAXIF와 같은 함수는 아직 없다. 그런데 VBA의 WorksheetFunction개체에는 MinIf, MaxIf함수가 보인다. 그런데 사용해보니 에러가 난다.
흠, 이넘들이 시험적으로 해보았다가, 현재는 함수를 삭제했구만~



그래서 이를 수식으로 만들어 계산하도록 하였다. 다음은 A열의 값을 가져와서 B열에 1000을 곱한 결과를 출력하는 예이다.
Set rng = Sheet1.Range(Sheet1.Range("A6"), Sheet1.Range("A6").End(xlDown))
'// A열 값에 1000을 곱하여 B열에 출력
Sheet1.Range("B6").Resize(rng.Rows.Count, 1) = Sheet1.Evaluate(rng.Address & "*1000")
이때 계산은 Evaluate 함수를 사용한다 ( 참고 : http://timebird.egloos.com/7380115) 여기서 Evaluate 함수는 엑셀의 배열수식연산을 해준다.
간단한 예인데, 보다 실용적인 수식을 소개하자면 다음과 같다.
' 지원하지 않는 함수를 생성가능 - 조건부Min,Max,Stdev
'min = .Evaluate("MIN(IF(" & rngB.Address & "=" & Chr(34) & strSym & Chr(34) & "," & rngD.Address & "))")
'max = .Evaluate("MAX(IF(" & rngB.Address & "=" & Chr(34) & strSym & Chr(34) & "," & rngD.Address & "))")
'std = .Evaluate("STDEV(IF(" & rngB.Address & "=" & Chr(34) & strSym & Chr(34) & "," & rngD.Address & "))")


파일이 없으면 새로 만들고, 있으면 그냥 사용하기 오피스/VBA/Office.JS

엑셀 프로그래밍을 하다보면 외부파일(텍스트이건 바이너리이건)을 다룰 일이 그리 많치 않다. 다만 VBA의 조상님인 VB가 가진 위대한 유산을 물려받아 사용가능하다.
(Open) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/open-statement
(Input) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/inputstatement
(Line Input) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/line-inputstatement
(Get) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/get-statement
(Put) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/put-statement
(Print) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/printstatement
(Write) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/writestatement
(Close) https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/close-statement

오랜만에 텍스트 파일을 열어 작업을 하려는 데, 만일 파일이 없다면 새로 만들지만, 이미 파일이 있다면 오버라이트하지 말고 내용만 추가하려고 한다. 따라서 파일이 있는 지 없는 지 알아보는 절차를 줄여야 한다. 레거시인 Open문은 이게 가능한데, Append, Binary, Output, Random 모드중 하나로 파일을 오픈하면 된다.

하지만 레거시만 있는 것은 아니다. VBA는 외부라이브러리를 사용할 수 있다. 다음은 스크립팅 라이브러리를 이용하여 파일을 만들거나 여는 예이다. 매개변수중 Create를 True로 주면 앞서 말한 레거시 오픈문처럼 파일이 없으면 새로 만들고, 있으면 내용만 추가한다.
Sub fsoOpenTextFile()
'object.OpenTextFile(filename[, iomode[, create[, format]]])
' Arguments
' [...]
' Create
' Optional. Boolean value that indicates whether a new file can be created
' if the specified filename doesn't exist.
' The value is True if a new file is created, False if it isn't created.
' If omitted, a new file isn't created.
' Call OpenTextFile with the 3rd parameter set to True
' in order to create the file if it doesn't exist:


Dim fso, f
Dim fileName As String

fileName = "fsoOpenTextFile.txt"
Set fso = CreateObject("scripting.filesystemobject")
Set f = fso.OpenTextFile(fileName, 8, True)

f.WriteLine "TE+St"

f.Close
Set f = Nothing
Set fso = Nothing
End Sub

참고로 파일의 존재여부를 확인하려면
If Len(Dir(ThisWorkbook.Path & "\" & Date & "_calloptions.csv")) = 0 Then '파일이 없군~블라블라'
If Len(Dir(ThisWorkbook.Path & "\" & Date & "_calloptions.csv")) <> 0 Then '파일이 있네~블라블라'

INDEX(...MATCH...)(2) 오피스/VBA/Office.JS

Index(…Match…)는 의외로 쓸모가 많은 수식이다. 일단 첫 번째 컬럼에서만 검색이 가능한 VLOOKUP()의 단점을 극복하고, 직전 포스팅에서 모든 컬럼의 내용을 검색할 수 있다. 이번에는 검색결과가 여러 개 존재하는 경우 마지막 결과에 해당하는 데이터를 돌려주는 수식이다. 이것은 Oscar Cronquist가 쓴 Index Macth-Last – Last value포스팅을 옮긴 것이다.



위의 그림에서 찾는 값 'BB'에 해당하는 데이터는 네 가지이다(50, 100, 70, 10) 그중에서 가장 마지막에 나오는 '10'을 찾으려는 것이다.

수식의 핵심은 역시 MATCH()함수의 성질을 이용한 것이다.

MATCH(lookup_value, lookup_array, [match_type])


첫 번째 성질은 MATCH() 함수는 검색영역(lookup_array)중 에러부분은 무시한다는 것이다.
= MATCH(2, 1/(B3:B12=E3))
= MATCH(2, 1/({"AA";"BB";"CC";"BB";"DD";"BB";"EE";"GG";"VV";"BB"}="BB"))
= MATCH(2, 1/({FALSE; TRUE; FALSE; TRUE; FALSE; TRUE; FALSE; FALSE; FALSE; TRUE}))
= MATCH(2, 1/({0; 1; 0; 1; 0; 1; 0; 0; 0; 1}))
= MATCH(2, {#DIV/0!; 1; #DIV/0!; 1; #DIV/0!; 1; #DIV/0!; #DIV/0!; #DIV/0!; 1})

위의 식에서 '2'를 찾는 데, 에러 #DIV/0!은 무시한다는 것이다. (B3:B12=E3)의 결과는 비교연산인데, 그 결과는 TRUE 또는 FALSE를 돌려준다. TRUE는 1, FALSE는 0으로 TRUE인 경우 1/1이므로 결과는 1이 된다. FALSE인 경우 1/0으로 그 결과는 에러 #DIV/0!으로 표시된다. 전체적인 결과는 {#DIV/0!; 1; #DIV/0!; 1; #DIV/0!; 1; #DIV/0!; #DIV/0!; #DIV/0!; 1} 배열이 된다.

두 번째 성질은 ‘2’에 해당하는 데이터가 없다면 ‘2’보다 작은 값중 마지막 값의 위치(위의 수식에선 10번째에 위치한 ‘1’)를 돌려준다는 것이다.

INDEX함수는 MATCH함수가 돌려주는 위치값(10)에 따라 10번째 위치한 '10'을 돌려주게 된다.

INDEX(...MATCH...)(1) 오피스/VBA/Office.JS

엑셀이 많은 워크시트 함수를 제공하고, 우리는 VLOOKUP과 같은 함수 하나로 해결하면 좋겠지만 반드시 그리 쉽게 해결하지 못하는 경우가 생긴다. 이런 경우 내가 해결할 수 있는 수준으로 데이터의 구조를 바꾸거나 불가피하게 컬럼을 추가해서 보조적인 정보를 담아 그걸로 해결해야만 한다.

VLOOKUP함수의 경우 매우 편리한 함수이지만, 찾으려는 데이터가 반드시 첫 번째 컬럼에 있어야 한다는 점이 문제이다. 그래서 INDEX(...MATCH...)와 같은 형식으로 수식을 만든다.

다음의 문제는 VLOOKUP이나 기존의 INDEX(...MATCH...)로 해결하기 곤란한 문제이다. 요약하면 구성원을 입력하면 구성원이 어느 그룹소속인지를 표시하는 것이다(다만 같은 이름의 구성원은 아직 해결못한다)


F컬럼의 'C'를 입력하면 'C'는 'Group AC'소속임을 알려준다

다음은 G1에 입력한 수식이다. 수식은 행렬연산을 하고 그 결과는 A열 그룹중 하나를 가리킨다. 그리고 배열수식이므로 입력하고 CTRL+SHIFT+ENTER로 마무리한다

=INDEX(A1:A4,MATCH(1,MMULT(--(B1:D4=F1),TRANSPOSE(COLUMN(B1:D4)^0)),0))

다음은 'C'를 입력할 때 행렬연산에 대한 개요이다.

(B1:D4=F1)의 결과는 TRUE/FALSE 결과를 돌려주는데, --를 붙여 결과를 1과 0으로 바꾼다. 그리고 COLUMN()^0은 값을 모두 1로 바꾼다. 이를 TRANSPOSE()하여 MMULT()를 사용하기 위한 차원으로 바꾼다.

다음은 앞서와 비슷하지만 찾으려는 데이터 위치가 가로방향이다.

E컬럼에 'E'를 입력하면 'E'가 속한 'BEHK'를 돌려준다
=INDEX(A1:C1,1,MATCH(1,MMULT(TRANSPOSE(--(A2:C5=E1)),ROW(A2:C5)^0),0))


나만의 이벤트 정의하기 오피스/VBA/Office.JS

프로그래밍을 하다보면 개체가 제공해주는 이벤트가 불만족스런 경우가 생긴다. 혹시 나만의 이벤트를 만들 수 있지 않을 까… 물론 가능하다. 다음은 Userform의 콤보상자를 바탕으로 여기에 내 이벤트를 만드는 예이다. 콤보상자에 데이터를 입력하면 새로 입려된 데이터는 콤보상자의 리스트로 들어간다. 그런데 이미 입력한 데이터라면 입력을 취소하는 예이다.

[DataComboBox.cls]
Option Explicit

'// define DataComboBox's an event, 'ItemAdded'
Public Event ItemAdded(strValue As String, blnCancel As Boolean)

'// p_cboCombo is a variable that refers to Userform's combobox
Private p_cboCombo As ComboBox

Public Property Set ComboBox(cboCombo As ComboBox)
'// reference to Userform's combobox
If p_cboCombo Is Nothing Then
Set p_cboCombo = cboCombo
End If
End Property

Public Property Get ComboBox() As ComboBox
'// return combox
Set ComboBox = p_cboCombo
End Property

Public Function AddDataItem(strValue As String)
'// blnCancel is a variable that determine
'// whether strValue can be added

Dim blnCancel As Boolean

blnCancel = False

'// Trigger User-defined Event, ItemAdded
RaiseEvent ItemAdded(strValue, blnCancel)

'// if blnCancel is True, strValue not added
If blnCancel = False Then
Me.ComboBox.AddItem strValue
End If
End Function
다음은 Userform코드이다
[frmDataCombo.frm]
Option Explicit

'// declare DataComboBox with its event
Private WithEvents mdcbCombo As DataComboBox

Private Sub cmdAdd_Click()
Dim strValue As String

'// save data of combobox to strValue
strValue = Me.cboData.Text

'// call DataComboBox's AddDataItem method
mdcbCombo.AddDataItem strValue
End Sub

Private Sub UserForm_Initialize()
'// when this form begins, make DataComboBox into instance
Set mdcbCombo = New DataComboBox

'// connect object to frmDataCombo's combobox
Set mdcbCombo.ComboBox = Me.cboData
End Sub

Private Sub mdcbCombo_ItemAdded(strValue As String, blnCancel As Boolean)
Dim lngCount As Long
Dim lngLoop As Long
Dim strItem As String
Dim varItem As Variant

'// count of list
lngCount = mdcbCombo.ComboBox.ListCount

'// while the loop, detemine if new Item already added.
'// if blnCancel is True, new item already exists
For lngLoop = 1 To lngCount
strItem = mdcbCombo.ComboBox.List(lngLoop - 1)
If strItem = strValue Then
blnCancel = True
Exit For
End If
Next
End Sub

사용자를 배려한 한/영 입력 오피스/VBA/Office.JS

옛날 얘기인데, 각종 업무용 프로그램을 액세스로 개발하여 밥먹고 사는 사람이 있었는 데, 개발해놓은 걸 보니 날짜입력을 그냥 텍스트박스로 처리해두고 있었다. Calendar컨트롤을 사용하면 입력도 편리하고 제대로 일자 포맷에 맞게 입력되었는 지 체크하는 수고도 덜 수 있을 텐데 그리 하지 않았다.

이유를 물어보니 , 본인도 그런 점을 알지만 다년 간의 경험상 그런 컨트롤을 사용하다보면 프로그램도 무거워지고(액세스는 디비를 같이 담고 있으니 더욱 그럴 만하다) 결정적으로 불안정해져 실행중 프로그램이 죽는 경우가 있다고 한다. 잔뜩 입력하다가 프로그램이 죽는 것보단 불편한게 나을 것이다. 게다가 달력컨트롤이 기본이 아니어서 PC마다 없는 경우가 생길 수 있다. '가지 많은 나무에 바람 잘 날 없다'는 속담이 통하는 대목이다.

입력중 불편한 사항중 하나는 화면을 안보고 입력하다보니, IME가 한글이 아니라 영문상태 또는 그 반대의 경우이다. 한글입력이 필요하거나 영문입력이 필요한 시점에서 IME가 자동으로 입력모드를 변경해주는 배려를 해줄 수 있다. 다음은 API를 사용하여 입력모드를 바꾸는 예이다.
Private Declare Function ImmGetContext _
Lib "imm32.dll" (ByVal hwnd As Long) As Long
Private Declare Function ImmSetConversionStatus _
Lib "imm32.dll" (ByVal himc As Long, _
ByVal dw1 As Long, ByVal dw2 As Long) As Long
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long

Private Const IME_CMODE_NATIVE = &H1
Private Const IME_CMODE_HANGEUL = IME_CMODE_NATIVE
Private Const IME_CMODE_ALPHANUMERIC = &H0
Private Const IME_SMODE_NONE = &H0

Private Function GetAppHandle() As Long
Dim dVersion As Double

dVersion = Application.Version

If dVersion < 10 Then
GetAppHandle = FindWindow("XLMAIN", _
Application.Caption)
Else
GetAppHandle = Application.hwnd
End If
End Function

Private Sub demoChangeIME()
Dim hWndIME As Long
Dim hWndApp As Long

'// 엑셀의 핸들을 구함
hWndApp = GetAppHandle

'// 입력모드의 핸들을 구한다
hWndIME = ImmGetContext(hWndApp)

'// 영어 모드로 변환
Call ImmSetConversionStatus(hWndIME, _
IME_CMODE_ALPHANUMERIC, IME_SMODE_NONE)
InputBox "English 입력모드"

'// 한글 모드로 변환
Call ImmSetConversionStatus(hWndIME, _
IME_CMODE_HANGEUL, IME_SMODE_NONE)
InputBox "한국어 입력모드"

End Sub

Collection의 Key는 어디서 찾아야 하나? 오피스/VBA/Office.JS

이런 경우 VBA는 한심한 언어이다. 무슨 얘기인가 하면 Collection개체의 Add메서드를 사용할 때 딕셔너리처럼 {Key, Item}와 같이 Key를 같이 입력할 수 있다. 물론 Key는 유일해야 하므로 중복된 Key값은 받아주질 않는다.
그런데 Key를 입력하고 나면 Key값을 알 수 있는 방법이 없다. 도대체 Key를 만들어 입력해서 Key를 갖고 Item을 찾을 수 있게 해주면 뭐하나? Key을 돌려주는 프로퍼티나 메서드가 없는데…
이런 문제점에 대한 대안으로 Item에 Key를 포함해서 넣어주는 방법으로 해결한다. 즉 프로그래밍언어 차원이 아닌 개발방법으로서 몸으로 때우는 것이다.
Dim col As New Collection

col.Add Array("key#1", "first string"), "key#1"
col.Add Array("key#2", "second string"), "key#2"
col.Add Array("key#3", "third string"), "key#3"

'// print item's value in the collection
Dim item As Variant

For Each item In col
Debug.Print item(1)
Next

'// print item's key in the collection
Dim key As Variant

For Each key In col
Debug.Print key(0)
Next
위의 코드에서 Key#1, Key#2, Key#3는 종전처럼 입력하지만 이와 동시에 아이템 선두에 키값을 한번 더 입력하고 아이템 조회시 0번 인덱스를 Key로 삼아 사용하려는 것이다.

이런 VBA를 계속 쓰는 오피스개발자로서 눈물이 앞을 가린다.

[관련 포스팅]: VBA Collection

VBA로 만드는 알고리즘-리스트 오피스/VBA/Office.JS

데이터를 다룰 때 가장 많이 사용하는 것이 배열과 리스트인데, 오늘은 리스트 관련한 얘기를 하려고 한다. 리스트는 가장 인기있는 데이터구조인데, 배열과 달리 미리 크기가 정해져 있지 않고, 필요에 따라 데이터를 추가/삭제가 가능한 기능을 가지고 있다. VBA는 컬렉션(Collecton)개체라는 리스트를 제공하는 데(제공하는 것만도 감지덕지 하다), 그외 외부참조를 통해 딕셔너리나 닷넷의 ArrayList들이 사용가능하다.
프로그래밍하면서 가장 편리하다고 생각하는 것이 컬렉션개체인데, 역설적으로 기능이 정말 단촐해서 맘에 든다. 기능은 많은 데, 자주 사용하지 않는 것보단 단순하게 배우기 쉽고 활용도 좋다. 그렇지만 사람 욕심이라는 게, 내가 쓸 연장은 내가 만들고 싶은 욕구라는 게 있다.
다음은 별 기능은 없는 리스트 클래스이다. 내부구조를 보면 어차피 배열인데, 다만 배열보다 사용하기 편리한 메서드를 가지고 있다. 사실 언제 만들었는 지 모르겠다. 내가 만든 게 맞나 싶기도...(나의 기억은 조작되었을 지도...)
[CList.cls]
Option Explicit
Option Base 1

'// 사용자 정의 에러
'// 리스트의 상하한을 넘어 액세스하려는 경우 에러
Const ERR_LIST_BEYOND_BOUNDS As Long = 999

'// 리스트가 초기화되지 않은 상태에서 리스트를 사용하려는 경우 에러
Const ERR_LIST_NOT_INITIALIZED As Long = 998

Private Item_Data() As Variant
Private Item_Pointer As Long
Private Item_Init As Boolean

Sub Add(vNewValue As Variant)
Item_Pointer = Item_Pointer + 1
If Item_Init Then
ReDim Preserve Item_Data(Item_Pointer)
Else
ReDim Item_Data(Item_Pointer)
Item_Init = True
End If
Item_Data(Item_Pointer) = vNewValue
End Sub

Sub Remove(Index As Long)
Dim Imsi_Data() As Variant
Dim i As Long
Dim j As Long

If Item_Init Then
ReDim Imsi_Data(Item_Pointer - 1)
j = 1
For i = 1 To UBound(Item_Data)
If i <> Index Then
Imsi_Data(j) = Item_Data(i)
j = j + 1
End If
Next

Item_Pointer = Item_Pointer - 1
ReDim Item_Data(1 To Item_Pointer)

For i = 1 To UBound(Imsi_Data)
Item_Data(i) = Imsi_Data(i)
Next
End If
End Sub

Function Count() As Long
Count = Item_Pointer
End Function

Function Item(Index As Long) As Variant
If Item_Init Then
If Index < 1 Or Index > Item_Pointer Then
Err.Raise ERR_LIST_BEYOND_BOUNDS, "CList", "범위를 벗어났습니다"
Else
Item = Item_Data(Index)
End If
Else
Err.Raise ERR_LIST_NOT_INITIALIZED, "CList", "List()가 초기화되지 않았습니다"
End If
End Function

Private Sub Class_Initialize()
Item_Pointer = 0
Item_Init = False
Erase Item_Data()
End Sub

Private Sub Class_Terminate()
Erase Item_Data()
End Sub

다음은 위의 클래스모듈을 사용한 예이다.
Option Explicit

Sub demo_CListClass()
Dim lstDemo As New CList

'// 6개의 데이터를 입력한다
lstDemo.Add "Tumin"
lstDemo.Add "Lenski"
lstDemo.Add "Davis"
lstDemo.Add "Moore"
lstDemo.Add "Ogburn"
lstDemo.Add "Simmel"

'// 2번째 입력한 데이터를 삭제한다
lstDemo.Remove 2

Dim i As Long

'// 리스트의 처음부터 끝까지 보여준다
For i = 1 To lstDemo.Count
Debug.Print lstDemo.Item(i)
Next

Set lstDemo = Nothing
End Sub


[관련포스팅]없으면 빌려쓰는 ArrayList

구글앱스크립트로 실시간 데이터 업데이트하기 오피스/VBA/Office.JS

이번에는 구글 앱스크립트로 실시간 페이지를 업데이트하는 예를 소개하고자 하는 데, 직접 만든 건 아니고(아직 그럴 짠밥이 못된다) 깃헙(Github)에 좋은 프로젝트가 하나 있어 소개한다.
이 프로젝트는 일정간격으로 TheSimpsons Quote API(심슨이 들려주는 격언, https://thesimpsonsquoteapi.glitch.me/)에서 격언과 이미지를 가져오는 것이다. 그 결과물은 다음과 같다.

깃허브에 가면 소스코드를 볼 수 있는 데, 프로젝트는 매우 단촐하다. 서버사이드 코드 Code.gs와 클라이언트 사이드 Index.html(자바스크립트 포함)가 전부이다(이런 간단명료한 거 좋아한다) 클라이언트 사이드 소스코드에서
  • google.script.run.withSuccessHandler는 비동기 클라이언트 사이드 API인데, 서버 사이드 함수를 사용가능하게 해준다.
  • setInterval()함수는 업데이트 주기를 설정하는 클라이언트 사이드 API이다.

  • 그런데 직접 코드를 가져와 몇 번 돌려보고, 저자의 데모를 돌려보니 심슨격언을 가져오지 못한다. 이는 구글웹앱에 많은 HTTP 리퀘스트를 날리기 때문이다. 무료의 한계이다. (참고: Quotas for Google Services, https://developers.google.com/apps-script/guides/services/quotas)

    [Code.gs 서버사이드]
    function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('Index').setTitle('Realtime Data');
    }

    function randomQuotes() {
    var baseURL = 'https://thesimpsonsquoteapi.glitch.me/quotes';
    var quotesData = UrlFetchApp.fetch(baseURL, { muteHttpExceptions: true });
    var quote;
    var imageURL;
    if (quotesData.getResponseCode() == 200 || quotesData.getResponseCode() == 201) {
    var response = quotesData.getContentText();
    var data = JSON.parse(response)[0];
    quote = data["quote"];
    imageURL = data["image"];
    } else {
    quote = 'Random Quote Generator is broken!';
    imageURL = 'https://cdn.shopify.com/s/files/1/1061/1924/products/Sad_Face_Emoji_large.png?v=1480481055';
    }
    var randomQuote = {
    "quote": quote,
    "imageTag": '<img class="responsive-img" src="' + imageURL + '">'
    }
    return randomQuote;
    }

    function getTime() {
    var now = new Date();
    return now;
    }

    [Index.html내 자바스크립트 클라이언트 사이드]
    <script>
    // 격언 업데이트
    function onSuccess1(quotedata) {
    var quoteactual = document.getElementById('quote');
    quoteactual.innerhtml = quotedata.quote;
    var quoteimg = document.getElementById('quoteImage');
    quoteimg.innerhtml = quotedata.imagetag;
    }

    setInterval(function() {
    console.log("getting quote...")
    google.script.run.withSuccessHandler(onsuccess1).randomQuotes();
    }, 10000);
    </script>

    <script>
    // 시간 업데이트
    function onSuccess2(now) {
    var div = document.getElementById('time');
    var today = new Date();
    var time = today.getHours() + " : " + today.getMinutes() + " : " + today.getSeconds();
    div.innerhtml = time;
    }

    setInterval(function() {
    console.log("getting time...")
    google.script.run.withSuccessHandler(onsuccess2).getTime();
    }, 1000);
    </script>

    VBA로 만드는 알고리즘-큐 오피스/VBA/Office.JS

    큐는 선입선출형 알고리즘이다. 즉 먼저 들어온 넘이 먼저 나가는 구조이다. 극장이든, 식당이든, 코스트코이든 줄을 서서 기다리다 순서대로 입장하는 구조이다.
    _
    매일 우리가 만나는 큐는 키보드나 프로그램큐이다. 예전에 컴퓨터성능이 거지같았을 때 급한 마음에 입력한 입력한 글자들이 나중에 한꺼번에 입력되거나, 이것저것 클릭한 프로그램들이 당장 안 보이고 대충 실행한 순서대로 화면에 뜨는 기적을 보았을 것이다. 사용자의 입력을 기억해두는 일을 하는 것이 큐이다. 키보드에 글자를 입력하면 키값이 큐에 저장되고 윈도는 여유있을 때마다 키값을 읽어와 화면에 렌더링하는 것이다.
    _
    보통 큐에 데이터를 입력하는 것을 'Enqueue'라고 하고, 데이터를 꺼내는 것을 'Dequeue'라고 한다. 다음은 VBA로 만들어 본 큐이다. 두 개의 클래스모듈을 사용하는 데, 하나는 큐에 들어갈 데이터를 정의한 클래스 QueueItem, 나머지 하나는 큐를 구현한 Queue클래스이다.
    [QueueItem.cls]
    Public nItem As QueueItem
    Public Value As Variant

    Private Sub Class_Initialize()
    Set nItem = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set nItem = Nothing
    End Sub
    [Queue.cls]
    '// 큐의 머리와 꼬리를 가리키는 멤버변수 선언
    Dim qFront As QueueItem
    Dim qRear As QueueItem

    Public Sub Enqueue(newItem As Variant)
    '// 큐에 입력하는 메서드
    Dim qNew As New QueueItem

    qNew.Value = newItem

    '// 큐가 비어있다면 qNew는 '머리=꼬리'이고
    '// 비어 있지 않다면 이전 꼬리에 qNew를 입력하고
    '// qRear에 qNew를 지정한다
    If IsEmpty Then
    Set qFront = qNew
    Set qRear = qNew
    Else
    Set qRear.nItem = qNew
    Set qRear = qNew
    End If
    End Sub

    Public Function Dequeue() As Variant
    '// 먼저 큐가 비어있는 지 확인하고
    If IsEmpty Then
    Dequeue = Null
    Else '// 비어 있지 않다면 일단 큐의 머리값을 꺼낸다
    Dequeue = qFront.Value

    '// 그러나 큐에 데이터가 1개여서
    '// 머리=꼬리인 상태라면 앞서 데이터를 꺼낸 상황이라
    '// 이제 큐가 빈 상태가 된다.
    '// 그래서 머리,꼬리는 Nothing으로 만든다
    If qFront Is qRear Then
    Set qFront = Nothing
    Set qRear = Nothing
    Else '// 그러나 큐에 데이터가 1개 이상이어서
    '// 머리 != 꼬리인 상황이라면
    '// 머리의 다음 데이터가 머리가 된다
    Set qFront = qFront.pItem
    End If
    End If
    End Function

    Property Get IsEmpty() As Boolean
    '// 머리와 꼬리가 모두 Nothing 이면
    '// 비어 있다는 의미이다
    IsEmpty = ((qFront Is Nothing) And (qRear Is Nothing))
    End Property

    Property Get Peek() As Variant
    '// Dequeue를 하지 않고 머리 값을 얻는다
    If Not IsEmpty Then Peek = qFront.Value
    End Property

    Private Sub Class_Initialize()
    Set qFront = Nothing
    Set qRear = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set qFront = Nothing
    Set qRear = Nothing
    End Sub

    다음은 큐를 사용하는 데모이다.
    [QueueTest]
    Dim myQueue As New Queue

    With myQueue
    .Enqueue "A queue is "
    .Enqueue "a useful data structure "
    .Enqueue "in programming. "
    .Enqueue "It is similar to the ticket queue "
    .Enqueue "outside a cinema hall, "
    .Enqueue "where the first person "
    .Enqueue "entering the queue "
    .Enqueue "is the first person "
    .Enqueue "who gets the ticket."

    Debug.Print ">> Get the value of the front of queue :", .Peek

    Do While Not .IsEmpty
    Debug.Print .Dequeue()
    Loop

    Debug.Print ">> Is myQueue empty?", .IsEmpty
    End With

    신입직원 인사기록을 위한 작은 예제 오피스/VBA/Office.JS

    직원을 채용하면 인사담당자는 신입직원에 대한 기록을 만들게 된다. 이번에는 구글앱스크립트와 구글시트를 이용하여 이름, 직위, 아이디(#사번)를 워크시트에 입력하는 앱스크립트이다. 사실 이건 전혀 실무에 도움이 되지 않는 아주 초보적인 수준으로 학습을 위한 용도이다(그냥 시트에 손으로 입력하는 게 더 낫다)

    function addEmployee() {

    var ui = SpreadsheetApp.getUi();
    var ss = SpreadsheetApp.getActiveSpreadsheet();

    ss.setActiveSheet(ss.getSheetByName("Employee"));

    var answer1 = ui.prompt(
    '신규직원추가',
    '이름을 입력하세요',
    ui.ButtonSet.OK);
    var btnEmployeeName = answer1.getSelectedButton();
    var txtEmployeeName = answer1.getResponseText();

    if (btnEmployeeName == ui.Button.OK) {
    var answer2 = ui.prompt(
    '직책을 입력하세요',
    '직책:',
    ui.ButtonSet.OK_CANCEL);
    var btnEmployeePosition = answer2.getSelectedButton();
    var txtEmployeePosition = answer2.getResponseText();

    if (btnEmployeePosition == ui.Button.OK) {
    var answer3 = ui.prompt(
    '사번을 입력하세요',
    '사번(ID#):',
    ui.ButtonSet.OK_CANCEL);
    var btnEmployeeId = answer3.getSelectedButton();
    var txtEmployeeId = answer3.getResponseText();
    }

    if (btnEmployeeId == ui.Button.OK) {
    var sheet = SpreadsheetApp.getActiveSheet();
    sheet.appendRow([txtEmployeeName, txtEmployeePosition, txtEmployeeId]);

    } else if (btnEmployeeId == ui.Button.CANCEL) {
    ui.alert('작업 취소');
    }

    } else if (btnEmployeePosition == ui.Button.CANCEL) {
    ui.alert('작업 취소');

    } else if (btnEmployeeName == ui.Button.CLOSE) {
    ui.alert('작업 취소');
    }
    }

    VBA로 만드는 알고리즘-스택 오피스/VBA/Office.JS

    토지라는 물건을 하나 두고, 상업용,공업용,주거용,농업용 토지 등 우리는 여러 가지로 구분하여 사용하듯이, '메모리'라는 공간을 두고 필요에 따라 여러 알고리즘으로 활용한다. 스택,큐,리스트(링크드),트리 등등. 현대의 프로그래밍언어는 이런 알고리즘을 아예 지원을 해주지만, 예전에는 직접 만들어 사용하였다. 그래서 이런 알고리즘은 학교에서 배우거나 프로그래머 면접용으로 사용되거나 한다. 이런 알고리즘을 직접 만들 필요는 많이 줄었지만, 기초에 해당하는 것이라 알아둘 필요는 있어 보인다.

    스택(Stack)은 끝이 막혀있는 구조로 나중에 입력(push)된 항목이 먼저 나오는(pop) 후입선출형 알고리즘이다. 데이터를 입력하는 것을 PUSH라고 하고, 데이터를 꺼내는 것을 POP이라고 한다. POP을 하면 해당 항목은 스택에서 제거된다. 위의 그림에 '1'을 PUSH하고 , 다음에 '2'를 PUSH한다. 이제 POP을 하면 '2'가 스택에서 나오고 '1'만 남는다.

    프로그래밍을 하면서 만나는 스택은 재귀적 호출 함수를 만드는 경우이다. 함수가 자신을 다시 호출하는 구조인데, 호출할때마자 메모리주소를 스택에 쌓아두었다가, 연산이 끝나면 스택에 저장된 함수의 주소를 메모리에서 풀어주게 된다. 즉 마지막에 입력된 함수주소가 먼저 꺼내지는 것이다.


    VBA에서 스택을 구현하려면 클래스모듈을 사용하는 데, 이번 예에서는 두 개의 클래스를 만든다. 하나는 스택에 들어갈 항목을 정의하는 StackItem클래스이고, 나머지 하나는 스택의 메서드 PUSH, POP을 구현한 Stack클래스이다.
    [StackItem.cls]
    Public Value As Variant '// 항목을 저장할 멤버변수
    Public pItem As StackItem '// 이전항목을 위한 변수

    Private Sub Class_Initialize()
    '// 클래스 생성자로서 처음에 빈 스택이므로 Nothing이다
    '// PUSH를 할때 실행된다
    Set pItem = Nothing
    End Sub

    Private Sub Class_Terminate()
    '// 클래스 소멸자, POP을 할 때 실행된다
    Set pItem = Nothing
    End Sub
    다음은 본격적인 스택클래스이다
    [Stack.cls]
    Dim Top As StackItem '// 마지막 입력된 항목을 가리키는 StackItem 이다.

    Public Function Pop() As Variant
    '// 스택에서 마지막 입력된 항목을 꺼내는 메서드함수이다
    If Not IsEmpty Then
    '// 스택이 비어 있지 않다면 마지막 항목을 돌려준다
    '// 그리고 다음항목을 Top으로 지정한다
    Pop = Top.Value
    Set Top = Top.pItem
    End If
    End Function

    Public Sub Push(ByVal nItem As Variant)
    '// 스택에 데이터를 입력하는 PUSH 메서드이다.
    Dim NewTop As New StackItem

    NewTop.Value = nItem
    '// 기존 TOP을 예전항목으로 돌리고, 새 항목을
    '// TOP으로 설정한다
    Set NewTop.pItem = Top
    Set Top = NewTop
    End Sub

    '// 클래스모듈에서만 작성하는 프로퍼티 메서드이다
    '// 멤버변수값을 돌려주는 프로퍼티 메서드는 Get이고
    '// 멤버변수값을 설정하는 프로퍼티 메서드는 Set이다.
    Property Get GetTop() As Variant
    '// TOP을 돌려준다
    If IsEmpty Then
    GetTop = Null
    Else
    GetTop = Top.Value
    End If
    End Property

    Property Get IsEmpty() As Boolean
    '// 스택이 비어 있는 지 여부를 알려준다
    IsEmpty = (Top Is Nothing)
    End Property

    Private Sub Class_Initialize()
    Set Top = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set Top = Nothing
    End Sub
    다음에는 이를 사용하는 데모이다.

    A stack is a useful data structure in programming. It is just like a pile of plates kept on top of each other.
        Dim myStack As New Stack

    myStack.Push "kept on top of each other."
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "a pile of plates"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "It is just like"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "in programming."
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "a useful data structure"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "is"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "A stack"
    Debug.Print "Current Top Item :", myStack.GetTop

    Debug.Print
    Debug.Print "--------------------------------------"
    Debug.Print "It's time to pop out"
    Do While Not myStack.IsEmpty
    Debug.Print "Item just popped :", myStack.Pop()
    Debug.Print "Current Top Item :", myStack.GetTop
    Debug.Print
    Loop

    환율, 구글시트 그리고 앱스크립트 오피스/VBA/Office.JS

    사람이 아닌 소프트웨어를 이용하여 각종 정보를 가져오는 방법중 가장 쉬운 방법은 구글시트 함수를 이용하는 것이다. 그리고 이렇게 얻어온 정보를 단순히 시트를 열어보는 것으로 끝나는 게 아니라, 일정 간격으로 메일로 보내주는 작업까지 생각해볼 수 있다.

    이번 예에서는 구글시트에서 importxml(url, xpath_query)함수를 사용하여 네이버 환율정보를 가져오는 문서를 구성하고 정기적으로 메일로 전송하는 것이다.

    첫 번째 단계는 환율정보를 가져오는 시트를 구성하는 것인데, importxml()를 사용한다. importxml()에 넘겨줄 정보는 url과 xpath_query이다. 네이버 환율정보를 가져올 url과 xpath는 다음과 같다.
    가령 미국달러환율이라면 'http://finance~FX_' 뒤에 'USDKRW'를 붙이면 된다.

    http://finance.naver.com/marketindex/exchangeDetail.nhn?marketindexCd=FX_USDKRW

    그리고 xpath는 다음과 같다.

    '//*[@id="content"]/div[2]/table[1]/tbody/tr/td[1]'


    이 정보는 크롬브라우저나 파이어폭스브라우저의 개발자도구에서 알아낼 수 있다. 이렇게 간단하게 시트를 구성하면 열어볼때 마다 최근 환율을 가져오게 된다.

    두 번째 단계는 시트의 환율정보를 읽어와서 메일로 보내주는 것이다. 시트의 메뉴 [Tools]-[<>Script editor]를 클릭하여 다음과 같은 앱스크립트를 작성할 것이다.
    function myFunction() {
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('FX');
    var rng = sheet.getRange(6, 1, sheet.getLastRow(), 2).getValues();
    var date_today = new Date();

    var email_body = "날짜: "
    + Utilities.formatDate(date_today, Session.getScriptTimeZone(), "yyyy-MM-dd");

    for(var r=0;r<rng.length;r++){
    if(rng[r][0]!=='')
    email_body = email_body + '<br>'+rng[r][0]+' : '+ rng[r][1];
    }

    var email_to = "your mail@address";
    var email_subject = "NAVER Finance 환율";
    MailApp.sendEmail({
    to: email_to,
    subject: email_subject,
    htmlBody: email_body,
    });
    }
    세 번째 단계는 정기적으로 메일을 보낼 수 있도록 이벤트를 만든다. [Edit]-[Current project's triggers] 또는 도구모음의 아이콘을 클릭하여 이벤트 트리거를 만든다. 즉 예약작업을 걸어 두 번째 단계의 코드를 정기적으로 실행하는 것이다.


    Google Apps Script와 OfficeJs 오피스/VBA/Office.JS



    Google Apps Script라는 물건이 있는 데, 게을러서 북마크만 해두고 있다가 묵은지마냥 꺼내본다. 예전에는 단순한 VBA 대용품 정도로만 생각했는데, G Suite에 속한 제품을 위한 강력한 업무애플리케이션 개발환경이다.
    다음은 Google Apps Script 예이다.
    여기서도 Hello, World는 계속된다.
    /**
    * Creates a Google Doc and sends an email to the current user with a link to the doc.
    */
    function createAndSendDocument() {
    // Create a new Google Doc named 'Hello, world!'
    var doc = DocumentApp.create('Hello, world!');

    // Access the body of the document, then add a paragraph.
    doc.getBody().appendParagraph('This document was created by Google Apps Script.');

    // Get the URL of the document.
    var url = doc.getUrl();

    // Get the email address of the active user - that's you.
    var email = Session.getActiveUser().getEmail();

    // Get the name of the document to use as an email subject line.
    var subject = doc.getName();

    // Append a new string to the "url" variable to use as an email body.
    var body = 'Link to your doc: ' + url;

    // Send yourself an email with a link to the document.
    GmailApp.sendEmail(email, subject, body);
    }
    G Suite에 해당하는 것이 MS-Office365인데, MS의 현금상자인 만큼 MS도 머신러닝을 이용한 기능을 추가하는 등 많은 공을 들이고 있다. 그런데 Google Apps Script에 해당하는 업무애플리케이션 개발환경은 VBA가 아니다. OfficeJS API라는 물건인데, 이것 역시 자바스크립트 기반이다.



    호기심으로 몇 개의 토이수준 OfficeJS 앱을 만들어 보았는 데, 하다보면 자괴감이 든다. 'VBA라는 편리한 물건을 놔두고 왜 이 짓을 하는 거지?' 복잡한 개발환경 셋팅을 마치고 고인물 VBE 같은 전문개발툴도 없어 vscode와 같은 텍스트에디터로 작업을 한다. VBA로 할 수 있는 작업을 자바스크립트로 만드는 게 의미가 있나 싶었다(기껏 제공하는 게 'Script Lab'정도이다)

    '중앙집중적인 배포'라는 대의명분외에는 아무 장점이 안보인다. 그냥 자바스크립트를 이용한 웹개발을 오피스용으로 돌려놓은 것이다. OfficeJS에서 앵귤러,리액트,뷰,제이쿼리등을 사용할 수 있는 걸 보면, 전혀 새로운 것이 아닌 웹개발을 위한 자원을 오피스개발로 돌려놓은 것임을 알 수 있다. 웹보다 짠밥이 높은 오피스에 대세인 웹개발을 갖다 붙인 것이다. 덕분에 웹용으로 개발된 여러 소스를 응용하여 사용할 수 있을 것으로 보인다. 가령 웹크롤링으로 데이터를 가져오는 등...

    Google Apps Script은 MS의 OfficeJS보단 나아 보인다. 구글답게 공돌이스런 미적감각(비하아님)을 가진 개발환경(script.google.com) 을 제공해준다. 이것 역시 구글오피스의 제품을 커스터마이징하여 비지니스 애플리케이션으로 이용하는 게 주된 목적이다. 그런데 오피스제품말고 웹사이트도 만들 수 있다. 특히 sites.google.com에 붙이면 더욱 그럴 듯하다. 별도의 웹서버를 장만할 필요없이 웹프로그램을 돌릴 수 있다. 웹프로그래밍을 위해 호스팅등을 사용할 필요가 없어 보인다(쟁고django로 웹프로그램 만들려다가 웹서버세팅에 진을 빼버린 걸 생각하면 이건 신세계이다. 오로지 프로그래밍만 신경쓰면 되는 것이니...)


    Google Apps Script와 OfficeJs간의 몇 가지 공통점을 나름 느낀대로 써보았는데, 가장 큰 공통점은 둘다 그닥 인기가 없어 보인다. RPA 시대에 나름 중요한 업무용 앱 개발수단으로 보이는데, 많은 전문개발자들의 관심을 받지 못하는 듯 하다. 이럴 바엔 MS는 오피스 스크립팅 언어로 파이썬이나 돌아가게 해주는 게 나아 보이는 데..아니면 쓸데없는 OfficeJs말고 일렉트론같은 라이브러리를 이용한 스탠드얼론 제품을 만들 수 있는 자바스크립팅을 해주는 게 나을 것 같다.

    망해라 django, 아니지 흥해라 django 파이썬/쟁고

    프로그래밍이 생업이 아니다보니까 뭘 새로 해볼려면 항상 초보자이다. django를 새로 셋팅하려는 데, 까먹은 게 넘 많다. 설치는 그나마 쉽다. 각종 웹서버 설정이 골치 아프다. 개발서버는 그나마 나은데, 실제 서버에서 하면 부닺치는 문제가 많다.

    병아리반 학생이 중딩반가서 고생하는 꼴이다. 이러다 보니 django말고 flask, fastapi 등을 해볼까 하는 생각도 든다. 경험상 안된다고 돌아가려고 해봐야 좋을 꼴을 본 적이 없는 것 같다. 희망사항이지만 워드프레스와 같은 CMS에 필요한 앱을 생성하고 코딩하는 호스팅이 어디 있을 것 같은데... 그래서 장고 호스팅 업체를 검색해보았다.
    Top 6 Django Compatible Hosting Services를 읽어보니 디지털오션이 괜찮아 보인다. 다음 번에 이짓을 또한다면 여기로 해봐야 겠다.

    호스팅은 나중에 하고 어드민 화면의 CSS를 못읽어들이는 문제가 발생하였다. 일단 다음 파일에서 static에 대한 alias와 디렉토리를 만든다
    [/etc/httpd/conf.d/vhost.conf]
    <VirtualHost *:80>
    WSGIScriptAlias / /opt/rpa/rpa/wsgi.py
    WSGIDaemonProcess rpa python-path=/opt/rpa/rpa
    <Directory /opt/rpa/rpa>
    <Files wsgi.py>
    #Order allow, deny
    #Allow from all
    Require all granted
    </Files>
    </Directory>
    Alias /static/ /opt/rpa/static/
    <Directory /opt/rpa/static>
    #Order allow,deny
    #Allow from all
    Require all granted
    </Directory>

    </VirtualHost>

    [etc/httpd/conf/httpd.conf]
    Alias /static/ /opt/rpa/static

    [settings.py]
    ...
    ALLOWED_HOSTS = ['*']
    ...
    STATIC_ROOT = os.path.join(BASE_DIR, "static")

    [wsgi.py]
    import os
    import sys

    from django.core.wsgi import get_wsgi_application

    path='/opt/rpa'
    if path not in sys.path:
    sys.path.append(path)

    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rpa.settings')
    application = get_wsgi_application()
    그리고 settings.py에서 STATIC_ROOT 설정하고 , manage.py collectstatic를 실행한다. 자세한 내용은 How to setup static files in Django에서 볼 수 있다.

    기타 실서버에서 설정하다보니 걸리는 몇 가지 애로사항은 슈퍼유저를 만들려니 sqlite디비 권한문제가 있다. attempt to write a readonly database, unable to open database file같은 에러 메시지를 만나기도 한다. 파일이나 폴더에 대한 권한 문제인데, 사용자 apache에게 권한을 부여해야 한다.
    chown apache:apache /opt/rpa
    chown apache:apache /opt/rpa/db.sqlite3


    웹 개발이 대세인 시대라 어쩔 수 없이 내가 필요해서 하지만, 데스크탑 앱 개발이 훨씬 좋았다.

    ndarray의 head, tail 구하기 파이썬/쟁고


    데이터프레임을 사용하는 경우 데이터의 앞뒤를 head와 tail을 사용하여 구하는 데, ndarray타입인 경우 head, tail이 따로 없다. 이런 경우 슬라이싱 연산자를 사용한다.
    import numpy as np

    # 데이터 머리부터 두 개의 데이터를 가져온다
    head = np.array((1,2,3,4,5))[:2]

    # 중간 3,4번째 데이터를 가져온다
    body = np.array((1,2,3,4,5))[2:4]

    # 꼬리에서 두 개의 데이터를 가져온다
    tail = np.array((1,2,3,4,5))[-2:]
    print(head)
    print(body)
    print(tail)

    # 슬라이싱 말고 아래와 같이 제한적이긴 하지만 일부 데이터를 가져 오는 데
    # 아래의 *는 나머지를 의미한다
    head, body, *tail = np.array((1,2,3,4,5))
    print(head)
    print(body)
    print(tail)


    게으른 자를 위한 코딩 학습법, 손이 게으른 거지, 머리가 게으른 게 아니다. C/C++

    프로그래밍 연습은 컴퓨터로 하지만, 반드시 그래야 하는 것은 아니라고 생각한다. 괜히 부팅하고 나서, 포털의 연애뉴스 분석하기, 스팸메일이나 구독메일이나 가득찬 메일상자 확인하기, 배그나 롤의 세계로 가서 '자식들~아직도 이러고 있네~' 하며 총알로 안부 챙기기 등 시간(낭비)여행을 하게 되기 때문이다.
    이런 유혹에 흔들리지 않고, 아까운 전기 낭비하지 않기 위해, 프로그래밍 퀴즈를 풀어 보는 것도 좋다. 뇌를 CPU로 다운그레이드하여 결과를 예상해보는 것도 좋을 것이다. PC없는 코딩연습이라는 관점에서 동질적인 방법인데, 예전에 종이에 코딩을 해보는 연습을 추천한 것도 보았는데, 사실 그런 정성을 들이는 자는 절대 게으른 자는 아니다(좋은 방법이지만 오래할 방법은 아니다)




    퀴즈중에 위의 같은 문제가 나왔는데, 힌트는 printf가 화면에 출력하는 것만 하는 게 아니라 출력한 문자의 갯수를 돌려주는 것이다. 언제 이런 기능이 생겼니? 그래서 정답은 비디오에...

    개눈에는 똥만 보인다 파이썬/쟁고


    요 며칠 해외뉴스를 크롤링하는 코드를 만지다 보니 환율도 욕심이 생겨 네이버 파이낸스에서 환율을 스크래핑해볼까 했다. 그런데 파이썬에는 환율정보를 갖다 바치는 라이브러리 forex-python이 있다. 한 가지만 생각하다 멍청하게 생고생할 뻔했다. 암튼 텔레그램봇에 환율조회 추가 완료!
    그런데 이거 간단한 라이브러리라 사용하기 어렵진 않은 데, 환율을 가져보는 속도가 좀 느린 단점이...그나저나 안 본 사이에 캐나다환율 많이 올랐네. 젠장~ 은행넘들 수수료 주기 아까워 미루다 보니...
    01 from datetime import datetime
    02 from forex_python.converter import get_rate

    03 def get_fx():
    04 t = datetime.today()
    05 currency = ['USD', 'JPY', 'EUR', 'GBP', 'CAD', 'HKD', 'AUD', 'SGD']
    06 fx=[]
    07 for c in currency:
    08 fx.append('{0} {1:0.2f}'.format(c, get_rate(c, 'KRW', t)))
    09 return fx

    10 fx=get_fx()

    11 for f in fx:
    12 print(f)
    06~08 사이의 행은 fx=['{0} {1:0.2f}'.format(c, get_rate(c, 'KRW', t)) for c in currency] 와 같이 한 개의 행으로 대체가능하다.

    [고전] The Tao of Programming

    위(魏)나라의 조정에 한 프로그래머가 있었다. 위후(魏候)가 프로그래머에게 묻기를

    " 회계 프로그램과 운영체제중에 설계하기 쉬운 것은 어느 쪽이요? "
    " 운영체제이옵니다."
    하고 프로그래머가 답했다. 위후는 믿을 수 없다는 표정으로 반문하였다.

    "어찌 회계 프로그램처럼 하찮은 것이 운영체제의 복잡함을 능가한다는 말이요? "

    "그렇지 않사옵니다. 회계 프로그램을 설계할 때는 프로그래머가 서로 다른 생각을 지닌 사람들을 조율해야만 하옵니다. 회계 프로그램이 어떻게 작동해야 하며, 보고서는 어떤 양식으로 출력되어야 하며, 세법에는 어느 정도로 충실해야 하는지 각양각색으로 떠들기 마련이옵니다.

    반면에 운영체제의 외양에는 아무도 신경쓰지 않사옵니다. 운영체제를 설계할 때는 프로그래머는 기계와 아이디어의 가장 단순한 조화만 추구하면 되옵나이다. 이것이 운영체제가 설계하기 쉬운 까닭이옵니다. 옛말에는 이를 일컬어 사공이 많으면 배가 산으로 간다고 하옵니다.
    "

    크게 감탄한 위후가 미소를 지으며 다른 질문을 던졌다.

    "그렇구려, 그런데 어느 쪽이 더 디버깅하기 쉽소? "

    프로그래머는 아무런 답도 하지 못했다.

    VBA에서 람다함수 만들기 오피스/VBA/Office.JS

    블로그 단골손님, 우왕님의 천재적인 아이디어에 힘입어 VBA에서 람다함수를 만들어 보았다. 진짜 람다함수는 아니고, 엑셀의 이름기능과 VBA의 EVALUATE함수를 이용한 것이다.

    이번에 만드는 람다함수는 간단하다. 변수X와 변수Y 사이의 난수값을 생성하는 RANDBETWEEN()함수를 이용할 것이다.
    먼저 그림과 같이 엑셀에서 미리 이름을 만들어 둔다(코드로 만들수도 있지만)

    LAMBDA_RANDXY는 람다함수의 이름이라고 생각하고 수식 =RANDBETWEEN(ParamX, ParamY)은 람다함수의 본체이다.
    그리고 ParamX와 ParamY는 변수X, 변수Y이다. ParamX와 ParamY의 값을 람다함수 LAMBDA_RANDXY가 받아서 RANDBETWEEN(ParamX, ParamY)에 전달한다고 생각하면 된다.

    다음은 이 세 개의 이름을 가지고 람다함수 비스무리한 것을 구현한 것이다(내 눈을 바라봐~ 넌 람다함수가 보이고~)
    1    Dim nmLambda As Name
    2 Dim paramX As Name
    3 Dim paramY As Name

    4 Set nmLambda = ThisWorkbook.Names("LAMBDA_RANDXY")
    5 Set paramX = ThisWorkbook.Names("ParamX")
    6 Set paramY = ThisWorkbook.Names("ParamY")

    7 paramX.Value = 12
    8 paramY.Value = 20

    9 Debug.Print Evaluate(nmLambda.Value)
    1~3번 행은 워크북에 저장된 이름을 저장할 개체변수이다.
    4~6번 행은 선언한 개체변수에 이름을 할당한다.
    7~8번 행에서 변수X, 변수Y에 해당하는 ParamX와 ParamY의 값을 변경하다.
    9번 행에서 Evaluate()함수를 이용하여 LAMBDA_RANDXY의 값, =RANDBETWEEN(ParamX, ParamY)를 계산한다.

    이번에는 스칼라 값이 아니라 배열을 다루는 람다함수를 만들어 본다. 함수를 만들기에 앞서 먼저 풀어야할 썰이 하나 있다.
    엑셀의 배열은 컬럼은 컴마(,)로 구분하고 , 행은 세미콜론(;)으로 구분하여 만든다. 가령 아래와 같은 3x3 행렬이 있다고 하자.

    엑셀은 내부적으로 이것을 {1,4,7;2,5,8;3,6,9} 로 처리한다.
    그래서 VBA의 배열을 람다함수에 던져주려면 위와 같은 형식의 문자열을 만들어야 한다.

    다음은 SUMPRODUCT()함수에 배열을 던져주고 계산결과를 받아오는 람다함수이다.
    01    Dim nmLambda As Name
    02 Dim arrX As Name
    03 Dim arrY As Name
    04 Dim x(4), y(4)

    05 Set nmLambda = ThisWorkbook.Names("LAMBDA_SUMP")
    06 Set arrX = ThisWorkbook.Names("arrX")
    07 Set arrY = ThisWorkbook.Names("arrY")

    08 x(0) = 1990
    09 x(1) = 29290
    10 x(2) = 36990
    11 x(3) = 9790
    12 x(4) = 8590

    13 y(0) = 12490
    14 y(1) = 22990
    15 y(2) = 8990
    16 y(3) = 8190
    17 y(4) = 10990

    18 arrX.Value = "={" & Join(x, ";") & "}"
    19 arrY.Value = "={" & Join(y, ";") & "}"

    20 Debug.Print arrX.Value
    21 Debug.Print arrY.Value

    22 Debug.Print Evaluate(nmLambda.Value)
    01~04행은 변수를 선언한 부분이다. 04 행에 5칸짜리 배열이 선언되었다.

    05~07 행은 저장한 이름을 개체변수에 저장하는 것이다.
    08~ 17 행은 배열에 값을 넣는 숙달된 조교의 노가다이다. 실무에서 이런 일을 하지 않지만...
    18~19 행은 Join함수를 이용하여 배열의 각 원소를 세미콜론(;)으로 구분된 문자열로 만드는 것이다. 엑셀의 행렬 만드는 규칙을 따르는 것이다.
    20~21 행은 입력한 값을 한번 찍어 본다.
    22 행에서 드디어 저장한 배열을 가지고 SUMPRODUCT를 계산한다. 그러나 지난 번 포스팅(VBA 배열에 앙심을 품은 자)에서 언급한 바와 같이 'LAMBDA_SUMP'는 엑셀에서 사용가능한 이름개체이므로 다음과 같이

    Debug.Print [LAMBDA_SUMP]

    로 표현해도 된다. 뭔가 VBA같지 않은 이상한 결과이다.

    이걸 잘 활용하면 코드를 깔끔하게 - 나만 고칠 수 있어서 함부로 날 못짜르는 - 만들 수 있을 것 같다.

    결론은 '우왕님, 땡큐베리감사~'

    (추가)
    람다함수는 임시로 만드는 간단한 함수의 형태이므로 좀 더 완벽을 기하기 위해 혹시 같은 이름이 있다면 닥치고(On Error Resume Next) 지우고(Names(~~~).Delete), 만들어(Names.Add) 계산한 후([LAMBDA_SUMP]), 다시 증거를 인멸(Names(~~~).Delete)하는 완전범죄를 해본다. 변수도 전혀 사용하지 않는다
        On Error Resume Next
    With ThisWorkbook
    .Names("LAMBDA_SUMP").Delete
    .Names("arrX").Delete
    .Names("arrY").Delete

    .Names.Add "LAMBDA_SUMP", "=SUMPRODUCT(arrX,arrY)"
    .Names.Add "arrX", Sheet2.Range("J5:J7")
    .Names.Add "arrY", Sheet2.Range("K5:K7")

    Debug.Print [LAMBDA_SUMP]

    .Names("LAMBDA_SUMP").Delete
    .Names("arrX").Delete
    .Names("arrY").Delete
    End With
    물론 이것이 파이썬의 그것에 비해 부족하다. filter(), map(), reduce()와 같은 함수에 사용되는 파이썬을 따라가기는 역부족이다. 그러나 VBA에서도 이를 함수로 만들어 볼 수 있다(이건 준비중...)

    아리랑뉴스 시간입니다. 파이썬/쟁고

    이글루스 어느 블로거가 아리랑 뉴스를 훔쳐 가고 있다는 속보입니다. 이 블로거는 허접한 실력으로 아리랑 뉴스의 헤드라인 뉴스 링크와 제목을 훔쳐갔다는 소식입니다(이 뭐 하는 짓인가?...현타가 오는)

    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import urllib

    html = "http://www.arirang.com/Index.asp?sys_lang=Eng"

    def link(html):
    req = urllib.request.Request(html, headers={'User-Agent': 'Mozilla/5.0'})
    response = urllib.request.urlopen(req).read()
    return response

    bs0bj = BeautifulSoup(link(html), "html.parser")
    article_li = bs0bj.find('article', {'id':'ai_lastestnews'}).find('div',{'class':'ai_slide'})
    article_link = article_li.find_all('a')
    for a in article_link:
    print('http://www.arirang.com{0}'.format(a.get('href')) ,a.text.strip())


    암튼 그렇답니다. 다음은 장물입니다.
    http://www.arirang.com/News/News_View.asp?nseq=248216 S. Korea, Japan to resume talks next month on export controls
    http://www.arirang.com/News/News_View.asp?nseq=248228 S. Korea's GDP to grow 2.0% in 2019, 2.3...
    http://www.arirang.com/News/News_View.asp?nseq=248227 S. Korea's ruling party, gov't, Blue Hou...
    http://www.arirang.com/News/News_View.asp?nseq=248219 Korea's 3 major indicators of industrial...
    http://www.arirang.com/News/News_View.asp?nseq=248218 Bank of Korea keeps key interest rate un...
    http://www.arirang.com/News/News_View.asp?nseq=248204 Trump makes surprise Thanksgiving trip t...
    http://www.arirang.com/News/News_View.asp?nseq=248212 S. Korea, Japan to resume talks next mon...
    http://www.arirang.com/News/News_View.asp?nseq=248203 Hong Kong denounces Trump for signing 'H...

    자바스크립트가 멈추는 날 삽질의 추억

    자바스크립트는 간혹 애매모호한 문법으로 욕도 먹지만 인터넷세상을 지배하는 언어이다. 클라이언트와 웹서버부터 일렉트론을 이용하여 vscode, atom 에디터같은 이용한 데스크톱 응용프로그램을 만드는 걸 보면 놀라울 정도이다.

    오늘도 여느 때처럼 자료를 찾아 산기슭을 어슬렁거리는 하이에나처럼 남들의 자료를 찾아다니던 중, 이미지가 뿌연 안개처럼 보이는 사이트가 신경을 긁는다. 애꿎은 애드블럭을 꺼보았지만 범인은 아니었다.

    이게 무슨 현대미술 추상화가의 작품인가?


    '그러고 보니 네이버 블로그에 이런 현상이 생기네~ 애드블럭탓인가'

    무신경하게 넘어가던 여느 날과 달리 뭔가 머리에 하나가 스친다. 얼마 전에 마우스 클릭을 막아둔 블로그 땜에 자바스크립트 실행을 막아두었는데, 네이버 블로그도 자바스크립트 실행을 막아둔 웹 사이트중 하나였다.

    자바스크립트가 멈추었더니 생각하지 못한 부작용이 생긴다. 자바스크립트를 차단한 이유는 이 블로그들이 복사를 막아 두었기 때문이다. 개발자 도구를 켜서 복사할 수 도 있는 세상인데, 이런 치사한...암튼 예전에는 이상한 블로그들이 많았다. 남의 자료를 가져다 그대로 복붙해놓고, 자신의 자료인 양 포스팅을 복사해가지 못하게 막아두는 어불성설같은 ... 지금은 그런 양심이가 이상한 블로그가 많이 보이지 않는다.

    과연 원인이 무엇인가?
  • 양심이가 이상한 블로그는 전혀 줄지 않았다. 다만 내가 예전처럼 자료를 찾지 않아, 그런 블로그를 예전만큼 보지 못했기 때문이다.
  • 양심이가 이상한 블로그는 줄었다. 텍스트가 아니라 비디오의 시대라서, 이런 일을 하는 사람들이 줄었기 때문이다.
  • 양심이가 이상한 블로그는 줄었다. 사람들의 저작권 개념이 예전과 달리 선진적으로 발전하였다.



  • 처음으로 써보는 crontab 파이썬/쟁고

    최근에 텔레그램봇을 만들면서 든 생각중 하나는 공부를 제대로 안하고 대충 했구나 하는 생각이다. 그냥 무심하게 개발관련 글들을 보았는데, 봇이 개인을 상대로 메시지를 보내는 경우도 있고, 채널에 메시지를 보내는 것이 있는 데, 미처 그런 생각을 하지 못하고 넘겨왔다. 또 많은 블로그나 정보에서 보여주는 내용이 봇의 구현인데, 속 시원한 답을 얻지 못하는 것은...

    봇이 상시적으로 서비스를 하려면 웹서버같은 물건이 필요한 거 아닌가? 이런 서비스를 하려면 무엇을 해야 하는 지 알려주는 내용이 없을 까?

    아무튼 간신히 시험삼아 시간을 뿌려주는 봇과 이를 받아주는 채널을 만들었다. 봇을 원하는 대로 만들기에 앞서 전체적인 프로세스를 체크하기 위한 목적이다.

    이제 봇을 일정시간마다 뿌려주려면 crontab을 이용해야 한다. 서버가 필요한 건 아니고, 그냥 백그라운드로 스크립트를 실행하면 되는 것이다. 사실 리눅스에서 스케줄링을 하는 도구로 cron이라는 서비스(데몬)가 해주는 걸 알고는 있었다. 그러나 실제 그걸 사용할 일이 나에게는 없었다. 이번에 알게 된 것은 스케줄에 걸어둔 프로세스에 에러가 있으면, 얘가 아무 것도 안한다는 점이다. 파이썬 스크립트에 에러가 있어도 안되고, >>를 사용하여 로그를 만드는 부분의 에러가 있어도 안된다는 점이다.
    [참고자료][CentOS] Linux 반복 예약작업(스케줄러) - Crontab

    그리고

    python my_script.py

    형식으로 하면 정상동작하는 넘이 쉘스크립트로 작성해서 실행하면 "ImportError No named module~" 에러를 낸다. 여러 가지 구글검색을 통해 결국 성공한 방법은 경로를 꼼꼼히 적어두는 것이다.
    다음은 crontab의 내용과 쉘스크립트의 내용이다.
    [root@localhost tbot]# crontab -l
    5 23 * * * /usr/bin/python3.6 /opt/tbot/teli.py >> /opt/tbot/finews.log
    [root@localhost tbot]# ./start_bot.sh
    [root@localhost tbot]# ll
    total 12
    -rw-r--r-- 1 root root 28 Nov 21 23:24 finews_11212324.log
    -rwxrwxrwx 1 root root 84 Nov 21 23:24 start_bot.sh
    -rwxr-xr-x 1 root root 337 Nov 21 11:24 teli.py
    [root@localhost tbot]# cat start_bot.sh
    /usr/bin/python3.6 /opt/tbot/teli.py >> /opt/tbot/finews_$(date +\%m\%d\%H\%M).log


    야 이넘아! 고만 보내라~나 시계 있다

    Beta Is Dead; Rock Is Dead 증권



    투자자는 위험을 기피하는 속성이 있다. 그러나 더 많은 보상이 있다면 더 큰 위험을 수용할 수 있다. 위험이 작으면 작은 보상에도 만족한다. 그러면 위험을 어떻게 측정하나? 마코위츠와 샤프가 내린 정의는 변동성이다. 주식이나 포트폴리오의 가치가 오르락 내리락 하는 정도가 변동성이고 , 변동성이 크면 위험이 큰 것이다. 구체적으로 변동성은 표준편차로 측정하거나 베타로 측정한다.

    흔히 위험과 수익률의 관계를 그래프로 그리면 우상향할 것으로 생각한다. 왜냐하면 우리의 머리속엔 ‘High Risk, High Return’이라는 명제가 박혀 있기 때문이다. 그러나 실제 아무런 상관관계를 보이질 않는다. 변동성이 크다고 해서 수익률이 좋은 것은 아니다. 반대의 경우도 마찬가지이다.

    변동성의 측정, 즉 척도도 여러 논란이 있다. 가령 공매도를 한 투자자를 제외한 투자자들은 하락장의 변동성을 좋아하진 않는다. 오히려 상승장의 변동성은 반가워 한다. 상승장의 변동성이 클 수록 좋아한다. 변동성으로 대변되는 위험을 기피하는 속성과는 맞지 않는다. 변동성의 척도 가운데 하나인 베타는 많은 연구 결과 베타는 미래의 변동성을 예측할 수 없다는 결론을 얻었지만 지금도 많이 쓰인다.

    1992년 유진 파마는 같은 시카고대학 교수인 케네스 프렌치와 함께 위험과 수익을 주제로 논문을 발표한다. 이 논문은 1963년부터 1990년까지 9,500개의 주식을 대상으로 주가순자산비율, 주가수익비율, 시가총액이 가장 낮은 주식들의 수익률이 장기적으로 가장 높았으며, 베타가 높은 주식과 낮은 주식을 비교해보면 수익률이 거의 비슷하다는 결론을 얻는다. 그들의 결론은 베타로 측정한 주식의 위험은 수익률을 예측할만한 지표가 아니라는 것이다.

    파마와 프렌치의 연구결과 주가순자산비율과 주가수익비율이 가장 낮은 주식의 수익률이 가장 높고, 대형주보다 소형주 비율이 높다. 주가의 수익률과 높은 상관관계를 보이는 것은 베타를 비롯한 위험지표가 아니다.

    "Beta as the sole variable in explaining returns on stocks is dead."


    ‘하나의 위험지표로 수익률을 예측할 수 없다’는 논문 이후 1년이 지나 파마는 위험을 계산할 때 베타외에 가치척도(high book to market)와 다른 기준(market capitalization, smaller caps are riskier) 을 고려해야 한다는 새 위험이론을 발표했다

    VBA 아니고 엑셀에서 람다함수 만들기 오피스/VBA/Office.JS

    파이썬 람다함수는 익명함수로서  변수에 저장할 수 있다. 아래의 예는 람다함수인데, 만들어진 함수는 변수 yAxb에 저장한다. 이 함수는 값을 x에 받아 2를 곱하고 다시 1을 더하여 돌려준다.

    >>> yAxb = lambda x : 2*x + 1
    >>> yAxb(4)
    9

    자바스크립트(ES6)에서는 일명 화살표함수라는 것이 있다. 파이썬의 람다함수처럼 이 역시 함수, 변수 yAxb에 저장한다.

    // ES6
    const yAxb = x => 2 * x + 1;
    console.log(yAxb(4)); // 9

    그런데, VBA도 할 수 없는 것을 엑셀이 비슷한 것을 만들 수 있다. 엑셀의 '이름'기능을 이용하는 것이다. 엑셀의 이름기능은 A1, B2, C4:E7 과 같은 의미없는 셀 주소 대신 의미있고 기억하기 좋은 이름을 부여주는 것이다.

    워크시트의 각 셀주소는 일종의 메모리 영역이다. 프로그래밍에서 변수를 선언하면 자동으로 메모리에 변수를 붙여 주는 것인데, 셀주소가 메모리 주소이고, '이름'은 변수를 지정한 것으로 비유할 수 있다.

    파이썬과 자바스크립트(ES6)에서 변수에 함수를 저장한 것 처럼 엑셀은 '이름'에 수식(함수포함)을 지정할 수 있다. 다음 그림을 보면

    'yAxb'라는 이름을 주고 '=Sheet1!$B2*2+1'라는 수식을 정의하고 있다. B2라는 셀이 위의 파이썬, 자바스크립트 코드의 변수 x에 해당한다. 그래서 셀안에 '=yAxb'를 입력하면 B열의 각 행 값을 읽어와서 계산을 하게 된다.


    1 2 3 4 5 6 7 8 9 10 다음