이번 글에서는 객체지향 프로그래밍 원칙을 활용한 PHP 웹 크롤러 코드를 분석해보겠습니다. 이 코드는 여러 웹사이트(뽐뿌, 클리앙)에서 인기글을 수집하는 크롤러를 구현하고 있으며, 다양한 디자인 패턴을 활용해 확장성 있는 구조를 갖추고 있습니다.
주요 구성요소
1. 인터페이스 정의 (CrawlerInterface)
interface CrawlerInterface {
public function crawl();
public function getSiteName();
}
모든 크롤러가 구현해야 할 기본 인터페이스를 정의하고 있습니다. 각 크롤러는 반드시 crawl() 메소드와 getSiteName() 메소드를 구현해야 합니다.
2. 추상 클래스 (AbstractCrawler)
abstract class AbstractCrawler implements CrawlerInterface {
protected $url;
protected $html;
protected $doc;
protected $xpath;
protected $encoding = 'UTF-8';
// 공통 메소드 구현
public function crawl() { ... }
protected function fetchHtml() { ... }
protected function parseHtml() { ... }
protected function getHeaders() { ... }
// 자식 클래스에서 반드시 구현해야 하는 추상 메소드
abstract protected function extractData();
}
AbstractCrawler 클래스는 크롤러의 공통 기능을 구현하고 있습니다. 템플릿 메소드 패턴을 활용하여 크롤링 프로세스의 큰 흐름(crawl())은 정의하되, 데이터 추출(extractData()) 부분은 자식 클래스에서 구현하도록 강제하고 있습니다.
3. 구체적인 크롤러 클래스
PpomppuCrawler
class PpomppuCrawler extends AbstractCrawler {
public function __construct() {
$this->url = "https://www.ppomppu.co.kr/hot.php";
}
public function getSiteName() {
return 'ppomppu';
}
protected function getHeaders() { ... }
protected function extractData() { ... }
}
ClienCrawler
class ClienCrawler extends AbstractCrawler {
public function __construct() {
$this->url = "https://www.clien.net/service/board/park";
}
public function getSiteName() {
return 'clien';
}
protected function getHeaders() { ... }
protected function parseHtml() { ... }
protected function extractData() { ... }
}
각 사이트별 크롤러는 AbstractCrawler를 상속받아 사이트별 특성에 맞게 메소드를 오버라이드하고 있습니다. 특히 extractData() 메소드에서는 해당 사이트의 HTML 구조에 맞게 데이터를 추출하는 로직을 구현합니다.
4. 팩토리 패턴 (CrawlerFactory)
class CrawlerFactory {
private static $crawlers = [
'ppomppu' => 'PpomppuCrawler',
'clien' => 'ClienCrawler'
];
public static function createCrawler($site) { ... }
public static function getAvailableCrawlers() { ... }
}
팩토리 패턴을 사용하여 크롤러 인스턴스를 생성하는 부분을 캡슐화했습니다. 새로운 크롤러를 추가할 때는 $crawlers 배열에만 추가하면 됩니다.
5. 관리자 클래스 (CrawlerManager)
class CrawlerManager {
private $crawlers = [];
public function addCrawler($site) { ... }
public function crawlSpecific($site) { ... }
public function crawlAll() { ... }
}
여러 크롤러를 관리하고 실행하는 클래스입니다. 단일 책임 원칙(SRP)에 따라 크롤러 관리만을 담당합니다.
주요 디자인 패턴 분석
- 인터페이스 분리 원칙(ISP): CrawlerInterface를 통해 모든 크롤러가 구현해야 할 최소한의 메소드만 정의했습니다.
- 템플릿 메소드 패턴: AbstractCrawler의 crawl() 메소드에서 전체 프로세스의 흐름을 정의하고, 세부 구현은 자식 클래스에 위임합니다.
- 팩토리 패턴: CrawlerFactory를 통해 크롤러 객체 생성 로직을 캡슐화했습니다.
- 전략 패턴: 각 크롤러는 동일한 인터페이스를 구현하지만 다른 전략(사이트별 크롤링 방식)을 사용합니다.
코드의 장점
- 확장성: 새로운 사이트를 크롤링하려면 AbstractCrawler를 상속받는 새 클래스를 만들고 CrawlerFactory에 등록하기만 하면 됩니다.
- 유지보수성: 각 크롤러는 자신의 사이트만 담당하므로, 한 사이트의 HTML 구조가 변경되어도 다른 크롤러에는 영향을 주지 않습니다.
- 코드 재사용: 크롤링의 공통 로직(HTTP 요청, HTML 파싱 등)은 추상 클래스에서 구현되어 재사용됩니다.
- 캡슐화: 각 클래스는 자신의 책임에만 집중하며, 내부 구현 상세는 숨겨져 있습니다.
실행 흐름
- index.php에서 CrawlerManager 인스턴스를 생성합니다.
- 필요한 크롤러를 addCrawler() 메소드로 추가합니다.
- crawlAll() 메소드를 호출하여 모든 사이트를 크롤링합니다.
- 각 사이트의 크롤링 결과를 출력합니다.
개선할 수 있는 부분
- 예외 처리 강화: 현재는 기본적인 예외 처리만 되어 있지만, 네트워크 오류, 파싱 오류 등 더 세분화된 예외 처리가 필요합니다.
- 로깅 기능: 크롤링 과정의 로그를 남기는 기능을 추가하면 디버깅에 도움이 될 것입니다.
- 비동기 처리: 여러 사이트를 동시에 크롤링하기 위해 비동기 처리를 적용할 수 있습니다.
- 데이터 저장: 현재는 결과를 출력만 하지만, 데이터베이스나 파일에 저장하는 기능을 추가할 수 있습니다.
결론
이 PHP 크롤러 코드는 객체지향 설계 원칙과 디자인 패턴을 잘 활용한 좋은 예시입니다. 인터페이스와 추상 클래스를 통한 계층 구조, 팩토리 패턴을 통한 객체 생성 캡슐화, 템플릿 메소드 패턴을 통한 알고리즘 구조화 등의 기법이 적절히 사용되었습니다. 이러한 설계 방식은 코드의 확장성과 유지보수성을 크게 향상시킵니다.
전체 소스 코드
CrawlerInterface.php
<?php
interface CrawlerInterface {
public function crawl();
public function getSiteName();
}
AbstractCrawler.php
<?php
abstract class AbstractCrawler implements CrawlerInterface {
protected $url;
protected $html;
protected $doc;
protected $xpath;
protected $encoding = 'UTF-8'; // 기본 인코딩 설정
public function crawl() {
$this->fetchHtml();
$this->parseHtml();
return $this->extractData();
}
protected function fetchHtml() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHeaders());
$this->html = curl_exec($ch);
curl_close($ch);
}
protected function parseHtml() {
$this->doc = new DOMDocument();
// 사이트별 인코딩에 따라 변환
if ($this->encoding !== 'UTF-8') {
$this->html = iconv($this->encoding, 'UTF-8//IGNORE', $this->html);
}
@$this->doc->loadHTML($this->html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$this->xpath = new DOMXPath($this->doc);
}
protected function getHeaders() {
return [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Cache-Control: no-cache',
'Connection: keep-alive',
'Pragma: no-cache',
'Sec-Fetch-Dest: document',
'Sec-Fetch-Mode: navigate',
'Sec-Fetch-Site: same-origin',
'Sec-Fetch-User: ?1',
'Upgrade-Insecure-Requests: 1'
];
}
abstract protected function extractData();
}
PpomppuCrawler.php
url = "https://www.ppomppu.co.kr/hot.php";
}
public function getSiteName() {
return 'ppomppu';
}
protected function getHeaders() {
$headers = parent::getHeaders();
$headers[] = 'Referer: https://www.ppomppu.co.kr/';
return $headers;
}
protected function extractData() {
$results = [];
$allRows = $this->xpath->query('//table/tr');
foreach ($allRows as $row) {
$board = $this->xpath->query('.//td[1]', $row)->item(0);
$title = $this->xpath->query('.//td[3]', $row)->item(0);
$author = $this->xpath->query('.//td[4]', $row)->item(0);
$date = $this->xpath->query('.//td[5]', $row)->item(0);
$recommend = $this->xpath->query('.//td[6]', $row)->item(0);
$views = $this->xpath->query('.//td[7]', $row)->item(0);
if ($title) {
$link = '';
$linkNode = $this->xpath->query('.//a', $title)->item(0);
if ($linkNode) {
$href = $linkNode->getAttribute('href');
if (strpos($href, 'http') !== 0) {
$link = 'https://www.ppomppu.co.kr' . $href;
} else {
$link = $href;
}
}
$results[] = [
'board' => $board ? trim($board->nodeValue) : '',
'title' => $title ? trim($title->nodeValue) : '',
'link' => $link,
'author' => $author ? trim($author->nodeValue) : '',
'date' => $date ? trim($date->nodeValue) : '',
'recommend' => $recommend ? trim($recommend->nodeValue) : '',
'views' => $views ? trim($views->nodeValue) : ''
];
}
}
return $results;
}
}
ClienCrawler.php
url = "https://www.clien.net/service/board/park";
}
public function getSiteName() {
return 'clien';
}
protected function getHeaders() {
$headers = parent::getHeaders();
$headers[] = 'Referer: https://www.clien.net/';
$headers[] = 'Accept-Charset: UTF-8';
return $headers;
}
protected function parseHtml() {
$this->doc = new DOMDocument();
// UTF-8 그대로 사용
$this->html = mb_convert_encoding($this->html, 'HTML-ENTITIES', 'UTF-8');
@$this->doc->loadHTML($this->html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$this->xpath = new DOMXPath($this->doc);
}
protected function extractData() {
$results = [];
$articles = $this->xpath->query('//div[contains(@class, "list_item")]');
foreach ($articles as $article) {
$title = $this->xpath->query('.//span[contains(@class, "subject")]', $article)->item(0);
$author = $this->xpath->query('.//span[contains(@class, "nickname")]', $article)->item(0);
$date = $this->xpath->query('.//span[contains(@class, "time")][1]', $article)->item(0);
$views = $this->xpath->query('.//span[contains(@class, "hit")]', $article)->item(0);
if ($title) {
$results[] = [
'title' => $title ? trim($title->nodeValue) : '',
'author' => $author ? trim($author->nodeValue) : '',
'date' => $date ? trim($date->nodeValue) : '',
'views' => $views ? trim($views->nodeValue) : ''
];
}
}
return $results;
}
}
CrawlerFactory.php
<?php
class CrawlerFactory {
private static $crawlers = [
'ppomppu' => 'PpomppuCrawler',
'clien' => 'ClienCrawler'
];
public static function createCrawler($site) {
if (!isset(self::$crawlers[$site])) {
throw new Exception("Unknown crawler type: $site");
}
$className = self::$crawlers[$site];
require_once $className . '.php';
return new $className();
}
public static function getAvailableCrawlers() {
return array_keys(self::$crawlers);
}
}
CrawlerManager.php
<?php
class CrawlerManager {
private $crawlers = [];
public function addCrawler($site) {
try {
$this->crawlers[$site] = CrawlerFactory::createCrawler($site);
} catch (Exception $e) {
echo "Error adding crawler for $site: " . $e->getMessage() . "\n";
}
}
// 특정 크롤러만 실행하는 메소드 추가
public function crawlSpecific($site) {
if (!isset($this->crawlers[$site])) {
throw new Exception("Crawler not found for site: $site");
}
return $this->crawlers[$site]->crawl();
}
public function crawlAll() {
$results = [];
foreach ($this->crawlers as $site => $crawler) {
try {
$results[$site] = $crawler->crawl();
} catch (Exception $e) {
echo "Error crawling $site: " . $e->getMessage() . "\n";
}
}
return $results;
}
}
index.php
<?php
header('Content-Type: text/html; charset=UTF-8');
mb_internal_encoding('UTF-8');
setlocale(LC_ALL, 'ko_KR.UTF-8');
require_once 'CrawlerInterface.php';
require_once 'AbstractCrawler.php';
require_once 'CrawlerFactory.php';
require_once 'CrawlerManager.php';
// 크롤러 매니저 생성
$manager = new CrawlerManager();
// 크롤러 추가
$manager->addCrawler('ppomppu');
$manager->addCrawler('clien');
// 모든 사이트 크롤링 실행
$results = $manager->crawlAll();
// 결과 출력
foreach ($results as $site => $siteResults) {
echo "\n=== " . ucfirst($site) . " 인기글 ===\n";
foreach ($siteResults as $result) {
echo implode('|', $result) . "\n";
}
}
'프로그래밍 > Php' 카테고리의 다른 글
php 로 만드는 socket Server 와 socket Client (0) | 2023.02.16 |
---|---|
php 디자인 패턴의 템플릿 메소드 패턴 크롤링 puppeteer 이용한 예제 (0) | 2023.02.02 |
easyOCR 이미지 한글 추출 기능 (php, python, fetch 사용) (1) | 2021.10.28 |
PHP에서 Fetch API를 사용하여 JavaScript로 파일 업로드 (1) | 2021.10.28 |
php exec 사용하여 파이썬 호출후 한글이 안나올때 (0) | 2021.10.26 |