Ver3.0 ๐ท๏ธ PHP ์น ์คํฌ๋ํ ์์ ์ ๋ณต

๐ท๏ธ PHP ์น ์คํฌ๋ํ ์์ ์ ๋ณต
cURL๊ณผ Guzzle๋ก ์น ๋ฐ์ดํฐ๋ฅผ ์์ ์์ฌ๋ก ์์งํ๋ ๋ฐฉ๋ฒ
์ผ, ์ธํฐ๋ท์ ์๋ ์์ฒญ๋ ๋ฐ์ดํฐ๋ค ๋ณด๋ฉด ์ ๋ง ๋ค์์ง ์์? ๐คค ๊ฒฝ์์ฌ ๊ฐ๊ฒฉ ์ ๋ณด, ๋ถ๋์ฐ ์์ธ, ๋ด์ค ํค๋๋ผ์ธ, SNS ํธ๋ ๋... ์ด๋ฐ ๊ฒ๋ค์ ์ผ์ผ์ด ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐ ํ๋ ๊ฑด 21์ธ๊ธฐ์ ๋ง์ง ์์์? ๋ฐ๋ก ์ฌ๊ธฐ์ ์น ์คํฌ๋ํ(Web Scraping)์ด ๋ฑ์ฅํ๋ ๊ฑฐ์ผ!
์น ์คํฌ๋ํ์ ์ฝ๊ฒ ๋งํด์ ์น์ฌ์ดํธ์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ๊ธ์ด์ค๋ ๊ธฐ์ ์ด์ผ. ๋ง์น ๋ก๋ด ๋น์๊ฐ ๋์ ์น์ํํ๋ฉด์ ์ ๋ณด๋ฅผ ์ ๋ฆฌํด์ฃผ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ง. ์ค๋์ PHP๋ก ์ด๊ฑธ ์ด๋ป๊ฒ ํ๋์ง, ํนํ cURL๊ณผ Guzzle์ด๋ผ๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ค์ ์ด๋ป๊ฒ ํ์ฉํ๋์ง ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์๋ ค์ค๊ฒ! ๐
๐ฏ ์น ์คํฌ๋ํ์ด ๋ญ๋ฐ?
์น ์คํฌ๋ํ์ HTML ๋ฌธ์์์ ํ์ํ ์ ๋ณด๋ฅผ ์ถ์ถํ๋ ๊ณผ์ ์ด์ผ. ์น์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ HTML, CSS, JavaScript๋ฅผ ๋ฐ์์ ์์ ํ๋ฉด์ ๋ณด์ฌ์ฃผ์์? ์ฐ๋ฆฌ๋ ๊ทธ HTML ์ฝ๋ ์์์ ์ฐ๋ฆฌ๊ฐ ์ํ๋ ๋ฐ์ดํฐ๋ง ์์ ๋ฝ์๋ด๋ ๊ฑฐ์ง! ๐
2. ๋ถ๋์ฐ ์ ๋ณด ์์ง: ๋งค๋ฌผ ์ ๋ณด๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋ง
3. ๋ด์ค ์ ๊ทธ๋ฆฌ๊ฒ์ดํฐ: ๋ค์ํ ์ธ๋ก ์ฌ์ ๊ธฐ์ฌ๋ฅผ ํ๊ณณ์ ๋ชจ์์ ์ ๊ณต
4. SNS ๋ถ์: ํน์ ํค์๋๋ ํด์ํ๊ทธ์ ํธ๋ ๋ ๋ถ์
5. ์ฑ์ฉ ์ ๋ณด ํตํฉ: ์ฌ๋ฌ ๊ตฌ์ธ๊ตฌ์ง ์ฌ์ดํธ์ ๊ณต๊ณ ๋ฅผ ํ๋์
๊ทผ๋ฐ ์ฌ๊ธฐ์ ์ค์ํ ์ ! โ ๏ธ ์น ์คํฌ๋ํ์ ํฉ๋ฒ์ ์ผ๋ก ํด์ผ ํด. ์น์ฌ์ดํธ์ robots.txt ํ์ผ์ ํ์ธํ๊ณ , ์ด์ฉ์ฝ๊ด์ ์ค์ํ๊ณ , ์๋ฒ์ ๊ณผ๋ถํ๋ฅผ ์ฃผ์ง ์๋๋ก ์ ์ ํ ๋๋ ์ด๋ฅผ ๋๋ ๊ฒ ๋งค๋์ผ. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ธ์ ๋ณด๋ ์ ์๊ถ์ด ์๋ ์ฝํ
์ธ ๋ ์กฐ์ฌํด์ผ ํ๊ณ !
๐ ๏ธ PHP๋ก ์น ์คํฌ๋ํํ๋ ๋ฐฉ๋ฒ๋ค
PHP์์ ์น ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ด. ๊ฐ๊ฐ์ ํน์ง์ ์ดํด๋ณด์!
1. file_get_contents() - ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ
์ด๊ฑด ์ ๋ง ์ด๋ณด์๋ ์ฝ๊ฒ ์ธ ์ ์๋ ๋ฐฉ๋ฒ์ด์ผ. ๋จ ํ ์ค๋ก ์น ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๊ฑฐ๋ ! ๐
<?php
// ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ๋ฒ
$html = file_get_contents('https://example.com');
echo $html;
// ์ต์
์ ์ถ๊ฐํ ์ฌ์ฉ๋ฒ
$options = [
'http' => [
'method' => 'GET',
'header' => 'User-Agent: Mozilla/5.0'
]
];
$context = stream_context_create($options);
$html = file_get_contents('https://example.com', false, $context);
?>
โ ๏ธ ๋จ์ : ๋ณต์กํ ์์ฒญ ์ฒ๋ฆฌ ์ด๋ ค์, ์๋ฌ ํธ๋ค๋ง ์ ํ์ , HTTPS ์ธ์ฆ์ ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
2. cURL - ๊ฐ๋ ฅํ ๋ค์ดํฐ๋ธ ๋๊ตฌ
cURL์ PHP์ ๊ธฐ๋ณธ์ผ๋ก ๋ด์ฅ๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ๋ณต์กํ HTTP ์์ฒญ๋ ์์ ์์ฌ๋ก ์ฒ๋ฆฌํ ์ ์์ง! ๐ช
<?php
// cURL ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
$ch = curl_init();
// ์ต์
์ค์
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0');
// ์คํ
$response = curl_exec($ch);
// ์๋ฌ ์ฒดํฌ
if(curl_errno($ch)) {
echo 'Error: ' . curl_error($ch);
}
// ์ฐ๊ฒฐ ์ข
๋ฃ
curl_close($ch);
echo $response;
?>
3. Guzzle - ํ๋์ ์ธ HTTP ํด๋ผ์ด์ธํธ
Guzzle์ PHP ์ปค๋ฎค๋ํฐ์์ ๊ฐ์ฅ ์ฌ๋๋ฐ๋ HTTP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ๊ฐ์ฒด์งํฅ์ ์ด๊ณ ์ฌ์ฉํ๊ธฐ ํธํด์ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ ํธํ์ง! ๐
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
// ํด๋ผ์ด์ธํธ ์์ฑ
$client = new Client();
// GET ์์ฒญ
$response = $client->request('GET', 'https://example.com');
// ์๋ต ์ฒ๋ฆฌ
$statusCode = $response->getStatusCode();
$body = $response->getBody()->getContents();
echo $body;
?>
๐ cURL ์์ ์ ๋ณตํ๊ธฐ
์, ์ด์ cURL์ ์ ๋๋ก ํํค์ณ๋ณด์! cURL์ Client URL์ ์ฝ์๋ก, ๋ค์ํ ํ๋กํ ์ฝ์ ์ง์ํ๋ ๋ฐ์ดํฐ ์ ์ก ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. HTTP๋ฟ๋ง ์๋๋ผ FTP, SMTP ๋ฑ๋ ์ง์ํ์ง๋ง, ์ฐ๋ฆฌ๋ ์น ์คํฌ๋ํ์ ์ง์คํ ๊ฒ! ๐ฏ
cURL์ ํต์ฌ ์ต์ ๋ค
cURL์ ์ ๋๋ก ํ์ฉํ๋ ค๋ฉด ๋ค์ํ ์ต์ ๋ค์ ์์์ผ ํด. ์์ฃผ ์ฌ์ฉํ๋ ์ต์ ๋ค์ ์ ๋ฆฌํด๋ดค์ด!
| ์ต์ | ์ค๋ช | ์์ |
|---|---|---|
| CURLOPT_URL | ์์ฒญํ URL ์ง์ | curl_setopt($ch, CURLOPT_URL, 'https://api.example.com') |
| CURLOPT_RETURNTRANSFER | ๊ฒฐ๊ณผ๋ฅผ ๋ฌธ์์ด๋ก ๋ฐํ (true) ๋๋ ์ง์ ์ถ๋ ฅ (false) | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true) |
| CURLOPT_FOLLOWLOCATION | ๋ฆฌ๋ค์ด๋ ์ ์๋ ๋ฐ๋ผ๊ฐ๊ธฐ | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true) |
| CURLOPT_TIMEOUT | ํ์์์ ์๊ฐ ์ค์ (์ด) | curl_setopt($ch, CURLOPT_TIMEOUT, 30) |
| CURLOPT_USERAGENT | User-Agent ํค๋ ์ค์ | curl_setopt($ch, CURLOPT_USERAGENT, 'MyBot/1.0') |
์ค์ cURL ์์ - POST ์์ฒญํ๊ธฐ
GET ์์ฒญ๋ง ํ ์ค ์๋ฉด ์ฌ๋ฏธ์์ง! POST ์์ฒญ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๋ฐฉ๋ฒ๋ ์์๋ณด์. ๋ก๊ทธ์ธ์ด๋ ํผ ์ ์ถํ ๋ ํ์์ผ! ๐
<?php
$ch = curl_init();
// POST ๋ฐ์ดํฐ ์ค๋น
$postData = [
'username' => 'myuser',
'password' => 'mypass',
'remember' => 'on'
];
// cURL ์ต์
์ค์
curl_setopt_array($ch, [
CURLOPT_URL => 'https://example.com/login',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postData),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEJAR => 'cookies.txt', // ์ฟ ํค ์ ์ฅ
CURLOPT_COOKIEFILE => 'cookies.txt', // ์ฟ ํค ์ฌ์ฉ
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml',
'Accept-Language: ko-KR,ko;q=0.9',
'Content-Type: application/x-www-form-urlencoded'
]
]);
$response = curl_exec($ch);
if(curl_errno($ch)) {
die('cURL Error: ' . curl_error($ch));
}
curl_close($ch);
echo $response;
?>
CURLOPT_COOKIEJAR์ CURLOPT_COOKIEFILE์ ์ฌ์ฉํ๋ฉด ์ฟ ํค๋ฅผ ํ์ผ๋ก ์ ์ฅํ๊ณ ์ฌ์ฌ์ฉํ ์ ์์ด!
JSON API ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
์์ฆ์ ๋ง์ ์น์ฌ์ดํธ๊ฐ JSON API๋ฅผ ์ ๊ณตํด. HTML์ ํ์ฑํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๊น๋ํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ง! ๐ฆ
<?php
function fetchJsonApi($url, $headers = []) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge([
'Accept: application/json',
'Content-Type: application/json'
], $headers),
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($httpCode !== 200) {
throw new Exception("HTTP Error: $httpCode");
}
return json_decode($response, true);
}
// ์ฌ์ฉ ์์
try {
$data = fetchJsonApi('https://api.example.com/products');
foreach($data['products'] as $product) {
echo "์ํ๋ช
: {$product['name']}, ๊ฐ๊ฒฉ: {$product['price']}์\n";
}
} catch(Exception $e) {
echo "์๋ฌ ๋ฐ์: " . $e->getMessage();
}
?>
๋ฉํฐ cURL๋ก ๋์์ ์ฌ๋ฌ ์์ฒญํ๊ธฐ
ํ ๋ฒ์ ํ๋์ฉ ์์ฒญํ๋ฉด ๋๋ฌด ๋๋ฆฌ์์? ๋ฉํฐ cURL์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์์ฒญ์ ๋์์ ์ฒ๋ฆฌํ ์ ์์ด์ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ! โก
<?php
function multiCurlRequest($urls) {
$multiHandle = curl_multi_init();
$curlHandles = [];
// ๊ฐ URL์ ๋ํ cURL ํธ๋ค ์์ฑ
foreach($urls as $i => $url) {
$curlHandles[$i] = curl_init();
curl_setopt_array($curlHandles[$i], [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
curl_multi_add_handle($multiHandle, $curlHandles[$i]);
}
// ๋ชจ๋ ์์ฒญ ์คํ
$running = null;
do {
curl_multi_exec($multiHandle, $running);
curl_multi_select($multiHandle);
} while($running > 0);
// ๊ฒฐ๊ณผ ์์ง
$results = [];
foreach($curlHandles as $i => $ch) {
$results[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}
// ์ฌ์ฉ ์์
$urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3'
];
$responses = multiCurlRequest($urls);
foreach($responses as $index => $html) {
echo "ํ์ด์ง " . ($index + 1) . " ๊ธธ์ด: " . strlen($html) . " bytes\n";
}
?>
โจ Guzzle๋ก ์ฐ์ํ๊ฒ ์คํฌ๋ํํ๊ธฐ
์, ์ด์ Guzzle์ ์ธ๊ณ๋ก ๋ค์ด๊ฐ๋ณด์! Guzzle์ cURL์ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค์ด์ง ํ๋์ ์ธ HTTP ํด๋ผ์ด์ธํธ์ผ. ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํ๊ณ ์ฝ๊ธฐ ์ฌ์์ ์ ์ง๋ณด์๊ฐ ํธํด! ๐จ
Guzzle ์ค์นํ๊ธฐ
Guzzle์ Composer๋ฅผ ํตํด ์ค์นํด์ผ ํด. ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํ๋ฉด ๋ผ!
composer require guzzlehttp/guzzle
Guzzle ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
Guzzle์ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ์ ์ ๋ง ์ง๊ด์ ์ด์ผ. ๊ฐ์ฒด์งํฅ ๋ฐฉ์์ด๋ผ ์ฝ๋๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์ฝํ!
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
// ํด๋ผ์ด์ธํธ ์์ฑ (๊ธฐ๋ณธ ์ค์ ํฌํจ)
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 30.0,
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'application/json'
]
]);
try {
// GET ์์ฒญ
$response = $client->request('GET', '/products');
// ์๋ต ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
$statusCode = $response->getStatusCode(); // 200
$contentType = $response->getHeaderLine('content-type');
$body = $response->getBody()->getContents();
echo "์ํ ์ฝ๋: $statusCode\n";
echo "์๋ต ๋ด์ฉ: $body\n";
} catch(RequestException $e) {
echo "์์ฒญ ์คํจ: " . $e->getMessage();
if($e->hasResponse()) {
echo "\n์๋ต ์ฝ๋: " . $e->getResponse()->getStatusCode();
}
}
?>
Guzzle๋ก POST ์์ฒญํ๊ธฐ
POST ์์ฒญ๋ cURL๋ณด๋ค ํจ์ฌ ๊ฐ๋จํด! ๋ค์ํ ํ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์ ์กํ ์ ์์ด. ๐
<?php
use GuzzleHttp\Client;
$client = new Client();
// 1. ํผ ๋ฐ์ดํฐ ์ ์ก
$response = $client->request('POST', 'https://example.com/login', [
'form_params' => [
'username' => 'myuser',
'password' => 'mypass'
]
]);
// 2. JSON ๋ฐ์ดํฐ ์ ์ก
$response = $client->request('POST', 'https://api.example.com/users', [
'json' => [
'name' => 'ํ๊ธธ๋',
'email' => 'hong@example.com',
'age' => 25
]
]);
// 3. ๋ฉํฐํํธ ํผ ๋ฐ์ดํฐ (ํ์ผ ์
๋ก๋)
$response = $client->request('POST', 'https://example.com/upload', [
'multipart' => [
[
'name' => 'file',
'contents' => fopen('/path/to/file.jpg', 'r'),
'filename' => 'photo.jpg'
],
[
'name' => 'description',
'contents' => 'ํ๋กํ ์ฌ์ง'
]
]
]);
echo $response->getBody();
?>
Guzzle์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค
Guzzle์ด ์ธ๊ธฐ ์๋ ์ด์ ๋ ๋จ์ํ ์ฌ์ฉํ๊ธฐ ์ฌ์์๋ง์ด ์๋์ผ. ์ ๋ง ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค์ด ๋ง๊ฑฐ๋ ! ๐ฅ
2. ๋ฏธ๋ค์จ์ด: ์์ฒญ/์๋ต์ ๊ฐ๋ก์ฑ์ ์ปค์คํ ๋ก์ง ์ถ๊ฐ
3. ์๋ ์ฌ์๋: ์คํจํ ์์ฒญ์ ์๋์ผ๋ก ์ฌ์๋
4. ์คํธ๋ฆฌ๋ฐ: ๋์ฉ๋ ํ์ผ์ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ
5. ์ฟ ํค ๊ด๋ฆฌ: ์ธ์ ์ ์ง๋ฅผ ์ํ ์ฟ ํค ์๋ ๊ด๋ฆฌ
๋น๋๊ธฐ ์์ฒญ์ผ๋ก ์ฑ๋ฅ ๊ทน๋ํ
Guzzle์ ๋น๋๊ธฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ํ์ด์ง๋ฅผ ๋์์ ๊ฐ์ ธ์ฌ ์ ์์ด. ๋ฉํฐ cURL๊ณผ ๋น์ทํ์ง๋ง ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด! ๐
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
$client = new Client();
// ๋น๋๊ธฐ ์์ฒญ ์์ฑ
$promises = [
'page1' => $client->getAsync('https://example.com/page1'),
'page2' => $client->getAsync('https://example.com/page2'),
'page3' => $client->getAsync('https://example.com/page3')
];
// ๋ชจ๋ ์์ฒญ์ด ์๋ฃ๋ ๋๊น์ง ๋๊ธฐ
$results = Promise\Utils::unwrap($promises);
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
foreach($results as $key => $response) {
echo "$key: " . $response->getStatusCode() . "\n";
echo "๋ด์ฉ ๊ธธ์ด: " . strlen($response->getBody()) . " bytes\n\n";
}
// ๋๋ ๊ฐ ์์ฒญ์ ์ฝ๋ฐฑ ์ถ๊ฐ
$promises = [];
$urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3'
];
foreach($urls as $url) {
$promises[] = $client->getAsync($url)->then(
function($response) use ($url) {
echo "์ฑ๊ณต: $url - " . $response->getStatusCode() . "\n";
},
function($exception) use ($url) {
echo "์คํจ: $url - " . $exception->getMessage() . "\n";
}
);
}
// ๋ชจ๋ Promise ์คํ
Promise\Utils::settle($promises)->wait();
?>
์ฌ์๋ ๋ก์ง ๊ตฌํํ๊ธฐ
๋คํธ์ํฌ๋ ๋ถ์์ ํ ์ ์์ด. ์ผ์์ ์ธ ์ค๋ฅ๋ก ์คํจํ ์์ฒญ์ ์๋์ผ๋ก ์ฌ์๋ํ๋ ๊ฑด ํ์์ผ! ๐
<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
// ์ฌ์๋ ๋ฏธ๋ค์จ์ด ์์ฑ
$handlerStack = HandlerStack::create();
$handlerStack->push(Middleware::retry(
function($retries, RequestInterface $request, ResponseInterface $response = null, $exception = null) {
// ์ต๋ 3๋ฒ๊น์ง ์ฌ์๋
if($retries >= 3) {
return false;
}
// 5xx ์๋ฌ๋ ์ฐ๊ฒฐ ์คํจ ์ ์ฌ์๋
if($exception !== null) {
return true;
}
if($response && $response->getStatusCode() >= 500) {
return true;
}
return false;
},
function($retries) {
// ์ฌ์๋ ๊ฐ๊ฒฉ (์ง์ ๋ฐฑ์คํ)
return 1000 * pow(2, $retries);
}
));
$client = new Client([
'handler' => $handlerStack,
'timeout' => 10
]);
try {
$response = $client->request('GET', 'https://unstable-api.example.com/data');
echo $response->getBody();
} catch(\Exception $e) {
echo "3๋ฒ ์ฌ์๋ ํ์๋ ์คํจ: " . $e->getMessage();
}
?>
๐ HTML ํ์ฑํ๊ธฐ - ๋ฐ์ดํฐ ์ถ์ถ์ ํต์ฌ
HTML์ ๊ฐ์ ธ์์ผ๋ฉด ์ด์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํด์ผ์ง! ์ด ๊ณผ์ ์ ํ์ฑ(Parsing)์ด๋ผ๊ณ ํด. PHP์๋ ์ฌ๋ฌ ํ์ฑ ๋๊ตฌ๊ฐ ์๋๋ฐ, ๊ฐ์ฅ ๋ง์ด ์ฐ์ด๋ ๊ฒ๋ค์ ์์๋ณด์! ๐ฃ
DOMDocument์ DOMXPath
PHP ๋ด์ฅ ํด๋์ค์ธ DOMDocument๋ HTML/XML์ ํ์ฑํ๋ ๊ธฐ๋ณธ ๋๊ตฌ์ผ. ์ข ํฌ๋ฐํ์ง๋ง ๊ฐ๋ ฅํด!
<?php
$html = '<html>
<body>
<div class="product">
<h2>๋
ธํธ๋ถ</h2>
<span class="price">1,200,000์</span>
</div>
<div class="product">
<h2>๋ง์ฐ์ค</h2>
<span class="price">35,000์</span>
</div>
</body>
</html>';
// DOMDocument ์์ฑ
$dom = new DOMDocument();
@$dom->loadHTML($html); // @ ๋ ๊ฒฝ๊ณ ๋ฉ์์ง ์จ๊น
// XPath ์์ฑ
$xpath = new DOMXPath($dom);
// ๋ชจ๋ ์ํ๋ช
์ถ์ถ
$productNames = $xpath->query('//div[@class="product"]/h2');
foreach($productNames as $name) {
echo "์ํ๋ช
: " . $name->textContent . "\n";
}
// ๋ชจ๋ ๊ฐ๊ฒฉ ์ถ์ถ
$prices = $xpath->query('//span[@class="price"]');
foreach($prices as $price) {
echo "๊ฐ๊ฒฉ: " . $price->textContent . "\n";
}
// ํน์ ์กฐ๊ฑด์ผ๋ก ๊ฒ์
$expensiveProducts = $xpath->query('//div[@class="product"][contains(span[@class="price"], "1,200,000")]');
echo "\n๊ณ ๊ฐ ์ํ ๊ฐ์: " . $expensiveProducts->length;
?>
Simple HTML DOM Parser
DOMDocument๊ฐ ๋๋ฌด ๋ณต์กํ๋ค๋ฉด Simple HTML DOM Parser๋ฅผ ์จ๋ด! jQuery์ฒ๋ผ ์ง๊ด์ ์ด์ผ. ๐
<?php
// Composer๋ก ์ค์น: composer require simplehtmldom/simplehtmldom
require 'vendor/autoload.php';
use simplehtmldom\HtmlWeb;
$client = new HtmlWeb();
$html = $client->load('https://example.com');
// CSS ์ ํ์๋ก ์์ ์ฐพ๊ธฐ
foreach($html->find('.product') as $product) {
$name = $product->find('h2', 0)->plaintext;
$price = $product->find('.price', 0)->plaintext;
echo "์ํ: $name - $price\n";
}
// ์์ฑ ๊ฐ ๊ฐ์ ธ์ค๊ธฐ
foreach($html->find('a') as $link) {
echo "๋งํฌ: " . $link->href . "\n";
}
// ์ฒซ ๋ฒ์งธ ์์๋ง ๊ฐ์ ธ์ค๊ธฐ
$firstProduct = $html->find('.product', 0);
if($firstProduct) {
echo "์ฒซ ๋ฒ์งธ ์ํ: " . $firstProduct->find('h2', 0)->plaintext;
}
?>
Symfony DomCrawler - ํ๋กํ์ ๋์ ์ ํ
Symfony์ DomCrawler ์ปดํฌ๋ํธ๋ ์ ๋ง ๊ฐ๋ ฅํด! CSS ์ ํ์์ XPath๋ฅผ ๋ชจ๋ ์ง์ํ๊ณ , ์ฒด์ด๋ ๋ฐฉ์์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ด. ๐
<?php
// Composer๋ก ์ค์น: composer require symfony/dom-crawler symfony/css-selector
require 'vendor/autoload.php';
use Symfony\Component\DomCrawler\Crawler;
$html = file_get_contents('https://example.com');
$crawler = new Crawler($html);
// CSS ์ ํ์๋ก ํํฐ๋ง
$products = $crawler->filter('.product')->each(function(Crawler $node) {
return [
'name' => $node->filter('h2')->text(),
'price' => $node->filter('.price')->text(),
'link' => $node->filter('a')->attr('href')
];
});
print_r($products);
// ํน์ ํ
์คํธ๋ฅผ ํฌํจํ๋ ์์ ์ฐพ๊ธฐ
$laptops = $crawler->filter('.product')->reduce(function(Crawler $node) {
return strpos($node->filter('h2')->text(), '๋
ธํธ๋ถ') !== false;
});
echo "๋
ธํธ๋ถ ์ํ ์: " . $laptops->count() . "\n";
// ํ
์ด๋ธ ๋ฐ์ดํฐ ์ถ์ถ
$tableData = $crawler->filter('table tr')->each(function(Crawler $row) {
return $row->filter('td')->each(function(Crawler $cell) {
return $cell->text();
});
});
print_r($tableData);
?>
Simple HTML DOM: ๋น ๋ฅธ ํ๋กํ ํ์ดํ, ๊ฐ๋จํ ์คํฌ๋ํ ์์
Symfony DomCrawler: ๋ณต์กํ ์คํฌ๋ํ, ๋๊ท๋ชจ ํ๋ก์ ํธ, ํ ์คํธ ์ฉ์ด์ฑ ์ค์ํ ๋
๐ญ ์ค์ ํ๋ก์ ํธ: ์ผํ๋ชฐ ๊ฐ๊ฒฉ ๋น๊ต ์คํฌ๋ํผ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์ง! ์ค์ ๋ก ๋์ํ๋ ๊ฐ๊ฒฉ ๋น๊ต ์คํฌ๋ํผ๋ฅผ ๋ง๋ค์ด๋ณด์. ์ฌ๋ฌ ์ผํ๋ชฐ์ ์ํ ๊ฐ๊ฒฉ์ ์๋์ผ๋ก ์์งํ๋ ํ๋ก๊ทธ๋จ์ด์ผ! ๐
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use Symfony\Component\DomCrawler\Crawler;
class PriceComparator {
private $client;
private $results = [];
public function __construct() {
$this->client = new Client([
'timeout' => 30,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
]
]);
}
public function searchProduct($keyword) {
$promises = [
'siteA' => $this->searchSiteA($keyword),
'siteB' => $this->searchSiteB($keyword),
'siteC' => $this->searchSiteC($keyword)
];
$results = Promise\Utils::settle($promises)->wait();
foreach($results as $site => $result) {
if($result['state'] === 'fulfilled') {
$this->results[$site] = $result['value'];
} else {
echo "[$site] ๊ฒ์ ์คํจ: " . $result['reason']->getMessage() . "\n";
}
}
return $this->getLowestPrice();
}
private function searchSiteA($keyword) {
return $this->client->getAsync("https://siteA.com/search?q=" . urlencode($keyword))
->then(function($response) {
$html = $response->getBody()->getContents();
$crawler = new Crawler($html);
$products = $crawler->filter('.product-item')->each(function(Crawler $node) {
return [
'name' => $node->filter('.product-name')->text(),
'price' => $this->parsePrice($node->filter('.price')->text()),
'url' => $node->filter('a')->attr('href')
];
});
return $products;
});
}
private function searchSiteB($keyword) {
return $this->client->getAsync("https://siteB.com/products?keyword=" . urlencode($keyword))
->then(function($response) {
$html = $response->getBody()->getContents();
$crawler = new Crawler($html);
$products = $crawler->filter('.item')->each(function(Crawler $node) {
return [
'name' => $node->filter('h3')->text(),
'price' => $this->parsePrice($node->filter('.item-price')->text()),
'url' => 'https://siteB.com' . $node->filter('a')->attr('href')
];
});
return $products;
});
}
private function searchSiteC($keyword) {
// API๋ฅผ ์ ๊ณตํ๋ ๊ฒฝ์ฐ
return $this->client->getAsync("https://api.siteC.com/search", [
'query' => ['q' => $keyword]
])->then(function($response) {
$data = json_decode($response->getBody(), true);
return array_map(function($item) {
return [
'name' => $item['title'],
'price' => $item['price'],
'url' => $item['link']
];
}, $data['items']);
});
}
private function parsePrice($priceText) {
// "1,200,000์" -> 1200000
$price = preg_replace('/[^0-9]/', '', $priceText);
return intval($price);
}
private function getLowestPrice() {
$allProducts = [];
foreach($this->results as $site => $products) {
foreach($products as $product) {
$product['site'] = $site;
$allProducts[] = $product;
}
}
// ๊ฐ๊ฒฉ์ ์ ๋ ฌ
usort($allProducts, function($a, $b) {
return $a['price'] - $b['price'];
});
return $allProducts;
}
public function displayResults() {
if(empty($this->results)) {
echo "๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.\n";
return;
}
$products = $this->getLowestPrice();
echo "\n=== ๊ฐ๊ฒฉ ๋น๊ต ๊ฒฐ๊ณผ ===\n\n";
foreach($products as $index => $product) {
echo ($index + 1) . ". [{$product['site']}] {$product['name']}\n";
echo " ๊ฐ๊ฒฉ: " . number_format($product['price']) . "์\n";
echo " ๋งํฌ: {$product['url']}\n\n";
}
if(count($products) > 0) {
$cheapest = $products[0];
echo "๐ฐ ์ต์ ๊ฐ: " . number_format($cheapest['price']) . "์ ({$cheapest['site']})\n";
}
}
}
// ์ฌ์ฉ ์์
$comparator = new PriceComparator();
$comparator->searchProduct('๋ฌด์ ์ด์ดํฐ');
$comparator->displayResults();
?>
์ด ์ฝ๋๋ ์ค์ ๋ก ๋์ํ๋ ๊ฐ๊ฒฉ ๋น๊ต ์์คํ ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ผ. ๋ฌผ๋ก ์ค์ ์ผํ๋ชฐ์ HTML ๊ตฌ์กฐ์ ๋ง๊ฒ ์ ํ์๋ฅผ ์์ ํด์ผ ํ์ง๋ง, ํต์ฌ ๋ก์ง์ ์ด๋ ๊ฒ ๊ตฌํํ ์ ์์ด! ๐
โก ์ฑ๋ฅ ์ต์ ํ์ ๋ฒ ์คํธ ํ๋ํฐ์ค
์น ์คํฌ๋ํ์ ํ ๋๋ ์ฑ๋ฅ๊ณผ ์์ ์ฑ์ด ์ ๋ง ์ค์ํด. ๋ช ๊ฐ์ง ํต์ฌ ํ์ ์๋ ค์ค๊ฒ! ๐
1. ์บ์ฑ ์ ๋ต
๊ฐ์ ํ์ด์ง๋ฅผ ๋ฐ๋ณตํด์ ๊ฐ์ ธ์ค๋ ๊ฑด ๋นํจ์จ์ ์ด์ผ. ์บ์ฑ์ ํ์ฉํ๋ฉด ์๋๋ ๋น ๋ฅด๊ณ ์๋ฒ ๋ถ๋ด๋ ์ค์ผ ์ ์์ด!
<?php
class CachedScraper {
private $cacheDir = './cache/';
private $cacheTime = 3600; // 1์๊ฐ
public function __construct() {
if(!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
public function fetch($url) {
$cacheFile = $this->getCacheFile($url);
// ์บ์๊ฐ ์ ํจํ๋ฉด ์บ์ ๋ฐํ
if(file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $this->cacheTime) {
return file_get_contents($cacheFile);
}
// ์๋ก ๊ฐ์ ธ์ค๊ธฐ
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $url);
$content = $response->getBody()->getContents();
// ์บ์ ์ ์ฅ
file_put_contents($cacheFile, $content);
return $content;
}
private function getCacheFile($url) {
return $this->cacheDir . md5($url) . '.cache';
}
public function clearCache() {
$files = glob($this->cacheDir . '*.cache');
foreach($files as $file) {
unlink($file);
}
}
}
// ์ฌ์ฉ ์์
$scraper = new CachedScraper();
$html = $scraper->fetch('https://example.com');
?>
2. Rate Limiting (์๋ ์ ํ)
์๋ฒ์ ๋๋ฌด ๋น ๋ฅด๊ฒ ์์ฒญํ๋ฉด IP๊ฐ ์ฐจ๋จ๋ ์ ์์ด. ์ ์ ํ ๋๋ ์ด๋ฅผ ๋๋ ๊ฒ ์ค์ํด! โฑ๏ธ
<?php
class RateLimitedScraper {
private $lastRequestTime = 0;
private $minDelay = 1; // ์ด
public function fetch($url) {
// ๋ง์ง๋ง ์์ฒญ ์ดํ ์ถฉ๋ถํ ์๊ฐ์ด ์ง๋ฌ๋์ง ํ์ธ
$elapsed = microtime(true) - $this->lastRequestTime;
if($elapsed < $this->minDelay) {
$sleepTime = ($this->minDelay - $elapsed) * 1000000; // ๋ง์ดํฌ๋ก์ด
usleep($sleepTime);
}
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $url);
$this->lastRequestTime = microtime(true);
return $response->getBody()->getContents();
}
public function fetchMultiple($urls) {
$results = [];
foreach($urls as $url) {
echo "Fetching: $url\n";
$results[] = $this->fetch($url);
}
return $results;
}
}
// ์ฌ์ฉ ์์
$scraper = new RateLimitedScraper();
$urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3'
];
$results = $scraper->fetchMultiple($urls);
// ๊ฐ ์์ฒญ ์ฌ์ด์ 1์ด์ฉ ๋๊ธฐ
?>
3. User-Agent ๋กํ ์ด์
๊ฐ์ User-Agent๋ก ๊ณ์ ์์ฒญํ๋ฉด ๋ด์ผ๋ก ์ธ์๋ ์ ์์ด. ์ฌ๋ฌ User-Agent๋ฅผ ๋ฒ๊ฐ์ ์ฌ์ฉํ๋ฉด ๋ ์์ฐ์ค๋ฌ์!
<?php
class SmartScraper {
private $userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'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',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
];
private $client;
public function __construct() {
$this->client = new \GuzzleHttp\Client([
'timeout' => 30,
'allow_redirects' => true
]);
}
private function getRandomUserAgent() {
return $this->userAgents[array_rand($this->userAgents)];
}
public function fetch($url) {
$response = $this->client->request('GET', $url, [
'headers' => [
'User-Agent' => $this->getRandomUserAgent(),
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language' => 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding' => 'gzip, deflate, br',
'DNT' => '1',
'Connection' => 'keep-alive',
'Upgrade-Insecure-Requests' => '1'
]
]);
return $response->getBody()->getContents();
}
}
?>
4. ์๋ฌ ํธ๋ค๋ง๊ณผ ๋ก๊น
์คํฌ๋ํ์ ์ธ์ ๋ ์คํจํ ์ ์์ด. ๊ฒฌ๊ณ ํ ์๋ฌ ํธ๋ค๋ง๊ณผ ๋ก๊น ์ด ํ์์ผ! ๐
<?php
class RobustScraper {
private $logFile = 'scraper.log';
public function fetch($url, $maxRetries = 3) {
$attempt = 0;
while($attempt < $maxRetries) {
try {
$attempt++;
$this->log("[$attempt/$maxRetries] Fetching: $url");
$client = new \GuzzleHttp\Client(['timeout' => 30]);
$response = $client->request('GET', $url);
$statusCode = $response->getStatusCode();
if($statusCode === 200) {
$this->log("Success: $url");
return $response->getBody()->getContents();
} else {
$this->log("Unexpected status code: $statusCode for $url");
}
} catch(\GuzzleHttp\Exception\ConnectException $e) {
$this->log("Connection error for $url: " . $e->getMessage());
} catch(\GuzzleHttp\Exception\RequestException $e) {
$this->log("Request error for $url: " . $e->getMessage());
if($e->hasResponse()) {
$statusCode = $e->getResponse()->getStatusCode();
// 4xx ์๋ฌ๋ ์ฌ์๋ํด๋ ์์ฉ์์
if($statusCode >= 400 && $statusCode < 500) {
$this->log("Client error ($statusCode), stopping retries");
throw $e;
}
}
} catch(\Exception $e) {
$this->log("Unexpected error for $url: " . $e->getMessage());
}
// ์ฌ์๋ ์ ๋๊ธฐ
if($attempt < $maxRetries) {
$waitTime = pow(2, $attempt); // ์ง์ ๋ฐฑ์คํ
$this->log("Waiting {$waitTime}s before retry...");
sleep($waitTime);
}
}
$this->log("Failed to fetch $url after $maxRetries attempts");
throw new \Exception("Failed to fetch $url");
}
private function log($message) {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[$timestamp] $message\n";
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
echo $logMessage;
}
}
// ์ฌ์ฉ ์์
$scraper = new RobustScraper();
try {
$html = $scraper->fetch('https://example.com');
echo "Successfully fetched " . strlen($html) . " bytes\n";
} catch(\Exception $e) {
echo "Fatal error: " . $e->getMessage() . "\n";
}
?>
1. robots.txt ํ์ธ: ์ฌ์ดํธ๊ฐ ํฌ๋กค๋ง์ ํ์ฉํ๋์ง ํ์ธ
2. ์ด์ฉ์ฝ๊ด ๊ฒํ : ์๋ํ๋ ์ ๊ทผ์ ๊ธ์งํ๋์ง ํ์ธ
3. ๊ฐ์ธ์ ๋ณด ๋ณดํธ: ๊ฐ์ธ์ ๋ณด๋ฅผ ์์งํ๊ฑฐ๋ ์ ์ฅํ์ง ์๊ธฐ
4. ์ ์๊ถ ์กด์ค: ์์งํ ๋ฐ์ดํฐ์ ์์ ์ ์ด์ฉ ์ ๊ถ๋ฆฌ ํ์ธ
5. ์๋ฒ ๋ถํ ์ต์ํ: ์ ์ ํ ๋๋ ์ด์ ๋์ ์ฐ๊ฒฐ ์ ์ ํ
๐ ๊ณ ๊ธ ๊ธฐ๋ฒ: JavaScript ๋ ๋๋ง ํ์ด์ง ์คํฌ๋ํ
์์ฆ ๋ง์ ์น์ฌ์ดํธ๊ฐ React, Vue, Angular ๊ฐ์ ํ๋ ์์ํฌ๋ก ๋ง๋ค์ด์ ธ์ JavaScript๋ก ์ฝํ ์ธ ๋ฅผ ๋์ ์ผ๋ก ์์ฑํด. ์ผ๋ฐ์ ์ธ HTTP ์์ฒญ์ผ๋ก๋ ๋น HTML๋ง ๋ฐ๊ฒ ๋๋ ๊ฑฐ์ง. ์ด๋ด ๋๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น? ๐ค
Headless Browser ํ์ฉ
Headless Browser๋ ํ๋ฉด ์์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋๋ ๋ธ๋ผ์ฐ์ ์ผ. JavaScript๋ฅผ ์คํํ๊ณ ์์ ํ ๋ ๋๋ง๋ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด!
2. Symfony Panther: Symfony์์ ๋ง๋ ์น ์คํฌ๋ํ ๋๊ตฌ
3. Puppeteer (Node.js): PHP์์ Node.js ์คํฌ๋ฆฝํธ ์คํ
4. Selenium WebDriver: ๋ธ๋ผ์ฐ์ ์๋ํ์ ํ์ค
Chrome PHP ์ฌ์ฉ ์์
<?php
// Composer๋ก ์ค์น: composer require chrome-php/chrome
require 'vendor/autoload.php';
use HeadlessChromium\BrowserFactory;
$browserFactory = new BrowserFactory('chromium-browser');
// ๋ธ๋ผ์ฐ์ ์์
$browser = $browserFactory->createBrowser([
'headless' => true,
'noSandbox' => true,
]);
try {
// ์ ํ์ด์ง ์์ฑ
$page = $browser->createPage();
// URL ์ด๋
$page->navigate('https://example.com')->waitForNavigation();
// JavaScript ์คํ ๋๊ธฐ
$page->evaluate('document.readyState')->waitForResponse();
// ํน์ ์์๊ฐ ๋ํ๋ ๋๊น์ง ๋๊ธฐ
$page->waitForElement('.product-list', 10000); // 10์ด ๋๊ธฐ
// HTML ๊ฐ์ ธ์ค๊ธฐ
$html = $page->evaluate('document.documentElement.outerHTML')->getReturnValue();
// ์คํฌ๋ฆฐ์ท ์ฐ๊ธฐ
$page->screenshot()->saveToFile('screenshot.png');
// ํน์ ์์ ํด๋ฆญ
$page->mouse()->find('.load-more-button')->click();
// ์คํฌ๋กค
$page->evaluate('window.scrollTo(0, document.body.scrollHeight)');
// ํผ ์
๋ ฅ
$page->evaluate('document.querySelector("#search").value = "๋
ธํธ๋ถ"');
$page->keyboard()->typeText('๋
ธํธ๋ถ');
echo "ํ์ด์ง ๊ธธ์ด: " . strlen($html) . " bytes\n";
} finally {
// ๋ธ๋ผ์ฐ์ ์ข
๋ฃ
$browser->close();
}
?>
Symfony Panther๋ก ๋ ์ฝ๊ฒ
Symfony Panther๋ ์ฌ์ฉํ๊ธฐ ๋ ์ฝ๊ณ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ ๊ณตํด!
<?php
// Composer๋ก ์ค์น: composer require symfony/panther
require 'vendor/autoload.php';
use Symfony\Component\Panther\Client;
// Chrome ํด๋ผ์ด์ธํธ ์์ฑ
$client = Client::createChromeClient();
// ํ์ด์ง ๋ฐฉ๋ฌธ
$crawler = $client->request('GET', 'https://example.com');
// JavaScript๊ฐ ์คํ๋ ๋๊น์ง ๋๊ธฐ
$client->waitFor('.product-list');
// ์์ ์ฐพ๊ธฐ (CSS ์ ํ์)
$products = $crawler->filter('.product-item')->each(function($node) {
return [
'name' => $node->filter('.product-name')->text(),
'price' => $node->filter('.price')->text()
];
});
print_r($products);
// ๋ฒํผ ํด๋ฆญ
$crawler->filter('.load-more')->click();
// ํผ ์ ์ถ
$form = $crawler->selectButton('๊ฒ์')->form();
$crawler = $client->submit($form, ['keyword' => '๋
ธํธ๋ถ']);
// ์คํฌ๋ฆฐ์ท
$client->takeScreenshot('result.png');
$client->quit();
?>
2. ์๋: JavaScript ์คํ๊ณผ ๋ ๋๋ง ๋๋ฌธ์ ๋๋ฆผ
3. ์๋ฒ ํ๊ฒฝ: Chrome/Chromium์ด ์ค์น๋์ด ์์ด์ผ ํจ
4. ๋น์ฉ: ํด๋ผ์ฐ๋ ํ๊ฒฝ์์๋ ์ถ๊ฐ ๋น์ฉ ๋ฐ์ ๊ฐ๋ฅ
๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ๊ณ , ๊ฐ๋ฅํ๋ฉด API๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์!
๐ ์ธ์ฆ์ด ํ์ํ ํ์ด์ง ์คํฌ๋ํ
๋ก๊ทธ์ธ์ด ํ์ํ ํ์ด์ง๋ฅผ ์คํฌ๋ํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น? ์ธ์ ๊ณผ ์ฟ ํค ๊ด๋ฆฌ๊ฐ ํต์ฌ์ด์ผ! ๐
์ฟ ํค ๊ธฐ๋ฐ ์ธ์ฆ
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
class AuthenticatedScraper {
private $client;
private $cookieJar;
public function __construct() {
$this->cookieJar = new CookieJar();
$this->client = new Client([
'cookies' => $this->cookieJar,
'timeout' => 30
]);
}
public function login($username, $password) {
// ๋ก๊ทธ์ธ ํ์ด์ง ๋ฐฉ๋ฌธ (CSRF ํ ํฐ ๋ฑ์ ์ป๊ธฐ ์ํด)
$response = $this->client->request('GET', 'https://example.com/login');
$html = $response->getBody()->getContents();
// CSRF ํ ํฐ ์ถ์ถ (ํ์ํ ๊ฒฝ์ฐ)
preg_match('/name="csrf_token" value="([^"]+)"/', $html, $matches);
$csrfToken = $matches[1] ?? '';
// ๋ก๊ทธ์ธ ์์ฒญ
$response = $this->client->request('POST', 'https://example.com/login', [
'form_params' => [
'username' => $username,
'password' => $password,
'csrf_token' => $csrfToken
]
]);
// ๋ก๊ทธ์ธ ์ฑ๊ณต ํ์ธ
if($response->getStatusCode() === 200) {
echo "๋ก๊ทธ์ธ ์ฑ๊ณต!\n";
return true;
}
return false;
}
public function fetchProtectedPage($url) {
// ์ฟ ํค๊ฐ ์๋์ผ๋ก ํฌํจ๋จ
$response = $this->client->request('GET', $url);
return $response->getBody()->getContents();
}
public function saveCookies($filename) {
// ์ฟ ํค๋ฅผ ํ์ผ๋ก ์ ์ฅ
$cookies = [];
foreach($this->cookieJar as $cookie) {
$cookies[] = [
'Name' => $cookie->getName(),
'Value' => $cookie->getValue(),
'Domain' => $cookie->getDomain(),
'Path' => $cookie->getPath(),
'Expires' => $cookie->getExpires()
];
}
file_put_contents($filename, json_encode($cookies));
}
public function loadCookies($filename) {
// ์ ์ฅ๋ ์ฟ ํค ๋ถ๋ฌ์ค๊ธฐ
if(file_exists($filename)) {
$cookies = json_decode(file_get_contents($filename), true);
foreach($cookies as $cookieData) {
$this->cookieJar->setCookie(new \GuzzleHttp\Cookie\SetCookie($cookieData));
}
}
}
}
// ์ฌ์ฉ ์์
$scraper = new AuthenticatedScraper();
// ์ ์ฅ๋ ์ฟ ํค๊ฐ ์์ผ๋ฉด ๋ก๋
$scraper->loadCookies('session_cookies.json');
// ์ฟ ํค๊ฐ ์ ํจํ์ง ์์ผ๋ฉด ๋ก๊ทธ์ธ
try {
$html = $scraper->fetchProtectedPage('https://example.com/mypage');
} catch(\Exception $e) {
echo "์ธ์
๋ง๋ฃ, ์ฌ๋ก๊ทธ์ธ...\n";
$scraper->login('myusername', 'mypassword');
$scraper->saveCookies('session_cookies.json');
$html = $scraper->fetchProtectedPage('https://example.com/mypage');
}
echo "ํ์ด์ง ๊ฐ์ ธ์ค๊ธฐ ์ฑ๊ณต!\n";
?>
API ํ ํฐ ์ธ์ฆ
๋ง์ ํ๋์ ์ธ ์น์ฌ์ดํธ๋ API ํ ํฐ์ ์ฌ์ฉํด. ์ด๊ฒ ํจ์ฌ ๊น๋ํ๊ณ ์์ ํด! ๐ซ
<?php
use GuzzleHttp\Client;
class TokenAuthScraper {
private $client;
private $token;
public function __construct() {
$this->client = new Client(['timeout' => 30]);
}
public function authenticate($apiKey, $apiSecret) {
// OAuth 2.0 ํ ํฐ ๋ฐ๊ธ
$response = $this->client->request('POST', 'https://api.example.com/oauth/token', [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => $apiKey,
'client_secret' => $apiSecret
]
]);
$data = json_decode($response->getBody(), true);
$this->token = $data['access_token'];
echo "ํ ํฐ ๋ฐ๊ธ ์ฑ๊ณต!\n";
}
public function fetchData($endpoint) {
$response = $this->client->request('GET', "https://api.example.com/$endpoint", [
'headers' => [
'Authorization' => "Bearer {$this->token}",
'Accept' => 'application/json'
]
]);
return json_decode($response->getBody(), true);
}
public function saveToken($filename) {
file_put_contents($filename, json_encode([
'token' => $this->token,
'created_at' => time()
]));
}
public function loadToken($filename) {
if(file_exists($filename)) {
$data = json_decode(file_get_contents($filename), true);
// ํ ํฐ์ด 24์๊ฐ ์ด๋ด๋ฉด ์ฌ์ฌ์ฉ
if(time() - $data['created_at'] < 86400) {
$this->token = $data['token'];
return true;
}
}
return false;
}
}
// ์ฌ์ฉ ์์
$scraper = new TokenAuthScraper();
if(!$scraper->loadToken('api_token.json')) {
$scraper->authenticate('your_api_key', 'your_api_secret');
$scraper->saveToken('api_token.json');
}
$products = $scraper->fetchData('products');
print_r($products);
?>
๐ ๋ฐ์ดํฐ ์ ์ฅ๊ณผ ๊ด๋ฆฌ
์คํฌ๋ํํ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ ๊น? ์ฉ๋์ ๋ฐ๋ผ ๋ค์ํ ๋ฐฉ๋ฒ์ด ์์ด! ๐พ
1. CSV ํ์ผ๋ก ์ ์ฅ
๊ฐ๋จํ ํ ํํ์ ๋ฐ์ดํฐ๋ CSV๊ฐ ์ต๊ณ ์ผ. Excel์์๋ ๋ฐ๋ก ์ด ์ ์๊ณ !
<?php
class CsvExporter {
private $filename;
private $handle;
public function __construct($filename) {
$this->filename = $filename;
$this->handle = fopen($filename, 'w');
// UTF-8 BOM ์ถ๊ฐ (Excel์์ ํ๊ธ ๊นจ์ง ๋ฐฉ์ง)
fprintf($this->handle, chr(0xEF).chr(0xBB).chr(0xBF));
}
public function writeHeader($headers) {
fputcsv($this->handle, $headers);
}
public function writeRow($data) {
fputcsv($this->handle, $data);
}
public function writeRows($rows) {
foreach($rows as $row) {
$this->writeRow($row);
}
}
public function close() {
fclose($this->handle);
}
}
// ์ฌ์ฉ ์์
$products = [
['๋
ธํธ๋ถ', 1200000, '์ผ์ฑ์ ์', '์ฌ๊ณ ์์'],
['๋ง์ฐ์ค', 35000, '๋ก์งํ
', '์ฌ๊ณ ์์'],
['ํค๋ณด๋', 89000, '๋ ์คํด๋', 'ํ์ ']
];
$exporter = new CsvExporter('products.csv');
$exporter->writeHeader(['์ํ๋ช
', '๊ฐ๊ฒฉ', '์ ์กฐ์ฌ', '์ฌ๊ณ ์ํ']);
$exporter->writeRows($products);
$exporter->close();
echo "CSV ํ์ผ ์์ฑ ์๋ฃ!\n";
?>
2. JSON ํ์ผ๋ก ์ ์ฅ
๋ณต์กํ ๊ตฌ์กฐ์ ๋ฐ์ดํฐ๋ JSON์ด ํธํด. ๋์ค์ ๋ค์ ์ฝ์ด์ ์ฒ๋ฆฌํ๊ธฐ๋ ์ฝ๊ณ !
<?php
class JsonStorage {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
}
public function save($data) {
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents($this->filename, $json);
}
public function load() {
if(file_exists($this->filename)) {
return json_decode(file_get_contents($this->filename), true);
}
return [];
}
public function append($newData) {
$existing = $this->load();
$existing[] = $newData;
$this->save($existing);
}
}
// ์ฌ์ฉ ์์
$storage = new JsonStorage('products.json');
$product = [
'id' => 1,
'name' => '๋ฌด์ ์ด์ดํฐ',
'price' => 89000,
'specs' => [
'battery' => '6์๊ฐ',
'bluetooth' => '5.0',
'color' => ['๋ธ๋', 'ํ์ดํธ']
],
'reviews' => [
['rating' => 5, 'comment' => '์์ง ์ข์์'],
['rating' => 4, 'comment' => '๊ฐ์ฑ๋น ์ต๊ณ ']
]
];
$storage->append($product);
echo "JSON ์ ์ฅ ์๋ฃ!\n";
?>
3. MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ค๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ์์ผ! ๐๏ธ
<?php
class DatabaseStorage {
private $pdo;
public function __construct($host, $dbname, $username, $password) {
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
$this->pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
public function createTable() {
$sql = "CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price INT NOT NULL,
manufacturer VARCHAR(100),
stock_status VARCHAR(50),
url TEXT,
scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_name (name),
INDEX idx_price (price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->pdo->exec($sql);
}
public function insertProduct($product) {
$sql = "INSERT INTO products (name, price, manufacturer, stock_status, url)
VALUES (:name, :price, :manufacturer, :stock_status, :url)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($product);
return $this->pdo->lastInsertId();
}
public function insertBatch($products) {
$this->pdo->beginTransaction();
try {
$sql = "INSERT INTO products (name, price, manufacturer, stock_status, url)
VALUES (:name, :price, :manufacturer, :stock_status, :url)";
$stmt = $this->pdo->prepare($sql);
foreach($products as $product) {
$stmt->execute($product);
}
$this->pdo->commit();
echo count($products) . "๊ฐ ์ํ ์ ์ฅ ์๋ฃ!\n";
} catch(\Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
public function updatePrice($productId, $newPrice) {
$sql = "UPDATE products SET price = :price WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['price' => $newPrice, 'id' => $productId]);
}
public function findByName($name) {
$sql = "SELECT * FROM products WHERE name LIKE :name ORDER BY price ASC";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['name' => "%$name%"]);
return $stmt->fetchAll();
}
public function getPriceHistory($productName) {
$sql = "SELECT price, scraped_at FROM products
WHERE name = :name
ORDER BY scraped_at DESC
LIMIT 30";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['name' => $productName]);
return $stmt->fetchAll();
}
}
// ์ฌ์ฉ ์์
$db = new DatabaseStorage('localhost', 'scraping_db', 'username', 'password');
$db->createTable();
$products = [
[
'name' => '๋ฌด์ ์ด์ดํฐ',
'price' => 89000,
'manufacturer' => '์ผ์ฑ',
'stock_status' => '์ฌ๊ณ ์์',
'url' => 'https://example.com/product/1'
],
[
'name' => '๋ธ๋ฃจํฌ์ค ์คํผ์ปค',
'price' => 125000,
'manufacturer' => 'JBL',
'stock_status' => '์ฌ๊ณ ์์',
'url' => 'https://example.com/product/2'
]
];
$db->insertBatch($products);
// ๊ฒ์
$results = $db->findByName('์ด์ดํฐ');
print_r($results);
?>
JSON: ๋ณต์กํ ๊ตฌ์กฐ, API ์๋ต ์ ์ฅ, ์ค๊ฐ ๊ท๋ชจ ๋ฐ์ดํฐ
SQLite: ์ค๊ฐ ๊ท๋ชจ, ๋จ์ผ ํ์ผ๋ก ๊ด๋ฆฌ, ์๋ฒ ์ค์ ๋ถํ์
MySQL/PostgreSQL: ๋๋ ๋ฐ์ดํฐ, ๋ณต์กํ ์ฟผ๋ฆฌ, ๋ค์ค ์ฌ์ฉ์, ํ๋ก๋์ ํ๊ฒฝ
๐ ์ค๋ฌด์์ ์์ฃผ ๋ง๋๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ๋ฒ
์ด๋ก ์ ์๋ฒฝํด๋ ์ค์ ์์๋ ์์์น ๋ชปํ ๋ฌธ์ ๋ค์ด ์๊ฒจ. ์์ฃผ ๋ง๋๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์๋ ค์ค๊ฒ! ๐ง
๋ฌธ์ 1: CAPTCHA์ ๋ด ํ์ง
๋ง์ ์ฌ์ดํธ๊ฐ ๋ด์ ๋ง๊ธฐ ์ํด CAPTCHA๋ฅผ ์ฌ์ฉํด. ์ด๊ฑธ ์ด๋ป๊ฒ ์ฐํํ ๊น?
2. CAPTCHA ํด๊ฒฐ ์๋น์ค: 2Captcha, Anti-Captcha ๊ฐ์ ์ ๋ฃ ์๋น์ค ํ์ฉ
3. ์ฌ๋์ฒ๋ผ ํ๋: ๋ง์ฐ์ค ์์ง์, ์คํฌ๋กค, ๋๋ค ๋๋ ์ด ์ถ๊ฐ
4. ์ธ์ ์ฌ์ฌ์ฉ: ํ ๋ฒ ์ธ์ฆ๋ฐ์ ์ธ์ ์ ์ต๋ํ ์ค๋ ์ฌ์ฉ
5. ํ์: ์ฌ์ดํธ ์ด์์์๊ฒ ์ฐ๋ฝํด์ ํ๊ฐ๋ฐ๊ธฐ
๋ฌธ์ 2: ๋์ ์ผ๋ก ๋ก๋๋๋ ์ฝํ ์ธ
๋ฌดํ ์คํฌ๋กค์ด๋ "๋๋ณด๊ธฐ" ๋ฒํผ์ผ๋ก ์ฝํ ์ธ ๊ฐ ๋ก๋๋๋ ๊ฒฝ์ฐ!
<?php
// Panther๋ก ๋ฌดํ ์คํฌ๋กค ์ฒ๋ฆฌ
use Symfony\Component\Panther\Client;
$client = Client::createChromeClient();
$crawler = $client->request('GET', 'https://example.com/infinite-scroll');
$allProducts = [];
$previousCount = 0;
$maxScrolls = 10;
$scrollCount = 0;
while($scrollCount < $maxScrolls) {
// ํ์ฌ ์ํ ์์ง
$products = $crawler->filter('.product-item')->each(function($node) {
return [
'name' => $node->filter('.name')->text(),
'price' => $node->filter('.price')->text()
];
});
// ์๋ก์ด ์ํ์ด ์์ผ๋ฉด ์ข
๋ฃ
if(count($products) === $previousCount) {
break;
}
$allProducts = $products;
$previousCount = count($products);
// ํ์ด์ง ๋๊น์ง ์คํฌ๋กค
$client->executeScript('window.scrollTo(0, document.body.scrollHeight)');
// ์ ์ฝํ
์ธ ๋ก๋ฉ ๋๊ธฐ
sleep(2);
$scrollCount++;
echo "์คํฌ๋กค $scrollCount: " . count($products) . "๊ฐ ์ํ ๋ฐ๊ฒฌ\n";
}
echo "์ด " . count($allProducts) . "๊ฐ ์ํ ์์ง ์๋ฃ!\n";
$client->quit();
?>
๋ฌธ์ 3: IP ์ฐจ๋จ
๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋ด๋ฉด IP๊ฐ ์ฐจ๋จ๋ ์ ์์ด. ํ๋ก์๋ฅผ ์ฌ์ฉํ๋ฉด ํด๊ฒฐ๋ผ!
<?php
use GuzzleHttp\Client;
class ProxyScraper {
private $proxies = [
'http://proxy1.example.com:8080',
'http://proxy2.example.com:8080',
'http://proxy3.example.com:8080'
];
private $currentProxyIndex = 0;
public function fetch($url) {
$proxy = $this->getNextProxy();
$client = new Client([
'proxy' => $proxy,
'timeout' => 30,
'verify' => false // SSL ๊ฒ์ฆ ๋นํ์ฑํ (ํ๋ก์ ์ฌ์ฉ ์)
]);
try {
$response = $client->request('GET', $url);
return $response->getBody()->getContents();
} catch(\Exception $e) {
echo "ํ๋ก์ $proxy ์คํจ: " . $e->getMessage() . "\n";
// ๋ค๋ฅธ ํ๋ก์๋ก ์ฌ์๋
return $this->fetch($url);
}
}
private function getNextProxy() {
$proxy = $this->proxies[$this->currentProxyIndex];
$this->currentProxyIndex = ($this->currentProxyIndex + 1) % count($this->proxies);
return $proxy;
}
}
// ๋๋ ํ๋ก์ ๋กํ
์ด์
์๋น์ค ์ฌ์ฉ
class RotatingProxyScraper {
private $client;
public function __construct($proxyApiKey) {
$this->client = new Client([
'proxy' => "http://api:$proxyApiKey@proxy.example.com:8080",
'timeout' => 30
]);
}
public function fetch($url) {
// ํ๋ก์ ์๋น์ค๊ฐ ์๋์ผ๋ก IP๋ฅผ ๋กํ
์ด์
$response = $this->client->request('GET', $url);
return $response->getBody()->getContents();
}
}
?>
๋ฌธ์ 4: ์ธ์ฝ๋ฉ ๋ฌธ์
ํ๊ธ์ด ๊นจ์ง๋ ๋ฌธ์ ๋ ์ ๋ง ํํด. ์ธ์ฝ๋ฉ์ ์ ๋๋ก ์ฒ๋ฆฌํด์ผ ํด! ๐ค
<?php
class EncodingHandler {
public function fetchAndConvert($url) {
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $url);
$html = $response->getBody()->getContents();
// ์ธ์ฝ๋ฉ ๊ฐ์ง
$encoding = mb_detect_encoding($html, ['UTF-8', 'EUC-KR', 'CP949', 'ISO-8859-1'], true);
echo "๊ฐ์ง๋ ์ธ์ฝ๋ฉ: $encoding\n";
// UTF-8๋ก ๋ณํ
if($encoding && $encoding !== 'UTF-8') {
$html = mb_convert_encoding($html, 'UTF-8', $encoding);
}
return $html;
}
public function fixBrokenText($text) {
// ๊นจ์ง ํ๊ธ ๋ณต๊ตฌ ์๋
$encodings = ['UTF-8', 'EUC-KR', 'CP949', 'ISO-8859-1'];
foreach($encodings as $from) {
foreach($encodings as $to) {
$converted = mb_convert_encoding($text, $to, $from);
// ํ๊ธ์ด ์ ๋๋ก ๋ณด์ด๋์ง ํ์ธ
if(preg_match('/[๊ฐ-ํฃ]/', $converted)) {
return $converted;
}
}
}
return $text;
}
}
// ์ฌ์ฉ ์์
$handler = new EncodingHandler();
$html = $handler->fetchAndConvert('https://example.com');
// DOMDocument์์ ์ธ์ฝ๋ฉ ์ฒ๋ฆฌ
$dom = new DOMDocument();
@$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
?>
๐ ์คํฌ๋ํ ํ๋ก์ ํธ ๋ฐฐํฌํ๊ธฐ
๋ก์ปฌ์์ ์ ๋์ํ๋ ์คํฌ๋ํผ๋ฅผ ์๋ฒ์ ๋ฐฐํฌํด์ ์๋ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! โ๏ธ
Cron์ผ๋ก ์ ๊ธฐ ์คํ
๋ฆฌ๋ ์ค ์๋ฒ์์ Cron์ ์ฌ์ฉํ๋ฉด ์คํฌ๋ํผ๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ์คํํ ์ ์์ด!
# crontab -e ๋ก ํธ์ง
# ๋งค์ผ ์ค์ 9์์ ์คํ
0 9 * * * /usr/bin/php /path/to/scraper.php >> /var/log/scraper.log 2>&1
# ๋งค์๊ฐ ์คํ
0 * * * * /usr/bin/php /path/to/scraper.php
# 5๋ถ๋ง๋ค ์คํ
*/5 * * * * /usr/bin/php /path/to/scraper.php
# ์์์ผ ์ค์ 10์์๋ง ์คํ
0 10 * * 1 /usr/bin/php /path/to/scraper.php
Docker๋ก ํ๊ฒฝ ๊ตฌ์ฑ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋์๋ ๋์ผํ ํ๊ฒฝ์์ ์คํํ ์ ์์ด!
# Dockerfile
FROM php:8.2-cli
# ํ์ํ ํ์ฅ ์ค์น
RUN apt-get update && apt-get install -y \
libzip-dev \
zip \
unzip \
git \
curl \
&& docker-php-ext-install zip pdo pdo_mysql
# Composer ์ค์น
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# ์์
๋๋ ํ ๋ฆฌ ์ค์
WORKDIR /app
# ์์กด์ฑ ์ค์น
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
# ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋ ๋ณต์ฌ
COPY . .
# ์คํ
CMD ["php", "scraper.php"]
# docker-compose.yml
version: '3.8'
services:
scraper:
build: .
volumes:
- ./data:/app/data
- ./logs:/app/logs
environment:
- DB_HOST=mysql
- DB_NAME=scraping_db
- DB_USER=scraper
- DB_PASS=secret
depends_on:
- mysql
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: scraping_db
MYSQL_USER: scraper
MYSQL_PASSWORD: secret
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
์ด๋ ๊ฒ ์ค์ ํ๋ฉด docker-compose up ๋ช
๋ น์ด ํ๋๋ก ์ ์ฒด ํ๊ฒฝ์ด ์คํ๋ผ! ๐
- โ ์๋ฌ ๋ก๊น ์ค์
- โ ํ์์์ ์ค์ (์๋ฒ ํ๊ฒฝ์ ๋ง๊ฒ)
- โ ๋ฉ๋ชจ๋ฆฌ ์ ํ ํ์ธ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ ์ค์
- โ ์คํจ ์ ์๋ฆผ ์ค์ (์ด๋ฉ์ผ, Slack ๋ฑ)
- โ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ์ฐ๋
- โ ๋ฐฑ์ ์ ๋ต ์๋ฆฝ
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ๋น์ ์ ์ด์ PHP ์น ์คํฌ๋ํ ์ ๋ฌธ๊ฐ์ผ! ๐ cURL์ ๊ฐ๋ ฅํจ๋ถํฐ Guzzle์ ์ฐ์ํจ, ๊ทธ๋ฆฌ๊ณ ์ค์ ์์ ๋ง๋๋ ๋ค์ํ ๋ฌธ์ ๋ค๊น์ง ๋ชจ๋ ๋ค๋ค์ด.
์น ์คํฌ๋ํ์ ์ ๋ง ์ ์ฉํ ๊ธฐ์ ์ด์ง๋ง, ํญ์ ์ค๋ฆฌ์ ์ผ๋ก ์ฌ์ฉํด์ผ ํ๋ค๋ ๊ฑธ ์์ง ๋ง! ๐ ๋ค๋ฅธ ์ฌ๋์ ์๋ฒ์ ๋ถ๋ด์ ์ฃผ์ง ์๊ณ , ๋ฒ์ ๋ฌธ์ ๊ฐ ์๋์ง ํ์ธํ๊ณ , ๊ฐ๋ฅํ๋ฉด ๊ณต์ API๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ต์ ์ด์ผ.
ํน์ ์น ์คํฌ๋ํ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ค๊ฐ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด? ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์ ์๋ ์์ด! ํ๋ก๊ทธ๋๋ฐ๋ถํฐ ๋ฐ์ดํฐ ๋ถ์๊น์ง ๋ค์ํ ์ฌ๋ฅ์ ๊ฐ์ง ์ฌ๋๋ค์ด ๋ชจ์ฌ ์๊ฑฐ๋ . ๋๋ ๋น์ ์ด ์คํฌ๋ํ ์ ๋ฌธ๊ฐ๊ฐ ๋์ด์ ์ฌ๋ฅ๋ท์์ ๋ค๋ฅธ ์ฌ๋๋ค์ ๋์์ค ์๋ ์๊ณ ! ๐
2. ์๋ฌ ํธ๋ค๋ง ๊ฐํ: ๋ค์ํ ์์ธ ์ํฉ์ ์ฒ๋ฆฌํ๋ ๊ฒฌ๊ณ ํ ์ฝ๋ ์์ฑ
3. ์ฑ๋ฅ ์ต์ ํ: ๋น๋๊ธฐ ์ฒ๋ฆฌ, ์บ์ฑ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
4. ์ค์ผ์ผ์ : ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ์์คํ ๊ตฌ์ถ
5. ์ปค๋ฎค๋ํฐ ์ฐธ์ฌ: GitHub์์ ์คํ์์ค ํ๋ก์ ํธ ๊ธฐ์ฌํ๊ธฐ
์, ์ด์ ๋น์ ์ ์ฒซ ์คํฌ๋ํ ํ๋ก์ ํธ๋ฅผ ์์ํด๋ณผ๊น? ์ฝ๋ฉ์ ์ง์ ํด๋ด์ผ ์ค๋ ฅ์ด ๋์ด! ๐ช ํ์ดํ !
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.