기타/WWW

PHP XML Parser expat을 사용하여 XML을 mySQL에 저장하기

하늘이푸른오늘 2016. 5. 5. 00:30

현재... GPX 파일(XML 파일의 일종입니다.)을 해석해서 mySQL DB에 넣기 위해 고민 중에 있었습니다. 


제가 예전에 만들어 둔 프로그램에서는 그냥 javascript - ajax로 읽어 들인 후 사용했었습니다. 원래는 이 부분을 사용하려고 했습니다. 그런데, 이 기능을 이용할 경우, 한 개의 waypoint를 추출해서 배열을 만들고, ajax를 통해 PHP로 넘겨주면, PHP속에서 mySQL에 INSERT 시키는 방법을 사용해야 합니다. 그러면 당연히 터무니 없이 속도가 떨어질 것으로 예상됩니다.


그러던 중, W3C PHP 투토리얼 사이트에서 XML Parser에 관한 내용을 보게 되었습니다. 이걸 이용하면 그냥 PHP에서 XML(GPX파일)을 Parsing 한 후, 바로 MySQL로 넣어줄 수 있으니, 훨씬 작업이 간편할 것이라 생각했습니다.


PHP XML Parser는 두 가지 종류가 있습니다. 하나는 SimpleXML Parser이고, 다른 하나는 XML expat 입니다. SimpleXML은 말 그대로 아주 간단합니다. 그냥 파일이나 string을 지정하면, 전체를 구조체로 읽어 들이는 방식입니다. 그 다음엔 $xml->book[0]->title 과 같이 간단한 참조 방식으로 사용하면 됩니다. 


이에 비해 expat는 상당히 복잡합니다. expat는 이벤트 방식의 Parser라고 하고요, 예를 들어 아래와 같은 문장이 있다면...

<from>Jani</from>

이것을 다음과 같은 3개의 이벤트(사실은 4개의 이벤트)로 보내줍니다. 이런 방식으로 파일 전체가 이벤트로 처리되는 겁니다.

    • Start element: from
    • Start CDATA section, value: Jani
    • Close element: from

언뜻 보기에는 많이 복잡해 보였습니다. 그래서 포기할까 생각했습니다. 그런데, 파일의 크기가 클 수록 이 방식이 빠르고 편하다는 설명을 보고 시도해 보기로 했습니다. (simpleXML Parser의 경우 한꺼번에 메모리로 읽어 들이는 방식이라서 시스템에 따라서 문제가 발생할 수도 있답니다.)


expat Parser는 4개의 함수만 사용하면 거의 80%의 문제는 해결할 수 있다고 합니다. 제가 설명할 것도 바로 이 4가지 함수입니다. (일단 예제는 W3C의 예제를 이용하겠습니다.)


    • $parser=xml_parser_create(); // parser를 생성합니다.
    • xml_set_element_handler($parser,"start","stop"); // 생성한 Parser에 Start event handler 와 Stop event handler를 등록. 당연히 "start"/"stop" 은 함수명입니다.
    • xml_set_character_data_handler($parser,"char"); // Parser에 Data event handler를 등록합니다. "char"가 그 함수고요, 물론 이름은 다른 이름을 사용해도 됩니다.
    • xml_parse($parser,$data,feof($fp)); // parser를 실행합니다.

아래는 W3C 사이트에 있는 예제입니다. 빨간 표시를 한게 위에서 설명한 4개의 함수입니다.

xml_parser_create() 아래로 있는 start(), stop(), char()가 바로 event 핸들러입니다. 이 함수들 3개를 적당히 이용해서 데이터를 처리해주면 됩니다.


<?php
// Initialize the XML parser
$parser=xml_parser_create();

// Function to use at the start of an element
function start($parser,$element_name,$element_attrs) {
  switch($element_name) {
    case "NOTE":
    echo "-- Note --<br>";
    break;
    case "TO":
    echo "To: ";
    break;
    case "FROM":
    echo "From: ";
    break;
    case "HEADING":
    echo "Heading: ";
    break;
    case "BODY":
    echo "Message: ";
  }
}

// Function to use at the end of an element
function stop($parser,$element_name) {
  echo "<br>";
}

// Function to use when finding character data
function char($parser,$data) {
  echo $data;
}

// Specify element handler
xml_set_element_handler($parser,"start","stop");

// Specify data handler
xml_set_character_data_handler($parser,"char");

// Open XML file
$fp=fopen("note.xml","r");

// Read data
while ($data=fread($fp,4096)) {
  xml_parse($parser,$data,feof($fp)) or 
  die (sprintf("XML Error: %s at line %d", 
  xml_error_string(xml_get_error_code($parser)),
  xml_get_current_line_number($parser)));
}

// Free the XML parser
xml_parser_free($parser);
?>


이 파일을 사용해서 note.xml을 parsing하는 순서를 보여드리면...


<?xml version="1.0" encoding="UTF-8"?>

<note>

<to>Tove</to>

<from>Jani</from>

<heading>Reminder</heading>

<body>Don't forget me this weekend!</body>

</note>


먼저 첫줄 <?xml... 은 그냥 무시됩니다. 

두번째 줄 <note>가 들어오면, start event handler로 등록된 start() 함수가 호출되는데, 이때 $element_name에는 "NOTE"가 지정됩니다. 따라서 case 문 등을 사용해 이를 처리해 줍니다.


function start($parser,$element_name,$element_attrs) {

  switch($element_name) {

    case "NOTE":

    echo "-- Note --<br>";

    break;

    case "TO":

    ....


그 다음엔 data event handler가 호출됩니다. (<NOTE>의 경우에는 data에 공백 "  " 만 전달됩니다.)


function char($parser,$data) {

  echo $data;

}


그 다음엔 <to>Tove</to>가 처리될 차례죠. 이것도 비슷한 방식으로 아래와 같은 순서로 처리됩니다.


TO ->start event

Tove -> data event

TO ->end event

" " -> data event


이런 식으로 계속 이벤트를 처리하는 방식으로 처리하면 됩니다. 하나의 노드가 여러개의 이벤트로 나눠오기 때문에 데이터를 처리하는 게 복잡합니다만, 그래도 가능합니다.


====

아래는 제가 테스트해본 프로그램입니다. W3C의 books 예제파일 을 읽어들여서 이를 mySQL 에 삽입하는 예제입니다. 


여기에서 약간 중요한 점이 두 가지 있습니다. 위의 예제와는 달리, xml에 attribute가 있어서 이를 처리하는 부분이 추가되었는데요, 여기에서 each() 함수를 사용했습니다. 또 다른 하나는, mySQL에 입력할 때, prepared statement를 사용했습니다. $stmt=$conn->prepare()를 사용하여 미리 구문만 mySQL로 보내어 미리 최적화를 시킨 후, $stmt->execute();로 실행시켰습니다. 이렇게 해야 실행속도가 빨라진 답니다.



나머지는 아래의 코멘트를 참고하세요.


민, 푸른하늘


<?php


// mySQL DB 연결

require_once 'mySQL_login.php'; //이 파일은 각자의 환경에 맞게 준비해야 함

$conn = new mysqli($db_hostname,$db_username,$db_password, $db_database);

if($conn->connect_errno) {

    die("Connection failed: " . $connection->connect_error);

}


// Table이 미리 있다면 삭제함


$sql = "DROP TABLE IF EXISTS books";

if (!$conn->query($sql)) {

die("Error droping table: " . $conn->error);

}


// Table 생성

$sql = "CREATE TABLE `books` (

  `title` varchar(50) NOT NULL,

  `category` varchar(30) NOT NULL,

  `author` varchar(30) NOT NULL,

  `lang` varchar(10) NOT NULL,

  `year` smallint(6) NOT NULL,

  `price` float NOT NULL

) ENGINE=MyISAM";


if (!$conn->query($sql)) {

die("Error creating table: " . $conn->error);

}


// bind_para 에 사용되는 변수 초기화

$b_title = $b_author = $b_category = $b_lang = "";

$b_year = $b_price = 0;


// prepared statement 준비

$stmt = $conn->prepare("INSERT INTO books (title, author, category, lang, year, price) VALUES (?, ?, ?, ?, ?, ?)");

$stmt->bind_param("ssssid", $b_title, $b_author, $b_category, $b_lang, $b_year, $b_price);


// XML parser 생성

$parser=xml_parser_create();


// XML을 parsing 한 후 저장할 array 초기화

$num_books = 0;

$book = array("TITLE"=>"", "AUTHOR"=>"", "CATEGORY" => "" , "LANG" => "", "YEAR" => 0, "PRICE" => 0);

$index = "";


// Element Start event handler. 여기에서는 $index만 저장함.

function start($parser,$element_name,$element_attrs) {


    global $index,$book;

    

    switch($element_name) {

      case "BOOK":

          break;

      case "TITLE":

          $index = "TITLE";

          break;

      case "AUTHOR":

          $index = "AUTHOR";

          break;

      case "YEAR":

          $index = "YEAR";

          break;

      case "PRICE":

          $index = "PRICE";

          break;

    }


    // attribute의 경우엔 each로 처리함. 

    if(!empty($element_attrs)){

        $temp = each($element_attrs);

        $book[$temp[0]] = $temp[1];

    }

}


// Element Stop event handler. BOOK이 끝나는 시점에서 $stmt를 실행하여 $book array에 들어 있는 내용을 mySQL로 저장. 

function stop($parser,$element_name) {

    

    global $num_books, $book, $index, $stmt;

    global $b_title, $b_author, $b_category,$b_lang, $b_year, $b_price;

    $b_title = $book['TITLE'];

    $b_author = $book['AUTHOR'];

    $b_category = $book['CATEGORY'];

    $b_lang=$book['LANG'];

    $b_year = $book['YEAR'];

    $b_price = $book['PRICE'];

    

    if($element_name == "BOOK") {

          $num_books++;

          $stmt->execute();

          print_r($book);

          echo "<br>";

    }

}


// Element Data event handler. 실제의 값을 $book 배열에 저장.

function char($parser,$data) {

  global $book, $index;

  

  if($index != "") {

    $book[$index] = $data;

    $index = "";

  }

}


// Specify element handler

xml_set_element_handler($parser,"start","stop");


// Specify data handler

xml_set_character_data_handler($parser,"char");


// Open XML file

$fp=fopen("books.xml","r");


// Read data. feof()을 만날 때까지 계속 파일을 읽어들이고 parsing 함.

while ($data=fread($fp,4096)) {

  xml_parse($parser,$data,feof($fp)) or 

    die (sprintf("XML Error: %s at line %d", 

  xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser)));

}


// Free the XML parser

xml_parser_free($parser);

?>