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

최근에 받은 트랙백

글 보관함

calendar

    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    


이번에는 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번째 만에 마무리 했네요.





저작자 표시 비영리 동일 조건 변경 허락
신고


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


이제 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() 함수 부분을 잘 공부해야 될 것 같습니다.


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




저작자 표시 비영리 동일 조건 변경 허락
신고


지난번 글에 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 해 보겠습니다.





저작자 표시 비영리 동일 조건 변경 허락
신고


지난 글까지는 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 들이 조금씩 편집을 해서 올리기 때문에 어떤 공통된 패턴을 찾아내기가 어렵더라구요.


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


저작자 표시 비영리 동일 조건 변경 허락
신고


지난번 글에서는 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() 메소드에서 받아서 어떻게 처리하는지 다음에 알아 볼께요.


저작자 표시 비영리 동일 조건 변경 허락
신고


그럼 이전 글에 이어서 계속 진행하겠습니다.


pdf2text($filename) 을 하다가 마쳤었는데요.


이어서 알아볼 것은 preg_match_all 함수입니다.


preg_match_all("#obj(.*)endobj#ismU", $infile, $objects);


이런 초반부터 어려운게 걸렸네요.

이 함수에는 3개의 파라미터가 들어가는데요.

첫번쨰 파라미터는 정규표현식이 들어갑니다.

이 첫번째 파라미터의 패턴을 가지고 두번쨰 파라미터에 있는 객체내를 search 하는 겁니다.

그래서 그 정규식대로 구분해서 세번째 파라미터에 있는 변수에 결과를 담는거죠.


이 정규식과 관련해서는 여기여기 에 있는 글들을 참조하세요.

그리고 PHP 튜토리얼 사이트는 여기여기 가 있습니다.


이 preg_match_all 메소드에 대한 튜토리얼은 여기에 있습니다.


예제를 하나 보시면...


<?php
preg_match_all
("|<[^>]+>(.*)</[^>]+>|U",
    
"<b>example: </b><div align=left>this is a test</div>",
    
$outPREG_PATTERN_ORDER);
echo 
$out[0][0] . ", " $out[0][1] . "\n";
echo 
$out[1][0] . ", " $out[1][1] . "\n";
?>


위 코드를 실행하면 아래와 같은 결과를 얻습니다.


<b>example: </b>, <div align=left>this is a test</div> example: , this is a test


그러니까 첫번째 파라미터로 들어간 정규식은 모든 태그를 없애는 거나보네요.


그래서 결과가 배열 변수  $out 에 들어가게 되는데요. 

이 배열의 첫번째 열에는 full pattern 이 들어가고 두번쨰 열이 바로 이 태그가 없어진 값들이 들어갑니다.


정규식을 잘 몰라서 왜 그런지는 모르겠구요.


하여간 우리가 공부하는 소스코드는 PDF 의 내용을 담은 변수를 #obj(.*)endobj#ismU 라는 정규식을 써서 Search하고 그 값이 $objects 에 들어갑니다.


참고로 pdf 파일은 수많은 obj/endobj, stream/endstream , Startxref/Xref,Trailer 등으로 구성 돼 있습니다.



위와 같은 PDF 파일이 있다면 바이너리 파일은 아래와 같습니다.


%PDF-1.7

1 0 obj  % entry point
<<
  /Type /Catalog
  /Pages 2 0 R
>>
endobj

2 0 obj
<<
  /Type /Pages
  /MediaBox [ 0 0 200 200 ]
  /Count 1
  /Kids [ 3 0 R ]
>>
endobj

3 0 obj
<<
  /Type /Page
  /Parent 2 0 R
  /Resources <<
    /Font <<
      /F1 4 0 R 
    >>
  >>
  /Contents 5 0 R
>>
endobj

4 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /BaseFont /Times-Roman
>>
endobj

5 0 obj  % page content
<<
  /Length 44
>>
stream
BT
70 50 TD
/F1 12 Tf
(Hello, world!) Tj
ET
endstream
endobj

xref
0 6
0000000000 65535 f 
0000000010 00000 n 
0000000079 00000 n 
0000000173 00000 n 
0000000301 00000 n 
0000000380 00000 n 
trailer
<<
  /Size 6
  /Root 1 0 R
>>
startxref
492
%%EOF


자세한 내용을 알고 싶으시면 여기 를 참조하세요.


이 프로그램 하나 이해하기 위해 공부해야 할 것들이 무지 많군요.






원래 소스로 돌아가서요.


    for ($i = 0; $i < count($objects); $i++) {
        $currentObject = $objects[$i];
        if (preg_match("#stream(.*)endstream#ismU", $currentObject, $stream)) {
            $stream = ltrim($stream[1]);
            $options = getObjectOptions($currentObject);
            if (!(empty($options["Length1"]) && empty($options["Type"]) && empty($options["Subtype"])))
                continue;

            $data = getDecodedStream($stream, $options);
            if (strlen($data)) {
                if (preg_match_all("#BT(.*)ET#ismU", $data, $textContainers)) {
                    $textContainers = @$textContainers[1];
                    getDirtyTexts($texts, $textContainers);
                } else
                    getCharTransformations($transformations, $data);
            }
        }
    }


아까 얻었던 $objects 배열의 count 만큼 for 문을 돌립니다.

바이너리 파일을 하나하나 텍스트 문자로 변환시키기 위해 전체 $objects 를 for 문 돌리는 거겠죠.

이 for 문 안에서는 $objects 배열에 있는 각각의 객체들을 일단 $currentObject 에 담네요.

그 다음에 다시 PCRE 함수가 나왔네요.

이와 관련해서는 저 위에 튜토리얼 페이지 링크를 걸어놨죠?  


if 문안에 preg_match("#stream(.*)endstream#ismU", $currentObject, $stream) 이 있습니다.


위 정규식으로 search 한 내용을 $stream 에 담슴니다.

한개의 stream 을 담는거네요.


PDF 소개글을 보면 Stream 은 아래와 같이 정의 돼 있습니다.

Stream (<< /Length ... >> stream ... endstream): embedded data, can be compressed. It starts with a dictionary that describes the stream such as its length or the encoding (/Filter) is uses.


stream 에 대해 자세히 공부하시려면 여기 로 가세요.


그 다음은 php2text.php 에 있는 getObjectOptions($currentObject)를 콜 합니다.


흠.... 소스를 자세히 뜯어 보니까 그 흐름은 좀 더 이해하게 되는데요...


이 소스를 완벽하게 이해하고 또 마음대로 수정해서 사용하려면

PHP 정규 표현식 (regular expressions) 를 알아야 하고 PCRE 함수를 알아야 하고 PDF 의 구조를 알아야 하네요.


일단 오늘의 성과는 이 소스를 이해하기 위해 필요한 지식 중 내가 뭐가 부족한지 알았습니다.


시간이 되면 파고 들겠는데... 그럴 시간이 있을 지 모르겠네요.


일단 오늘은 여기까지 하구요. 다음 글에서는 getObjectOptions() 메소드를 공부할 생각입니다.


그럼...


저작자 표시 비영리 동일 조건 변경 허락
신고


오늘은 PDF 파일에 있는 내용을 가져와서 브라우저에 텍스트로 뿌려 주는 것을 공부해 보겠습니다.

저희가 만든 웹앱에 식당 메뉴를 제공하려고 하는데요.
식당을 운영하는 회사에서 메뉴를 PDF 형식으로 그 회사 웹에서 제공하고 있어요.


그런데 회사사람들 대부분이 블랙베리 폰을 사용하고 있어서 구형 BB 에서는 PDF 형식을 보여주기가 힘들거든요.


그 회사랑 따로 EDI 시스템 개발해서 데이터 주고 받고 하는 번거로운일을 하지 않고 그냥 그 회사에서 제공하는 메뉴에서 우리가 필요한 데이터만 추출해서 서비스하는 방법을 고민중입니다.


일단 인터넷 서핑을 통해서 PDF 를 TEXT 로 바꿔주는 소스코드는 구했습니다.

이 소스코드를 한번 분석해 보고 어떤 방법이 있을지 없을지 알아보려구요.




메뉴는 이렇게 생긴 PDF 파일인데요.

왼쪽의 내용들은 필요 없고 오른쪽에 있는 요일별 메뉴들만 뽑고 싶거든요.


원본 pdf 파일과 이 내용을 text 로 바꿔주는 PHP 파일은 여기 있습니다.



original.pdf

pdf2text.php


이걸 돌리면 결과가 아래와 같이 나옵니다.



흠 다 텍스트로 나오긴 나왔는데 이거 가지고는 따로 메뉴만 추출해 내기 쉽지 않네요.

이렇게 모두 한줄로 나오는게 아니라 라인별로 따로 출력이 돼면 좀 더 낫지 않을까요?


일단 pdf2text.php 를 분석해 봐야 겠습니다.


pdf2text.php 파일 안에는 9개의 함수가 있습니다.



이 pdf2text.php 를 실행하면 제일 먼저 어떤게 실행될까요?

함수들은 어디서 호출을 해 줘야 실행되니까 다 그냥 건너뛰겠고...

$result = pdf2text ('original.pdf'); 가 제일 먼저 실행되겠죠?


이건 pdf2text($filename) 함수를 call 하는 겁니다. 거기서 처리된 값을 $result 변수에 담고 그 내용을 echo 함수로 브라우저에 뿌려주는게 이 프로그램이 하는일의 전부 다 입니다.


이제 그 중간의 처리 과정을 자세히 공부해 보겠습니다.


우선 pdf2text($filename) 함수는 아래와 같습니다.


function pdf2text($filename) {
// file_get_contents return the contents of a file as a string
    $infile = @file_get_contents($filename, FILE_BINARY);
    if (empty($infile))
        return "";

    $transformations = array();
    $texts = array();

    preg_match_all("#obj(.*)endobj#ismU", $infile, $objects);
    $objects = @$objects[1];
   
   

    for ($i = 0; $i < count($objects); $i++) {
        $currentObject = $objects[$i];
//echo $currentObject . "<p> end";
        if (preg_match("#stream(.*)endstream#ismU", $currentObject, $stream)) {
            $stream = ltrim($stream[1]);
//echo $stream . "<p> end";
            $options = getObjectOptions($currentObject);
            if (!(empty($options["Length1"]) && empty($options["Type"]) && empty($options["Subtype"])))
                continue;

            $data = getDecodedStream($stream, $options);
            if (strlen($data)) {
                if (preg_match_all("#BT(.*)ET#ismU", $data, $textContainers)) {
                    $textContainers = @$textContainers[1];
                    getDirtyTexts($texts, $textContainers);
                } else
                    getCharTransformations($transformations, $data);
            }
        }
    }

    return getTextUsingTransformations($texts, $transformations);
}

맨 처음으로는 해당 파일의 내용을 받아옵니다. 이것은 바이너리 형식이라서 사람이 읽을 수 있는건 아닙니다. ($infile = @file_get_contents($filename, FILE_BINARY);)

그 내용을 출력해 보면 아래와 같이 나옵니다.



이걸 보면 헤더정보만 사람이 읽을 수 있는 텍스트죠?

이 문서는 PDF 1.5 버전이라는 것을 알 수 있습니다.

그리고 이 헤더정보는 endobj 부분에서 끝나는 겁니다.
나머지는 사람이 읽을 수 없는 바이너리 형식입니다.

그 다음 if 문에서는 해당 파일이 비어있는건지 봅니다. 비어있으면 나머지 로직을 실행할 필요가 없겠죠? 괜히 시간만 낭비니까 그 cost 를 줄이기 위해 비어있으면 그냥 return 을 해 버립니다.


파일이 empty 가 아니면 그 다음을 실행할 텐데요.

(참고로 if 문의 {} 가 없습니다. 그 안에 내용이 단 1줄이면 {} 없이 사용할 수 있습니다.

그러면 바로 그 다음줄까지만 if 절 안에 포함돼 있는 겁니다.)


그 다음은 $transformations 라는 배열과 $texts 라는 배열을 만들어 줍니다.


오늘은 여기까지만 하고 다음에 계속 분석해 보겠습니다.

지금 토요일 아침 7시 40분인데요.

가족이랑 어디 가기로 해서 지금 나가봐야 되네요.


여행 다녀와서 계속 공부할텐데 그 내용도 여기 정리해 놓을께요.



저작자 표시 비영리 동일 조건 변경 허락
신고

PHP와 XSL 로 XML 변환하기

2012.12.21 05:31 | Posted by 솔웅


How to parse XML with PHP5




Feeds 는 content 의 stream 으로 사람들이 웹사이트를 통해 정보의 조각들을 공유하기 위해 만든 것입니다. PHP5의 simpleXML 함수는 이 feed를 웹페이지에서 사용하기 편하도록 interpret 하는 방법을 아주 간단하게 구현할 수 있도록 도와 줍니다.


저는 최근 저희의 라디오 방송국 playlist 에서 제공하는 노래에 대한 live feed를 display 하는 widget 과 관련해서 작업을 했었습니다. radio 방송국에서 운영하는 시스템에서는 XML을 제공하는데요 이 자료들은 데이터베이스에서 나옵니다. 프로그램은 그 데이터를 feed로 만들어서 제공합니다.






서로 다른 서버끼리 데이터를 공유하기 위해 XML 을 사용하는 것은 일반적인 방법입니다. playlist 를 generate 하는 시스템은 proprietary 입니다. XML은 어떤 데이터가 공유될 수 있도록 하는 easy format 을 제공합니다.


XML 은 뭘까요?


XML 은 eXtensible Markup Language 의 줄임말로 site들 사이에서 공유하기 위해 데이터를 structure 하는 방법입니다. RSS (Real Simple Syndication) 와 Podcasts 같은 기술들도 XML 의 특별한 형태입니다. XML이 좋은 점은 여러분이 필요로 하는것을 여러분이 원하는 대로 표현할 수 있다는 겁니다.



XML은 쉽게 생성할 수 있습니다. 왜냐하면 HTML 이랑 아주 비슷하거든요. HTML 과 다른 점은 여러분만의 tag 를 따로 만들 수 있다는 겁니다. 예를 들어 여러분의 라디오 방송국에서 틀어줄 노래들의 list 에 대한 feed 를 put 한다고 가정합시다. 그러기 위해서 할 일은 당지 artist 이름, 노래 제목, 노래가 방송된 시긴 등을 encode 합니다. 이와 관련된 tag들을 만들 겁니다. <title> <artist> 그리고 이것들은 <song> tag 가 감쌀 겁니다. 또 우리는 각 노래가 방송된 시간을 표시할 건데요. 날짜와 시간이 여기에 들어갈 겁니다.

여러분은 이것을 아래 처럼 encode 할 수 있을 겁니다.


<songs>
    <song dateplayed="2011-07-24 19:40:26">
        <title>I left my heart on Europa</title>
        <artist>Ship of Nomads</artist>
    </song>
    <song dateplayed="2011-07-24 19:27:42">
        <title>Oh Ganymede</title>
        <artist>Beefachanga</artist>
    </song>
    <song dateplayed="2011-07-24 19:23:50">
        <title>Kallichore</title>
        <artist>Jewitt K. Sheppard</artist>
    </song>
</songs>



여기서 XML 데이터를 만들 때 지켜야 될 몇가지 규칙이 있습니다. 만약 XHTML 에 대해 잘 아신다면 쉽게 이해가 가실겁니다. 한번 볼까요?

   *  XML is case sensitive so <Title>` is not the same as <title>.
   *  All XML elements must have closing tags.
   *  XML requires a root element (the <songs> tag above serves as our root element)
   *  Attributes must be quoted
   *  Special characters (like & (&amp;) and < (&lt;) and > (&gt;) signs) must be encoded.

XML 은 HTML 보다 좀 더 strict 합니다. 하지만 정말 만들고 다루기가 쉽습니다.




Introducing simpleXML



simpleXML 을 사용해서 XML을 아주 쉽게 읽어서 그 내용에 접근할 수 있습니다.

위에 있는 XML 을 songs.xml 로 저장했다고 칩시다. 같은 폴더에 php 파일을 만들 겁니다.

이 전체 XML 을 아래 코드를 사용해서 읽을 겁니다.


<?php
    $mysongs = simplexml_load_file('songs.xml');
?>



이렇게 하면 다 읽은 겁니다. 저 xml 파일이 웹상의 어딘가에 있어도 그 URL 만 가지고 이렇게 읽을 수 있습니다. xml 파일이 여러분 서버에 있을 필요도 없는 거죠.

이제 우리는 이 파일의 내용을 담은 object (객체)를 가지게 됐습니다.

이 song 객체는 $mysongs 라는 변수에 담겨져 있습니다. 만약에 첫번째 artist 의 이름을 출력하고 싶다면 이렇게 하시면 됩니다.


<?php
    $mysongs = simplexml_load_file('songs.xml');
    echo $mysongs->song[0]->artist;
?>






이 객체의 일부분으로 XML tag 들이 매핑돼 있다는 걸 기억하세요. 그래서 그 name 을 통해 어느 element 이든 우리가 얻어 낼 수 있는 겁니다. 그리고 PHP 에서는 배열이 0번부터 시작한다는 것도 기억하시구요. 그래서 배열의 0번째 title을 위와 같이 출력한 겁니다.

자 이제 3번째 노래 제목을 출력해 볼까요?


<?php
    $mysongs = simplexml_load_file('songs.xml');
    echo $mysongs->song[2]->title;
?>





Working with Attributes



date 들을 얻으려면 그 attribute들에 어떻게 접근해야 되는지 알아야 됩니다. 이 notation은 tag 랑은 약간 다릅니다. 하지만 사용하는 건 똑 같이 쉽습니다.
바로 아래 처럼 사용하시면 됩니다.

<?php
    $mysongs = simplexml_load_file('songs.xml');
    echo $mysongs->song[1]['dateplayed'];
?>




Making a list of songs


So now that we’ve got the basics of accessing elements, let’s write the code to make a complete list of our songs parsed by interpreting our XML file.

이제 우리는 element들에 어떻게 접근하는지 그 기본을 알게 됐습니다.

이제 이 XML 파일을 interpret 함으로서 전체 노래를 parse 하도록 하겠습니다.


<?php
    $mysongs = simplexml_load_file('songs.xml');
    echo "<ul id="songlist">n";
    foreach ($mysongs as $songinfo):
        $title=$songinfo->title;
        $artist=$songinfo->artist;
        $date=$songinfo['dateplayed'];
        echo "<li><div class="title">",$title,"</div><div class="artist">by ",$artist,"</div><time>",$date,"</time></li>n";
    endforeach;
    echo "</ul>";
?>



각각의 노래들에 대해 접근하기 위해 우리는 foreach statement 를 사용했습니다. 그리고 그 정보를 간단한 HTML list 로 parsing 했습니다. 여러분은 이것을 일반적인 HTML 문서로 사용하시거나 아니면 노래 리스트를 출력하는 widget 으로 사용하실 수 있습니다.





Parsing a Flickr feed from a set


온라인에는 여러분이 parse 할만한 수 많은 XML feed 들이 있습니다. 예를 들어 Flickr 로부터 feed를 받을 수 있죠. 만약 여러분이 Flickr 를 update 하면 이 widget 은 자동적으로 이것을 여러분 웹싸이트에 display 할 겁니다.

예제로 사용하기 위해 약간의 고양이 사진들을 준비했습니다.
이 feed 에 대한 XML 을 얻으려면 Flickr 내의 웹 페이지로 가야 합니다. 그리고 화면 왼쪽 아래에 있는 XML icon 을 보세요.





우선 이 XML 의 구조를 알기 위해 이것을 분석해야 합니다. 이 feed 링크에서 오른쪽 마우스를 클릭해서 여러분의 하드 드라이브에 저장해 보세요. 그리고 이것을 photoset.xml 이라고 이름 짓고 브라우저로 열어 보세요.







저는 Safari 로 이 XML 파일을 열었습니다. 이 구조를 한번 살펴 보죠. 각각의 photo 들은 <entry> tag 안에 있죠? 그 안에 두개의 <link> tag 가 있네요. 첫번째 것은 Flickr 상의 이미지 링크이고 두번째 것은 이 이미지의 medium size 버전에 대한 링크가 있습니다.



위 예제를 살짝 바꿔보죠. 그리고 이 이미지들에 대한 thumbnail 을 표시하기 위해 두번째 link 를 살짝 조절하겠습니다. 이 작업을 하기 전에 Flickr 로 돌아가서 그 XML 파일의 경로를 얻으세요. 그리고 아래와 같이 코딩해 보세요.



<?php
    $mypix = simplexml_load_file('http://api.flickr.com/services/feeds/photoset.gne?set=72157627229375826&nsid=73845487@N00&lang=en-us');
    foreach ($mypix->entry as $pixinfo):
        $title=$pixinfo->title;
        $link=$pixinfo->link['href'];
        $image=str_replace("_b.jpg","_s.jpg",$pixinfo->link[1]['href']);
        echo "<a href="",$link,""><img src="",$image,"" alt="",$title,"" /></a>n";
    endforeach;
?>



<img> tag 의 alt text 로 <title> tag 를 사용할 겁니다. 그리고 anchor tag 의 href attribute 로 첫번째 <link> tag 를 사용할 거구요. 두번째 link 는 약간의 트릭을 사용했습니다. 두번째 링크의 href attribute ($pixinfo->link[1]['href']) 를 얻기 위해 array notation 을 사용할 수 있습니다. 그렇게 해서 우리가 필요한 큰 이미지를 얻게 될 겁니다. 이미지 이름을 바꾸기 위해서 str_replace 함수를 사용할 겁니다. 그러면 아래와 같은 thumbnail 을 얻게 될 겁니다.



Conclusion


simpleXML 은 정말 쉬워서 이것을 이용하면  Flickr 예제 같은 복잡한 feed를 parse 하는 것이 재밌을 겁니다. <entry> tag 안의 <link> tag 의 두번째 instance를 얻는데 아무런 문제가 없었던 점을 기억하세요. 이 작업은 이전 버전의 PHP 에서는 구현하기가 매우 어려웠을 겁니다. simpleXML 에는 다른 많은 기능들이 있습니다. 더 자세한 내용은 PHP manual을 보세요.
객체지향적인 접근에 관심이 있다면 PHP5 가 제공하는 SimpleXMLElement class도 도움이 될 겁니다.







저작자 표시 비영리 동일 조건 변경 허락
신고

PHP로 XML , JSON 다루기

2012.08.24 21:14 | Posted by 솔웅


일하다가 동료가 처리할 데이터가 있는데 이 데이터를 어떻게 관리했으면 좋겠냐고 물어보더라구요.


XML 로 관리할지 JSON으로 관리 할지.....

저희 시스템은 PHP로 돼 있거든요.


그래서 일단은 JSON이 가장 가벼우니까 데이터를 파일로 관리하면 JSON으로 관리하는 법도 생각해 볼 수 있겠지만 데이터도 많지 않고 바뀌지도 않을 거고 그냥 a 가 1인 경우의 b 값을 화면에 출력만 하면 되니까 디비에 넣어서 관리하는게 제일 편하지 않을까 의견을 주었습니다.


다른 시스템간에 데이터를 인터페이스 할 것도 아니고....


하여간 그래도 그 얘기 듣고 PHP로 XML과 JSON을 처리하려면 어떻게 해야 하는지 그 구체적인 소스가 궁금해서 써핑 해 봤습니다.


* PHP 로 XML 다루기


<?xml version="1.0" encoding="utf-8"?>
<data>
    <total> 2 </total>
    <item>
        <name> Raj </name>
        <phone> 123456789 </phone>
        <age> 17 </age>
    </item>
    <item>
        <name> Akshay </name>
        <phone> 987654321 </phone>
        <age> 15 </age>
    </item>
</data>


이런 xml 데이터가 있을 때 php 코드는 아래와 같으면 됩니다.


<?php
 $xml_string = file_get_contents($_SERVER['DOCUMENT_ROOT'].'/data.xml');
 $xml = simplexml_load_string($xml_string);
 echo $xml->total . "<p>"; //2
 echo $xml->item[0]->name . "<p>";       // Raj
 echo $xml->item[0]->phone . "<p>";        // 123456789
 echo $xml->item[0]->age . "<p>";        // 17
 echo $xml->item[1]->name . "<p>";        // Akshay
 echo $xml->item[1]->phone . "<p>";         // 987654321
 echo $xml->item[1]->age;        // 15
?>


먼저 file_get_contents 로 data.xml 파일에 있는 모든 내용을 긁어 옵니다.

그러면 $xml_string 변수에 배열로 그 내용들이 담길 겁니다.


그 다음에는 복잡하게 코딩할 필요 없이 그냥 simplexml_load_string() 메소드를 사용하시면 됩니다.


그 안의 내용을 처리하는 방법은 $xml-> 다음에 원하는 내용이 있는 태그를 사용하면 됩니다.

total 은 한번만 있으니까 곧바로 얻어오고 그 다음 item 은 계속 반복되는 태그이니까 배열불러오듯이 불러오면 됩니다.


아래 샘플 파일 올려놓겠습니다.


data.xml


xml.php





* PHP 로 JSON 다루기


아래 코드도 어디에선가 긁어 온 건데요.


for(){
  $str[] = array('code'=>$bbs_no,'table'=>$table_type);
}

----------------------
code[0]      table[0]
code[1]      table[1]
----------------------

이렇게 데이터들이 담길 겁니다.

$jsonStr = json_encode($str); // ******* Make Json from array ********
<input type="hidden" name="json_list" id="json_list" value=$jsonStr  /> // Send Form


유저로부터 데이터를 입력받아서 이것을 json으로 encode 한 겁니다.


그러면 이 json 값을 한꺼번에 form 테그에서 send 할 수가 있습니다.


그 다음 받는 파일에서는 아래와 같이 처리하면 됩니다.


$json_list = $_REQUEST['json_list']; //get Form
$json_list = str_replace('\\', '', $json_list);
json_list= json_decode($json_list, true); // Decode json
echo $json_list[0]['code'];
echo $json_list[1]['table'];


간단하네요.


나중에 유용하게 사용될 수 있을 것 같습니다.


저작자 표시 비영리 동일 조건 변경 허락
신고


지금 회사에서 사용하고 있는 Kurogo 가 PHP 로 돼 있거든요.


그래서 그 시스템을 커스터마이징을 하려면 PHP 코딩을 해야되서...

거의 10여년만에 PHP를 다시 보고 있습니다.


중간 중간 기억하고 싶은 팁들이 생기면 여기다 저장해 놓으려구요.


일단 지금은 원격 서버의 이미지 파일을 저희 팀 웹페이지에 display 하는 로직을 만들고 있는데요.


우선 그 이미지가 있는지 없는지부터 확인해서 있으면 표시하고 없으면 다른 메세지나 디폴트 이미지를 하기로 했습니다.


이미지 파일이 있는지 없는지를 확인할 수 있는 방법으로는 첫번째로 이미지의 크기를 구하는 함수를 사용할 수 있습니다.


//// check Image size       
        function checkIMG($IMGPath) {
            $fileCheck = getImagesize($IMGPath);
            if($fileCheck) { $isExist = "OK"; } else { $isExist = "NO"; }
            return $isExist;
        }


함수에 원격 이미지 파일의 경로와 파일이름을 담은 $IMGPath 를 넘겨주면 됩니다.





두번째로는 이미지이외에도 다른 경우에도 사용할 수 있는 건데요. 헤더 정보를 얻는 겁니다.

//// get header info
        function checkHeader($filePath) {
            $AgetHeaders = get_headers($filePath);
            if(!$AgetHeaders) {
                echo "Network Problem. Too slow or No Network.<p>";
                $isExist = "Net";
            } else {
                if (preg_match("|200|", $AgetHeaders[0])) {
                        // file exists
                    $isExiset = "OK";
                } else {
                        // file doesn't exists
                    $isExiset = "NO";
                }
            }
            return $isExiset;
        }


여기선 중간에 살짝 정보를 얻어오지 못할 경우 Network 문제가 있다고 뿌려주는 로직도 있습니다.


그 다음으로는 cURL을 사용하는 건데 이건 정확히 get_headers()와 뭐가 다른지 모르겠네요.

cURL로는 파라미터 정보를 넘길수도 있다고 하는데...

하여간 사용법은 아래와 같습니다.


////////
        function curl1($filePath) {
            $curl = curl_init();
            curl_setopt_array( $curl, array(
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_URL => $filePath ) );
            curl_exec( $curl );
            $response_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
            curl_close( $curl );
            return $response_code;
        }       
       
        function curl2($filePath){
            //To get the whole header you can issue a HEAD request, like this:

            $curl = curl_init();
            curl_setopt_array( $curl, array(
                CURLOPT_HEADER => true,
                CURLOPT_NOBODY => true,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_URL => $filePath ) );
            $headers = explode( "\n", curl_exec( $curl ) );
            curl_close( $curl );
           
            return $headers[0];
        }


그런데 위 4가지 방법 모두 다 문제가 있었습니다.

왜냐하면 회사 서버에 Proxy 가 있는데 이걸 통해서는 아무 값도 못 받아오더라구요.


회사 내부 서버끼리 테스트 할 때는 잘 됐는데 회사 네트워크 밖에 있는 이미지(파일)을 가지고 하려니까 안되더라구요.


이럴 경우 Proxy 를 거쳐서 정보를 가져오도록 해야 되는데요.

stream_context_create() 함수와 file_get_contents() 함수를 사용했습니다.


/////// get contents via proxy               
        function getViaProxy($filePath){
                                // Define a context for HTTP.
                    $aContext = array(
                        'http' => array(
                        'proxy' => '프락시 정보', // This needs to be the server and the port of the NTLM Authentication Proxy Server.
                        'request_fulluri' => True,
                    ),
                    );
                    $cxContext = stream_context_create($aContext);

                    // Now all file stream functions can use this context.
                    $sFile = file_get_contents($filePath, False, $cxContext);
                    if($sFile){
                        $result = "Y";
                    }else{   
                        $result = "N";
                    }
                    return $result;
        }


이렇게 해서 어렵게 어렵게 문제를 해결했습니다.


저작자 표시 비영리 동일 조건 변경 허락
신고
이전 1 다음

티스토리 툴바