반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함




지난번 사용한 테이블에다 Option1 과 Option2 필드를 추가했습니다.

위 그림처럼 Option1 의 0.00 으로 필터링 한 후 Aug 필드의 합계를 구하겠습니다.


    Dim seqRng As Range
    Dim option2Rng As Range
    Dim firstRow As Long
    Dim lastRow As Long


며칠 전에 만든건데 지금와서 소스를 보니까 저도 생소하네요.


일단 Seq 필드와 option2 필드 Range를 담을 변수를 만들었습니다.

제 기억으로는 Option2는 사용되질 않는데 아마 Option1을 담을 Range인데 타이핑을 잘 못 했나 봅니다.


그리고 합계 구할 Row 의 범위를 알기 위해 firstRow와 lastRow를 Long 타입의 변수로 정의했습니다.


   Range("B4").Select
   
    ' Link Click
    Selection.Hyperlinks(1).Follow NewWindow:=False, AddHistory:=True
   
    On Error Resume Next
        ActiveSheet.ShowAllData
    On Error GoTo 0
   
    Cells.Find(What:="State", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
   
    ActiveCell.End(xlDown).Activate
   
    lastRow = ActiveCell.Row


첫번째 줄은 그냥 아무 셀이나 Select 한 거구요.

그 다음을 보니까 링크를 클릭했습니다. 이건 지난번에 만들었던 소스코드에도 그대로 나오는 겁니다. 이렇게 하면 링크 걸린 페이지로 가겠죠? 링크 걸린 페이지는 위에 보이는 테이블이 있는 페이지 입니다.


그 다음은 일단 이전에 작업할 때 필터링이 된게 있을 지도 모르니까 이 필터링 된 걸 풀고 모든 값을 보이도록 하는 코드 입니다.

그런데 필터링이 안 돼 있으면 이 부분에서 에러가 나니까 이 에러를 나지 않도록 처리도 했습니다.


다음은 State 라는 값을 가진 Cell을 찾아서 Activate 시키고 여기서 쭉 밑으로 데이터가 있는 마지막 Cell 까지 값니다.

그리고 그 셀의 Row를 lastRow에 담습니다.


   Cells.Find(What:="Seq", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
       
    Set seqRng = ActiveCell
   
    Cells.Find(What:="Option2", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
       
    Set option2Rng = ActiveCell
   
    Cells.Find(What:="Option1", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate

    ActiveSheet.Range(seqRng, option2Rng).AutoFilter Field:=option2Rng.Column - seqRng.Column, Criteria1:="0.00"
   
    Cells.Find(What:="Aug", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Offset(1, 0).Activate
       
    firstRow = ActiveCell.Offset(1, 0).Row

이 부분은 fistRow를 구하는 부분까지 잘랐는데 뭔 코드가 이렇게 긴지 모르겠네요.

한번 볼까요?

우선 Seq 라는 값을 가진 셀을 찾아서 그 셀을 seqRng에 담습니다. (이건 왜 만들었을까? 지금 봐서는 잘 이해가 안 가네요.)

하여간 그 다음에는 Option2로 검색해서 그것을 option2Rng에 담습니다.

이것도 왜 담는건지 모르겠네요.

다음은 Option1이라는 값을 가진 Cell을 찾아서 그 Cell을 Activate 시킵니다.

앗 잠깐만요...


그 아래에 필터링 하는 부분이 있군요.

거기서 seqRng와 option2Rng가 사용됩니다.

괜히 그 값을 구해서 변수에 담은게 아니었군요.

이렇게 되면 Option1을 Find로 찾아서 Activate 시킨 부분은 아무 의미 없는 코딩 같은데요.

한번 계속 볼까요?


일단 필터링이 있는 범위를 Range로 정하고 나서 몇번째 필드를 필터링 할 건지 알려주고 필터링 할 값은 0.00으로 정해 줍니다.


이렇게 되면 Option1 필드 중 0.00을 가진 놈들로만 필터링이 됩니다.


다음에 하는 일은 Aug라는 값을 가진 셀을 찾아서 그 아래 셀을 Activate 시킵니다.

여기까지 보니까 확실하게 Option1이라는 값을 가진 셀을 Find 한 부분은 필요가 없는 부분이란게 확실 하네요.


이제 Aug셀의 바로 아래 Row를 firstRow로 지정했습니다.


이렇게 되면 firstRow와 lastRow가 모두 구해 졌습니다.



    ActiveCell.Offset(lastRow - ActiveCell.Row, 0).Activate

    ActiveCell.FormulaR1C1 = "=SUBTOTAL(9,R[" & firstRow - lastRow & "]C:R[-1]C)"
   
    MsgBox firstRow - lastRow

    Selection.Copy
   
    Sheets("New_Sheet").Select
    Range("D5").Select
    Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
        :=False, Transpose:=False
    Range("C5").Select
    Application.CutCopyMode = False
    ActiveCell.FormulaR1C1 = "Aug Filter 0"
    Range("D8").Select


첫번째 줄은 Aug 필드의 맨 마지막 Row 바로 밑을 Activate 시킨 겁니다.

거기다가 구한 합계를 넣기 위해서죠.

그 밑에 줄에서 SUBTOTAL을 사용하는데요.

이 함수는 필터링된 값만 가지고 합계를 내기 위해 사용했습니다.

그 안에 9는 합계를 낼거라는 의미이구요.

끝에 R[-1]C는 현재 Activate 된 셀의 바로 윗줄을 말하는 겁니다.

Aug 컬럼 중 합계를 구할 맨 밑줄이 되죠.

그 바로 전에는 Aug 셀의 바로 밑셀을 구하는 겁니다.

Aug 셀의 바로 밑셀부터 맨 밑셀까지를 정해 준거구요. 그 값들을 SUBTOTAL로 합계를 내게 됩니다.


그러면 그 합계가 맨 위에 있던 테이블 그림 처럼 Aug 필드의 맨 밑에 있는 셀 바로 밑에 표시 됩니다.


그 다음에는 그 값을 Copy 하구요. New_Sheet로 가서 적당한 위치에 복사한 값을 붙여넣기를 합니다.


이제 이정도 까지만 하면 제가 업무 하면서 만들고 싶었던 매크로를 만들 수 있겠네요.

필요한 기술은 다 Research 한 것 같습니다.


저는 이제 제가 필요한 매크로를 만들어야 겠습니다.


처음에는 새로운 Sheet를 만들었고 그 다음에는 특정 필드의 값들의 합계를 구했고 이번에는 필터링한 후 특정 필드의 값들의 합을 구했습니다.


지금까지 만든 소스코드는 Research 목적으로 만든거라서 실제 사용하려면 Refactoring 과정을 거쳐야 합니다.

불필요한 코드들은 없애고 좀 더 간단하게 만들 수 있는 로직은 간단하게 고치고 반복적으로 사용되는 로직은 따로 Component 를 만들어서 사용할 수 있도록 고치고 등등이요.


혹시 이 Refactoring 과정을 거치지 않은 소스코드라도 참고하시고 싶으신 분들을 위해서 제가 작업한 파일을 아래에 올려 놓을께요.


ForTesting.xlsm



반응형

Comment


이번에는 특정 컬럼의 숫자를 더하는 코드를 만들어 보겠습니다.




제가 그냥 만들어본 표 입니다. (State는 제가 있는 Minnesota 의 약자이구요. 그 안의 값들은 그냥 임의로 넣은 겁니다.)


여기서 Aug 컬럼의 숫자들을 더한 후 New_Sheet 라는 Sheet 에 그 더한 값을 넣겠습니다.


여기서 조건을 문서를작성하다 보면 양식은 위와 같이 되지만 값의 Cell 값은 바뀔 수 있는 상황입니다.

예를 들어 어떤 사람은 이 표를 엑셀 쉬트의 A1 서부터 작성하는 사람이 있고 또 어떤 사람은 한 한 B3 정도부터 작성하는 사람이 있습니다.


그러면 각 값들의 위치들도 그에 따라 달라질 건데요.


이럴 경우에도 구애 받지 않고 Aug 컬럼 값들의 합계를 구해 보겠습니다.


그러기 위해 우선 Aug 의 위치를 알아 내겠습니다.

그리고 바로 그 밑의 Cell 값을 받고 맨 마지막의 Cell 값을 알아 냅니다.

그래서 그 Cell 들의 값을 더하겠습니다.

여기서 어떤 달들은 중간중간 혹은 맨 처음이나 맨 마지막 값이 비어 있을 수 있습니다.

그래서 맨 마지막 Cell 값은 항상 값이 채워져 있는 Seq 나 State로 가서 값이 있는 맨 마지막 Cell을 알아 내겠습니다.



우선 이런한 작업을 할 때 값을 담을 변수들을 만들겠습니다.


    Dim firstCell As Range
    Dim lastCell As Range
    Dim state1stCell As Range
    Dim sumCell As Range

    Dim lastRow As Long


firstCell과 lastCell 은 Aug 컬럼의 첫번째와 마지막번째 Cell 값입니다. 

state1stCell 은 state 컬럼의 첫번째 Cell 값이구요.


sumCell은 Aug 컬럼의 값들을 모두 더한 값을 넣을 변수 입니다.


그리고 lastRow는 Long 타입인데요. State 컬럼에서 값이 있는 맨 마지막 Row를 구해서 넣을 변수 입니다.


지난번에 올린 엑셀 파일을 보면 저 테이블은 두번째 Sheet에 있고 첫번째 Sheet 에 저 Sheet로 가는 링크가 있습니다.


우선 이 링크를 클릭하는 코드를 작성하겠습니다.


    Range("B4").Select
    Selection.Hyperlinks(1).Follow NewWindow:=False, AddHistory:=True
   
    Cells.Find(What:="Aug", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate


일단 첫번째 sheet 에서 이 함수를 실행시킨다고 가정하구요.

아무곳에나 커서를 놓습니다. (여기서는 B4).

그리고 첫번째 링크를 클릭합니다.

그러면 두번째 Sheet로 이동했을 겁니다. 여기서 Find 명령어로 Aug라는 필드를 찾아서 그 Cell 을 Activate 합니다.

(위 코드는 Macro를 실행해서 만든 코드 입니다.)


여기까지 하면 아래와 같이 Aug 컬럼이 선택 됩니다.



그러면 Aug 컬럼의 첫번째 Cell 을 구할 수 있습니다.


   ActiveCell.Offset(1, 0).Activate
   
    ''''''' Set FistCell
    Set firstCell = ActiveCell


현재 활성화된 Cell (여기서는 Aug) 에서 한칸 아래로 내려가서 그 Cell을 firstCell 에 담았습니다.

이제 firstCell 에는 62.28 이 있는 Cell 이 담겨져 있습니다.


    Cells.Find(What:="State", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
       
    ActiveCell.Offset(1, 0).Activate
    Set state1stCell = ActiveCell
    state1stCell.Activate


그 다음에는 Find 를 사용해서 State 가 있는 Cell을 찾아서 활성화 시킵니다.

이제 그 활성화 된 Cell 바로 아래 Cell을 state1stCell 에 담았습니다.

그리고 그 state1stCell을 다시 활성화 시킵니다.

이제 State 컬럼의 첫번째 Row 에 있는 Cell 이 활성화 돼 있을 겁니다.


   state1stCell.End(xlDown).Activate
    Set statelastCell = ActiveCell
   
    lastRow = statelastCell.Row


이제 End(xlDown)을 이용해서 State 컬럼의 맨 마지막 Row로 이동합니다.

그리고 그 row의 값을 lastRow에 담습니다.


    firstCell.Activate

    firstCell.Offset(lastRow - firstCell.Row, 0).Activate
   
    ''''''' Set LastCell
    Set lastCell = ActiveCell


이제 값이 있는 맨 마지막 Row의 값을 알았으니 다시 Aug 컬럼으로 가겠습니다.


firstCell 에서 State 컬럼에서 얻은 맨 마지막 Row 값을 이용해서 Aug의 맨 마지막 Row로 갑니다.

그리고 이 Aug의 맨 마지막 Row를 lastCell에 담습니다.


이러면 Aug의 맨 첫번째 Cell 과 맨 마지막 Cell을 얻게 됐습니다.

이것만 알면 Aug의 첫번째 Cell 에서 마지막 Cell 에 걸쳐 있는 값의 총계를 알아 낼 수가 있습니다.


   ''''''' Set sumCell
    Set sumCell = lastCell.Offset(1, 0)
   
    Range(firstCell, lastCell).Activate
   
    '''''' Get Sum of Aug
    sumCell.Value = Application.WorksheetFunction.Sum(Selection)
    sumCell.Activate
    Selection.Copy


구한 합계는 Aug의 맨 마지막 Cell의 밑에 Row에 적어 넣을겁니다.

그래서 sumCell은 lastCell의 바로 밑의 Cell로 설정해 두었습니다.

위에서도 사용했던 Offset을 여기서도 사용했습니다.


이제 Aug의 첫번째 셀에서부터 마지막 셀까지 범위 설정을 하고 그 합을 sumCell에 넣었습니다.

그리고 그 값을 Copy 했습니다.


이제 클립보드 내에는 Aug 컬럼의 값들의 합이 담겨져 있습니다.


    Sheets("New_Sheet").Select
    Range("D3").Select
    Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
        :=False, Transpose:=False
    Range("C3").Select
    Application.CutCopyMode = False
    ActiveCell.FormulaR1C1 = "Aug Sum"
    Range("D6").Select


이제 New_Sheet로 이동해서 D3 위치에 복사한 값을 붙여넣기 합니다.

Selection.PasteSpecial은 Macro를 이용해서 구한 코드 입니다.

여기서는 단순하게 붙여넣기를 한게 아니라 그 Value 를 붙여넣기 했습니다.

제가 필요한 것은 그 셀의 공식이 아니라 값이니까요.


이제 복사해 넣은 값의 왼쪽 셀(C3)로 가서 AugSum 이라고 글자를 써 넣습니다.




이렇게 해서 Aug 컬럼의 모든 값을 더해서 New_Sheet 에 복사해 넣는 코드가 완성됐습니다.


이제 저 양식만 맞추면 표를 어느 위치에 작성하든 Aug 필드의 값들의 합을 구할 수가 있게 됐습니다.



전체 소스코드는 아래와 같습니다.


Sub getSumofRows()
'
' getSumofRows Macro
'
' Keyboard Shortcut: Ctrl+Shift+B
'

    Dim firstCell As Range
    Dim lastCell As Range
    Dim state1stCell As Range
    Dim sumCell As Range
    Dim statelastCell As Range

    Dim lastRow As Long
   
    Range("B4").Select
    Selection.Hyperlinks(1).Follow NewWindow:=False, AddHistory:=True
   
    Cells.Find(What:="Aug", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
       
    ActiveCell.Offset(1, 0).Activate
   
    ''''''' Set FistCell
    Set firstCell = ActiveCell
   
    Cells.Find(What:="State", After:=ActiveCell, LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
        , SearchFormat:=False).Activate
       
    ActiveCell.Offset(1, 0).Activate
    Set state1stCell = ActiveCell
    state1stCell.Activate
   
    state1stCell.End(xlDown).Activate
    Set statelastCell = ActiveCell
   
    lastRow = statelastCell.Row
   
    firstCell.Activate

    firstCell.Offset(lastRow - firstCell.Row, 0).Activate
   
    ''''''' Set LastCell
    Set lastCell = ActiveCell
   
    ''''''' Set sumCell
    Set sumCell = lastCell.Offset(1, 0)
   
    Range(firstCell, lastCell).Activate
   
    '''''' Get Sum of Aug
    sumCell.Value = Application.WorksheetFunction.Sum(Selection)
    sumCell.Activate
    Selection.Copy
 
    Sheets("New_Sheet").Select
    Range("D3").Select
    Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
        :=False, Transpose:=False
    Range("C3").Select
    Application.CutCopyMode = False
    ActiveCell.FormulaR1C1 = "Aug Sum"
    Range("D6").Select
End Sub


제가 엑셀 프로그래머가 아니라서 코드가 깔끔하지는 않을 겁니다.

배우시는 분들은 이걸 좀 더 깔끔하게 다듬으시면 더 공부가 잘 될 겁니다.


ForTesting.xlsm


필요하신 분은 위 파일 다운 받으시면 안에 소스코드가 있습니다.

Cntr-Shift-A 는 지난 글에서 다뤘던 Sheet 만드는 매크로이고 Cntr-Shift-B 는 그 새로 만든 매크로에 Aug 컬럼의 값들의 합계를 구해 넣는 매크로 입니다.

둘 다 첫번째 Sheet 에서 실행 하셔야 됩니다.


반응형

Comment


이번에는 새로운 Sheet 를 생성하는 코드를 작성해 보겠습니다.


일단 Excel file 내의 sheet 들의 맨 마지막 위치에 새로운 sheet 를 생성해서 New_Sheet 라고 이름을 붙이겠습니다.


다만 이 New_Sheet 라는 sheet 가 있으면 새로운 sheet를 생성하지 않고 이 New_Sheet 가 있다는 Message Box를 보이겠습니다.


코드는 아래와 같습니다.


Sub CreateNewSheet()
'
' CreateNewSheet Macro
'
' Keyboard Shortcut: Ctrl+Shift+A
'
    Dim sh As Worksheet, flg As Boolean
    For Each sh In Worksheets
        If sh.Name Like "New_Sheet" Then flg = True: Exit For
   
    Next
   
    If flg = True Then
        MsgBox "New_Sheet Exist!"
    Else
        ActiveWindow.ScrollWorkbookTabs Position:=xlLast
        Sheets.Add After:=Sheets(Sheets.Count)
        Sheets(Sheets.Count).Name = "New_Sheet"
    End If
End Sub


이 함수의 이름은 CreateNewSheet() 로 했습니다.

그다음두 줄은 주석입니다.

이 매크로를 생성할 때 단축키는 Ctrl+Shift+A 로 만들었습니다.


이것은 지난 글에서 다룬 Macro 만들기 할 때 단축키를 넣은 것입니다.

아직 코드 내에서 단축키를 만드는 것은 모릅니다.


첫째 줄에 나오는 DIM 은 변수에 공간을 할당하는 statement 입니다.

Worksheet 형태인 sh 라는 변수를 생성했습니다.

그리고 Boolean type 의 flg라는 변수도 만들었구요.


그리고 for loop문이 나옵니다.

Worksheets 내에 있는 Worksheets들을 차례로 불러내서 어떤 일을 할 겁니다.

이때 불러 낼 때는 해당 Worksheet를 sh라는 변수에 담을 거구요.

이것을 하려고 첫째줄에 sh라는 변수 공간을 만들어 둔 겁니다.


For 문 안을 보면 sh에 담겨있는 Worksheets의 이름을 가져와서 "New_Sheet" 라는 문자와 비교를 하네요.

이 문자가 포함돼 있는 sheet 이름이 있으면 flg 라는 Boolean type 변수에 True를 할당합니다.

그리고 For 문을 Exit 하구요.

만약에 없으면 Next 가 실행되서 그 다음 Worksheet를 살펴봅니다.


이 엑셀 파일에 있는 Worksheet 들을 다 돌았으면 For 문에서 빠져 나오게 되겠죠.



For Loop 문의 신택스는 아래와 같습니다.

For counter [ As datatype ] = start To end [ Step step ]
    [ statements ]
    [ Continue For ]
    [ statements ]
    [ Exit For ]
    [ statements ]
Next [ counter ]


자세한 것은 링크를 따라가서 보시구요.


보니까 For Each Loop는 좀 다르군요.


신택스는 아래와 같습니다.

For Each element [ As datatype ] In group
    [ statements ]
    [ Continue For ]
    [ statements ]
    [ Exit For ]
    [ statements ]
Next [ element ]


이것도 자세한 것은 링크를 따라가서 보세요.



이제 For Each Loop 에서 flg 에 대한 값이 할당이 됐습니다.

그 다음 if 문에서는 이 flg 가 True 일 경우에는 "New_Sheet Exist!" 라는 글자가 있는 MsgBox를 띄우고 flg가 True가 아닐 경우에는 그 다음일을 합니다.


우선 현재엑셀 파일에서 Worksheet이 맨 마지막으로 가구요.

그 다음에 맨 마지막 Sheet 다음에 새로운 Sheet를 만듭니다.

그리고 그 맨 마지막에 있을 Sheet의 이름을 "New_Sheet"라고 지어 줍니다.


If 문의 신택스는 이렇습니다.

' Multiple-line syntax:
If condition [ Then ]
    [ statements ]
[ ElseIf elseifcondition [ Then ]
    [ elseifstatements ] ]
[ Else
    [ elsestatements ] ]
End If

' Single-line syntax:
If condition Then [ statements ] [ Else [ elsestatements ] ]


이렇게 하면 처음에 원했던 대로 엑셀 파일의 맨 마지막 Sheet 에 Work_Sheet 라는 새로운 Sheet를 만듭니다.

기존에 이 Sheet가 있으면 안 만들구요.


ForTesting.xlsm


제가 작업한 파일을 올려 놓으니까 필요하신 분들은 참조하세요.


한가지 Tip 으로 말씀 드릴것은 Macro를 쓰려면 엑셀파일을 Macro가 가능한 버전으로 Save As를 해야 합니다.

그러면 저렇게 확장자가 xlsm 이 됩니다.

그렇게 하지 않으면 기껏 작업한것을 모두 날릴 수 있습니다.


반응형

Comment


직장 생활을 하다 보면 엑셀을 잘 사용하면 일을 훨씬 편하게 할 수 있겠다라는 생각을 많이 합니다.


그래서 한번 엑셀 프로그래밍을 배우려고 합니다.


그동안 마음만 먹고 있었는데 모처럼 연휴에 여유가 있어서 마음만 먹었던 것을 시작 하려고 합니다.


우선 첫번째로 Macro 를 사용하는 방법을 보겠습니다.




일단 1에서 9까지 적어 넣고 11번째 줄에서는 그 수를 모두 더했습니다. (=SUM(B2:B10))

그 오른쪽은 랜덤 함수를 이용해서 수를 랜덤하게 불러오도록 했습니다. (=RAND()*B2)


이제 이 숫자와 수식을 오른쪽에 복사해 넣을 건데요. 이 과정을 매크로로 저장하겠습니다.




첫번째로 View - Macro - Record Macro 를 선택합니다.


그러면 위와 같은 화면이 뜨는데요. 매크로 이름을 넣고 단축키를 정한 다음에 OK 버튼을 클릭합니다.

그러면 그 이후부터 진행되는 과정은 전부다 저장이 될 겁니다.

OK 버튼을 누르고 아까 작성했던 숫자와 수식들을 복사해서 그 옆에 붙여 넣겠습니다.


위와 같이 복사해 넣고 다시 View-Macro로 가셔서 이번에는 Stop Recording을 선택합니다.

그리고 나서 오른쪽에 있는 부분을 지워보세요.

아까 저는 제가 만든 매크로(Macrotest01)의 단축키를 Ctrl-Shift-a 로 했으니까 이 단축키를 누르겠습니다.


여러분은 여러분이 정한 단축키를 눌러 보세요.

그러면 아까 범위를 정하고 복사한 다음에 복사해 넣을 위치에 와서 복사한 내용을 붙여넣기 한 과정이 자동으로 빠르게 반복되면서 값이 복사될 겁니다.




View-Macro로 가셔서 View Macro를 선택하시면 위와 같은 화면을 보실 수 있습니다.

방금 전에 만들었던 Marcotest01이 있죠?

여기서 Run 버튼을 누르셔도 해당 매크로가 실행 됩니다.

지금 프로그래밍을 하나도 하지는 않았지만 사실은 Macro를 Recoding 하는 과정에서 Excel 이 프로그래밍을 자동으로 한 겁니다.


여기서는 Visual Basic이 사용되는데요.

이 과정이 어떻게 프로그래밍이 됐는지 보시죠.


소스를 보시려면 View-Macro로 View Macro를 선택하신 후 Step Info 단추를 누르셔도 되고 그냥 엑셀쉬트에서 Alt-F11 을 누르셔도 됩니다.




Alt-F11을 누르시면 위와 같이 Visual Basic Editor가 나옵니다.

Module1을 더블 클릭하면 소스가 보이는데요.

Visual Basic은 잘 모르지만 소스를 한번 보죠.


Sub라고 돼 있는데 이건 잘 모르겠구요. 자바에서 말하는 메소드나 C에서 말하는 함수 정도 되는것 같습니다.

메소드 이름이 Macrotest01() 이지요. 아까 매크로 만들때 정했던 이름입니다.

그리고 주석을 보면 Ctrl+Shift+A 가 단축키라고 나와 있구요.

그 밑에서부터 메소드의 내용인데요.

우선 B2부터 C11 까지 선택을 합니다.

그리고 선택한 부분을 Copy 하구요.

그리고 E2 부분으로 가서 Paste를 합니다.

그 다음에 나오는 Application.CutCopyMode=False는 Copy 할 부분을 설정하면 엑셀에서는 그 부분이 점선으로 표시되는데 Esc를 누르지 않는한 그 부분이 계속 클립보드 최 우선순위에 있게 되죠.

여기서는 그 선택한 것을 해제 하는 겁니다.

그 다음 줄은 End Sub으로 메소드의 끝을 알립니다.

자바에선 {} 로 시작과 끝을 처리했는데 비주얼 베이직에서는 Sub과 End Sub으로 메소드 시작과 끝을 처리하나 봅니다.


오늘은 첫시간으로 엑셀의 매크로 기능 활용하는 법과 해당 매크로의 비주얼 베이직 소스 보기를 살펴 봤습니다.


반응형

Comment

  1. simuyoung 2013.12.25 21:07

    저도 코드를 잘 모를 경우 주로 이런식으로 매크로로 코드를 확인 한 후에, 작업을 하곤 합니다.

    • 솔웅 2014.01.01 16:01 신고

      전 이제 시작이예요.
      새해 복 많이 받으시고 원하시는 일 다 성취하는 한해 되세요.

  2. 014년 새해 아침을 맞이하고 있습니다.올 한해에도 사랑과 행복이 넘치는 시간이 되시길 기원 합니다.늘 분에 넘치는 관심에 다시 한번 감사의 말씀을 올립니다.

    • 솔웅 2014.01.01 16:02 신고

      댓글 감사합니다.
      모르세님도 새해 건강하시고 모든 일 순조롭게 잘 진행되는 한해 되세요.
      일년 내내 평온한 한해 보내세요..


Corona SDK 가 facebook 계정을 통해서 아래 글을 공유했습니다.

좋은 글이라서 정리해 둡니다.

안드로이드 개발자가 유념해야 할 사항 10가지 인데 주로 SEO 와 관련이 있습니다.

Corona SDK 카테고리로 분류할 지 안드로이드 카테고리로 분류할 지 고민하다가 SEO 카테고리로 넣었습니다.








Top 10 Google Play Developer Mistakes


What you need to know about app store optimization and SEO for the Google Play Store and Android.
Google Play Store 와 안드로이드에 대한 앱 스토어 최적화와 SEO를 위해 알아두어야 할 것들



Top 10 Google Play Developer Mistakes



#1: Using Unfriendly SEO App Names

It’s important to have clear and descriptive app names. For improved discoverability, consider long names that combine branding and description of the app.


분명하고 앱을 잘 설명하는 앱 이름을 짓는 것은 중요하다. 



#2: Not Optimizing App Description

The first lines of the description are the most important from user and SEO perspective. First tell the user what the app is about then list out the features and awards.


사용자나 SEO적인 관점에서 보면 앱을 설명할 때 그 첫번째 문장이 아주 중요하다. 유저에게 이 앱이 무엇인지 얘기하고 나서 그 기능들을 설명하라.



#3: Not Having a Video Trailer

A short, keyword optimized video can help your app’s SEO and gain user adoption. Users find the video as the most convincing feature of the app detail page.


짧고 핵심을 설명하는 비디오는 유저들에게 매력적으로 다가가고 여러분 앱의 SEO에 도움이 될 것이다. 유저들은 그 비디오를 앱의 자세한 기능을 설명하고 있다는 확신을 줄 것이다.



#4: Forgetting About Personalization

Allow users to easily rate, review, and +1 your app. Google shows recommendations based on 80 signals including: location, circles, and +1 within circles.


유저가 여러분 앱에 대해 쉽게 rate, review 그리고 +1을 하도록 하라.



#5: Only Focusing on Downloads

Focus on your app’s engagement. Google measures length of install and app activity and will not penalize if a user uninstalls after using an app for awhile.





#6: Not Designing for Tablets

Designing your app for the tablet experience and failing to get the “Designed for Tablets” designation.


여러분 앱을 태블렛에 맞추는 작업을 하라.



#7: Using Unhelpful Anchor Tags

Instead of using the words “Google Play” to link to your app’s Google Play URL, use descriptive terms to help boost your app’s ranking for those keywords.


여러분 앱의 Google Play URL 을 달 때 단지 Google Play 라고만 하지 말고 여러분 앱의 ranking을 올리는데 도움을 줄 수 있는 키워드 같은 것들을 사용하라.



#8: Misspelling Popular Apps

While 50% of search queries are misspellings, do not choose app names that are close variations or misspellings of popular apps. Google will auto correct the query.


검색 단어 중 50%는 철자가 틀리다. 유명한 앱의 다른 철자나 비슷한 단어를 사용하지 마라. 구글이 자동으로 그 검색 단어의 철자를 고쳐 여러분 앱을 보여주지 않을 것이다.



#9: Making Your APK Too Large


Smaller APKs get installed more and uninstalled less. As users run out of memory they will uninstall apps based on size and percentage of usage.


APK 파일 사이즈가 작을수록 더 많이 인스톨 되고 덜 uninstall 된다. 유저들은 메모리가 부족하면 사이즈를 보고 uninstall 을 결정하는 경우가 많다.



#10: Not Creating a Viral Loop

Use Google+ Recommendations API to allow users to +1 an app from within your app. Integrate with Google Play’s game services to make your app more social.


유저가 여러분 앱 안에서 +1을 할 수 있도록 Google+ Recommendations API를 사용하라. Google Play 의 게임 서비스를 사용해서 여러분 앱을 좀 더 social 하게 만들어라.


반응형

Comment


이번에는 getTextUsingTransformations() 함수를 살펴볼 차례죠?


제 생각에 오늘로 소스 분석이 모두 끝날 것 같습니다.

사실 제가 할 작업은 지금까지 분석한 것으로도 충분하거든요.

벌써 미팅에서 결과를 공유했고 그 다음 단계가 진행중입니다.


하지만 시작했으니 마저 끝마쳐보죠.


pdf2text() 함수의 맨 마지막 return 문에 있는 코드 입니다.


return getTextUsingTransformations($texts, $transformations);


파라미터로는 getDirtyTexts() 함수에서 얻었던 $texts 가 첫번째에 있네요.

이 내용은 지난 글 말미에 보여드렸습니다.


두번째 파라미터는 getDecodedStream() 함수를 통해서 얻었던 $data 입니다.


getTextUsingTransformations($texts, $transformations) 함수를 볼까요?


function getTextUsingTransformations($texts, $transformations) {
    $document = "";
    for ($i = 0; $i < count($texts); $i++) {
        $isHex = false;
        $isPlain = false;

        $hex = "";
        $plain = "";
        for ($j = 0; $j < strlen($texts[$i]); $j++) {
            $c = $texts[$i][$j];
            switch($c) {
                case "<":
                    $hex = "";
                    $isHex = true;
                break;
                case ">":
                    $hexs = str_split($hex, 4);
                    for ($k = 0; $k < count($hexs); $k++) {
                        $chex = str_pad($hexs[$k], 4, "0");
                        if (isset($transformations[$chex]))
                            $chex = $transformations[$chex];
                        $document .= html_entity_decode("&#x".$chex.";");
                    }
                    $isHex = false;
                break;
                case "(":
                    $plain = "";
                    $isPlain = true;
                break;
                case ")":
                    $document .= $plain;
                    $isPlain = false;
                break;
                case "\\":
                    $c2 = $texts[$i][$j + 1];
                    if (in_array($c2, array("\\", "(", ")"))) $plain .= $c2;
                    elseif ($c2 == "n") $plain .= '\n';
                    elseif ($c2 == "r") $plain .= '\r';
                    elseif ($c2 == "t") $plain .= '\t';
                    elseif ($c2 == "b") $plain .= '\b';
                    elseif ($c2 == "f") $plain .= '\f';
                    elseif ($c2 >= '0' && $c2 <= '9') {
                        $oct = preg_replace("#[^0-9]#", "", substr($texts[$i], $j + 1, 3));
                        $j += strlen($oct) - 1;
                        $plain .= html_entity_decode("&#".octdec($oct).";");
                    }
                    $j++;
                break;

                default:
                    if ($isHex)
                        $hex .= $c;
                    if ($isPlain)
                        $plain .= $c;
                break;
            }
        }
        $document .= "\n";
    }

    return $document;
}


먼저 $document 변수를 만들어 놓네요. 맨 밑에 보니까 return 될 값이 들어갈 변수입니다.

즉 사람이 읽을 수 있는 완전한 데이터가 들어갈 변수죠.


다음은 $texts 의 count 만큼 for 문을 돌립니다.

그 for문 안에서 맨 먼저 하는 일은 4개의 변수를 정의해 놓는 거네요.

$isHex와 $isPlain 는 boolean 값이 들어갈 변수고 $hex와 $plain 은 어떤 문자 같은게 들어갈 변수인것 같습니다.


여기서 다시 for 문을 돌립니다. $texts 가 이중배열이라서 이렇게 작업하나 봅니다.

두번째 for 문에서는 $c 변수에 $texts[$i][$j] 를 담습니다.


그 다음에 switch 문이 나오는데... 처음에 < 와 > 를 체크하는 군요.


편의를 위해서 어제 봤던 데이터 중 일부를 아래 복사해 넣겠습니다.


(W)-36(e)7(e)7(k )46(o)7(f)
( )
(Oct)-11(o)5(ber )10(1)4( )
<00B2>
( )
(Oct)-11(o)5(ber )10(5)
( )
( )
( )
(Who)5(l)7(e)7( )-19(+ )-2(S)3(um)5( )
( )
( )
(Choice)
( )
(+)
( )
(F)20(lavor)


<00B2> 가 있네요. < 인 경우에는 $isHex 가 true 이고 > 인 경우에는 false 입니다.

그리고 > 인 경우에는 $hex 값을 array 로 바꿉니다.

str_split() 함수에서 그 일을 하죠.


<?php

$str 
"Hello Friend";

$arr1 str_split($str);
$arr2 str_split($str3);

print_r($arr1);
print_r($arr2);

?>


위 소스를 돌리면 아래 값을 얻습니다.

Array
(
    [0] => H
    [1] => e
    [2] => l
    [3] => l
    [4] => o
    [5] =>
    [6] => F
    [7] => r
    [8] => i
    [9] => e
    [10] => n
    [11] => d
)

Array
(
    [0] => Hel
    [1] => lo
    [2] => Fri
    [3] => end
)


어떤 일을 하는 함수인지 아시겠죠?

그 다음에는 $hexs 의 count 만큼  for 문을 돌립니다.

그 다음 그 각각의 값을 지난번에도 나왔던 str_pad() 함수를 사용해서 0을 4칸 붙이네요.


그 다음에 if 문에서 $transformations[$chex] 이 세팅돼 있으면 $chex 변수에 $transformations[$chex]을 대입합니다. 


그리고 나서 $document 에 값을 집어 넣는데요.

html_entity_decode() 함수를 사용합니다.


이 것은 html 을 string 으로 바꿔 주는 함수입니다.


<?php
$orig 
"I'll \"walk\" the <b>dog</b> now";

$a htmlentities($orig);

$b html_entity_decode($a);

echo 
$a// I'll &quot;walk&quot; the &lt;b&gt;dog&lt;/b&gt; now

echo $b// I'll "walk" the <b>dog</b> now
?>


위 예제를 보시면 어떤 일을 하는지 아시겠죠?


그러니까 <> 감싸여진 데이터는 hex 코드라서 이 case 문에서 그것을 처리하는 거네요.


그 다음 case 문에서는 (,) 를 체크합니다.


() 로 둘러싸인 부분은 그냥 text 죠. 사람이 읽을 수 있는..

그러니까 별다른 처리를 하지 않아도 되기 때문에 ) 인 경우에 $document 에 $plain 을 그냥 추가해 버리는 겁니다.


다음에는 \\ 를 체크하는데요.


우리가 다루는 pdf 파일에서는 이 값이 추출 되지 않았습니다.

어쨌든 내용을 보면 줄바꿈, 탭 뭐 이런 것들을 해당 sign 으로 바꿔서 $plain 에 넣는 일을 하네요. 숫자인 경우에는 거기에 맞게 또 처리를 하구요.


디폴트로는 $isHex 일 경우 $hex 에 $c를 추가하고 $isPlain 일 경우 $plain 에 $c를 추가합니다.


그리고 이렇게 만든 $document 를 이전 for 문에서 만든 $document 에 가를 하구요.

이렇게 for 문이 다 돌고 $document 에 값이 다 쌓였으면 이 값을 return 합니다.


그 return 값이 사람이 볼 수 있는 text 입니다.


이렇게 해서 얻은 결과는 아래와 같습니다.



잘 안 보이실 텐데요. 1번 글에서 업로드한 파일들을 다운 받아서 돌려 보시면 됩니다.


참고로 이 데이터를 가지고 요일별 메뉴를 display 하는 함수를 제가 만들어 봤는데요.



function menus($sources){
$resultlen  = strlen($sources);

$menuDate = substr($sources,0,48);
echo "<b><font size=6>Start Menu</font> <p><br> ". $menuDate. "<p></b>";


$result = preg_replace("/\s+/",'_',$sources);

$fs1 = strrpos($result,'_M_');

$menu1 = substr($result,$fs1,$resultlen);

$startTue = strpos($menu1,'_T_');
$startWed = strpos($menu1,'_W_');
$startThu = strrpos($menu1,'_T_');
$startFri = strpos($menu1,'_F_');
$endFri = strpos($menu1,'WEEKLY');

$Monday = str_replace('_', ' ' ,str_replace('_M_','Monday <br>',str_replace('FIT','FIT<br>',substr($menu1,0,$startTue))));
$Tuesday = str_replace('_', ' ' ,str_replace('_T_','Tuesday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startTue,$startWed-$startTue))));
$Wednsday = str_replace('_', ' ' ,str_replace('_W_','Wednsday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startWed,$startThu-$startWed))));
$Thuesday = str_replace('_', ' ' ,str_replace('_T_','Thusday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startThu,$startFri-$startThu))));
$Friday = str_replace('_', ' ' ,str_replace('_F_','Friday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startFri))));

echo "<table width=50%><tr><td>";

echo "<p> ". $Monday . "<p>";
echo "<p> ". $Tuesday . "<p>";
echo "<p> ". $Wednsday . "<p>";
echo "<p> ". $Thuesday . "<p>";
echo "<p> ". $Friday . "<p>";

echo "</td></tr></table><p><br><p> ";
}


그리고 함수 밖에서 이걸 부르면 되죠.

menus($result);

이렇게 하면 아래와 같은 결과가 나옵니다.




이렇게 해서 PDF 를 TEXT 로 변환하는 PHP 프로그램을 모두 분석해 봤습니다.

오랫만에 목욕해서 때를 싹 밀었을 때 처럼 개운하네요.


분석결과 위 소스는 제가 일하는데에서는 맞지 않아서 사용하지 않기로 했거든요.

그래서 저 결과도 깔끔하게 나온 것은 아닙니다.

참고하시구요.


다음에 또 소스 분석할 일 있으면 블로그에 정리해 놓을 께요.

이번 글은 기분 좋게 7번째 만에 마무리 했네요.





반응형

Comment


그럼 지난 글에 이어서 계속 분석해 보겠습니다.


이제 pdf2text 함수에서 getCharTransformations 함수 call 하는 부분을 볼 차례입니다.

지난번에 preg_match_all 을 사용해서 BT,ET 로 구분해서 데이터를 따로 모으고 그 데이터를 압축해제 해서 그나마 사람이 조금 알아 볼 수 있는 데이터로까지 만들었었는데요.


이 BT,ET  구분에 해당하지 않는 else 부분에서 getCharTransformations 함수를 호출합니다.


전달하는 인자는 첫번째 인자로 pdf2text 초반에 만들어 뒀던 $transformations 배열 변수하고 getDecodedStream 함수를 통해서 얻었던 $data 변수입니다.





function getCharTransformations(&$transformations, $stream) {
    preg_match_all("#([0-9]+)\s+beginbfchar(.*)endbfchar#ismU", $stream, $chars, PREG_SET_ORDER);
    preg_match_all("#([0-9]+)\s+beginbfrange(.*)endbfrange#ismU", $stream, $ranges, PREG_SET_ORDER);

    for ($j = 0; $j < count($chars); $j++) {
        $count = $chars[$j][1];
        $current = explode("\n", trim($chars[$j][2]));

        for ($k = 0; $k < $count && $k < count($current); $k++) {
            if (preg_match("#<([0-9a-f]{2,4})>\s+<([0-9a-f]{4,512})>#is", trim($current[$k]), $map))
                $transformations[str_pad($map[1], 4, "0")] = $map[2];
        }
    }
    for ($j = 0; $j < count($ranges); $j++) {
        $count = $ranges[$j][1];
        $current = explode("\n", trim($ranges[$j][2]));
        for ($k = 0; $k < $count && $k < count($current); $k++) {
            if (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+<([0-9a-f]{4})>#is", trim($current[$k]), $map)) {
                $from = hexdec($map[1]);
                $to = hexdec($map[2]);
                $_from = hexdec($map[3]);

                for ($m = $from, $n = 0; $m <= $to; $m++, $n++)
                    $transformations[sprintf("%04X", $m)] = sprintf("%04X", $_from + $n);
            } elseif (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+\[(.*)\]#ismU", trim($current[$k]), $map)) {
                $from = hexdec($map[1]);
                $to = hexdec($map[2]);
                $parts = preg_split("#\s+#", trim($map[3]));
                
                for ($m = $from, $n = 0; $m <= $to && $n < count($parts); $m++, $n++)
                    $transformations[sprintf("%04X", $m)] = sprintf("%04X", hexdec($parts[$n]));
            }
        }
    }
}

이 함수에서도 처음부터 preg_match_all 함수가 사용됩니다. 정규 표현식은 '#([0-9]+)\s+beginbfchar(.*)endbfchar#ismU' 와 '#([0-9]+)\s+beginbfrange(.*)endbfrange#ismU' 가 사용됩니다.

처음것은 숫자와 관련돼 있고 그 다음에는 뭔지는 모르지만 begin bf char 와 end bf char 가 사용됐습니다.

다른 형식의 파일들은 BT, ET 가 아니라 beginbfchar 와 endbfchar 로 블럭이 나뉘나 봅니다.

이 부분은 우리가 테스트 하는 pdf 파일형식이랑은 상관없는 것 같지만 그래도 시작했으니 마저 보겠습니다.


하여간 이 두 preg_match_all 함수를 사용해서 $chars 와 $ranges 라는 변수를 반들어 냈습니다.


그 다음에는 chars 에 대해서 먼저 for 문을 돌리는데요.

explode() 함수를 사용해서 줄바뀜 '\n'  이 일어나는 단위로 분리를 하고 trim() 으로 좌우의 공백을 없앱니다.


다음 그 분리된 값들로 다시 for 문을 돌리는데요.

preg_match 를 사용해서 다시 나눕니다. 정규표현식인 '#<([0-9a-f]{2,4})>\s+<([0-9a-f]{4,512})>#is' 를 사용해서 나누는데.. 이게 뭔뜻인지 속시원히 알면 좋겠네요.

혹시 이와 관련해서 쉽고 좋은 글 있는 곳아시는 분 계시면 알려 주세요.


하여간 이렇게 해서 만들어진 값을 $map 에 담습니다.

그리고 그 값을 $transformations 배열 변수에 원하는 형식으로 담는군요.


str_pad() 함수 관련한 예제는 아래에 있습니다.


<?php
$input 
"Alien";
echo 
str_pad($input10);                      // produces "Alien     "
echo str_pad($input10"-="STR_PAD_LEFT);  // produces "-=-=-Alien"
echo str_pad($input10"_"STR_PAD_BOTH);   // produces "__Alien___"
echo str_pad($input"___");               // produces "Alien_"
?>


어떤 string 에 공백이나 일정한 문자나 기호를 넣을 수 있는 함수 같네요.

우리가 공부하는 소스에서는 아래와 같이 이용했습니다.

$transformations[str_pad($map[1], 4, "0")] = $map[2];


그럼 $map[1] 다음에 4칸을 0으로 채운 곳에 $map[2] 를 대입한다는 얘기네요.


그 다음은 $ranges 에 대해 for 문을 돌립니다.

이 경우도 줄바꿈을 기준으로 데이터를 분리하고 위 for 문과 비슷하게 돌리는데요.


hexdec() 함수가 보입니다.


<?php
var_dump
(hexdec("See"));
var_dump(hexdec("ee"));
// both print "int(238)"

var_dump(hexdec("that")); // print "int(10)"
var_dump(hexdec("a0")); // print "int(160)"
?>


매뉴얼에는  hexadecimal string 을 decimal number 로 변환해 주는 함수로 나와 있습니다.


이 두번째 for 문은 자세히 안 보겠습니다. 지금 사용하는 pdf 파일을 decode 하는데는 사용되지 않는 부분 같으니까요.


그러면 이제 다 분석 한 거구요.

pdf2text() 함수의 마지막 줄만 남았습니다.


return getTextUsingTransformations($texts, $transformations);


이 return 값이 완전히 사람이 읽을 수 있는 텍스트로 바뀐 값입니다.


지난 글에서 살짝 사람이 읽을 수 있는 데이터로 압축을 해제해서 만든 값을 getTextUsingTransformations() 함수에서 돌려서 완전히 사람이 읽을 수 있는 글자로 만들어 주는 겁니다.


제가 얻고 싶은 대로 데이터를 가공하려면 이 getTextUsingTransformations() 함수 부분을 잘 공부해야 될 것 같습니다.


이 함수는 다음 글에서 자세히 분석해 보겠습니다.




반응형

Comment


지난번 글에 getDecodedStream 까지 분석 했습니다.

그 글 올린지 일주일이 지났네요.
회사에서 다른 일을 하느라 이 부분 분석을 못했는데요.
그 일이 어느정도 마무리 됐으니 다시 마저 분석에 들어가야 겠습니다.


지난번 다루다 끝났던 getDecodedStream 함수를 보면요.$key값이  ASCIIHexDecode, ASCII85Decode 그리고 FlateDecode 냐에 따라서 실행하는 함수가 달랐습니다.

제가 테스트 하는 PDF 문서는 FlateDecode 를 사용하더라구요.


그러면 

            if ($key == "FlateDecode"){
                $_stream = decodeFlate($_stream);
                echo $_stream . "<p>";
                }

가 실행이 될 텐데요.


그 결과값을 미리 브라우저에 찍어 봤습니다.





뭐 이런게 찍히네요.

지금까지 쪼개고 쪼개고 쪼갠 값은 $_stream 의 값을 해당 함수에 넣고 돌려서 얻은 결과가 위 데이터입니다.

사람이 볼 수 있는 데이터가 되려면 아직 멀어보이네요.

그러면 decodeFlate($_stream) 을 살펴 보겠습니다.


function decodeFlate($input) {
    return @gzuncompress($input);
}



뭐 이 함수는 아주 간단합니다. 다른 decodeAsciiHex($_stream) 이나 decodeAscii85($_stream) 은 그래도 뭔가 로직이 있는데 이 함수는 달랑 @gzuncompress() 메소드만 사용했네요.

이 메소드를 알아보겠습니다.

gzuncompress() 메소드의 PHP 매뉴얼을 보면 This function uncompress a compressed string. 라고 돼 있습니다. 그러니까 이 함수는 압축된 string 을 압축해제 하는 함수라고 합니다.

지금까지 작업한 $_stream 을 압축해제하면 저 위에 있는 화면 같은 데이터가 추출 되는군요.


<?php
$compressed   
gzcompress('Compress me'9);
$uncompressed gzuncompress($compressed);
echo 
$uncompressed;
?>


위 코드를 실행 해 보세요. 그러면 Compress me 그대로 출력 될 겁니다. 압축했다가 다시 압축을 풀었으니까요.

그럼 이제 어디로 가야 될까요? 1주일이 지나서 감이 멀어졌는데. 일단 getDecodedStream 함수에서 일을 처리하고 return 하는 부분까지 했으니까 이 함수를 호출했던 pdf2text() 함수로 다시 가야겠죠.


$data = getDecodedStream($stream, $options);


이 부분이었습니다. 저 $data 변수에는 아까 브라우저에 출력했던 그 이상한 데이터가 담겨져 있을 겁니다. $_stream 의 압축을 푼 데이터죠.

그 다음 소스는 아래 내용입니다.


            if (strlen($data)) {
                if (preg_match_all("#BT(.*)ET#ismU", $data, $textContainers)) {
                    $textContainers = @$textContainers[1];
                    getDirtyTexts($texts, $textContainers);

                } else
                    getCharTransformations($transformations, $data);
            }


strlen 함수는 C언어에서도 똑같이 쓰는 것 같은데요. 문자열의 길이를 구하는 함수입니다.

그러니까 strlen($data) 는 $data 의 길이를 구하는거고 0 이면 데이터가 없다는 겂니다. 그러면 PHP는 0 을 false 로 취급하니까 이 if 문을 처리하지 않고 건너 뛸 겁니다.

$data 에 값이 있다면 그것이 길던 짧던 상관 안하고 if 문 안의 코드를 실행합니다.

그 다음은 다시 정규식 표현을 사용하는 preg_match_all 함수가 나오네요.


$data를 BT, ET 등으로 구분해서 $textContainers 에 넣는 것 같습니다.

BT, ET 는 아까 뽑았던 이상한 데이터들에 많이 있습니다. 잘은 모르겠지만 그냥 통박을 굴려보면 Begin Text, End Text 의 준말이 아닐까 생각되네요.

그 다음에 @가 나오는데요. 이건 PHP 에서 에러 관련해서 처리하도록 하는 부호입니다.


<?php
/* Intentional file error */
$my_file = @file ('non_existent_file') or
    die (
"Failed opening file: error was '$php_errormsg'");

// this works for any expression, not just functions:
$value = @$cache[$key];
// will not issue a notice if the index $key doesn't exist.

?>


위에 관련 예제가 있습니다.

그 다음에는 getDirtyTexts($texts, $textContainers); 가 나옵니다.


새로운 함수 호출인데요. 두번째 인자는 방금 뽑아낸 $textContainers 변수를 전달하고 첫번째 인자는 pdf2text 초반에 만들어 뒀던 배열변수네요.

이 변수에 아직 아무 값도 안 담겼던 것 같은데...


그럼 이 호출된 함수를 볼까요?

function getDirtyTexts(&$texts, $textContainers) {
    for ($j = 0; $j < count($textContainers); $j++) {
        if (preg_match_all("#\[(.*)\]\s*TJ#ismU", $textContainers[$j], $parts))
            $texts = array_merge($texts, @$parts[1]);
        elseif(preg_match_all("#Td\s*(\(.*\))\s*Tj#ismU", $textContainers[$j], $parts))
            $texts = array_merge($texts, @$parts[1]);
    }
}


짧은 함수네요.

$textContainers 의 수만큼 for 문을 돌리는데 다시 preg_match_all 를 사용해서 $textContainers 파편들을 '#\[(.*)\]\s*TJ#ismU' 이 정규식 형식에 맞게 구분을 해서 $parts 에 집어넜습니다.


아까 받았던 $texts 에는 이 값들을 계속 차곡차곡 쌓아놓네요.

array_merge() 함수를 사용해서인데요. 이 함수는 한개 이상의 배열을 합하는 함수입니다.

elseif 문 안에도 똑 같은 일을 하죠. 다만 구분하는 정규표현식이 다를 뿐이죠.

즉 이 함수는 BT, ET 별로 쪼갠 값을 다시 위 정규표현식에 맞게 쪼개서 $texts 에 담는 함수입니다.


그 다음은 다시 pdf2text 함수로 돌아가야 되는데요.

BT,ET 로 나누지 않을 경우는 getCharTransformations 함수를 사용하네요.

이 함수부터는 다음 글에서 다룰께요.

마무리 하면서 getDirtyTexts($texts, $textContainers); 바로 밑에 아래 코드를 추가해 봤습니다.


                    for ($k = 0; $k < count($texts); $k++) {
                        echo $texts[$k] . "<br>";
                    }


바로 지금까지 만든 $texts 내용을 찍어 본 건데요.

아래 내용입니다.



이제 조금 사람이 알아볼 수 있는 글들이 나타나기 시작했습니다.


(W)-36(e)7(e)7(k )46(o)7(f) 에는 Week of  가 있죠?

그 중간에 숫자는 뭔지 잘 모르겠지만요. 빈괄호 ()는 space 일까요?


(Oct)-11(o)5(ber )10(1)4( ) 는 October 1 하고 빈괄호가 있네요.

이제 뭔가 보이기 시작합니다.

그럼 다음 시간에는 이 데이터를 마저 decode 해 보겠습니다.





반응형

Comment


지난 글까지는 pdf 의 내용을 바이너리로 받아서 이걸 obj별로 나누고 다시 stream 별로 나눈 값을 getObjectOptions() 로 보내서 그 값을 다시 여러 정규식으로 나누고 공백으로 나누고 뭐 그런 처리를 한 값을 return 하는 것까지 했습니다.


이제 getObjectOptions() 함수에서 던져준 값을 adf2text() 함수내에서 받아서 어떤 처리를 해 줘야 할 텐데요.

여기서는 받은 값으로 맨 처음 Length1, Type, Subtype 요소가 그 안에 있는지 여부를 체크합니다.

PHP 의 empty() 함수를 이용해서 체크하네요.

Length1, Type, Subtype 값들은 맨 처음 글에서 본 Hello World PDF 의 바이너리 파일에서 보면 <<, >> 안에 들어있는 텍스트 입니다.

여기서 continue; 를 사용했는데요. 예제를 보고 이해해 보죠.


<?php
for ($i 0$i 5; ++$i) {
    if (
$i == 2)
        continue
    print 
"$i\n";
}
?>


이렇게 하면 뭐가 나올까요? 정답은 2 입니다.

if 문에서 true 가 아니면 그 아래는 실행하지 않고 다시 loop 문으로 갑니다.

그러니까 우리가 공부하는 소스의 내용을 보면 Length1 과 Type 이 비어있지 않고 Subtype 이 비어있는 경우에만 아래 내용을 실행하고 그렇지 않으면 이 값은 무시되서 for 루프문으로 돌아갑니다.

아마 Length1 과 Type 이 있고 Subtype 이 없는 경우가 실제 Text 로 처리할 내용인가 봅니다.

이렇게 해서 얻어진 값을 getDecodedStream() 함수로 보냅니다.

실제 Text 로 처리할 내용만 담았으니까 이제 이것을 바이너리에서 사람이 읽을 수 있는 형식으로 바꿔야 겠죠. 그 작업을 하는 함수가 getDecodedStream() 인가 봅니다.

이 함수에는 파라미터로 $stream 과 $options 를 던져 줍니다.

$stream 은 pdf2text() 함수에서 만든 값이고 $options 는 이전 글인 getObjectOptions() 함수에서 만든 값이죠?

이 두 값을 getDecodedStream()으로 보내는데  이 함수에서는 구체적으로 무엇을 할까요?


Twilight O.S.T 인 Decode 를 부른 Paramore 입니다. :)


function getDecodedStream($stream, $options) {
    $data = "";
    if (empty($options["Filter"]))
        $data = $stream;
    else {
        $length = !empty($options["Length"]) ? $options["Length"] : strlen($stream);
        $_stream = substr($stream, 0, $length);

        foreach ($options as $key => $value) {
            if ($key == "ASCIIHexDecode")
                $_stream = decodeAsciiHex($_stream);
            if ($key == "ASCII85Decode")
                $_stream = decodeAscii85($_stream);
            if ($key == "FlateDecode")
                $_stream = decodeFlate($_stream);
        }
        $data = $_stream;
    }
    return $data;
}


여기서는 우선 $options["Filter"] 를 체크해서 없으면 $data 에 $stream 을 넣습니다.

이 $data 가 나중에 return 할 값입니다.

그리고 Filter 가 있으면 else 구문을 실행하는데요.

$options["Length"] 가 있으면 $options["Length"] 를 없으면 $stream 의 길이를 $length 에 넣습니다.

여기서 Length 는 데이터의 길이이고 이미 PDF 바이너리 파일 안에 있습니다.

그 다음에 문자열을 다룰 때 가장 많이 사용되는 substr() 함수가 나오는데요.
아래 예제를 보시면 금방 이해가 되실 겁니다.


<?php
echo substr('abcdef'1);     // bcdef
echo substr('abcdef'13);  // bcd
echo substr('abcdef'04);  // abcd
echo substr('abcdef'08);  // abcdef
echo substr('abcdef', -11); // f

// Accessing single characters in a string
// can also be achieved using "square brackets"
$string 'abcdef';
echo 
$string[0];                 // a
echo $string[3];                 // d
echo $string[strlen($string)-1]; // f

?>


설명은 굳이 달지 않을께요. 아주 자주 쓰이고 또 쉬운거니까요.



다시 공부하던 소스로 돌아가면 $stream 을 처음부터 $length 까지만 잘라서 $_stream 에 넣네요. 즉 딱 데이터 부분만 $_stream 에 담는 겁니다.

그 다음에는 $options 배열 수만큼 foreach 문을 돌리는데요.

이 값이 ASCIIHexDecode 이면 decodeAsciiHex() 메소드를 사용하고 ASCII85Decode 이면 decodeAscii85() 메소드를 사용하고 FlateDecode 이면 decodeFlate() 메소드를 사용해서 $_stream 을 디코딩 하는 겁니다.


지금까지 한 일은 이 디코딩 메소드로 데이터를 돌려서 사람이 읽을 수 있는 텍스트로 뽑아내기 위해서 딱 그 데이터 부분만 분리하는 작업을 한 겁니다.


이제 본격적으로 데이터를 디코딩 하는 함수를 볼 차레네요.

이 부분은 집중해서 자세히 보고 싶군요.

다음 글에서 하나하나 분석해 보도록 하겠습니다.


그래서 제가 저희 회사 식당 Menu PDF 에 있는 데이터를 제대로 필요한 내용만 추출해서 어떻게 사용할 수 있는지 찾아낼 수 있도록요.

이 식당 Menu PDF 가 한가지만 있으면 좋은데 텍사스, 켄터키, 메사추세츠 등 등 여러 지역에 또 여러 도시에 나눠져 있어서 10개가 넘거든요.

각 빌딩의 chef 들이 조금씩 편집을 해서 올리기 때문에 어떤 공통된 패턴을 찾아내기가 어렵더라구요.


하여간 다음 글에서 이 바이너리 파일을 디코딩하는 소스를 세밀하게 분석해 보겠습니다.


반응형

Comment


지난번 글에서는 pdf2text($filename) 함수에 있는 for 문 중간 까지 했습니다.

그 중간에 getObjectOptions() 함수를 call 하는 부분 바로 전까지 했는데요.


그 전까지는 PDF 안의 내용을 일단 바이너리 파일로 받아서 obj,endobj 구문별로 나는 다음에 다시 그 안의 내용을 stream,endstream 구문별로 나눴습니다.


그 stream 별로 나눈 값을 for 문 안에서 계속 getObjectOptions() 함수로 던져서 어떤 일을 하도록 시키는데.. 오늘은 그 어떤일이 어떤일인지 공부할 것 같습니다.


PDF 안의 내용을 가져오는 함수는 file_get_contents() 함수였고 그 바이너리 파일들을 다시 세분화 시킬 때 사용했던 함수가 PCRE 함수인 preg_match_all() 과 preg_match() 였었습니다.


그리고 추가로 PHP 정규 표현식 (regular expressions)과 PDF 구조에 대해서 알아야 했구요.


이제 getObjectOptions() 함수에 대해 알아보겠습니다.


function getObjectOptions($object) {
    $options = array();
    if (preg_match("#<<(.*)>>#ismU", $object, $options)) {
        $options = explode("/", $options[1]);
        @array_shift($options);

        $o = array();
        for ($j = 0; $j < @count($options); $j++) {
            $options[$j] = preg_replace("#\s+#", " ", trim($options[$j]));
           
            if (strpos($options[$j], " ") !== false) {
                $parts = explode(" ", $options[$j]);
                $o[$parts[0]] = $parts[1];
            } else
                $o[$options[$j]] = true;
        }
        $options = $o;
        unset($o);
    }

    return $options;
}


여기서도 PCRE 함수가 쓰이네요.

바이너리 파일 내용을 처리하기 위해서 이 함수들은 필수로 이해하고 있어야 할 것 같습니다.


우선 처음에 $options 라는 배열을 만들고 pdf2text() 함수 내에서 던져준 PDF 내의 각 stream 을 가지고 다룹니다.


preg_match("#<<(.*)>>#ismU", $object, $options)


받은 stream 블럭인 $object 안의 내용을 정규표현식인 #<<(.*)>>#ismU 로 구분해서 아까 만든 배열변수인 $options 에 담습니다.

정규표현식 #<<(.*)>>#ismU 가 뭔지 정확히 알면 좋겠는데...

혹시 정규표현식에 강하신 분 계시면 댓글에 이게 정확히 무엇을 말하는지 알려 주시면 감사하겠습니다.





그 다음은 explode() 함수를 썼습니다. 이 함수는 문자열을 특정 기준으로 자르는 함수입니다.

예제를 하나 보면요.


<?php
$str 
'one|two|three|four';

// positive limit
print_r(explode('|'$str2));

// negative limit (since PHP 5.1)
print_r(explode('|'$str, -1));
?>


이 소스를 돌리면 그 결과값은 아래와 같습니다.

Array
(
    [0] => one
    [1] => two|three|four
)
Array
(
    [0] => one
    [1] => two
    [2] => three
)


첫번째 파라미터는 구분자이고 두번째 파라미터는 작업할 문자열 그리고 마지막 파라미터는 limit 이라고 하는데요. 이 세번째는 옵션입니다.

위의 소스를 보면 2를 넣으면 두개로 구분해서 첫번째 | 를 기준으로 구분하고 나머지는 모두 한개의 값으로 처리하는군요.

두번째 소스는 마이너스 값을 넣었는데 | 구분자 이전의 값을 나누는 것 같습니다.

비슷한 함수로는 정규식을 사용해서 구분하는 preg_split() 이 있구요 반대되는 함수로는 implode() 함수가 있습니다.


지금 배우는 소스에서는 $options = explode("/", $options[1]); 로 구분자 / 을 기준으로 options 를 모두 나눴습니다.

여기서 / 는 뭐를 나타내는 것일까요?


지난번 글에 Hello World! 를 표시한 PDF 의 바이너리 파일을 보면 << , >> 구문 안에 어떤 정보들이 들어있고 그 안에 각각의 정보들이 / 나눠져 있는 걸 볼 수 있습니다.

/ 이 무슨 의미인지 알려면 PDF 구조에 대해 공부해야 할 것 같습니다.


obj, stream,<<,>>,/ 대충 지금까지 나온 PDF 내부에서 사용되는 기호입니다. 


하여간 / 로 나는 값을 PHP 의 array_shift() 함수로 돌립니다.

배열의 첫번쨰 요소를 없애버리는 겁니다. 그리고 두번째 요소가 첫번째 요소로 되는거죠.


<?php
$stack 
= array("orange""banana""apple""raspberry");
$fruit array_shift($stack);
print_r($stack);
?>


이렇게 하면 결과가 아래와 같이 됩니다.

Array
(
    [0] => banana
    [1] => apple
    [2] => raspberry
)


Hello World! PDF 의 바이너리를 보면 / 다음엔 Size, Root 이런 문자가 오는데 이걸 없애 버리는 건가요?


하여간 그 다음으로 넘어가면 $o라는 배열 변수를 만듭니다.

그래서 방금전에 explode 까지 한 결과값인 $options 배열을 for 문으로 돌립니다.



$options 배열의 각각의 요소를 다시 preg_replace() 함수를 이용해서 변환을 시켜주네요.


preg_replace("#\s+#", " ", trim($options[$j]));


#\s+# 로 변환을 하고 그 결과값의 좌우 공백을 없앤 값을 $options 배열에 다시 넣어줍니다.


그리고 $options[$j] 안에 공백이 있으면 다시 그 공백을 기준으로 나눠서 $parts 변수에 담습니다.

그 다음에 방금 전 만들어 주었던 $o 의 $parts[0] 에 $parts[1] 을 담습니다.

만약 공백이 없다면 $o[$options[$j]] 에 true 값을 대입시킵니다.


그래서 이 $o  변수를 $options 에 담은 다음에 그 값을 return 합니다.


여기까지가 getObjectOptions() 메소드가 하는 일 입니다.


정리하면 #<<(.*)>>#ismU 으로 나누고 그걸 다시 / 으로 나누고 배열을 shift 한 다음에 배열안의 내용을 #\s+# 로 나눠서 공백이 있으면 공백을 기준으로 다시 나누고 공백이 없으면 $o[$options[$j]] 를 true 로 해서 이 $o의 값을 $options 에 담아서 return 을 해 줍니다.


정규표현식이 정확히 무엇을 하라는 것인지 알고 싶네요.

혹시 아시는 분 설명 부탁드려요.


오늘은 여기까지 할거구요.


위의 만들어진 $options 값을 pdf2text() 메소드에서 받아서 어떻게 처리하는지 다음에 알아 볼께요.


반응형

Comment

이전 1 2 3 4 5 6 다음