์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” Perl๋กœ ๋ฐฐ์šฐ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘ ์™„์ „์ •๋ณต: ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋”ฐ๋ผํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์˜ ๋ชจ๋“  ๊ฒƒ ๐Ÿ”

๐Ÿ” Perl๋กœ ๋ฐฐ์šฐ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘ ์™„์ „์ •๋ณต: ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋”ฐ๋ผํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์˜ ๋ชจ๋“  ๊ฒƒ ๐Ÿ”

์•ˆ๋…•? ์˜ค๋Š˜์€ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์„ธ๊ณ„์˜ ์ˆจ์€ ๋ณด์„ ๊ฐ™์€ ์–ธ์–ด์ธ Perl์„ ์ด์šฉํ•œ ์›น ์Šคํฌ๋ž˜ํ•‘ ๊ธฐ๋ฒ•์— ๋Œ€ํ•ด ํ•จ๊ป˜ ์•Œ์•„๋ณผ ๊ฑฐ์•ผ. ์›น์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๊ธฐ์ˆ ์€ ์š”์ฆ˜ ์ •๋ง ํ•ซํ•œ ์Šคํ‚ฌ์ด์ง€! ๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€๋ถ€ํ„ฐ ๋งˆ์ผ€ํ„ฐ, ์—ฐ๊ตฌ์›๊นŒ์ง€ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์—์„œ ํ™œ์šฉ๋˜๊ณ  ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์›น ์Šคํฌ๋ž˜ํ•‘ ๊ด€๋ จ ์žฌ๋Šฅ์ด ์ธ๊ธฐ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค, ์•Œ๊ณ  ์žˆ์—ˆ์–ด? ๐Ÿ˜‰

์ด ๊ธ€์—์„œ๋Š” Perl์ด๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ A๋ถ€ํ„ฐ Z๊นŒ์ง€ ์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ์„ค๋ช…ํ•ด ์ค„๊ฒŒ. ์ฝ”๋“œ ์˜ˆ์ œ์™€ ์‹ค์ „ ํŒ๊นŒ์ง€ ๋ชจ๋‘ ๋‹ด์•˜์œผ๋‹ˆ ๋๊นŒ์ง€ ๋”ฐ๋ผ์™€ ๋ณด์ž! ๐Ÿš€

๐Ÿ“š ๋ชฉ์ฐจ

  1. Perl๊ณผ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋งŒ๋‚จ: ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ
  2. Perl ์„ค์น˜๋ถ€ํ„ฐ ์ฒซ ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ๊นŒ์ง€
  3. ํ•„์ˆ˜ Perl ๋ชจ๋“ˆ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ: LWP, HTML::Parser, Mojo::DOM
  4. ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”์ถœํ•˜๊ธฐ
  5. ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์ „๋žต
  6. ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ฐ€๊ณตํ•˜๊ธฐ
  7. ์ž๋™ํ™”์™€ ์Šค์ผ€์ค„๋ง: ์ฃผ๊ธฐ์  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
  8. ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ๋””๋ฒ„๊น… ๊ธฐ๋ฒ•
  9. ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ฒ•์ , ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ
  10. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ๋งŒ๋“ค๊ธฐ

1. Perl๊ณผ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋งŒ๋‚จ: ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ ๐Ÿค

Perl(Practical Extraction and Report Language)์€ 1987๋…„์— ๋ž˜๋ฆฌ ์›”(Larry Wall)์ด ๊ฐœ๋ฐœํ•œ ๊ณ ๊ธ‰ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์•ผ. ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ์— ํŠนํ™”๋œ ์–ธ์–ด๋กœ, ์ •๊ทœํ‘œํ˜„์‹ ์ง€์›์ด ๊ฐ•๋ ฅํ•ด์„œ ์›น ์Šคํฌ๋ž˜ํ•‘์— ๋”ฑ ๋งž๋Š” ์–ธ์–ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด. ์›น ์ดˆ์ฐฝ๊ธฐ๋ถ€ํ„ฐ CGI ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ์— ๋งŽ์ด ์‚ฌ์šฉ๋˜์—ˆ๋˜ ์–ธ์–ด์ด๊ธฐ๋„ ํ•˜์ง€!

๐Ÿ”Ž ์›น ์Šคํฌ๋ž˜ํ•‘์ด๋ž€?

์›น ์Šคํฌ๋ž˜ํ•‘(Web Scraping)์€ ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ. ๋ธŒ๋ผ์šฐ์ €๋กœ ์›นํŽ˜์ด์ง€๋ฅผ ๋ณด๋Š” ๋Œ€์‹ , ํ”„๋กœ๊ทธ๋žจ์ด ์ž๋™์œผ๋กœ ์›นํŽ˜์ด์ง€์˜ HTML์„ ๊ฐ€์ ธ์™€์„œ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ์ถ”์ถœํ•˜๋Š” ๊ฑฐ์ง€. ๋งˆ์น˜ ์ธํ„ฐ๋„ท์—์„œ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๋กœ๋ด‡ ๊ฐ™์€ ๋А๋‚Œ์ด๋ž„๊นŒ? ๐Ÿค–

Perl์ด ์›น ์Šคํฌ๋ž˜ํ•‘์— ์ ํ•ฉํ•œ ์ด์œ 

1๏ธโƒฃ ๊ฐ•๋ ฅํ•œ ์ •๊ทœํ‘œํ˜„์‹ ์ง€์›

Perl์€ ์ •๊ทœํ‘œํ˜„์‹์˜ ์™•์ด๋ผ๊ณ  ๋ถˆ๋ฆด ๋งŒํผ ํ…์ŠคํŠธ ํŒจํ„ด ๋งค์นญ์ด ๋›ฐ์–ด๋‚˜. HTML์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฐ ์ตœ์ ํ™”๋˜์–ด ์žˆ์–ด.

2๏ธโƒฃ ํ’๋ถ€ํ•œ ์›น ๊ด€๋ จ ๋ชจ๋“ˆ

CPAN(Comprehensive Perl Archive Network)์—๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๋ชจ๋“ˆ์ด ์žˆ์–ด. LWP, Mechanize, Mojo::DOM ๋“ฑ ํ•„์š”ํ•œ ๋„๊ตฌ๊ฐ€ ๋‹ค ์ค€๋น„๋˜์–ด ์žˆ์ง€!

3๏ธโƒฃ ๊ฐ„๊ฒฐํ•œ ์ฝ”๋“œ

"์‰ฌ์šด ์ผ์€ ์‰ฝ๊ฒŒ, ์–ด๋ ค์šด ์ผ์€ ๊ฐ€๋Šฅํ•˜๊ฒŒ" - Perl์˜ ์ฒ ํ•™์ด ์›น ์Šคํฌ๋ž˜ํ•‘์— ๋”ฑ ๋งž์•„. ๋ณต์žกํ•œ ์ž‘์—…๋„ ๋ช‡ ์ค„์˜ ์ฝ”๋“œ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด.

4๏ธโƒฃ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์›

Windows, macOS, Linux ๋“ฑ ๋‹ค์–‘ํ•œ ์šด์˜์ฒด์ œ์—์„œ ๋™์ž‘ํ•ด. ์–ด๋””์„œ๋“  ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์ง€!

Perl Web Web Scraping ๋ฐ์ดํ„ฐ ์ถ”์ถœ & ๊ฐ€๊ณต ์ •๊ทœํ‘œํ˜„์‹ LWP::UserAgent HTML API

์›น ์Šคํฌ๋ž˜ํ•‘์€ ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธ์–ด์˜ค๋Š” ๊ฒƒ ์ด์ƒ์˜ ์˜๋ฏธ๊ฐ€ ์žˆ์–ด. ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์˜์‚ฌ๊ฒฐ์ •์ด ์ค‘์š”ํ•ด์ง„ ์š”์ฆ˜, ์›น์— ํฉ์–ด์ ธ ์žˆ๋Š” ์ •๋ณด๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๋Šฅ๋ ฅ์€ ์—„์ฒญ๋‚œ ๊ฒฝ์Ÿ๋ ฅ์ด ๋  ์ˆ˜ ์žˆ์ง€. ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋Ÿฐ ๊ธฐ์ˆ ์„ ๊ฐ€์ง„ ์‚ฌ๋žŒ๋“ค์˜ ์žฌ๋Šฅ์ด ๋†’์€ ๊ฐ€์น˜๋ฅผ ์ธ์ •๋ฐ›๊ณ  ์žˆ์–ด. ๐Ÿ˜Š

์ด์ œ Perl๋กœ ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ๊ฐœ๋…์€ ์ดํ•ดํ–ˆ์œผ๋‹ˆ, ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์‹ค์ œ ํ™˜๊ฒฝ ์„ค์ •๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด ๋ณผ๊ฒŒ!

2. Perl ์„ค์น˜๋ถ€ํ„ฐ ์ฒซ ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ๊นŒ์ง€ ๐Ÿ› ๏ธ

Perl ์„ค์น˜ํ•˜๊ธฐ

๋จผ์ € Perl์„ ์„ค์น˜ํ•ด์•ผ ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์–ด. ์šด์˜์ฒด์ œ๋ณ„ ์„ค์น˜ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž!

๐ŸชŸ Windows์—์„œ ์„ค์น˜ํ•˜๊ธฐ

Windows์—์„œ๋Š” Strawberry Perl์ด๋‚˜ ActivePerl์„ ์„ค์น˜ํ•˜๋ฉด ๋ผ.

  1. Strawberry Perl ๋‹ค์šด๋กœ๋“œ์—์„œ ์„ค์น˜ ํŒŒ์ผ์„ ๋ฐ›์•„ ์‹คํ–‰
  2. ์„ค์น˜ ๋งˆ๋ฒ•์‚ฌ์˜ ์•ˆ๋‚ด์— ๋”ฐ๋ผ ์ง„ํ–‰
  3. ์„ค์น˜ ์™„๋ฃŒ ํ›„ ๋ช…๋ น ํ”„๋กฌํ”„ํŠธ์—์„œ perl -v ๋ช…๋ น์–ด๋กœ ์„ค์น˜ ํ™•์ธ

๐ŸŽ macOS์—์„œ ์„ค์น˜ํ•˜๊ธฐ

macOS์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Perl์ด ์„ค์น˜๋˜์–ด ์žˆ์ง€๋งŒ, ์ตœ์‹  ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด:

  1. Homebrew๋ฅผ ์ด์šฉ: brew install perl
  2. ๋˜๋Š” Perl ๊ณต์‹ ์‚ฌ์ดํŠธ์—์„œ ๋‹ค์šด๋กœ๋“œ
  3. ํ„ฐ๋ฏธ๋„์—์„œ perl -v๋กœ ์„ค์น˜ ํ™•์ธ

๐Ÿง Linux์—์„œ ์„ค์น˜ํ•˜๊ธฐ

๋Œ€๋ถ€๋ถ„์˜ Linux ๋ฐฐํฌํŒ์—๋Š” Perl์ด ๊ธฐ๋ณธ์œผ๋กœ ์„ค์น˜๋˜์–ด ์žˆ์–ด. ์—†๋‹ค๋ฉด:

  1. Ubuntu/Debian: sudo apt-get install perl
  2. CentOS/RHEL: sudo yum install perl
  3. ํ„ฐ๋ฏธ๋„์—์„œ perl -v๋กœ ์„ค์น˜ ํ™•์ธ

ํ•„์ˆ˜ ๋ชจ๋“ˆ ์„ค์น˜ํ•˜๊ธฐ

Perl๋กœ ์›น ์Šคํฌ๋ž˜ํ•‘์„ ํ•˜๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ชจ๋“ˆ์ด ํ•„์š”ํ•ด. CPAN์„ ํ†ตํ•ด ์‰ฝ๊ฒŒ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด.

cpan LWP::UserAgent
cpan HTML::Parser
cpan HTML::TreeBuilder
cpan Mojo::DOM
cpan HTTP::Cookies
cpan JSON

์ด ๋ชจ๋“ˆ๋“ค์€ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ธฐ๋ณธ ๋„๊ตฌ๋“ค์ด์•ผ. ์›น ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , HTML์„ ํŒŒ์‹ฑํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ๊ฑฐ์•ผ.

์ฒซ ๋ฒˆ์งธ ์›น ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

์ด์ œ ๊ฐ„๋‹จํ•œ ์›น ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณผ๊ฒŒ. ์›น ํŽ˜์ด์ง€์˜ ์ œ๋ชฉ์„ ๊ฐ€์ ธ์˜ค๋Š” ์•„์ฃผ ๊ธฐ๋ณธ์ ์ธ ์˜ˆ์ œ์•ผ.

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use HTML::TreeBuilder;

# ์›น ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
my $ua = LWP::UserAgent->new;
$ua->agent('Mozilla/5.0'); # ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ์„ค์ •

# ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
my $url = 'https://www.example.com';
my $response = $ua->get($url);

# ์š”์ฒญ ์„ฑ๊ณต ํ™•์ธ
if ($response->is_success) {
    # HTML ํŒŒ์‹ฑ
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($response->decoded_content);
    
    # ํŽ˜์ด์ง€ ์ œ๋ชฉ ์ถ”์ถœ
    my $title = $tree->look_down('_tag', 'title');
    print "ํŽ˜์ด์ง€ ์ œ๋ชฉ: ", $title->as_text, "\n";
    
    # ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
    $tree->delete;
} else {
    print "์˜ค๋ฅ˜ ๋ฐœ์ƒ: ", $response->status_line, "\n";
}

๐Ÿ” ์ฝ”๋“œ ์„ค๋ช…

  1. LWP::UserAgent: ์›น ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์—ฌ ์›น ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š” ๋ชจ๋“ˆ
  2. HTML::TreeBuilder: HTML์„ ํŒŒ์‹ฑํ•˜์—ฌ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ชจ๋“ˆ
  3. User-Agent๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋ด‡์ด ์•„๋‹Œ ์ผ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•จ
  4. ์›น ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๊ณ  ์‘๋‹ต์„ ๋ฐ›์Œ
  5. HTML์„ ํŒŒ์‹ฑํ•˜์—ฌ ์ œ๋ชฉ ํƒœ๊ทธ๋ฅผ ์ฐพ์•„ ๋‚ด์šฉ์„ ์ถœ๋ ฅ

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด example.com ์›น์‚ฌ์ดํŠธ์˜ ์ œ๋ชฉ์ด ์ถœ๋ ฅ๋  ๊ฑฐ์•ผ. ๊ฐ„๋‹จํ•˜์ง€? ์ด๊ฒŒ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์•ผ. ์›น ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์™€์„œ โ†’ ํŒŒ์‹ฑํ•˜๊ณ  โ†’ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๊ณผ์ •์ด์ง€.

๐Ÿ’ก ์ดˆ๋ณด์ž ํŒ

์ฝ”๋“œ๋ฅผ ์ €์žฅํ•  ๋•Œ๋Š” .pl ํ™•์žฅ์ž๋ฅผ ์‚ฌ์šฉํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด my_scraper.pl๋กœ ์ €์žฅํ•˜๊ณ , ํ„ฐ๋ฏธ๋„์—์„œ perl my_scraper.pl ๋ช…๋ น์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

Windows์—์„œ๋Š” ํŒŒ์ผ์„ ๋”๋ธ”ํด๋ฆญํ•ด๋„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ธฐ ์ „์— ์ฐฝ์ด ๋‹ซํž ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋ช…๋ น ํ”„๋กฌํ”„ํŠธ์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

1. ์›น ์š”์ฒญ LWP::UserAgent 2. HTML ํŒŒ์‹ฑ HTML::TreeBuilder 3. ๋ฐ์ดํ„ฐ ์ถ”์ถœ look_down, as_text ์›น ์Šคํฌ๋ž˜ํ•‘ ๊ธฐ๋ณธ ํ”„๋กœ์„ธ์Šค ์›น ํŽ˜์ด์ง€ ์š”์ฒญ ๋ฐ ์‘๋‹ต ๋ฐ›๊ธฐ HTML ๊ตฌ์กฐ๋ฅผ ํŠธ๋ฆฌ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์„ ํƒ ๋ฐ ์ถ”์ถœ ์ด ์„ธ ๋‹จ๊ณ„๊ฐ€ ๋ชจ๋“  ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ธฐ๋ณธ ์›๋ฆฌ์ž…๋‹ˆ๋‹ค!

์ด์ œ ๊ธฐ๋ณธ์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ดค์œผ๋‹ˆ, ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ๋” ๊ฐ•๋ ฅํ•œ Perl ๋ชจ๋“ˆ๋“ค์„ ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ. ์ด ๋ชจ๋“ˆ๋“ค์„ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ๋” ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ์—์„œ๋„ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿš€

3. ํ•„์ˆ˜ Perl ๋ชจ๋“ˆ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ: LWP, HTML::Parser, Mojo::DOM ๐Ÿงฐ

์›น ์Šคํฌ๋ž˜ํ•‘์„ ์œ„ํ•œ Perl ๋ชจ๋“ˆ๋“ค์€ ๊ฐ๊ฐ ํŠน๋ณ„ํ•œ ์—ญํ• ์„ ํ•ด. ๋งˆ์น˜ ๋„๊ตฌ ์ƒ์ž ์•ˆ์˜ ๋‹ค์–‘ํ•œ ๋„๊ตฌ๋“ค์ฒ˜๋Ÿผ, ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์ ˆํ•œ ๋ชจ๋“ˆ์„ ์„ ํƒํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ํ•ต์‹ฌ ๋ชจ๋“ˆ๋“ค์„ ์ž์„ธํžˆ ์‚ดํŽด๋ณผ๊ฒŒ.

LWP::UserAgent - ์›น ๋ธŒ๋ผ์šฐ์ € ์—ญํ• 

LWP(Library for WWW in Perl)๋Š” Perl์—์„œ HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ชจ๋“ˆ์ด์•ผ. ํŠนํžˆ LWP::UserAgent๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋ฉด์„œ ์›น ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• ์„ ํ•ด.

LWP::UserAgent ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

use LWP::UserAgent;

# ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ๊ฐ์ฒด ์ƒ์„ฑ
my $ua = LWP::UserAgent->new;

# ํƒ€์ž„์•„์›ƒ ์„ค์ • (์ดˆ ๋‹จ์œ„)
$ua->timeout(10);

# ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ์„ค์ •
$ua->agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

# ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
my $response = $ua->get('https://www.example.com');

# ์‘๋‹ต ํ™•์ธ
if ($response->is_success) {
    my $content = $response->decoded_content;
    print "ํŽ˜์ด์ง€ ๋‚ด์šฉ์„ ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค!\n";
} else {
    print "์˜ค๋ฅ˜ ๋ฐœ์ƒ: ", $response->status_line, "\n";
}

๐Ÿ”ฅ LWP::UserAgent ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

์ฟ ํ‚ค ์ฒ˜๋ฆฌ
use HTTP::Cookies;

# ์ฟ ํ‚ค jar ์ƒ์„ฑ
my $cookie_jar = HTTP::Cookies->new(
    file => "cookies.txt",
    autosave => 1
);

# ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ์— ์ฟ ํ‚ค jar ์„ค์ •
$ua->cookie_jar($cookie_jar);
POST ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
# POST ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
my $response = $ua->post(
    'https://www.example.com/login',
    [
        username => 'myuser',
        password => 'mypass'
    ]
);
ํ”„๋ก์‹œ ์‚ฌ์šฉํ•˜๊ธฐ
# ํ”„๋ก์‹œ ์„ค์ •
$ua->proxy(['http', 'https'], 'http://proxy.example.com:8080');

LWP::UserAgent๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์ฒซ ๋‹จ๊ณ„์ธ ์›น ํŽ˜์ด์ง€ ์š”์ฒญ๊ณผ ์‘๋‹ต ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•ด. ์ด ๋ชจ๋“ˆ์„ ์ž˜ ํ™œ์šฉํ•˜๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ๋‚˜ ๋ณต์žกํ•œ ํผ ์ œ์ถœ๋„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด!

HTML::Parser์™€ HTML::TreeBuilder - HTML ๋ถ„์„ํ•˜๊ธฐ

์›น ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์™”๋‹ค๋ฉด, ์ด์ œ HTML์„ ๋ถ„์„ํ•ด์„œ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ์ฐพ์•„์•ผ ํ•ด. HTML::Parser์™€ HTML::TreeBuilder๋Š” HTML ๋ฌธ์„œ๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ  ํƒ์ƒ‰ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“ˆ์ด์•ผ.

HTML::TreeBuilder ์‚ฌ์šฉ ์˜ˆ์ œ

use HTML::TreeBuilder;

# HTML ํŒŒ์„œ ๊ฐ์ฒด ์ƒ์„ฑ
my $tree = HTML::TreeBuilder->new;
$tree->parse($html_content);
$tree->eof; # ํŒŒ์‹ฑ ์™„๋ฃŒ

# ๋ชจ๋“  ๋งํฌ ์ฐพ๊ธฐ
my @links = $tree->look_down('_tag', 'a');
foreach my $link (@links) {
    print "๋งํฌ: ", $link->attr('href'), "\n";
    print "ํ…์ŠคํŠธ: ", $link->as_text, "\n";
}

# ํŠน์ • ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ ์š”์†Œ ์ฐพ๊ธฐ
my @elements = $tree->look_down('class', 'product-item');
foreach my $element (@elements) {
    print "์ƒํ’ˆ๋ช…: ", $element->look_down('_tag', 'h2')->as_text, "\n";
}

# ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ (์ค‘์š”!)
$tree->delete;

๐Ÿ’ก HTML::TreeBuilder ์‚ฌ์šฉ ํŒ

look_down ๋ฉ”์†Œ๋“œ๋Š” ํŠน์ • ์†์„ฑ์ด๋‚˜ ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง„ ์š”์†Œ๋ฅผ ์ฐพ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ์ฐพ๊ณ ์ž ํ•˜๋Š” ์†์„ฑ ์ด๋ฆ„, ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ๊ทธ ์†์„ฑ์˜ ๊ฐ’์ด์•ผ.

๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด $tree->delete๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์„ ์žŠ์ง€ ๋งˆ! ํŠนํžˆ ๋Œ€๋Ÿ‰์˜ ํŽ˜์ด์ง€๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•  ๋•Œ ์ค‘์š”ํ•ด.

Mojo::DOM - ํ˜„๋Œ€์ ์ธ HTML/XML ํŒŒ์„œ

์ตœ๊ทผ์—๋Š” Mojo::DOM์ด HTML ํŒŒ์‹ฑ์˜ ๊ฐ•๋ ฅํ•œ ๋Œ€์•ˆ์œผ๋กœ ๋– ์˜ค๋ฅด๊ณ  ์žˆ์–ด. jQuery์™€ ์œ ์‚ฌํ•œ CSS ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•ด์„œ HTML ์š”์†Œ๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์ง€.

Mojo::DOM ์‚ฌ์šฉ ์˜ˆ์ œ

use Mojo::DOM;

# HTML ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ DOM ๊ฐ์ฒด ์ƒ์„ฑ
my $dom = Mojo::DOM->new($html_content);

# CSS ์„ ํƒ์ž๋กœ ์š”์†Œ ์ฐพ๊ธฐ
my $title = $dom->at('title')->text;
print "์ œ๋ชฉ: $title\n";

# ์—ฌ๋Ÿฌ ์š”์†Œ ์ฐพ๊ธฐ
my @links = $dom->find('a')->map('attr', 'href')->each;
print "๋งํฌ: $_\n" for @links;

# ๋ณต์žกํ•œ ์„ ํƒ์ž ์‚ฌ์šฉ
my @products = $dom->find('div.product-item');
for my $product (@products) {
    my $name = $product->at('h2')->text;
    my $price = $product->at('.price')->text;
    print "์ƒํ’ˆ: $name, ๊ฐ€๊ฒฉ: $price\n";
}

HTML ํŒŒ์‹ฑ ๋ชจ๋“ˆ ๋น„๊ต

๋ชจ๋“ˆ ์žฅ์  ๋‹จ์  ์ ํ•ฉํ•œ ์ƒํ™ฉ
HTML::Parser ๋น ๋ฅธ ์ฒ˜๋ฆฌ ์†๋„, ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์  ๋ณต์žกํ•œ ๊ตฌ์กฐ ํƒ์ƒ‰์ด ์–ด๋ ค์›€ ๋‹จ์ˆœํ•œ HTML ์ฒ˜๋ฆฌ, ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ
HTML::TreeBuilder ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ์‰ฌ์šด ํƒ์ƒ‰, ์•ˆ์ •์  ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋งŽ์Œ, ์†๋„ ๋А๋ฆผ ๋ณต์žกํ•œ HTML ๊ตฌ์กฐ ๋ถ„์„
Mojo::DOM CSS ์„ ํƒ์ž ์ง€์›, ํ˜„๋Œ€์  API ์˜์กด์„ฑ ๋งŽ์Œ, ์„ค์น˜ ๋ณต์žกํ•  ์ˆ˜ ์žˆ์Œ ์ตœ์‹  ์›น์‚ฌ์ดํŠธ, ๋ณต์žกํ•œ ์„ ํƒ ์ž‘์—…
Perl ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ชจ๋“ˆ ์ƒํƒœ๊ณ„ Perl LWP ์›น ์š”์ฒญ/์‘๋‹ต HTML::Parser HTML ํŒŒ์‹ฑ Mojo::DOM CSS ์„ ํƒ์ž HTTP::Cookies ์ฟ ํ‚ค ๊ด€๋ฆฌ WWW::Mechanize ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”

WWW::Mechanize - ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”

WWW::Mechanize๋Š” LWP::UserAgent๋ฅผ ํ™•์žฅํ•œ ๋ชจ๋“ˆ๋กœ, ํผ ์ž‘์„ฑ, ๋งํฌ ํด๋ฆญ ๋“ฑ ๋ธŒ๋ผ์šฐ์ € ๋™์ž‘์„ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐ ํŠนํ™”๋˜์–ด ์žˆ์–ด. ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ๋‚˜ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ํƒ์ƒ‰ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์— ์œ ์šฉํ•ด.

WWW::Mechanize ์‚ฌ์šฉ ์˜ˆ์ œ

use WWW::Mechanize;

# Mechanize ๊ฐ์ฒด ์ƒ์„ฑ
my $mech = WWW::Mechanize->new();

# ์›น ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ
$mech->get('https://www.example.com/login');

# ํผ ์ž‘์„ฑ ๋ฐ ์ œ์ถœ
$mech->form_id('login-form');
$mech->field('username', 'myuser');
$mech->field('password', 'mypass');
$mech->click_button(value => 'Login');

# ๋กœ๊ทธ์ธ ํ›„ ํŽ˜์ด์ง€ ํ™•์ธ
if ($mech->success()) {
    print "๋กœ๊ทธ์ธ ์„ฑ๊ณต!\n";
    
    # ๋งํฌ ํด๋ฆญ
    $mech->follow_link(text => '๋‚ด ๊ณ„์ •');
    
    # ํ˜„์žฌ ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ ์ €์žฅ
    my $content = $mech->content();
    
    # ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
    $mech->get_image('profile-pic', 'profile.jpg');
}

WWW::Mechanize๋Š” ์‚ฌ๋žŒ์ด ๋ธŒ๋ผ์šฐ์ €๋กœ ํ•˜๋Š” ์ž‘์—…์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์–ด์„œ, ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ ํƒ์ƒ‰์ด๋‚˜ ๋กœ๊ทธ์ธ ํ›„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์— ๋งค์šฐ ์œ ์šฉํ•ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์‚ฌ์ดํŠธ์—์„œ ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ์„œ๋น„์Šค๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ฑฐ๋‚˜ ๋ถ„์„ํ•˜๋Š” ๋ฐ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์ง€!

๐Ÿ“Œ ๋ชจ๋“ˆ ์„ ํƒ ๊ฐ€์ด๋“œ

  • ๋‹จ์ˆœ ์›น ์š”์ฒญ: LWP::UserAgent
  • ๊ธฐ๋ณธ HTML ํŒŒ์‹ฑ: HTML::Parser
  • ๋ณต์žกํ•œ HTML ๊ตฌ์กฐ ํƒ์ƒ‰: HTML::TreeBuilder
  • CSS ์„ ํƒ์ž๋กœ ์š”์†Œ ์ฐพ๊ธฐ: Mojo::DOM
  • ๋ธŒ๋ผ์šฐ์ € ๋™์ž‘ ์ž๋™ํ™”: WWW::Mechanize

ํ”„๋กœ์ ํŠธ์˜ ๋ณต์žก๋„์™€ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ๋ชจ๋“ˆ์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

์ด์ œ Perl์˜ ์ฃผ์š” ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ชจ๋“ˆ๋“ค์„ ์•Œ์•„๋ดค์œผ๋‹ˆ, ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” Perl์˜ ๊ฐ•๋ ฅํ•œ ๋ฌด๊ธฐ์ธ ์ •๊ทœํ‘œํ˜„์‹์„ ํ™œ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณผ๊ฒŒ. ์ •๊ทœํ‘œํ˜„์‹์€ Perl์˜ ๊ฐ€์žฅ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋‹ˆ๊นŒ ๊ผญ ๋งˆ์Šคํ„ฐํ•ด์•ผ ํ•ด! ๐Ÿ”

4. ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”์ถœํ•˜๊ธฐ ๐Ÿงฉ

Perl์€ ์ •๊ทœํ‘œํ˜„์‹(Regular Expression)์˜ ์™•๊ตญ์ด๋ผ๊ณ  ๋ถˆ๋ฆด ๋งŒํผ ๊ฐ•๋ ฅํ•œ ํŒจํ„ด ๋งค์นญ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ์›น ์Šคํฌ๋ž˜ํ•‘์—์„œ ์ •๊ทœํ‘œํ˜„์‹์€ HTML ๊ตฌ์กฐ์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์ถ”์ถœํ•˜๋Š” ๋ฐ ์—„์ฒญ๋‚œ ๋„์›€์ด ๋ผ.

Perl ์ •๊ทœํ‘œํ˜„์‹ ๊ธฐ์ดˆ

์ •๊ทœํ‘œํ˜„์‹์€ ํ…์ŠคํŠธ ํŒจํ„ด์„ ์ฐพ๊ธฐ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ๋ฌธ๋ฒ•์ด์•ผ. Perl์—์„œ๋Š” =~ ์—ฐ์‚ฐ์ž์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด.

๊ธฐ๋ณธ ํŒจํ„ด ๋งค์นญ

my $text = "Hello, World!";

# ํŒจํ„ด ๋งค์นญ
if ($text =~ /World/) {
    print "ํŒจํ„ด์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค!\n";
}

# ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ๋งค์นญ
if ($text =~ /world/i) {
    print "๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ์ฐพ์•˜์Šต๋‹ˆ๋‹ค!\n";
}

# ํŒจํ„ด ์ถ”์ถœ
if ($text =~ /(Hello), (World)/) {
    print "์ฒซ ๋ฒˆ์งธ ๊ทธ๋ฃน: $1, ๋‘ ๋ฒˆ์งธ ๊ทธ๋ฃน: $2\n";
}

๐Ÿ” ์ •๊ทœํ‘œํ˜„์‹ ์น˜ํŠธ ์‹œํŠธ

ํŒจํ„ด ์„ค๋ช… ์˜ˆ์ œ
. ์ž„์˜์˜ ํ•œ ๋ฌธ์ž /h.t/ - "hat", "hot", "hit" ๋“ฑ ๋งค์นญ
* ์•ž ํŒจํ„ด์ด 0๋ฒˆ ์ด์ƒ ๋ฐ˜๋ณต /ab*c/ - "ac", "abc", "abbc" ๋“ฑ ๋งค์นญ
+ ์•ž ํŒจํ„ด์ด 1๋ฒˆ ์ด์ƒ ๋ฐ˜๋ณต /ab+c/ - "abc", "abbc" ๋“ฑ ๋งค์นญ ("ac"๋Š” ๋งค์นญ ์•ˆ ๋จ)
? ์•ž ํŒจํ„ด์ด 0๋ฒˆ ๋˜๋Š” 1๋ฒˆ ๋“ฑ์žฅ /colou?r/ - "color"์™€ "colour" ๋ชจ๋‘ ๋งค์นญ
{n} ์•ž ํŒจํ„ด์ด ์ •ํ™•ํžˆ n๋ฒˆ ๋ฐ˜๋ณต /a{3}/ - "aaa"๋งŒ ๋งค์นญ
{n,m} ์•ž ํŒจํ„ด์ด n๋ฒˆ ์ด์ƒ m๋ฒˆ ์ดํ•˜ ๋ฐ˜๋ณต /a{2,4}/ - "aa", "aaa", "aaaa" ๋งค์นญ
| OR ์—ฐ์‚ฐ์ž (๋‘˜ ์ค‘ ํ•˜๋‚˜) /cat|dog/ - "cat" ๋˜๋Š” "dog" ๋งค์นญ
[] ๋ฌธ์ž ํด๋ž˜์Šค (๊ด„ํ˜ธ ์•ˆ์˜ ๋ฌธ์ž ์ค‘ ํ•˜๋‚˜) /[aeiou]/ - ๋ชจ์Œ ์ค‘ ํ•˜๋‚˜ ๋งค์นญ
() ๊ทธ๋ฃนํ™” ๋ฐ ์บก์ฒ˜ /(ab)+/ - "ab", "abab", "ababab" ๋“ฑ ๋งค์นญ
\d ์ˆซ์ž ๋ฌธ์ž ([0-9]์™€ ๋™์ผ) /\d{3}/ - ์—ฐ์†๋œ 3์ž๋ฆฌ ์ˆซ์ž ๋งค์นญ
\w ๋‹จ์–ด ๋ฌธ์ž ([a-zA-Z0-9_]์™€ ๋™์ผ) /\w+/ - ๋‹จ์–ด ๋งค์นญ
\s ๊ณต๋ฐฑ ๋ฌธ์ž /\s+/ - ํ•˜๋‚˜ ์ด์ƒ์˜ ๊ณต๋ฐฑ ๋งค์นญ

HTML์—์„œ ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”์ถœํ•˜๊ธฐ

HTML ํŒŒ์‹ฑ ๋ชจ๋“ˆ๋„ ์ข‹์ง€๋งŒ, ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ์€ ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋” ๋น ๋ฅด๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ HTML ๊ตฌ์กฐ๊ฐ€ ์ผ๊ด€์ ์ผ ๋•Œ ์œ ์šฉํ•ด.

HTML์—์„œ ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”์ถœํ•˜๊ธฐ

use LWP::UserAgent;

my $ua = LWP::UserAgent->new;
my $response = $ua->get('https://www.example.com');
my $html = $response->decoded_content;

# ์ œ๋ชฉ ์ถ”์ถœ
if ($html =~ /<title>(.*?)<\/title>/is) {
    my $title = $1;
    print "ํŽ˜์ด์ง€ ์ œ๋ชฉ: $title\n";
}

# ๋ชจ๋“  ๋งํฌ URL ์ถ”์ถœ
my @links;
while ($html =~ /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/gi) {
    push @links, $1;
}
print "์ด " . scalar(@links) . "๊ฐœ์˜ ๋งํฌ๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.\n";

# ์ด๋ฏธ์ง€ URL ์ถ”์ถœ
my @images;
while ($html =~ /<img\s+[^>]*src=["']([^"']+)["'][^>]*>/gi) {
    push @images, $1;
}
print "์ด " . scalar(@images) . "๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.\n";

๐Ÿ’ก ์ •๊ทœํ‘œํ˜„์‹ ์‚ฌ์šฉ ํŒ

HTML ํŒŒ์‹ฑ์— ์ •๊ทœํ‘œํ˜„์‹๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ์–ด. HTML์€ ์ •๊ทœ ์–ธ์–ด๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณต์žกํ•œ ๊ตฌ์กฐ์—์„œ๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์ง€.

๊ฐ„๋‹จํ•œ ํŒจํ„ด ์ถ”์ถœ์—๋Š” ์ •๊ทœํ‘œํ˜„์‹์„, ๋ณต์žกํ•œ ๊ตฌ์กฐ ๋ถ„์„์—๋Š” HTML ํŒŒ์‹ฑ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ์ „๋žต์ด์•ผ!

์‹ค์ „ ์˜ˆ์ œ: ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ ์‡ผํ•‘๋ชฐ ํŽ˜์ด์ง€์—์„œ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ. HTML ํŒŒ์‹ฑ๊ณผ ์ •๊ทœํ‘œํ˜„์‹์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ.

์‡ผํ•‘๋ชฐ ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ ์˜ˆ์ œ

use strict;
use warnings;
use LWP::UserAgent;
use HTML::TreeBuilder;

my $ua = LWP::UserAgent->new;
$ua->agent('Mozilla/5.0');

my $url = 'https://example-shop.com/products';
my $response = $ua->get($url);

if ($response->is_success) {
    my $html = $response->decoded_content;
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($html);
    
    # ์ƒํ’ˆ ์ปจํ…Œ์ด๋„ˆ ์ฐพ๊ธฐ
    my @product_divs = $tree->look_down('_tag', 'div', 'class', 'product-item');
    
    foreach my $product (@product_divs) {
        # ์ƒํ’ˆ๋ช… ์ถ”์ถœ
        my $name_element = $product->look_down('_tag', 'h3', 'class', 'product-name');
        my $name = $name_element ? $name_element->as_text : "์ด๋ฆ„ ์—†์Œ";
        
        # ๊ฐ€๊ฒฉ ์ถ”์ถœ (์ •๊ทœํ‘œํ˜„์‹ ์‚ฌ์šฉ)
        my $price_html = $product->look_down('_tag', 'span', 'class', 'price')->as_HTML;
        my $price = "๊ฐ€๊ฒฉ ์ •๋ณด ์—†์Œ";
        
        if ($price_html =~ /โ‚ฉ([\d,]+)/i) {
            $price = $1;
        }
        
        # ํ‰์  ์ถ”์ถœ (์ •๊ทœํ‘œํ˜„์‹ ์‚ฌ์šฉ)
        my $rating_element = $product->look_down('_tag', 'div', 'class', 'rating');
        my $rating = "ํ‰์  ์ •๋ณด ์—†์Œ";
        
        if ($rating_element) {
            my $rating_html = $rating_element->as_HTML;
            if ($rating_html =~ /(\d\.\d)\/5/i) {
                $rating = $1;
            }
        }
        
        print "์ƒํ’ˆ๋ช…: $name\n";
        print "๊ฐ€๊ฒฉ: $price\n";
        print "ํ‰์ : $rating\n";
        print "------------------------\n";
    }
    
    $tree->delete;
} else {
    print "์˜ค๋ฅ˜ ๋ฐœ์ƒ: ", $response->status_line, "\n";
}
์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ HTML ๋ฐ์ดํ„ฐ ์ถ”์ถœํ•˜๊ธฐ <div class="product"> <h2>์Šค๋งˆํŠธํฐ X2000</h2> <span class="price">โ‚ฉ599,000</span> <div class="rating">4.5/5</div> <p>์ตœ์‹  ๊ธฐ์ˆ  ํƒ‘์žฌ!</p> </div> ์ •๊ทœํ‘œํ˜„์‹ ํŒจํ„ด ์ œ๋ชฉ: <h2>(.*?)</h2> ๊ฐ€๊ฒฉ: โ‚ฉ([\d,]+) ํ‰์ : (\d\.\d)/5 ์ถ”์ถœ ๊ฒฐ๊ณผ: ์Šค๋งˆํŠธํฐ X2000, โ‚ฉ599,000, 4.5 ์ •๊ทœํ‘œํ˜„์‹์˜ ๊ด„ํ˜ธ ()๋Š” ์ถ”์ถœํ•˜๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค

์ •๊ทœํ‘œํ˜„์‹ ๋””๋ฒ„๊น… ๋ฐ ์ตœ์ ํ™”

์ •๊ทœํ‘œํ˜„์‹์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ๋””๋ฒ„๊น…ํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด. Perl์—์„œ๋Š” ์ •๊ทœํ‘œํ˜„์‹์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด.

์ •๊ทœํ‘œํ˜„์‹ ๋””๋ฒ„๊น… ํŒ

# ์ •๊ทœํ‘œํ˜„์‹ ๋””๋ฒ„๊น… ๋ชจ๋“œ ํ™œ์„ฑํ™”
use re 'debug';

# ๋งค์นญ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
my $text = "The price is โ‚ฉ50,000";
if ($text =~ /(โ‚ฉ[\d,]+)/) {
    print "๋งค์นญ๋œ ํ…์ŠคํŠธ: $1\n";
} else {
    print "๋งค์นญ ์‹คํŒจ\n";
}

# ์ •๊ทœํ‘œํ˜„์‹ ๋ชจ๋“ˆ๋กœ ๋” ์ž์„ธํ•œ ์ •๋ณด ์–ป๊ธฐ
use Regexp::Debugger;

# ์ •๊ทœํ‘œํ˜„์‹ ์„ฑ๋Šฅ ์ตœ์ ํ™”
# ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด์€ ๋ฏธ๋ฆฌ ์ปดํŒŒ์ผ
my $price_regex = qr/โ‚ฉ([\d,]+)/;
if ($text =~ $price_regex) {
    print "์ปดํŒŒ์ผ๋œ ์ •๊ทœ์‹์œผ๋กœ ๋งค์นญ: $1\n";
}

โš ๏ธ ์ •๊ทœํ‘œํ˜„์‹ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ

  1. HTML ํŒŒ์‹ฑ์— ์ •๊ทœํ‘œํ˜„์‹๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์–ด. ๊ฐ€๋Šฅํ•˜๋ฉด HTML ํŒŒ์‹ฑ ๋ชจ๋“ˆ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด.
  2. ๋ณต์žกํ•œ ์ •๊ทœํ‘œํ˜„์‹์€ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ํ•„์š”ํ•œ ๊ฒฝ์šฐ qr//๋กœ ๋ฏธ๋ฆฌ ์ปดํŒŒ์ผํ•ด์„œ ์‚ฌ์šฉํ•ด.
  3. ๋„ˆ๋ฌด ํƒ์š•์ ์ธ(greedy) ํŒจํ„ด์€ ์˜๋„ํ•˜์ง€ ์•Š์€ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. .*?์™€ ๊ฐ™์€ ๋น„ํƒ์š•์ (non-greedy) ํŒจํ„ด์„ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•ด.
  4. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์—์„œ๋Š” ์ •๊ทœํ‘œํ˜„์‹ ๋Œ€์‹  ์ „์šฉ ํŒŒ์„œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„.

์ •๊ทœํ‘œํ˜„์‹์€ Perl ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ฐ•๋ ฅํ•œ ๋ฌด๊ธฐ์•ผ. HTML ํŒŒ์‹ฑ ๋ชจ๋“ˆ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๊ฑฐ์˜ ๋ชจ๋“  ์›น์‚ฌ์ดํŠธ์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์–ด. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ๋” ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•˜๋Š” ์ „๋žต์„ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿ•ธ๏ธ

5. ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์ „๋žต ๐Ÿง 

์ง€๊ธˆ๊นŒ์ง€ ๊ธฐ๋ณธ์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์› ์–ด. ํ•˜์ง€๋งŒ ํ˜„์‹ค ์„ธ๊ณ„์˜ ์›น์‚ฌ์ดํŠธ๋Š” ํ›จ์”ฌ ๋ณต์žกํ•ด. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๋™์  ์ฝ˜ํ…์ธ ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ๋ด‡ ์ฐจ๋‹จ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์ดํŠธ๋“ค์ด ๋งŽ์ง€. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” ์ด๋Ÿฐ ๋ณต์žกํ•œ ์ƒํ™ฉ์„ ๋‹ค๋ฃจ๋Š” ์ „๋žต์„ ์•Œ์•„๋ณผ๊ฒŒ.

๋™์  ์ฝ˜ํ…์ธ  ์Šคํฌ๋ž˜ํ•‘ํ•˜๊ธฐ

๋งŽ์€ ํ˜„๋Œ€ ์›น์‚ฌ์ดํŠธ๋“ค์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ํŽ˜์ด์ง€๊ฐ€ ๋กœ๋“œ๋œ ํ›„์— ์ฝ˜ํ…์ธ ๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•ด. ์ด๋Ÿฐ ์‚ฌ์ดํŠธ๋Š” ์ผ๋ฐ˜์ ์ธ HTTP ์š”์ฒญ๋งŒ์œผ๋กœ๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์–ด.

๋™์  ์ฝ˜ํ…์ธ  ์ฒ˜๋ฆฌ ์ „๋žต

1. API ํ™œ์šฉํ•˜๊ธฐ

๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ๋‚ด๋ถ€ API๋ฅผ ์‚ฌ์šฉํ•ด. ์ด API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋” ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด.

use LWP::UserAgent;
use JSON;

my $ua = LWP::UserAgent->new;
$ua->default_header('Accept' => 'application/json');

# API ์—”๋“œํฌ์ธํŠธ ์ง์ ‘ ํ˜ธ์ถœ
my $api_url = 'https://example.com/api/products?category=electronics';
my $response = $ua->get($api_url);

if ($response->is_success) {
    my $json = decode_json($response->decoded_content);
    
    # JSON ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    foreach my $product (@{$json->{products}}) {
        print "์ƒํ’ˆ๋ช…: $product->{name}\n";
        print "๊ฐ€๊ฒฉ: $product->{price}\n";
        print "------------------------\n";
    }
}
2. ํ—ค๋“œ๋ฆฌ์Šค ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์šฉํ•˜๊ธฐ

Perl์—์„œ WWW::Mechanize::Chrome์ด๋‚˜ WWW::Selenium ๊ฐ™์€ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•ด ํ—ค๋“œ๋ฆฌ์Šค ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด.

use WWW::Mechanize::Chrome;

# ํ—ค๋“œ๋ฆฌ์Šค Chrome ๋ธŒ๋ผ์šฐ์ € ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
my $mech = WWW::Mechanize::Chrome->new(
    headless => 1,
    incognito => 1
);

# ํŽ˜์ด์ง€ ๋กœ๋“œ ๋ฐ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๋Œ€๊ธฐ
$mech->get('https://example.com/products');
$mech->wait_until(sub { $mech->eval_in_page('return document.readyState') eq 'complete' });

# ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ํ›„ ์ฝ˜ํ…์ธ  ๊ฐ€์ ธ์˜ค๊ธฐ
my $content = $mech->content;

# ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ํŠน์ • ์š”์†Œ ์„ ํƒํ•˜๊ธฐ
my @product_names = $mech->eval_in_page(q{
    return Array.from(document.querySelectorAll('.product-name')).map(el => el.textContent);
});

print "์ƒํ’ˆ๋ช…: $_\n" for @product_names;

์ธ์ฆ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ํ•˜๊ธฐ

๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•˜๋ ค๋ฉด ์ธ์ฆ ๊ณผ์ •์„ ์ž๋™ํ™”ํ•ด์•ผ ํ•ด.

์ธ์ฆ ์ฒ˜๋ฆฌ ์ „๋žต

1. ์ฟ ํ‚ค ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ
use LWP::UserAgent;
use HTTP::Cookies;

# ์ฟ ํ‚ค ์ €์žฅ์†Œ ์ƒ์„ฑ
my $cookie_jar = HTTP::Cookies->new(
    file => "cookies.txt",
    autosave => 1
);

my $ua = LWP::UserAgent->new;
$ua->cookie_jar($cookie_jar);

# ๋กœ๊ทธ์ธ ํผ ์ œ์ถœ
my $login_url = 'https://example.com/login';
my $response = $ua->post($login_url, [
    username => 'myusername',
    password => 'mypassword',
    remember_me => 1
]);

# ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ™•์ธ
if ($response->is_redirect || $response->content =~ /welcome/i) {
    print "๋กœ๊ทธ์ธ ์„ฑ๊ณต!\n";
    
    # ๋กœ๊ทธ์ธ ํ›„ ๋ณดํ˜ธ๋œ ํŽ˜์ด์ง€ ์ ‘๊ทผ
    my $protected_url = 'https://example.com/account';
    my $account_page = $ua->get($protected_url);
    
    # ๊ณ„์ • ์ •๋ณด ์ถ”์ถœ
    if ($account_page->is_success) {
        print "๊ณ„์ • ํŽ˜์ด์ง€ ์ ‘๊ทผ ์„ฑ๊ณต!\n";
        # ๋ฐ์ดํ„ฐ ์ถ”์ถœ ์ฝ”๋“œ...
    }
}
2. WWW::Mechanize๋กœ ํผ ์ž๋™ํ™”
use WWW::Mechanize;

my $mech = WWW::Mechanize->new();

# ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘๊ทผ
$mech->get('https://example.com/login');

# ํผ ์ž‘์„ฑ ๋ฐ ์ œ์ถœ
$mech->submit_form(
    form_number => 1,
    fields => {
        username => 'myusername',
        password => 'mypassword'
    },
    button => 'login'
);

# ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ™•์ธ
if ($mech->success() && $mech->content =~ /welcome/i) {
    print "๋กœ๊ทธ์ธ ์„ฑ๊ณต!\n";
    
    # ๋งˆ์ดํŽ˜์ด์ง€๋กœ ์ด๋™
    $mech->follow_link(text => 'My Account');
    
    # ๋ฐ์ดํ„ฐ ์ถ”์ถœ
    if ($mech->success()) {
        print "๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ ์„ฑ๊ณต!\n";
        # ๋ฐ์ดํ„ฐ ์ถ”์ถœ ์ฝ”๋“œ...
    }
}

๋ด‡ ์ฐจ๋‹จ ์šฐํšŒํ•˜๊ธฐ

๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ์Šคํฌ๋ž˜ํ•‘ ๋ด‡์„ ์ฐจ๋‹จํ•˜๊ธฐ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•ด. ์ด๋ฅผ ์šฐํšŒํ•˜๊ธฐ ์œ„ํ•œ ์ „๋žต์„ ์•Œ์•„๋ณด์ž.

๋ด‡ ์ฐจ๋‹จ ์šฐํšŒ ์ „๋žต

1. ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ์„ค์ •
my $ua = LWP::UserAgent->new;

# ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ์„ค์ •
$ua->agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
2. ์š”์ฒญ ๊ฐ„ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€
use Time::HiRes qw(sleep);

foreach my $url (@urls) {
    my $response = $ua->get($url);
    # ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ...
    
    # ๋ฌด์ž‘์œ„ ์‹œ๊ฐ„ ๋”œ๋ ˆ์ด (1~3์ดˆ)
    my $delay = 1 + rand(2);
    sleep($delay);
}
3. ์š”์ฒญ ํ—ค๋” ์„ค์ •
# ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €์ฒ˜๋Ÿผ ํ—ค๋” ์„ค์ •
$ua->default_header('Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8');
$ua->default_header('Accept-Language' => 'en-US,en;q=0.5');
$ua->default_header('Referer' => 'https://www.google.com/');
$ua->default_header('DNT' => '1');
$ua->default_header('Connection' => 'keep-alive');
$ua->default_header('Upgrade-Insecure-Requests' => '1');
4. ํ”„๋ก์‹œ ์‚ฌ์šฉ
# ํ”„๋ก์‹œ ์„œ๋ฒ„ ์„ค์ •
$ua->proxy(['http', 'https'], 'http://proxy.example.com:8080');

# ๋˜๋Š” ์—ฌ๋Ÿฌ ํ”„๋ก์‹œ๋ฅผ ๋ฒˆ๊ฐˆ์•„ ์‚ฌ์šฉ
my @proxies = (
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    'http://proxy3.example.com:8080'
);

# ๋ฌด์ž‘์œ„ ํ”„๋ก์‹œ ์„ ํƒ
my $random_proxy = $proxies[int(rand(@proxies))];
$ua->proxy(['http', 'https'], $random_proxy);

โš ๏ธ ์œค๋ฆฌ์  ์Šคํฌ๋ž˜ํ•‘ ์ฃผ์˜์‚ฌํ•ญ

๋ด‡ ์ฐจ๋‹จ ๊ธฐ์ˆ ์„ ์šฐํšŒํ•˜๋Š” ๊ฒƒ์€ ์›น์‚ฌ์ดํŠธ์˜ ์ด์šฉ์•ฝ๊ด€์„ ์œ„๋ฐ˜ํ•  ์ˆ˜ ์žˆ์–ด. ํ•ญ์ƒ ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ณ ๋ คํ•ด:

  1. ์›น์‚ฌ์ดํŠธ์˜ robots.txt ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ณ  ์ค€์ˆ˜ํ•˜๊ธฐ
  2. ๊ณผ๋„ํ•œ ์š”์ฒญ์œผ๋กœ ์„œ๋ฒ„์— ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š๊ธฐ
  3. ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณต์‹ API ์‚ฌ์šฉํ•˜๊ธฐ
  4. ๊ฐœ์ธ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ๋Š” ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•˜๊ธฐ

์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ๋•Œ๋„ ์ด์šฉ์•ฝ๊ด€์„ ํ™•์ธํ•˜๊ณ  ์ค€์ˆ˜ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌํ•˜๊ธฐ

๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋กœ ๋‚˜๋ˆ„์–ด ์ œ๊ณตํ•ด. ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋ ค๋ฉด ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.

ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ ์ „๋žต

use LWP::UserAgent;
use HTML::TreeBuilder;
use Time::HiRes qw(sleep);

my $ua = LWP::UserAgent->new;
$ua->agent('Mozilla/5.0');

my $base_url = 'https://example.com/products?page=';
my $max_pages = 10; # ์ˆ˜์ง‘ํ•  ์ตœ๋Œ€ ํŽ˜์ด์ง€ ์ˆ˜

for my $page (1..$max_pages) {
    my $url = $base_url . $page;
    print "ํŽ˜์ด์ง€ $page ์ˆ˜์ง‘ ์ค‘...\n";
    
    my $response = $ua->get($url);
    
    if ($response->is_success) {
        my $tree = HTML::TreeBuilder->new;
        $tree->parse($response->decoded_content);
        
        # ์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ
        my @products = $tree->look_down('_tag', 'div', 'class', 'product');
        
        foreach my $product (@products) {
            # ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ์ฝ”๋“œ...
            print "์ƒํ’ˆ ์ •๋ณด ์ถ”์ถœ ์™„๋ฃŒ\n";
        }
        
        $tree->delete;
        
        # ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ
        if ($response->decoded_content !~ /๋‹ค์Œ ํŽ˜์ด์ง€|Next Page|page=$page+1/i) {
            print "๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.\n";
            last;
        }
        
        # ์š”์ฒญ ๊ฐ„ ๋”œ๋ ˆ์ด
        sleep(1 + rand(2));
    } else {
        print "ํŽ˜์ด์ง€ $page ์š”์ฒญ ์‹คํŒจ: ", $response->status_line, "\n";
    }
}
๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์ „๋žต ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ - ๋™์  ์ฝ˜ํ…์ธ  - ๋กœ๊ทธ์ธ ํ•„์š” - ๋ด‡ ์ฐจ๋‹จ - ํŽ˜์ด์ง€๋„ค์ด์…˜ API ํ™œ์šฉ ํ—ค๋“œ๋ฆฌ์Šค ๋ธŒ๋ผ์šฐ์ € ์ธ์ฆ ์ฒ˜๋ฆฌ ๋ด‡ ์ฐจ๋‹จ ์šฐํšŒ ์ ์ ˆํ•œ ์ „๋žต ์กฐํ•ฉ์œผ๋กœ ๊ฑฐ์˜ ๋ชจ๋“  ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์‹ค์ „ ์˜ˆ์ œ: ์†Œ์…œ ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘

์ด์ œ ์‹ค์ œ ์†Œ์…œ ๋ฏธ๋””์–ด ์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ. ์—ฌ๋Ÿฌ ์ „๋žต์„ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ.

์†Œ์…œ ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์˜ˆ์ œ

use strict;
use warnings;
use WWW::Mechanize;
use HTML::TreeBuilder;
use JSON;
use Time::HiRes qw(sleep);

# ๋ธŒ๋ผ์šฐ์ € ์—๋ฎฌ๋ ˆ์ด์…˜
my $mech = WWW::Mechanize->new(
    agent => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124',
    cookie_jar => {},
    autocheck => 0,
    timeout => 30
);

# ๋กœ๊ทธ์ธ
print "๋กœ๊ทธ์ธ ์ค‘...\n";
$mech->get('https://example-social.com/login');

$mech->submit_form(
    form_id => 'login-form',
    fields => {
        username => 'myusername',
        password => 'mypassword'
    }
);

# ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ™•์ธ
if ($mech->content =~ /Welcome|Dashboard|Profile/i) {
    print "๋กœ๊ทธ์ธ ์„ฑ๊ณต!\n";
} else {
    die "๋กœ๊ทธ์ธ ์‹คํŒจ!\n";
}

# ํŠน์ • ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰
my $hashtag = 'perl';
print "ํ•ด์‹œํƒœ๊ทธ #$hashtag ๊ฒ€์ƒ‰ ์ค‘...\n";
$mech->get("https://example-social.com/search?q=%23$hashtag");

# ํฌ์ŠคํŠธ ์ˆ˜์ง‘
my @collected_posts;
my $max_posts = 100;
my $page = 1;

while (@collected_posts < $max_posts) {
    print "ํŽ˜์ด์ง€ $page ์ˆ˜์ง‘ ์ค‘...\n";
    
    # ๋™์  ์ฝ˜ํ…์ธ  ๋กœ๋”ฉ ๋Œ€๊ธฐ
    sleep(2);
    
    # HTML ํŒŒ์‹ฑ
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($mech->content);
    
    # ํฌ์ŠคํŠธ ์š”์†Œ ์ฐพ๊ธฐ
    my @post_elements = $tree->look_down('_tag', 'div', 'class', 'post-container');
    
    if (@post_elements == 0) {
        print "๋” ์ด์ƒ ํฌ์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\n";
        last;
    }
    
    foreach my $post (@post_elements) {
        # ํฌ์ŠคํŠธ ID ์ถ”์ถœ
        my $post_id = $post->attr('data-post-id') || "unknown";
        
        # ์ž‘์„ฑ์ž ์ •๋ณด ์ถ”์ถœ
        my $author = $post->look_down('_tag', 'a', 'class', 'author-name');
        my $author_name = $author ? $author->as_text : "unknown";
        my $author_link = $author ? $author->attr('href') : "";
        
        # ํฌ์ŠคํŠธ ๋‚ด์šฉ ์ถ”์ถœ
        my $content_element = $post->look_down('_tag', 'div', 'class', 'post-content');
        my $content = $content_element ? $content_element->as_text : "";
        
        # ์ข‹์•„์š” ์ˆ˜ ์ถ”์ถœ
        my $likes_element = $post->look_down('_tag', 'span', 'class', 'like-count');
        my $likes = $likes_element ? $likes_element->as_text : 0;
        
        # ํ•ด์‹œํƒœ๊ทธ ์ถ”์ถœ
        my @hashtags;
        my $hashtag_elements = $post->look_down('_tag', 'a', sub { $_[0]->attr('href') && $_[0]->attr('href') =~ /\/hashtag\// });
        foreach my $tag (@$hashtag_elements) {
            push @hashtags, $tag->as_text;
        }
        
        # ํฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ €์žฅ
        push @collected_posts, {
            id => $post_id,
            author => {
                name => $author_name,
                profile => $author_link
            },
            content => $content,
            likes => $likes,
            hashtags => \@hashtags,
            collected_at => scalar(localtime)
        };
        
        print "ํฌ์ŠคํŠธ ID: $post_id ์ˆ˜์ง‘ ์™„๋ฃŒ\n";
        
        if (@collected_posts >= $max_posts) {
            last;
        }
    }
    
    $tree->delete;
    
    # ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ์ด๋™
    my $next_link = $mech->find_link(text_regex => qr/Next|๋‹ค์Œ/i);
    if ($next_link) {
        $mech->follow_link(link => $next_link);
        $page++;
        
        # ์š”์ฒญ ๊ฐ„ ๋”œ๋ ˆ์ด
        sleep(2 + rand(3));
    } else {
        print "๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.\n";
        last;
    }
}

# ๊ฒฐ๊ณผ ์ €์žฅ
my $json = encode_json(\@collected_posts);
open my $fh, '>', "hashtag_${hashtag}_posts.json" or die "ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
print $fh $json;
close $fh;

print "์ด ", scalar(@collected_posts), "๊ฐœ์˜ ํฌ์ŠคํŠธ๋ฅผ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.\n";
print "๋ฐ์ดํ„ฐ๊ฐ€ hashtag_${hashtag}_posts.json ํŒŒ์ผ์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";

๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•˜๋Š” ๊ฒƒ์€ ๋„์ „์ ์ธ ์ž‘์—…์ด์ง€๋งŒ, ์ ์ ˆํ•œ ์ „๋žต๊ณผ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฑฐ์˜ ๋ชจ๋“  ์›น์‚ฌ์ดํŠธ์—์„œ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์žˆ์–ด. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ฐ€๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿ“Š

6. ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ฐ€๊ณตํ•˜๊ธฐ ๐Ÿ’พ

์›น์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์Šคํฌ๋ž˜ํ•‘ํ–ˆ๋‹ค๋ฉด, ์ด์ œ ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ฐจ๋ก€์•ผ. ๋ฐ์ดํ„ฐ๋ฅผ ์ ์ ˆํ•œ ํ˜•์‹์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๊ฐ€๊ณตํ•˜๋Š” ๊ฒƒ์€ ์›น ์Šคํฌ๋ž˜ํ•‘ ํ”„๋กœ์„ธ์Šค์˜ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์ง€. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” Perl์„ ์‚ฌ์šฉํ•ด ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๊ฐ€๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ.

ํ…์ŠคํŠธ ํŒŒ์ผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๋ฐ˜ ํ…์ŠคํŠธ ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๋Š” ๊ฑฐ์•ผ. CSV(์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’) ํ˜•์‹์€ ์Šคํ”„๋ ˆ๋“œ์‹œํŠธ ํ”„๋กœ๊ทธ๋žจ์—์„œ ์‰ฝ๊ฒŒ ์—ด ์ˆ˜ ์žˆ์–ด ์ธ๊ธฐ๊ฐ€ ๋งŽ์•„.

CSV ํŒŒ์ผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

use strict;
use warnings;
use Text::CSV;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ)
my @products = (
    {
        name => "์Šค๋งˆํŠธํฐ X2000",
        price => "599000",
        rating => "4.5",
        available => "Yes"
    },
    {
        name => "๋…ธํŠธ๋ถ ์šธํŠธ๋ผ",
        price => "1200000",
        rating => "4.8",
        available => "No"
    },
    # ๋” ๋งŽ์€ ์ œํ’ˆ ๋ฐ์ดํ„ฐ...
);

# CSV ํŒŒ์ผ ์ƒ์„ฑ
my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
open my $fh, ">:encoding(utf8)", "products.csv" or die "products.csv: $!";

# ํ—ค๋” ์ž‘์„ฑ
$csv->print($fh, ["์ œํ’ˆ๋ช…", "๊ฐ€๊ฒฉ", "ํ‰์ ", "์žฌ๊ณ ์—ฌ๋ถ€"]);
print $fh "\n";

# ๋ฐ์ดํ„ฐ ์ž‘์„ฑ
foreach my $product (@products) {
    $csv->print($fh, [
        $product->{name},
        $product->{price},
        $product->{rating},
        $product->{available}
    ]);
    print $fh "\n";
}

close $fh;
print "๋ฐ์ดํ„ฐ๊ฐ€ products.csv ํŒŒ์ผ์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";

๐Ÿ’ก CSV ์ €์žฅ ํŒ

CSV ํŒŒ์ผ์„ ์ƒ์„ฑํ•  ๋•Œ๋Š” Text::CSV ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ์ด ๋ชจ๋“ˆ์€ ์‰ผํ‘œ, ๋”ฐ์˜ดํ‘œ, ์ค„๋ฐ”๊ฟˆ ๋“ฑ ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด ์ค˜.

ํ•œ๊ธ€์ด๋‚˜ ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” encoding(utf8)์„ ์ง€์ •ํ•ด์„œ ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

JSON ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

JSON์€ ์›น ๋ฐ์ดํ„ฐ ๊ตํ™˜์— ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ํ˜•์‹์ด์•ผ. ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ์— ์ ํ•ฉํ•˜๊ณ , ๋‹ค๋ฅธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

JSON ํŒŒ์ผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

use strict;
use warnings;
use JSON;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ)
my $data = {
    source => "example.com",
    date => scalar(localtime),
    products => [
        {
            name => "์Šค๋งˆํŠธํฐ X2000",
            price => 599000,
            specs => {
                display => "6.7์ธ์น˜ AMOLED",
                processor => "์Šค๋ƒ…๋“œ๋ž˜๊ณค 8 Gen 1",
                ram => "12GB"
            },
            reviews => [
                { user => "user1", rating => 5, comment => "์ตœ๊ณ ์˜ ์ œํ’ˆ!" },
                { user => "user2", rating => 4, comment => "๋ฐฐํ„ฐ๋ฆฌ๊ฐ€ ์กฐ๊ธˆ ์•„์‰ฌ์›Œ์š”" }
            ]
        },
        # ๋” ๋งŽ์€ ์ œํ’ˆ ๋ฐ์ดํ„ฐ...
    ]
};

# JSON์œผ๋กœ ๋ณ€ํ™˜
my $json = encode_json($data);

# ํŒŒ์ผ์— ์ €์žฅ
open my $fh, ">:encoding(utf8)", "products.json" or die "products.json: $!";
print $fh $json;
close $fh;

# ๋” ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•์‹์œผ๋กœ ์ €์žฅ (Pretty Print)
open my $fh_pretty, ">:encoding(utf8)", "products_pretty.json" or die "products_pretty.json: $!";
print $fh_pretty JSON->new->pretty->encode($data);
close $fh_pretty;

print "๋ฐ์ดํ„ฐ๊ฐ€ JSON ํŒŒ์ผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. Perl์˜ DBI(Database Interface) ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์–ด.

SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

use strict;
use warnings;
use DBI;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ)
my @products = (
    ["์Šค๋งˆํŠธํฐ X2000", "์ „์ž์ œํ’ˆ", 599000, 4.5],
    ["๋…ธํŠธ๋ถ ์šธํŠธ๋ผ", "์ „์ž์ œํ’ˆ", 1200000, 4.8],
    ["๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ", "์•ก์„ธ์„œ๋ฆฌ", 89000, 4.2],
    # ๋” ๋งŽ์€ ์ œํ’ˆ ๋ฐ์ดํ„ฐ...
);

# SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
my $dbh = DBI->connect("dbi:SQLite:dbname=products.db", "", "", {
    RaiseError => 1,
    AutoCommit => 0
});

# ํ…Œ์ด๋ธ” ์ƒ์„ฑ
$dbh->do("CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    category TEXT,
    price INTEGER,
    rating REAL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");

# ๋ฐ์ดํ„ฐ ์‚ฝ์ž…
my $sth = $dbh->prepare("INSERT INTO products (name, category, price, rating) VALUES (?, ?, ?, ?)");

foreach my $product (@products) {
    $sth->execute(@$product);
}

# ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ปค๋ฐ‹
$dbh->commit;

# ๋ฐ์ดํ„ฐ ํ™•์ธ
my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM products");
print "์ด $count๊ฐœ์˜ ์ œํ’ˆ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";

# ์—ฐ๊ฒฐ ์ข…๋ฃŒ
$dbh->disconnect;

๐Ÿ’ก ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ ํƒ ๊ฐ€์ด๋“œ

  1. SQLite: ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ, ๋กœ์ปฌ ์ €์žฅ, ์„ค์ • ๊ฐ„ํŽธ
  2. MySQL/MariaDB: ์ค‘๊ฐ„ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ, ๋‹ค์ค‘ ์‚ฌ์šฉ์ž ์ ‘๊ทผ
  3. PostgreSQL: ๋ณต์žกํ•œ ์ฟผ๋ฆฌ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ, ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ•„์š” ์‹œ
  4. MongoDB: ๊ตฌ์กฐ๊ฐ€ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฐ์ดํ„ฐ, JSON ํ˜•ํƒœ ๋ฐ์ดํ„ฐ

๋ฐ์ดํ„ฐ ์ •์ œ ๋ฐ ๋ณ€ํ™˜

์›น์—์„œ ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ข…์ข… ์ •์ œ๊ฐ€ ํ•„์š”ํ•ด. ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ, ํ˜•์‹ ๋ณ€ํ™˜, ์ค‘๋ณต ์ œ๊ฑฐ ๋“ฑ์˜ ์ž‘์—…์„ ํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์–ด.

๋ฐ์ดํ„ฐ ์ •์ œ ์˜ˆ์ œ

use strict;
use warnings;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ์›์‹œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ)
my @raw_data = (
    { name => " ์Šค๋งˆํŠธํฐ X2000 ", price => "โ‚ฉ599,000์›", date => "2023-05-15" },
    { name => "๋…ธํŠธ๋ถ ์šธํŠธ๋ผ", price => "โ‚ฉ1,200,000", date => "05/20/2023" },
    { name => "๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ ", price => "๊ฐ€๊ฒฉ: 89,000์›", date => "2023๋…„ 6์›” 1์ผ" },
    # ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ...
);

# ๋ฐ์ดํ„ฐ ์ •์ œ ํ•จ์ˆ˜
sub clean_data {
    my ($item) = @_;
    
    # ์ด๋ฆ„ ์ •์ œ (์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ)
    $item->{name} =~ s/^\s+|\s+$//g;
    
    # ๊ฐ€๊ฒฉ ์ •์ œ (์ˆซ์ž๋งŒ ์ถ”์ถœ)
    $item->{price} =~ s/[^\d]//g;
    
    # ๋‚ ์งœ ํ˜•์‹ ํ†ต์ผ (YYYY-MM-DD)
    if ($item->{date} =~ m{(\d{1,2})/(\d{1,2})/(\d{4})}) {
        # MM/DD/YYYY ํ˜•์‹
        $item->{date} = sprintf("%04d-%02d-%02d", $3, $1, $2);
    } elsif ($item->{date} =~ m{(\d{4})๋…„\s*(\d{1,2})์›”\s*(\d{1,2})์ผ}) {
        # YYYY๋…„ MM์›” DD์ผ ํ˜•์‹
        $item->{date} = sprintf("%04d-%02d-%02d", $1, $2, $3);
    }
    # ์ด๋ฏธ YYYY-MM-DD ํ˜•์‹์ด๋ฉด ๋ณ€๊ฒฝ ์—†์Œ
    
    return $item;
}

# ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ •์ œ
my @cleaned_data = map { clean_data($_) } @raw_data;

# ๊ฒฐ๊ณผ ์ถœ๋ ฅ
foreach my $item (@cleaned_data) {
    print "์ด๋ฆ„: $item->{name}\n";
    print "๊ฐ€๊ฒฉ: $item->{price}\n";
    print "๋‚ ์งœ: $item->{date}\n";
    print "------------------------\n";
}

๋ฐ์ดํ„ฐ ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™”

์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋ฉด ๋” ๋งŽ์€ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์–ด. Perl์—์„œ๋„ ๊ธฐ๋ณธ์ ์ธ ๋ฐ์ดํ„ฐ ๋ถ„์„๊ณผ ์ฐจํŠธ ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•ด.

๋ฐ์ดํ„ฐ ๋ถ„์„ ์˜ˆ์ œ

use strict;
use warnings;
use Statistics::Descriptive;
use GD::Graph::bars;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ: ์ œํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‰๊ท  ๊ฐ€๊ฒฉ)
my %category_prices = (
    '์Šค๋งˆํŠธํฐ' => [599000, 499000, 899000, 799000, 399000],
    '๋…ธํŠธ๋ถ' => [1200000, 950000, 1500000, 2000000, 800000],
    'ํƒœ๋ธ”๋ฆฟ' => [450000, 350000, 700000, 600000],
    '์ด์–ดํฐ' => [89000, 120000, 45000, 250000, 70000],
);

# ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„ ๊ณ„์‚ฐ
my %stats;
foreach my $category (keys %category_prices) {
    my $stat = Statistics::Descriptive::Full->new();
    $stat->add_data(@{$category_prices{$category}});
    
    $stats{$category} = {
        count => $stat->count(),
        mean => int($stat->mean()),
        min => $stat->min(),
        max => $stat->max(),
        standard_deviation => int($stat->standard_deviation())
    };
}

# ๊ฒฐ๊ณผ ์ถœ๋ ฅ
foreach my $category (sort keys %stats) {
    print "์นดํ…Œ๊ณ ๋ฆฌ: $category\n";
    print "  ์ œํ’ˆ ์ˆ˜: $stats{$category}{count}\n";
    print "  ํ‰๊ท  ๊ฐ€๊ฒฉ: $stats{$category}{mean}์›\n";
    print "  ์ตœ์ € ๊ฐ€๊ฒฉ: $stats{$category}{min}์›\n";
    print "  ์ตœ๊ณ  ๊ฐ€๊ฒฉ: $stats{$category}{max}์›\n";
    print "  ํ‘œ์ค€ ํŽธ์ฐจ: $stats{$category}{standard_deviation}์›\n";
    print "------------------------\n";
}

# ์ฐจํŠธ ์ƒ์„ฑ (ํ‰๊ท  ๊ฐ€๊ฒฉ ๋น„๊ต)
my @categories = sort keys %stats;
my @avg_prices = map { $stats{$_}{mean} / 10000 } @categories; # ๋งŒ์› ๋‹จ์œ„๋กœ ๋ณ€ํ™˜

my $graph = GD::Graph::bars->new(600, 400);
$graph->set(
    x_label => '์ œํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ',
    y_label => 'ํ‰๊ท  ๊ฐ€๊ฒฉ (๋งŒ์›)',
    title => '์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ‰๊ท  ๊ฐ€๊ฒฉ ๋น„๊ต',
    bar_spacing => 8,
    shadow_depth => 4,
    transparent => 0,
) or die $graph->error;

my $gd = $graph->plot([\@categories, \@avg_prices]) or die $graph->error;

# ์ด๋ฏธ์ง€ ์ €์žฅ
open my $fh, '>', 'price_comparison.png' or die "Cannot open file: $!";
binmode $fh;
print $fh $gd->png;
close $fh;

print "์ฐจํŠธ๊ฐ€ price_comparison.png ํŒŒ์ผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";
๋ฐ์ดํ„ฐ ์ˆ˜์ง‘๋ถ€ํ„ฐ ๋ถ„์„๊นŒ์ง€์˜ ํ๋ฆ„ ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ฐ์ดํ„ฐ ์ •์ œ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ์ดํ„ฐ ๋ถ„์„ CSV JSON DB ๋ถ„์„ ๊ฒฐ๊ณผ - ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ - ์ฐจํŠธ ๋ฐ ๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ์˜ ๊ฐ€์น˜๋Š” ์ˆ˜์ง‘ ํ›„ ์ ์ ˆํ•œ ๊ฐ€๊ณต๊ณผ ๋ถ„์„์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค

๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐ ๊ณต์œ 

์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค๊ณผ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์—์„œ ํ™œ์šฉํ•˜๋ ค๋ฉด ์ ์ ˆํ•œ ํ˜•์‹์œผ๋กœ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด.

Excel ํŒŒ์ผ๋กœ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ

use strict;
use warnings;
use Excel::Writer::XLSX;

# ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ (์˜ˆ์‹œ)
my @products = (
    ["์Šค๋งˆํŠธํฐ X2000", "์ „์ž์ œํ’ˆ", 599000, 4.5, "2023-05-15"],
    ["๋…ธํŠธ๋ถ ์šธํŠธ๋ผ", "์ „์ž์ œํ’ˆ", 1200000, 4.8, "2023-05-20"],
    ["๋ธ”๋ฃจํˆฌ์Šค ์ด์–ดํฐ", "์•ก์„ธ์„œ๋ฆฌ", 89000, 4.2, "2023-06-01"],
    # ๋” ๋งŽ์€ ์ œํ’ˆ ๋ฐ์ดํ„ฐ...
);

# Excel ํŒŒ์ผ ์ƒ์„ฑ
my $workbook = Excel::Writer::XLSX->new('products.xlsx');
my $worksheet = $workbook->add_worksheet('์ œํ’ˆ ๋ชฉ๋ก');

# ํ—ค๋” ์Šคํƒ€์ผ ์„ค์ •
my $header_format = $workbook->add_format();
$header_format->set_bold();
$header_format->set_bg_color('#D9E1F2');
$header_format->set_align('center');

# ํ—ค๋” ์ž‘์„ฑ
my @headers = ("์ œํ’ˆ๋ช…", "์นดํ…Œ๊ณ ๋ฆฌ", "๊ฐ€๊ฒฉ(์›)", "ํ‰์ ", "๋“ฑ๋ก์ผ");
for my $col (0..$#headers) {
    $worksheet->write(0, $col, $headers[$col], $header_format);
}

# ๋ฐ์ดํ„ฐ ์ž‘์„ฑ
for my $row (0..$#products) {
    for my $col (0..$#{$products[$row]}) {
        $worksheet->write($row + 1, $col, $products[$row][$col]);
    }
}

# ์—ด ๋„ˆ๋น„ ์ž๋™ ์กฐ์ •
$worksheet->autofilter(0, 0, $#products + 1, $#{$headers});
$worksheet->freeze_panes(1, 0); # ์ฒซ ๋ฒˆ์งธ ํ–‰ ๊ณ ์ •

$workbook->close();
print "๋ฐ์ดํ„ฐ๊ฐ€ products.xlsx ํŒŒ์ผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n";

๐Ÿ’ก ๋ฐ์ดํ„ฐ ๊ณต์œ  ํŒ

๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•  ๋•Œ๋Š” ๋Œ€์ƒ์— ๋งž๋Š” ํ˜•์‹์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด:

  • ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž: Excel ํŒŒ์ผ์ด๋‚˜ PDF ๋ณด๊ณ ์„œ
  • ๊ฐœ๋ฐœ์ž/๋ฐ์ดํ„ฐ ๋ถ„์„๊ฐ€: JSON, CSV ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋คํ”„
  • ์›น ์„œ๋น„์Šค: API๋ฅผ ํ†ตํ•œ JSON ๋˜๋Š” XML ํ˜•์‹

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ฐ์ดํ„ฐ ๋ถ„์„ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋ฉด, ๊ณ ๊ฐ์ด ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ํ˜•ํƒœ๋กœ ๊ฒฐ๊ณผ๋ฌผ์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด!

๋ฐ์ดํ„ฐ ์ €์žฅ๊ณผ ๊ฐ€๊ณต์€ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ฐ€์น˜๋ฅผ ์™„์„ฑํ•˜๋Š” ๋‹จ๊ณ„์•ผ. ์ž˜ ์ •๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋Š” ์˜์‚ฌ๊ฒฐ์ •, ํŠธ๋ Œ๋“œ ๋ถ„์„, ์‹œ์žฅ ์กฐ์‚ฌ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ชฉ์ ์œผ๋กœ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ์–ด. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์ž๋™ํ™”ํ•˜๊ณ  ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿค–

7. ์ž๋™ํ™”์™€ ์Šค์ผ€์ค„๋ง: ์ฃผ๊ธฐ์  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โฐ

์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์ง„์ •ํ•œ ํž˜์€ ์ž๋™ํ™”์—์„œ ๋‚˜์™€. ํ•œ ๋ฒˆ ์ž‘์„ฑํ•œ ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉด ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” Perl ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž๋™ํ™”ํ•˜๊ณ  ์Šค์ผ€์ค„๋งํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ.

์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ ๋ชจ๋“ˆํ™”ํ•˜๊ธฐ

์ž๋™ํ™”ํ•˜๊ธฐ ์ „์— ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ชจ๋“ˆํ™”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ๋ช…๋ น์ค„ ์ธ์ž๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋กœ๊น… ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด ๋ณด์ž.

๋ชจ๋“ˆํ™”๋œ ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use HTML::TreeBuilder;
use JSON;
use Getopt::Long;
use Log::Log4perl qw(:easy);
use File::Basename;
use File::Path qw(make_path);

# ๋กœ๊น… ์„ค์ •
Log::Log4perl->easy_init({
    level => $INFO,
    file => ">>scraper.log",
    layout => "%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n"
});

# ๋ช…๋ น์ค„ ์˜ต์…˜ ์ฒ˜๋ฆฌ
my $url = '';
my $output_dir = 'data';
my $output_format = 'json';
my $category = '';

GetOptions(
    "url=s" => \$url,
    "output-dir=s" => \$output_dir,
    "format=s" => \$output_format,
    "category=s" => \$category
) or die("์˜ต์…˜ ์˜ค๋ฅ˜\n");

# URL ํ•„์ˆ˜ ์ฒดํฌ
if (!$url) {
    die "URL์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. --url ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์„ธ์š”.\n";
}

# ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
make_path($output_dir) unless -d $output_dir;

# ๋ฉ”์ธ ํ•จ์ˆ˜
sub main {
    INFO("์Šคํฌ๋ž˜ํ•‘ ์‹œ์ž‘: $url");
    
    # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
    my $ua = LWP::UserAgent->new;
    $ua->agent('Mozilla/5.0');
    $ua->timeout(30);
    
    my $response = $ua->get($url);
    
    if (!$response->is_success) {
        ERROR("ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: " . $response->status_line);
        return;
    }
    
    INFO("ํŽ˜์ด์ง€ ์š”์ฒญ ์„ฑ๊ณต");
    
    # HTML ํŒŒ์‹ฑ
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($response->decoded_content);
    
    # ๋ฐ์ดํ„ฐ ์ถ”์ถœ (์˜ˆ: ์ œํ’ˆ ์ •๋ณด)
    my @items;
    my @product_elements = $tree->look_down('_tag', 'div', 'class', 'product-item');
    
    foreach my $product (@product_elements) {
        my $name = $product->look_down('_tag', 'h2')->as_text;
        my $price_element = $product->look_down('_tag', 'span', 'class', 'price');
        my $price = $price_element ? $price_element->as_text : "N/A";
        
        # ๊ฐ€๊ฒฉ์—์„œ ์ˆซ์ž๋งŒ ์ถ”์ถœ
        $price =~ s/[^\d]//g if $price ne "N/A";
        
        push @items, {
            name => $name,
            price => $price,
            category => $category,
            url => $url,
            timestamp => scalar(localtime)
        };
    }
    
    $tree->delete;
    
    my $count = scalar(@items);
    INFO("์ด $count๊ฐœ ํ•ญ๋ชฉ ์ถ”์ถœ ์™„๋ฃŒ");
    
    # ๊ฒฐ๊ณผ ์ €์žฅ
    my $timestamp = time();
    my $filename = $output_dir . "/" . ($category ? "${category}_" : "") . "products_${timestamp}";
    
    if ($output_format eq 'json') {
        $filename .= '.json';
        open my $fh, ">:encoding(utf8)", $filename or die "ํŒŒ์ผ ์—ด๊ธฐ ์‹คํŒจ: $!";
        print $fh encode_json(\@items);
        close $fh;
    } elsif ($output_format eq 'csv') {
        $filename .= '.csv';
        open my $fh, ">:encoding(utf8)", $filename or die "ํŒŒ์ผ ์—ด๊ธฐ ์‹คํŒจ: $!";
        print $fh "์ด๋ฆ„,๊ฐ€๊ฒฉ,์นดํ…Œ๊ณ ๋ฆฌ,URL,ํƒ€์ž„์Šคํƒฌํ”„\n";
        foreach my $item (@items) {
            print $fh join(",", 
                $item->{name}, 
                $item->{price}, 
                $item->{category}, 
                $item->{url}, 
                $item->{timestamp}
            ) . "\n";
        }
        close $fh;
    } else {
        ERROR("์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ถœ๋ ฅ ํ˜•์‹: $output_format");
        return;
    }
    
    INFO("๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ: $filename");
}

# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰
main();

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ๋ช…๋ น์ค„์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด:

perl scraper.pl --url="https://example.com/products" --category="electronics" --format="json" --output-dir="data"

cron์„ ์‚ฌ์šฉํ•œ ์Šค์ผ€์ค„๋ง (Linux/macOS)

Linux๋‚˜ macOS์—์„œ๋Š” cron์„ ์‚ฌ์šฉํ•ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

cron ์ž‘์—… ์„ค์ •ํ•˜๊ธฐ

# crontab -e ๋ช…๋ น์œผ๋กœ cron ์ž‘์—… ํŽธ์ง‘๊ธฐ ์—ด๊ธฐ

# ๋งค์ผ ์˜ค์ „ 9์‹œ์— ์‹คํ–‰
0 9 * * * /usr/bin/perl /path/to/scraper.pl --url="https://example.com/products" --category="electronics" --format="json" --output-dir="/path/to/data"

# ๋งค์ฃผ ์›”์š”์ผ ์˜คํ›„ 3์‹œ์— ์‹คํ–‰
0 15 * * 1 /usr/bin/perl /path/to/scraper.pl --url="https://example.com/products" --category="electronics" --format="json" --output-dir="/path/to/data"

# ๋งค์‹œ๊ฐ„ ์‹คํ–‰
0 * * * * /usr/bin/perl /path/to/scraper.pl --url="https://example.com/products" --category="electronics" --format="json" --output-dir="/path/to/data"

๐Ÿ’ก cron ํ‘œํ˜„์‹ ์„ค๋ช…

cron ํ‘œํ˜„์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•์‹์„ ๊ฐ€์ ธ: ๋ถ„ ์‹œ ์ผ ์›” ์š”์ผ

  • ๋ถ„: 0-59
  • ์‹œ: 0-23
  • ์ผ: 1-31
  • ์›”: 1-12
  • ์š”์ผ: 0-7 (0๊ณผ 7์€ ์ผ์š”์ผ)

ํŠน์ˆ˜ ๋ฌธ์ž:

  • *: ๋ชจ๋“  ๊ฐ’
  • ,: ๊ฐ’ ๋ชฉ๋ก (์˜ˆ: 1,3,5)
  • -: ๋ฒ”์œ„ (์˜ˆ: 1-5)
  • /: ๋‹จ๊ณ„ ๊ฐ’ (์˜ˆ: */2๋Š” 2๋งˆ๋‹ค)

์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ ์‚ฌ์šฉํ•˜๊ธฐ (Windows)

Windows์—์„œ๋Š” ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

Windows ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ • ๋‹จ๊ณ„

1. ๋ฐฐ์น˜ ํŒŒ์ผ ์ƒ์„ฑ

๋จผ์ € Perl ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋ฐฐ์น˜ ํŒŒ์ผ(.bat)์„ ์ƒ์„ฑํ•ด:

@echo off
cd /d C:\path\to\script\directory
perl scraper.pl --url="https://example.com/products" --category="electronics" --format="json" --output-dir="C:\path\to\data"
echo ์Šคํฌ๋ž˜ํ•‘ ์™„๋ฃŒ: %date% %time% >> scraper_log.txt
2. ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ ์—ด๊ธฐ

์‹œ์ž‘ ๋ฉ”๋‰ด์—์„œ '์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ'๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ฑฐ๋‚˜ ์ œ์–ดํŒ์—์„œ ์ฐพ์•„ ์‹คํ–‰ํ•ด.

3. ๊ธฐ๋ณธ ์ž‘์—… ๋งŒ๋“ค๊ธฐ
  1. ์˜ค๋ฅธ์ชฝ ํŒจ๋„์—์„œ '๊ธฐ๋ณธ ์ž‘์—… ๋งŒ๋“ค๊ธฐ'๋ฅผ ํด๋ฆญ
  2. ์ž‘์—… ์ด๋ฆ„๊ณผ ์„ค๋ช… ์ž…๋ ฅ (์˜ˆ: "์ œํ’ˆ ๋ฐ์ดํ„ฐ ์Šคํฌ๋ž˜ํ•‘")
  3. ํŠธ๋ฆฌ๊ฑฐ ์„ค์ • (๋งค์ผ, ๋งค์ฃผ, ๋งค์›” ๋“ฑ)
  4. ์ž‘์—… ์œ ํ˜•์œผ๋กœ 'ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘'์„ ์„ ํƒ
  5. ํ”„๋กœ๊ทธ๋žจ/์Šคํฌ๋ฆฝํŠธ์— ๋ฐฐ์น˜ ํŒŒ์ผ ๊ฒฝ๋กœ ์ž…๋ ฅ
  6. '๋งˆ์นจ'์„ ํด๋ฆญํ•˜์—ฌ ์ž‘์—… ์ƒ์„ฑ ์™„๋ฃŒ

์—ฌ๋Ÿฌ ์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์ž๋™ํ™”

์—ฌ๋Ÿฌ ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ์„ค์ • ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ด ์Šคํฌ๋ž˜ํ•‘ ๋Œ€์ƒ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„.

JSON ์„ค์ • ํŒŒ์ผ (sites.json)

{
  "sites": [
    {
      "name": "์ „์ž์ œํ’ˆ ์Šคํ† ์–ด",
      "url": "https://electronics-store.example.com/products",
      "category": "electronics",
      "selector": {
        "item": "div.product-item",
        "name": "h2.product-name",
        "price": "span.price"
      },
      "schedule": "daily"
    },
    {
      "name": "๋„์„œ ์‚ฌ์ดํŠธ",
      "url": "https://book-store.example.com/new-releases",
      "category": "books",
      "selector": {
        "item": "div.book-item",
        "name": "h3.book-title",
        "price": "div.book-price",
        "author": "span.author"
      },
      "schedule": "weekly"
    },
    {
      "name": "ํŒจ์…˜ ์‡ผํ•‘๋ชฐ",
      "url": "https://fashion-store.example.com/sale",
      "category": "fashion",
      "selector": {
        "item": "div.product-box",
        "name": "h4.item-name",
        "price": "p.item-price",
        "discount": "span.discount-rate"
      },
      "schedule": "daily"
    }
  ]
}

์—ฌ๋Ÿฌ ์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use HTML::TreeBuilder;
use JSON;
use Log::Log4perl qw(:easy);
use File::Path qw(make_path);
use Getopt::Long;

# ๋กœ๊น… ์„ค์ •
Log::Log4perl->easy_init({
    level => $INFO,
    file => ">>multi_scraper.log",
    layout => "%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n"
});

# ๋ช…๋ น์ค„ ์˜ต์…˜
my $config_file = 'sites.json';
my $output_dir = 'data';
my $site_filter = '';

GetOptions(
    "config=s" => \$config_file,
    "output-dir=s" => \$output_dir,
    "site=s" => \$site_filter
) or die("์˜ต์…˜ ์˜ค๋ฅ˜\n");

# ์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ
open my $fh, "<:encoding(utf8)", $config_file or die "์„ค์ • ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
my $config_json = do { local $/; <$fh> };
close $fh;

my $config = decode_json($config_json);

# ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
make_path($output_dir) unless -d $output_dir;

# ์‚ฌ์ดํŠธ๋ณ„ ์Šคํฌ๋ž˜ํ•‘ ํ•จ์ˆ˜
sub scrape_site {
    my ($site) = @_;
    
    INFO("์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์‹œ์ž‘: $site->{name}");
    
    # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
    my $ua = LWP::UserAgent->new;
    $ua->agent('Mozilla/5.0');
    $ua->timeout(30);
    
    my $response = $ua->get($site->{url});
    
    if (!$response->is_success) {
        ERROR("ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: " . $response->status_line);
        return;
    }
    
    # HTML ํŒŒ์‹ฑ
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($response->decoded_content);
    
    # ๋ฐ์ดํ„ฐ ์ถ”์ถœ
    my @items;
    my @item_elements = $tree->look_down('_tag', 'div', 'class', $site->{selector}{item});
    
    foreach my $item_element (@item_elements) {
        my $item = {
            site => $site->{name},
            category => $site->{category},
            url => $site->{url},
            timestamp => scalar(localtime)
        };
        
        # ์ด๋ฆ„ ์ถ”์ถœ
        my ($tag, $class) = split(/\./, $site->{selector}{name});
        my $name_element = $item_element->look_down('_tag', $tag, 'class', $class);
        $item->{name} = $name_element ? $name_element->as_text : "N/A";
        
        # ๊ฐ€๊ฒฉ ์ถ”์ถœ
        ($tag, $class) = split(/\./, $site->{selector}{price});
        my $price_element = $item_element->look_down('_tag', $tag, 'class', $class);
        $item->{price} = $price_element ? $price_element->as_text : "N/A";
        
        # ๊ฐ€๊ฒฉ์—์„œ ์ˆซ์ž๋งŒ ์ถ”์ถœ
        $item->{price} =~ s/[^\d]//g if $item->{price} ne "N/A";
        
        # ์ถ”๊ฐ€ ํ•„๋“œ ์ถ”์ถœ (์žˆ๋Š” ๊ฒฝ์šฐ)
        foreach my $field (keys %{$site->{selector}}) {
            next if $field eq 'item' || $field eq 'name' || $field eq 'price';
            
            ($tag, $class) = split(/\./, $site->{selector}{$field});
            my $element = $item_element->look_down('_tag', $tag, 'class', $class);
            $item->{$field} = $element ? $element->as_text : "N/A";
        }
        
        push @items, $item;
    }
    
    $tree->delete;
    
    my $count = scalar(@items);
    INFO("์‚ฌ์ดํŠธ $site->{name}์—์„œ ์ด $count๊ฐœ ํ•ญ๋ชฉ ์ถ”์ถœ ์™„๋ฃŒ");
    
    # ๊ฒฐ๊ณผ ์ €์žฅ
    my $timestamp = time();
    my $filename = $output_dir . "/" . $site->{category} . "_" . $timestamp . ".json";
    
    open my $out_fh, ">:encoding(utf8)", $filename or die "ํŒŒ์ผ ์—ด๊ธฐ ์‹คํŒจ: $!";
    print $out_fh encode_json(\@items);
    close $out_fh;
    
    INFO("๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ: $filename");
    return $count;
}

# ๋ฉ”์ธ ํ•จ์ˆ˜
sub main {
    my $total_sites = 0;
    my $total_items = 0;
    
    foreach my $site (@{$config->{sites}}) {
        # ์‚ฌ์ดํŠธ ํ•„ํ„ฐ ์ ์šฉ (์ง€์ •๋œ ๊ฒฝ์šฐ)
        if ($site_filter && $site->{name} !~ /$site_filter/i && $site->{category} !~ /$site_filter/i) {
            next;
        }
        
        $total_sites++;
        my $items = scrape_site($site);
        $total_items += $items if defined $items;
        
        # ์‚ฌ์ดํŠธ ๊ฐ„ ๋”œ๋ ˆ์ด
        sleep(3) if $total_sites < scalar(@{$config->{sites}});
    }
    
    INFO("์Šคํฌ๋ž˜ํ•‘ ์™„๋ฃŒ: ์ด $total_sites ์‚ฌ์ดํŠธ์—์„œ $total_items ํ•ญ๋ชฉ ์ˆ˜์ง‘");
}

# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰
main();
์›น ์Šคํฌ๋ž˜ํ•‘ ์ž๋™ํ™” ์‹œ์Šคํ…œ ์Šค์ผ€์ค„๋Ÿฌ (cron / ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ) ์„ค์ • ํŒŒ์ผ (sites.json) ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ (JSON, DB ๋“ฑ) ๋กœ๊ทธ ํŒŒ์ผ ์‚ฌ์ดํŠธ 1 ์‚ฌ์ดํŠธ 2 ์‚ฌ์ดํŠธ 3

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ์•Œ๋ฆผ ์„ค์ •

์ž๋™ํ™”๋œ ์Šคํฌ๋ž˜ํ•‘ ์‹œ์Šคํ…œ์—์„œ๋Š” ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ์•Œ๋ฆผ ๊ธฐ๋Šฅ์ด ์ค‘์š”ํ•ด. ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ฆ‰์‹œ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ด ๋ณด์ž.

์ด๋ฉ”์ผ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

use Email::Simple;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP;
use Try::Tiny;

# ์ด๋ฉ”์ผ ์•Œ๋ฆผ ํ•จ์ˆ˜
sub send_alert {
    my ($subject, $message) = @_;
    
    my $email = Email::Simple->create(
        header => [
            To      => 'your-email@example.com',
            From    => 'scraper-alert@example.com',
            Subject => $subject,
        ],
        body => $message,
    );
    
    my $transport = Email::Sender::Transport::SMTP->new({
        host => 'smtp.example.com',
        port => 587,
        ssl => 1,
        sasl_username => 'your-username',
        sasl_password => 'your-password',
    });
    
    try {
        sendmail($email, { transport => $transport });
        INFO("์•Œ๋ฆผ ์ด๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต");
    } catch {
        ERROR("์•Œ๋ฆผ ์ด๋ฉ”์ผ ์ „์†ก ์‹คํŒจ: $_");
    };
}

# ์Šคํฌ๋ž˜ํ•‘ ํ•จ์ˆ˜์— ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
sub scrape_site {
    my ($site) = @_;
    
    INFO("์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘ ์‹œ์ž‘: $site->{name}");
    
    # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
    my $ua = LWP::UserAgent->new;
    $ua->agent('Mozilla/5.0');
    $ua->timeout(30);
    
    my $response = $ua->get($site->{url});
    
    if (!$response->is_success) {
        my $error_msg = "ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: " . $response->status_line;
        ERROR($error_msg);
        
        # ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜์ผ ๊ฒฝ์šฐ ์•Œ๋ฆผ ์ „์†ก
        if ($response->code >= 500) {
            send_alert(
                "์Šคํฌ๋ž˜ํ•‘ ์˜ค๋ฅ˜: $site->{name}",
                "์‚ฌ์ดํŠธ $site->{name} ($site->{url}) ์Šคํฌ๋ž˜ํ•‘ ์ค‘ ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.\n\n" .
                "์˜ค๋ฅ˜ ์ฝ”๋“œ: " . $response->code . "\n" .
                "์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€: " . $response->status_line . "\n\n" .
                "์‹œ๊ฐ„: " . scalar(localtime)
            );
        }
        
        return;
    }
    
    # ๋‚˜๋จธ์ง€ ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ...
}

๐Ÿ’ก ์ž๋™ํ™” ๋ชจ๋ฒ” ์‚ฌ๋ก€

  1. ์ ์ง„์  ๋นˆ๋„ ์ฆ๊ฐ€: ์ฒ˜์Œ์—๋Š” ๋‚ฎ์€ ๋นˆ๋„๋กœ ์‹œ์ž‘ํ•ด์„œ ์•ˆ์ •์„ฑ์„ ํ™•์ธํ•œ ํ›„ ๋นˆ๋„๋ฅผ ๋†’์—ฌ๊ฐ€์„ธ์š”.
  2. ์˜ค๋ฅ˜ ์žฌ์‹œ๋„ ๋กœ์ง: ์ผ์‹œ์ ์ธ ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ์— ๋Œ€๋น„ํ•ด ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.
  3. ๋กœ๊ทธ ์ˆœํ™˜: ๋กœ๊ทธ ํŒŒ์ผ์ด ๋„ˆ๋ฌด ์ปค์ง€์ง€ ์•Š๋„๋ก ๋กœ๊ทธ ์ˆœํ™˜(rotation) ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.
  4. ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง: ๋””์Šคํฌ ๊ณต๊ฐ„, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋“ฑ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”.
  5. ๋ฐฑ์—… ์ „๋žต: ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๊ธฐ์ ์œผ๋กœ ๋ฐฑ์—…ํ•˜์„ธ์š”.

์ž๋™ํ™”๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์ง„์ •ํ•œ ๊ฐ€์น˜๋ฅผ ์‹คํ˜„ํ•˜๋Š” ๋‹จ๊ณ„์•ผ. ํ•œ ๋ฒˆ ์„ค์ •ํ•ด ๋†“์œผ๋ฉด ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์Šคํฌ๋ž˜ํ•‘ ์ค‘ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿ› ๏ธ

8. ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ๋””๋ฒ„๊น… ๊ธฐ๋ฒ• ๐Ÿ”ง

์›น ์Šคํฌ๋ž˜ํ•‘์€ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์–‘ํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ, ์›น์‚ฌ์ดํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ, ์ฐจ๋‹จ ๋“ฑ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ƒํ™ฉ์— ๋Œ€๋น„ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” Perl ์›น ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ์˜ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ๋””๋ฒ„๊น… ๊ธฐ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ.

์ผ๋ฐ˜์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘ ์˜ค๋ฅ˜ ์œ ํ˜•

1. ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜

  • ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ (Connection timeout)
  • DNS ํ•ด์„ ์‹คํŒจ
  • ์„œ๋ฒ„ ์‘๋‹ต ์—†์Œ

2. HTTP ์˜ค๋ฅ˜

  • 404 Not Found: ํŽ˜์ด์ง€๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ
  • 403 Forbidden: ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ (๋ด‡ ์ฐจ๋‹จ)
  • 500 Internal Server Error: ์„œ๋ฒ„ ์˜ค๋ฅ˜
  • 429 Too Many Requests: ์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ ์ดˆ๊ณผ

3. ํŒŒ์‹ฑ ์˜ค๋ฅ˜

  • HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ์„ ํƒ์ž ๋ถˆ์ผ์น˜
  • ์˜ˆ์ƒ์น˜ ๋ชปํ•œ HTML ํ˜•์‹
  • ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ

4. ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜

  • ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฐ์ดํ„ฐ ํ˜•์‹
  • ํ•„์ˆ˜ ๋ฐ์ดํ„ฐ ๋ˆ„๋ฝ
  • ํŒŒ์ผ ์ €์žฅ ์‹คํŒจ

Try::Tiny๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

Perl์—์„œ๋Š” Try::Tiny ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•ด ์˜ˆ์™ธ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.

Try::Tiny๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

use strict;
use warnings;
use Try::Tiny;
use LWP::UserAgent;
use HTML::TreeBuilder;
use Log::Log4perl qw(:easy);

# ๋กœ๊น… ์„ค์ •
Log::Log4perl->easy_init($INFO);

# ์›น ํŽ˜์ด์ง€ ์š”์ฒญ ํ•จ์ˆ˜
sub fetch_page {
    my ($url) = @_;
    
    my $ua = LWP::UserAgent->new;
    $ua->timeout(30);
    $ua->agent('Mozilla/5.0');
    
    try {
        my $response = $ua->get($url);
        
        if ($response->is_success) {
            return $response->decoded_content;
        } elsif ($response->code == 404) {
            die "ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (404)";
        } elsif ($response->code == 403) {
            die "์ ‘๊ทผ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค (403). ๋ด‡ ์ฐจ๋‹จ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.";
        } elsif ($response->code == 429) {
            die "๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค (429). ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.";
        } else {
            die "HTTP ์˜ค๋ฅ˜: " . $response->status_line;
        }
    } catch {
        ERROR("ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: $_");
        return undef;
    };
}

# HTML ํŒŒ์‹ฑ ํ•จ์ˆ˜
sub parse_html {
    my ($html) = @_;
    
    return undef unless $html;
    
    try {
        my $tree = HTML::TreeBuilder->new;
        $tree->parse($html);
        $tree->eof;
        
        return $tree;
    } catch {
        ERROR("HTML ํŒŒ์‹ฑ ์‹คํŒจ: $_");
        return undef;
    };
}

# ๋ฐ์ดํ„ฐ ์ถ”์ถœ ํ•จ์ˆ˜
sub extract_data {
    my ($tree, $selector) = @_;
    
    return [] unless $tree;
    
    try {
        my @elements = $tree->look_down(%$selector);
        
        if (@elements == 0) {
            WARN("์„ ํƒ์ž์™€ ์ผ์น˜ํ•˜๋Š” ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: " . join(", ", %$selector));
            return [];
        }
        
        my @data;
        foreach my $element (@elements) {
            push @data, $element->as_text;
        }
        
        $tree->delete; # ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
        
        return \@data;
    } catch {
        ERROR("๋ฐ์ดํ„ฐ ์ถ”์ถœ ์‹คํŒจ: $_");
        $tree->delete if $tree; # ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
        return [];
    };
}

# ๋ฉ”์ธ ์Šคํฌ๋ž˜ํ•‘ ํ•จ์ˆ˜
sub scrape {
    my ($url, $selector) = @_;
    
    INFO("์Šคํฌ๋ž˜ํ•‘ ์‹œ์ž‘: $url");
    
    my $html = fetch_page($url);
    if (!$html) {
        ERROR("ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์–ด ์Šคํฌ๋ž˜ํ•‘์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.");
        return [];
    }
    
    my $tree = parse_html($html);
    if (!$tree) {
        ERROR("HTML์„ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์–ด ์Šคํฌ๋ž˜ํ•‘์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.");
        return [];
    }
    
    my $data = extract_data($tree, $selector);
    
    INFO("์Šคํฌ๋ž˜ํ•‘ ์™„๋ฃŒ: " . scalar(@$data) . "๊ฐœ ํ•ญ๋ชฉ ์ถ”์ถœ");
    
    return $data;
}

# ์‚ฌ์šฉ ์˜ˆ
my $url = "https://example.com/products";
my $selector = { '_tag' => 'div', 'class' => 'product-name' };

my $product_names = scrape($url, $selector);

print "์ถ”์ถœ๋œ ์ œํ’ˆ๋ช…:\n";
print "- $_\n" for @$product_names;

์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋‚˜ ์ผ์‹œ์ ์ธ ์„œ๋ฒ„ ๋ฌธ์ œ๋Š” ์žฌ์‹œ๋„๋ฅผ ํ†ตํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด.

์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„

use Time::HiRes qw(sleep);

# ์žฌ์‹œ๋„ ๋กœ์ง์ด ํฌํ•จ๋œ ํŽ˜์ด์ง€ ์š”์ฒญ ํ•จ์ˆ˜
sub fetch_page_with_retry {
    my ($url, $max_retries, $initial_delay) = @_;
    $max_retries ||= 3;
    $initial_delay ||= 2;
    
    my $retry_count = 0;
    my $delay = $initial_delay;
    
    while ($retry_count < $max_retries) {
        try {
            my $ua = LWP::UserAgent->new;
            $ua->timeout(30);
            $ua->agent('Mozilla/5.0');
            
            my $response = $ua->get($url);
            
            if ($response->is_success) {
                return $response->decoded_content;
            } elsif ($response->code == 429) {
                # ์š”์ฒญ ์ œํ•œ์— ๊ฑธ๋ฆฐ ๊ฒฝ์šฐ ๋” ์˜ค๋ž˜ ๋Œ€๊ธฐ
                my $retry_after = $response->header('Retry-After') || $delay * 2;
                WARN("์š”์ฒญ ์ œํ•œ์— ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค. ${retry_after}์ดˆ ํ›„ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.");
                sleep($retry_after);
                $retry_count++;
                $delay *= 2; # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
            } elsif ($response->code >= 500) {
                # ์„œ๋ฒ„ ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„
                WARN("์„œ๋ฒ„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: " . $response->status_line . ". ${delay}์ดˆ ํ›„ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.");
                sleep($delay);
                $retry_count++;
                $delay *= 2; # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
            } else {
                # 4xx ๋“ฑ ๋‹ค๋ฅธ ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Œ
                die "HTTP ์˜ค๋ฅ˜: " . $response->status_line;
            }
        } catch {
            if ($_ =~ /timeout|connection refused|name resolution/i) {
                # ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„
                WARN("๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: $_. ${delay}์ดˆ ํ›„ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.");
                sleep($delay);
                $retry_count++;
                $delay *= 2; # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
            } else {
                # ๋‹ค๋ฅธ ์˜ˆ์™ธ๋Š” ๊ทธ๋Œ€๋กœ ์ „ํŒŒ
                ERROR("์˜ค๋ฅ˜ ๋ฐœ์ƒ: $_");
                die $_;
            }
        };
    }
    
    # ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ
    ERROR("์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜($max_retries)๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.");
    return undef;
}

๋””๋ฒ„๊น… ๊ธฐ๋ฒ•

์›น ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž.

1. ์ƒ์„ธ ๋กœ๊น… ํ™œ์„ฑํ™”

# ๋กœ๊น… ๋ ˆ๋ฒจ ์„ค์ •
Log::Log4perl->easy_init({
    level => $DEBUG,  # ๋” ์ƒ์„ธํ•œ ๋กœ๊ทธ
    file => ">>scraper_debug.log",
    layout => "%d{yyyy-MM-dd HH:mm:ss} [%p] %F{1}:%L %M - %m%n"
});

# ๋‹ค์–‘ํ•œ ์ง€์ ์— ๋กœ๊ทธ ์ถ”๊ฐ€
DEBUG("URL ์š”์ฒญ ์‹œ์ž‘: $url");
DEBUG("์‘๋‹ต ํ—ค๋”: " . $response->headers->as_string);
DEBUG("HTML ๊ธธ์ด: " . length($html) . " ๋ฐ”์ดํŠธ");
DEBUG("์„ ํƒ์ž: " . join(", ", %$selector));

2. HTML ์‘๋‹ต ์ €์žฅํ•˜๊ธฐ

# ๋””๋ฒ„๊น…์„ ์œ„ํ•ด HTML ์‘๋‹ต ์ €์žฅ
sub save_html_for_debug {
    my ($html, $url) = @_;
    
    # URL์—์„œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
    my $filename = $url;
    $filename =~ s/https?:\/\///;  # ํ”„๋กœํ† ์ฝœ ์ œ๊ฑฐ
    $filename =~ s/[\/\?\&=:]/_/g; # ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ์–ธ๋”์Šค์ฝ”์–ด๋กœ ๋ณ€๊ฒฝ
    $filename = "debug_${filename}.html";
    
    open my $fh, ">:encoding(utf8)", $filename or do {
        WARN("๋””๋ฒ„๊ทธ HTML์„ ์ €์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!");
        return;
    };
    
    print $fh $html;
    close $fh;
    
    INFO("๋””๋ฒ„๊ทธ์šฉ HTML ์ €์žฅ๋จ: $filename");
}

3. ์„ ํƒ์ž ํ…Œ์ŠคํŠธ ๋„๊ตฌ

# HTML์—์„œ ์„ ํƒ์ž ํ…Œ์ŠคํŠธ
sub test_selector {
    my ($html, $selector) = @_;
    
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($html);
    $tree->eof;
    
    my @elements = $tree->look_down(%$selector);
    my $count = scalar(@elements);
    
    INFO("์„ ํƒ์ž ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ: $count๊ฐœ ์š”์†Œ ๋ฐœ๊ฒฌ");
    
    if ($count > 0) {
        my $i = 0;
        foreach my $element (@elements) {
            $i++;
            DEBUG("์š”์†Œ $i: " . $element->as_HTML);
            DEBUG("ํ…์ŠคํŠธ: " . $element->as_text);
        }
    }
    
    $tree->delete;
    
    return $count;
}

4. ๋Œ€ํ™”ํ˜• ๋””๋ฒ„๊น…

use Devel::REPL;

# ํŠน์ • ์ง€์ ์—์„œ ๋Œ€ํ™”ํ˜• ์‰˜ ์‹œ์ž‘
sub start_debug_shell {
    my ($variables) = @_;
    
    print "๋Œ€ํ™”ํ˜• ๋””๋ฒ„๊น… ์‰˜ ์‹œ์ž‘...\n";
    print "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณ€์ˆ˜๋“ค: " . join(", ", keys %$variables) . "\n";
    
    my $repl = Devel::REPL->new;
    $repl->load_plugin($_) for qw(History LexEnv);
    
    # ๋ณ€์ˆ˜๋“ค์„ REPL ํ™˜๊ฒฝ์— ์ฃผ์ž…
    $repl->eval('use strict; use warnings;');
    foreach my $name (keys %$variables) {
        my $value = $variables->{$name};
        $repl->eval("my \$$name = \$variables->{'$name'};");
    }
    
    $repl->run;
}

# ์‚ฌ์šฉ ์˜ˆ
my $html = fetch_page($url);
my $tree = parse_html($html);

if (!$tree || !$tree->look_down(%$selector)) {
    WARN("์„ ํƒ์ž ๋งค์นญ ์‹คํŒจ. ๋””๋ฒ„๊น… ์‰˜ ์‹œ์ž‘...");
    start_debug_shell({
        html => $html,
        tree => $tree,
        selector => $selector,
        url => $url
    });
}

HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ํ•˜๊ธฐ

์›น์‚ฌ์ดํŠธ ๊ตฌ์กฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์–ด. ์ด๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ๋Œ€์‘ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž.

HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ์ง€

# HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ ํ•จ์ˆ˜
sub detect_structure_change {
    my ($html, $expected_patterns) = @_;
    
    foreach my $pattern (@$expected_patterns) {
        unless ($html =~ /$pattern/) {
            WARN("์˜ˆ์ƒ ํŒจํ„ด์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: $pattern");
            return 1; # ๋ณ€๊ฒฝ ๊ฐ์ง€๋จ
        }
    }
    
    return 0; # ๋ณ€๊ฒฝ ์—†์Œ
}

# ์‚ฌ์šฉ ์˜ˆ
my $html = fetch_page($url);

# ์˜ˆ์ƒ๋˜๋Š” HTML ํŒจํ„ด๋“ค
my @expected_patterns = (
    '<div class="product-list"',
    '<span class="price"',
    '<h2 class="product-name"'
);

if (detect_structure_change($html, \@expected_patterns)) {
    ERROR("์›น์‚ฌ์ดํŠธ ๊ตฌ์กฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.");
    save_html_for_debug($html, $url);
    # ์•Œ๋ฆผ ์ „์†ก ๋“ฑ ์ถ”๊ฐ€ ์กฐ์น˜
} else {
    INFO("์›น์‚ฌ์ดํŠธ ๊ตฌ์กฐ ํ™•์ธ ์™„๋ฃŒ. ์Šคํฌ๋ž˜ํ•‘ ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.");
    # ์ •์ƒ ์Šคํฌ๋ž˜ํ•‘ ์ง„ํ–‰
}
์›น ์Šคํฌ๋ž˜ํ•‘ ๋””๋ฒ„๊น… ํ”„๋กœ์„ธ์Šค ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๋กœ๊ทธ ๋ถ„์„ HTML ๊ฒ€์‚ฌ ์Šคํฌ๋ฆฝํŠธ ์ˆ˜์ • ๋””๋ฒ„๊น… ๋„๊ตฌ - ๋กœ๊ทธ ํŒŒ์ผ - HTML ์ €์žฅ - ๋Œ€ํ™”ํ˜• ์‰˜ ์ผ๋ฐ˜์ ์ธ ์˜ค๋ฅ˜ - ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ - ์„ ํƒ์ž ๋ถˆ์ผ์น˜ - ๋ด‡ ์ฐจ๋‹จ ํ•ด๊ฒฐ ์ „๋žต - ์žฌ์‹œ๋„ ๋กœ์ง - ์„ ํƒ์ž ์—…๋ฐ์ดํŠธ - ์šฐํšŒ ๊ธฐ๋ฒ• ์˜ˆ๋ฐฉ ์กฐ์น˜ - ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ - ์ •๊ธฐ์  ํ…Œ์ŠคํŠธ - ์•Œ๋ฆผ ์‹œ์Šคํ…œ

์„ฑ๋Šฅ ์ตœ์ ํ™”

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•  ๋•Œ๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™”๋„ ์ค‘์š”ํ•ด.

1. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

use Parallel::ForkManager;

# ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์—ฌ๋Ÿฌ URL ์Šคํฌ๋ž˜ํ•‘
sub scrape_urls_parallel {
    my ($urls, $max_processes) = @_;
    $max_processes ||= 5;
    
    my $pm = Parallel::ForkManager->new($max_processes);
    my %results;
    
    # ๊ฒฐ๊ณผ ์ˆ˜์ง‘ ์„ค์ •
    $pm->run_on_finish(sub {
        my ($pid, $exit_code, $ident, $exit_signal, $core_dump, $data) = @_;
        $results{$data->{url}} = $data->{items} if $data && $data->{items};
    });
    
    foreach my $url (@$urls) {
        $pm->start and next;
        
        # ์ž์‹ ํ”„๋กœ์„ธ์Šค์—์„œ ์Šคํฌ๋ž˜ํ•‘ ์ˆ˜ํ–‰
        my $items = scrape_url($url);
        
        # ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
        $pm->finish(0, { url => $url, items => $items });
    }
    
    $pm->wait_all_children;
    
    return \%results;
}

2. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”

# ๋Œ€์šฉ๋Ÿ‰ HTML ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”
sub process_large_html {
    my ($html_file) = @_;
    
    # ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œํ•˜์ง€ ์•Š๊ณ  ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ
    open my $fh, "<:encoding(utf8)", $html_file or die "ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
    
    my $parser = HTML::Parser->new(
        api_version => 3,
        start_h => [\&handle_start, "tagname, attr"],
        text_h => [\&handle_text, "text"],
        end_h => [\&handle_end, "tagname"]
    );
    
    # ์ฒญํฌ ๋‹จ์œ„๋กœ ํŒŒ์ผ ์ฝ๊ธฐ
    my $buffer;
    while (read($fh, $buffer, 4096)) {
        $parser->parse($buffer);
    }
    $parser->eof;
    
    close $fh;
}

# ํƒœ๊ทธ ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ (์˜ˆ์‹œ)
my $in_product = 0;
my $current_product = {};
my @products;

sub handle_start {
    my ($tagname, $attr) = @_;
    
    if ($tagname eq 'div' && $attr->{class} && $attr->{class} =~ /product-item/) {
        $in_product = 1;
        $current_product = {};
    } elsif ($in_product && $tagname eq 'h2' && $attr->{class} && $attr->{class} =~ /product-name/) {
        $in_product_name = 1;
    }
    # ๊ธฐํƒ€ ํƒœ๊ทธ ์ฒ˜๋ฆฌ...
}

sub handle_text {
    my ($text) = @_;
    
    if ($in_product_name) {
        $current_product->{name} = $text;
    }
    # ๊ธฐํƒ€ ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ...
}

sub handle_end {
    my ($tagname) = @_;
    
    if ($tagname eq 'div' && $in_product) {
        $in_product = 0;
        push @products, { %$current_product };
    } elsif ($tagname eq 'h2' && $in_product_name) {
        $in_product_name = 0;
    }
    # ๊ธฐํƒ€ ์ข…๋ฃŒ ํƒœ๊ทธ ์ฒ˜๋ฆฌ...
}

3. ์บ์‹ฑ ๊ตฌํ˜„

use Digest::MD5 qw(md5_hex);
use Storable qw(store retrieve);

# URL ์‘๋‹ต ์บ์‹ฑ
sub fetch_with_cache {
    my ($url, $cache_dir, $cache_ttl) = @_;
    $cache_dir ||= 'cache';
    $cache_ttl ||= 3600; # 1์‹œ๊ฐ„
    
    # ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
    mkdir $cache_dir unless -d $cache_dir;
    
    # URL์—์„œ ์บ์‹œ ํ‚ค ์ƒ์„ฑ
    my $cache_key = md5_hex($url);
    my $cache_file = "$cache_dir/$cache_key.cache";
    
    # ์บ์‹œ ํŒŒ์ผ์ด ์กด์žฌํ•˜๊ณ  TTL ๋‚ด์— ์žˆ์œผ๋ฉด ์‚ฌ์šฉ
    if (-f $cache_file && (time - (stat($cache_file))[9]) < $cache_ttl) {
        DEBUG("์บ์‹œ์—์„œ $url ๋กœ๋“œ");
        my $cached_data = retrieve($cache_file);
        return $cached_data->{content};
    }
    
    # ์บ์‹œ ์—†๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋˜์—ˆ์œผ๋ฉด ์ƒˆ๋กœ ์š”์ฒญ
    DEBUG("$url ์ƒˆ๋กœ ์š”์ฒญ");
    my $ua = LWP::UserAgent->new;
    my $response = $ua->get($url);
    
    if ($response->is_success) {
        my $content = $response->decoded_content;
        
        # ์บ์‹œ ์ €์žฅ
        store({ 
            url => $url,
            content => $content,
            timestamp => time()
        }, $cache_file);
        
        return $content;
    } else {
        ERROR("์š”์ฒญ ์‹คํŒจ: " . $response->status_line);
        return undef;
    }
}

๐Ÿ“Œ ๋””๋ฒ„๊น… ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  1. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ: ๋ชจ๋“  ์™ธ๋ถ€ ํ˜ธ์ถœ(HTTP ์š”์ฒญ, ํŒŒ์ผ ์ž‘์—… ๋“ฑ)์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
  2. ๋กœ๊น…: ์ƒ์„ธํ•œ ๋กœ๊ทธ ๊ธฐ๋ก์œผ๋กœ ๋ฌธ์ œ ์ถ”์  ๊ฐ€๋Šฅํ•˜๊ฒŒ ์„ค์ •
  3. ์žฌ์‹œ๋„ ๋กœ์ง: ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ตฌํ˜„
  4. HTML ์ €์žฅ: ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ์›๋ณธ HTML ์ €์žฅ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  5. ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ์ง€: ์›น์‚ฌ์ดํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๋Š” ๋กœ์ง ๊ตฌํ˜„
  6. ์•Œ๋ฆผ ์‹œ์Šคํ…œ: ์ค‘์š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ์„ค์ •
  7. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์†๋„ ์ตœ์ ํ™”

๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ๋””๋ฒ„๊น… ์‹œ์Šคํ…œ์€ ์žฅ๊ธฐ์ ์œผ๋กœ ์•ˆ์ •์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘ ์†”๋ฃจ์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ด์•ผ. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ฒ•์ , ์œค๋ฆฌ์  ์ธก๋ฉด์„ ์‚ดํŽด๋ณผ๊ฒŒ! ๐Ÿง‘โ€โš–๏ธ

9. ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ฒ•์ , ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ โš–๏ธ

์›น ์Šคํฌ๋ž˜ํ•‘์€ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ง€๋งŒ, ๋ฒ•์ , ์œค๋ฆฌ์  ๊ฒฝ๊ณ„ ๋‚ด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์„ ํ•  ๋•Œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ฒ•์ , ์œค๋ฆฌ์  ๋ฌธ์ œ๋“ค์„ ์•Œ์•„๋ณผ๊ฒŒ.

์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ฒ•์  ์ธก๋ฉด

์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์œค๋ฆฌ์  ์ธก๋ฉด

1. ์„œ๋ฒ„ ๋ถ€ํ•˜ ๊ณ ๋ ค

๊ณผ๋„ํ•œ ์š”์ฒญ์€ ๋Œ€์ƒ ์›น์‚ฌ์ดํŠธ์— ๋ถ€๋‹ด์„ ์ค„ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์†Œ๊ทœ๋ชจ ์‚ฌ์ดํŠธ๋Š” ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€๋กœ ์ธํ•ด ์„ฑ๋Šฅ ์ €ํ•˜๋‚˜ ๋น„์šฉ ์ฆ๊ฐ€๋ฅผ ๊ฒช์„ ์ˆ˜ ์žˆ์ง€.

๊ถŒ์žฅ์‚ฌํ•ญ: ์š”์ฒญ ๊ฐ„ ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด๋ฅผ ๋‘๊ณ , ํ”ผํฌ ์‹œ๊ฐ„์„ ํ”ผํ•ด ์Šคํฌ๋ž˜ํ•‘ํ•˜๋ฉฐ, ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ˆ˜์ง‘ํ•ด.

2. robots.txt ์ค€์ˆ˜

robots.txt ํŒŒ์ผ์€ ์›น์‚ฌ์ดํŠธ๊ฐ€ ํฌ๋กค๋Ÿฌ์—๊ฒŒ ์–ด๋–ค ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•ด๋„ ๋˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” ํ‘œ์ค€์ด์•ผ.

๊ถŒ์žฅ์‚ฌํ•ญ: ์Šคํฌ๋ž˜ํ•‘ ์ „์— ํ•ญ์ƒ robots.txt ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ณ  ๊ทธ ์ง€์นจ์„ ๋”ฐ๋ผ์•ผ ํ•ด.

use WWW::RobotRules;

# robots.txt ํ™•์ธ ํ•จ์ˆ˜
sub check_robots_txt {
    my ($url) = @_;
    
    my $ua = LWP::UserAgent->new;
    my $robot_name = "MyPerlScraper/1.0";
    
    # URL์—์„œ ๋„๋ฉ”์ธ ์ถ”์ถœ
    my ($domain) = $url =~ m{https?://([^/]+)};
    my $robots_url = "https://$domain/robots.txt";
    
    my $rules = WWW::RobotRules->new($robot_name);
    my $response = $ua->get($robots_url);
    
    if ($response->is_success) {
        $rules->parse($robots_url, $response->decoded_content);
        
        if ($rules->allowed($url)) {
            INFO("robots.txt: $url ์ ‘๊ทผ ํ—ˆ์šฉ");
            return 1;
        } else {
            WARN("robots.txt: $url ์ ‘๊ทผ ๊ธˆ์ง€");
            return 0;
        }
    } else {
        # robots.txt๊ฐ€ ์—†์œผ๋ฉด ์ ‘๊ทผ ํ—ˆ์šฉ์œผ๋กœ ๊ฐ„์ฃผ
        INFO("robots.txt ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ. ์ ‘๊ทผ ํ—ˆ์šฉ์œผ๋กœ ๊ฐ„์ฃผ");
        return 1;
    }
}

3. ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ์‹๋ณ„

์ž์‹ ์„ ์ˆจ๊ธฐ์ง€ ์•Š๊ณ  ์Šคํฌ๋ž˜ํผ์ž„์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์œค๋ฆฌ์ ์ด์•ผ.

๊ถŒ์žฅ์‚ฌํ•ญ: ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ๋ฌธ์ž์—ด์— ๋ด‡์˜ ์ด๋ฆ„๊ณผ ์—ฐ๋ฝ์ฒ˜ ์ •๋ณด๋ฅผ ํฌํ•จ์‹œ์ผœ:

my $ua = LWP::UserAgent->new;
$ua->agent('MyCompanyScraper/1.0 (+https://example.com/bot; bot@example.com)');

4. ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ๋ชฉ์ 

์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•  ๊ฒƒ์ธ์ง€๋„ ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ์ด์•ผ.

๊ถŒ์žฅ์‚ฌํ•ญ: ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์ •ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ , ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œํ•˜๋ฉฐ, ์›๋ณธ ์‚ฌ์ดํŠธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์„ ํ•ด์น˜์ง€ ์•Š๋„๋ก ํ•ด.

์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ฒ•์ , ์œค๋ฆฌ์  ๊ท ํ˜• ๋ฒ•์  ์ธก๋ฉด ์œค๋ฆฌ์  ์ธก๋ฉด ์ด์šฉ์•ฝ๊ด€ ์ €์ž‘๊ถŒ๋ฒ• CFAA ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ๋ฒ• ์„œ๋ฒ„ ๋ถ€ํ•˜ ๊ณ ๋ ค robots.txt ์ค€์ˆ˜ ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ๋ชฉ์  ์ฑ…์ž„๊ฐ ์žˆ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘์€ ๋ฒ•์  ๊ฒฝ๊ณ„์™€ ์œค๋ฆฌ์  ์›์น™ ์‚ฌ์ด์˜ ๊ท ํ˜•์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค

์•ˆ์ „ํ•œ ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์œ„ํ•œ ๊ฐ€์ด๋“œ๋ผ์ธ

  1. API ์šฐ์„ : ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณต์‹ API๋ฅผ ์‚ฌ์šฉํ•ด. API๋Š” ์›น์‚ฌ์ดํŠธ ์†Œ์œ ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•œ ๋ฐฉ๋ฒ•์ด์•ผ.
  2. ์ด์šฉ์•ฝ๊ด€ ํ™•์ธ: ์Šคํฌ๋ž˜ํ•‘ํ•˜๋ ค๋Š” ์›น์‚ฌ์ดํŠธ์˜ ์ด์šฉ์•ฝ๊ด€์„ ๋ฐ˜๋“œ์‹œ ์ฝ๊ณ  ์ค€์ˆ˜ํ•ด.
  3. robots.txt ์ค€์ˆ˜: ์›น์‚ฌ์ดํŠธ์˜ robots.txt ํŒŒ์ผ์— ๋ช…์‹œ๋œ ๊ทœ์น™์„ ๋”ฐ๋ผ.
  4. ์š”์ฒญ ์ œํ•œ: ๊ณผ๋„ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๋„๋ก ์š”์ฒญ ๊ฐ„ ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด๋ฅผ ๋‘๊ณ , ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ˆ˜์ง‘ํ•ด.
  5. ์‹ ์› ๊ณต๊ฐœ: ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ๋ฌธ์ž์—ด์— ๋ด‡์˜ ์ด๋ฆ„๊ณผ ์—ฐ๋ฝ์ฒ˜ ์ •๋ณด๋ฅผ ํฌํ•จ์‹œ์ผœ.
  6. ์บ์‹ฑ ํ™œ์šฉ: ๊ฐ™์€ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์š”์ฒญํ•˜์ง€ ์•Š๋„๋ก ์บ์‹ฑ์„ ์‚ฌ์šฉํ•ด.
  7. ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ: ๊ฐœ์ธ ์‹๋ณ„ ์ •๋ณด๋Š” ์ˆ˜์ง‘ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ์ˆ˜์ง‘ ์‹œ ๊ด€๋ จ ๋ฒ•๋ฅ ์„ ์ค€์ˆ˜ํ•ด.
  8. ์ถœ์ฒ˜ ํ‘œ์‹œ: ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œํ•ด.
  9. ๊ฒฝ์Ÿ ๊ณ ๋ ค: ๊ฒฝ์Ÿ์‚ฌ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ
    1. ๊ฒฝ์Ÿ ๊ณ ๋ ค: ๊ฒฝ์Ÿ์‚ฌ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์„ ํ•ด์น˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„.
    2. ๋ฒ•๋ฅ  ์ž๋ฌธ: ๋Œ€๊ทœ๋ชจ ์ƒ์—…์  ์Šคํฌ๋ž˜ํ•‘ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋ฒ•๋ฅ  ์ž๋ฌธ์„ ๊ตฌํ•ด.

    Perl๋กœ ์ฑ…์ž„๊ฐ ์žˆ๋Š” ์Šคํฌ๋ž˜ํ•‘ ๊ตฌํ˜„ํ•˜๊ธฐ

    ์ด์ œ ๋ฒ•์ , ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•œ Perl ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด์ž.

    ์ฑ…์ž„๊ฐ ์žˆ๋Š” ์Šคํฌ๋ž˜ํ•‘ ์ฝ”๋“œ

    #!/usr/bin/perl
    use strict;
    use warnings;
    use LWP::UserAgent;
    use LWP::RobotUA;
    use HTML::TreeBuilder;
    use HTTP::Cookies;
    use Time::HiRes qw(sleep);
    use Log::Log4perl qw(:easy);
    
    # ๋กœ๊น… ์„ค์ •
    Log::Log4perl->easy_init($INFO);
    
    # ์„ค์ •
    my $url = "https://example.com/products";
    my $cache_dir = "cache";
    my $delay = 5; # ์š”์ฒญ ๊ฐ„ 5์ดˆ ๋”œ๋ ˆ์ด
    my $user_agent = "ResponsibleScraper/1.0 (+https://example.com/bot; contact@example.com)";
    
    # ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
    mkdir $cache_dir unless -d $cache_dir;
    
    # robots.txt ๊ทœ์น™์„ ์ค€์ˆ˜ํ•˜๋Š” ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ์ƒ์„ฑ
    my $ua = LWP::RobotUA->new($user_agent, 'contact@example.com');
    $ua->delay($delay/60); # delay๋Š” ๋ถ„ ๋‹จ์œ„๋กœ ์„ค์ •
    $ua->timeout(30);
    
    # ์ฟ ํ‚ค ์„ค์ •
    my $cookie_jar = HTTP::Cookies->new(
        file => "$cache_dir/cookies.txt",
        autosave => 1
    );
    $ua->cookie_jar($cookie_jar);
    
    # ์›น์‚ฌ์ดํŠธ ๋ฐฉ๋ฌธ ์ „ ํ™•์ธ
    INFO("URL $url ์Šคํฌ๋ž˜ํ•‘ ์‹œ์ž‘");
    
    # robots.txt ํ™•์ธ
    if (!$ua->allowed($url)) {
        ERROR("robots.txt์— ์˜ํ•ด $url ์ ‘๊ทผ์ด ๊ธˆ์ง€๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.");
        exit;
    }
    
    # ์š”์ฒญ ์ „ ๋”œ๋ ˆ์ด
    INFO("$delay์ดˆ ๋Œ€๊ธฐ ์ค‘...");
    sleep($delay);
    
    # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
    my $response = $ua->get($url);
    
    if (!$response->is_success) {
        ERROR("ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: " . $response->status_line);
        exit;
    }
    
    # ์‘๋‹ต ํ—ค๋” ํ™•์ธ
    if ($response->header('X-Robots-Tag') && $response->header('X-Robots-Tag') =~ /noindex|nofollow/i) {
        WARN("์ด ํŽ˜์ด์ง€๋Š” X-Robots-Tag๋กœ ์ธ๋ฑ์‹ฑ/ํŒ”๋กœ์ž‰์ด ์ œํ•œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.");
        # ์—ฌ๊ธฐ์„œ ๊ณ„์†ํ• ์ง€ ๊ฒฐ์ •
    }
    
    # HTML ํŒŒ์‹ฑ
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($response->decoded_content);
    
    # ๋ฐ์ดํ„ฐ ์ถ”์ถœ (๊ฐœ์ธ์ •๋ณด ์ œ์™ธ)
    my @products;
    my @product_elements = $tree->look_down('_tag', 'div', 'class', 'product');
    
    foreach my $product (@product_elements) {
        my $name = $product->look_down('_tag', 'h2')->as_text;
        my $price_element = $product->look_down('_tag', 'span', 'class', 'price');
        my $price = $price_element ? $price_element->as_text : "N/A";
        
        # ๊ฐœ์ธ์ •๋ณด ํ•„ํ„ฐ๋ง (์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ)
        next if $name =~ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/; # ์ด๋ฉ”์ผ ํŒจํ„ด
        next if $name =~ /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/; # ์ „ํ™”๋ฒˆํ˜ธ ํŒจํ„ด
        
        push @products, {
            name => $name,
            price => $price,
            source_url => $url, # ์ถœ์ฒ˜ ๊ธฐ๋ก
            scraped_at => scalar(localtime)
        };
    }
    
    $tree->delete;
    
    # ๊ฒฐ๊ณผ ์ถœ๋ ฅ
    INFO("์ด " . scalar(@products) . "๊ฐœ ์ œํ’ˆ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.");
    foreach my $product (@products) {
        print "์ œํ’ˆ๋ช…: $product->{name}\n";
        print "๊ฐ€๊ฒฉ: $product->{price}\n";
        print "์ถœ์ฒ˜: $product->{source_url}\n";
        print "์ˆ˜์ง‘ ์‹œ๊ฐ„: $product->{scraped_at}\n";
        print "------------------------\n";
    }
    
    # ์ €์ž‘๊ถŒ ๊ณ ์ง€
    print "\n๋ฐ์ดํ„ฐ ์ถœ์ฒ˜: Example.com\n";
    print "์ด ๋ฐ์ดํ„ฐ๋Š” ๊ฐœ์ธ ์—ฐ๊ตฌ ๋ชฉ์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉ๋˜๋ฉฐ, ์žฌ๋ฐฐํฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n";

    โš ๏ธ ๋ฒ•์  ์ฑ…์ž„ ๋ฉด์ฑ… ๊ณ ์ง€

    ์ด ๊ธ€์—์„œ ์ œ๊ณตํ•˜๋Š” ์ •๋ณด๋Š” ์ผ๋ฐ˜์ ์ธ ๊ฐ€์ด๋“œ๋ผ์ธ์ผ ๋ฟ์ด๋ฉฐ, ๋ฒ•๋ฅ  ์ž๋ฌธ์„ ๋Œ€์ฒดํ•  ์ˆ˜ ์—†์–ด. ์›น ์Šคํฌ๋ž˜ํ•‘ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ํ•ด๋‹น ์ง€์—ญ์˜ ๋ฒ•๋ฅ ๊ณผ ๊ทœ์ •์„ ํ™•์ธํ•˜๊ณ , ํ•„์š”ํ•˜๋‹ค๋ฉด ๋ฒ•๋ฅ  ์ „๋ฌธ๊ฐ€์˜ ์กฐ์–ธ์„ ๊ตฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„.

    ๋ฒ•์ , ์œค๋ฆฌ์  ๊ฒฝ๊ณ„ ๋‚ด์—์„œ ์›น ์Šคํฌ๋ž˜ํ•‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํžˆ ๋ฌธ์ œ๋ฅผ ํ”ผํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์˜๋ฏธ๊ฐ€ ์žˆ์–ด. ์ด๋Š” ์›น ์ƒํƒœ๊ณ„๋ฅผ ์กด์ค‘ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ์†Œ์Šค์™€ ์ƒํ˜ธ ์ด์ต์ด ๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ–‰๋™ํ•˜๋Š” ๊ฒƒ์ด์ง€. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๊ฒƒ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ! ๐Ÿš€

    10. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ๋งŒ๋“ค๊ธฐ ๐Ÿ—ž๏ธ

    ์ด์ œ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์šฉ์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ ์‹œ๊ฐ„์ด์•ผ. ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์—ฌ๋Ÿฌ ๋‰ด์Šค ์‚ฌ์ดํŠธ์—์„œ ์ตœ์‹  ํ—ค๋“œ๋ผ์ธ์„ ์ˆ˜์ง‘ํ•˜๊ณ , ๋ถ„์„ํ•˜๋Š” ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ ๊ฑฐ์•ผ.

    ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

    ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ

    ๋ชฉ์ : ์—ฌ๋Ÿฌ ๋‰ด์Šค ์‚ฌ์ดํŠธ์—์„œ ์ตœ์‹  ํ—ค๋“œ๋ผ์ธ์„ ์ˆ˜์ง‘ํ•˜๊ณ , ์ฃผ์ œ๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•˜์—ฌ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•œ๋‹ค.

    ๊ธฐ๋Šฅ:

    • ์—ฌ๋Ÿฌ ๋‰ด์Šค ์‚ฌ์ดํŠธ์—์„œ ์ตœ์‹  ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘
    • ํ—ค๋“œ๋ผ์ธ ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์ •์ œ
    • ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ์ฃผ์ œ ๋ถ„๋ฅ˜
    • ์ˆ˜์ง‘ ๋ฐ์ดํ„ฐ JSON ๋ฐ CSV ํ˜•์‹์œผ๋กœ ์ €์žฅ
    • ๊ฐ„๋‹จํ•œ ํŠธ๋ Œ๋“œ ๋ถ„์„ (ํ‚ค์›Œ๋“œ ๋นˆ๋„ ๋“ฑ)
    • ๋งค์ผ ์ž๋™ ์‹คํ–‰ (cron ์ž‘์—…)

    ์‚ฌ์šฉ ๊ธฐ์ˆ : Perl, LWP::UserAgent, HTML::TreeBuilder, JSON, Text::CSV, ์ •๊ทœํ‘œํ˜„์‹

    ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

    news_scraper/
    โ”œโ”€โ”€ config/
    โ”‚   โ””โ”€โ”€ sites.json        # ๋‰ด์Šค ์‚ฌ์ดํŠธ ์„ค์ •
    โ”œโ”€โ”€ lib/
    โ”‚   โ”œโ”€โ”€ NewsScraper.pm    # ๋ฉ”์ธ ์Šคํฌ๋ž˜ํผ ๋ชจ๋“ˆ
    โ”‚   โ”œโ”€โ”€ NewsParser.pm     # ๋‰ด์Šค ํŒŒ์‹ฑ ๋ชจ๋“ˆ
    โ”‚   โ””โ”€โ”€ NewsAnalyzer.pm   # ๋‰ด์Šค ๋ถ„์„ ๋ชจ๋“ˆ
    โ”œโ”€โ”€ data/
    โ”‚   โ”œโ”€โ”€ headlines/        # ์ˆ˜์ง‘๋œ ํ—ค๋“œ๋ผ์ธ ์ €์žฅ
    โ”‚   โ””โ”€โ”€ analysis/         # ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ
    โ”œโ”€โ”€ logs/                 # ๋กœ๊ทธ ํŒŒ์ผ
    โ”œโ”€โ”€ run_scraper.pl        # ๋ฉ”์ธ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ
    โ””โ”€โ”€ analyze_trends.pl     # ํŠธ๋ Œ๋“œ ๋ถ„์„ ์Šคํฌ๋ฆฝํŠธ

    1. ์„ค์ • ํŒŒ์ผ (sites.json)

    config/sites.json

    {
      "sites": [
        {
          "name": "Tech News",
          "url": "https://example-tech-news.com",
          "category": "technology",
          "selectors": {
            "headlines": {
              "container": "div.news-item",
              "title": "h2.headline a",
              "link": "h2.headline a",
              "summary": "p.summary",
              "date": "span.date"
            }
          },
          "max_pages": 3,
          "pagination": {
            "pattern": "?page={page}",
            "start_page": 1
          }
        },
        {
          "name": "Business Daily",
          "url": "https://example-business-news.com/latest",
          "category": "business",
          "selectors": {
            "headlines": {
              "container": "article.news-article",
              "title": "h3.title",
              "link": "a.read-more",
              "summary": "div.excerpt",
              "date": "time.published"
            }
          },
          "max_pages": 2,
          "pagination": {
            "pattern": "/page/{page}",
            "start_page": 1
          }
        },
        {
          "name": "Science Portal",
          "url": "https://example-science-news.com/news",
          "category": "science",
          "selectors": {
            "headlines": {
              "container": "div.article-card",
              "title": "h4.article-title",
              "link": "a.article-link",
              "summary": "p.article-summary",
              "date": "div.article-date"
            }
          },
          "max_pages": 2,
          "pagination": {
            "pattern": "?p={page}",
            "start_page": 1
          }
        }
      ],
      "topics": {
        "ai": ["artificial intelligence", "machine learning", "neural network", "deep learning", "AI"],
        "climate": ["climate change", "global warming", "carbon emission", "renewable energy"],
        "crypto": ["bitcoin", "ethereum", "cryptocurrency", "blockchain", "token", "NFT"],
        "health": ["covid", "vaccine", "pandemic", "health", "medical", "medicine"]
      },
      "settings": {
        "request_delay": 3,
        "user_agent": "NewsScraper/1.0 (+https://example.com/bot; contact@example.com)",
        "cache_ttl": 3600,
        "max_headlines_per_site": 50
      }
    }

    2. ๋ฉ”์ธ ์Šคํฌ๋ž˜ํผ ๋ชจ๋“ˆ (NewsScraper.pm)

    lib/NewsScraper.pm

    package NewsScraper;
    
    use strict;
    use warnings;
    use LWP::UserAgent;
    use HTML::TreeBuilder;
    use JSON;
    use Log::Log4perl qw(:easy);
    use Time::HiRes qw(sleep);
    use HTTP::Cookies;
    use Digest::MD5 qw(md5_hex);
    use File::Path qw(make_path);
    use WWW::RobotRules;
    use Try::Tiny;
    
    # ์ดˆ๊ธฐํ™”
    sub new {
        my ($class, %args) = @_;
        
        # ๋กœ๊น… ์„ค์ •
        Log::Log4perl->easy_init({
            level => $args{debug} ? $DEBUG : $INFO,
            file => ">>logs/scraper.log",
            layout => "%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n"
        });
        
        # ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ
        my $config_file = $args{config_file} || 'config/sites.json';
        open my $fh, '<:encoding(utf8)', $config_file or die "์„ค์ • ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
        my $config_json = do { local $/; <$fh> };
        close $fh;
        
        my $config = decode_json($config_json);
        
        # ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
        make_path('data/headlines') unless -d 'data/headlines';
        make_path('data/analysis') unless -d 'data/analysis';
        make_path('logs') unless -d 'logs';
        
        # ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
        my $cache_dir = $args{cache_dir} || 'data/cache';
        make_path($cache_dir) unless -d $cache_dir;
        
        # ์‚ฌ์šฉ์ž ์—์ด์ „ํŠธ ์ƒ์„ฑ
        my $ua = LWP::UserAgent->new;
        $ua->agent($config->{settings}{user_agent});
        $ua->timeout(30);
        
        # ์ฟ ํ‚ค ์„ค์ •
        my $cookie_jar = HTTP::Cookies->new(
            file => "$cache_dir/cookies.txt",
            autosave => 1
        );
        $ua->cookie_jar($cookie_jar);
        
        # ๊ฐ์ฒด ์ƒ์„ฑ
        my $self = {
            ua => $ua,
            config => $config,
            cache_dir => $cache_dir,
            debug => $args{debug} || 0,
            robots_cache => {}
        };
        
        return bless $self, $class;
    }
    
    # robots.txt ํ™•์ธ
    sub check_robots_txt {
        my ($self, $url) = @_;
        
        # URL์—์„œ ๋„๋ฉ”์ธ ์ถ”์ถœ
        my ($domain) = $url =~ m{https?://([^/]+)};
        return 1 unless $domain; # URL ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ํ—ˆ์šฉ์œผ๋กœ ๊ฐ„์ฃผ
        
        # ์ด๋ฏธ ํ™•์ธํ•œ ๋„๋ฉ”์ธ์ด๋ฉด ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
        if (exists $self->{robots_cache}{$domain}) {
            return $self->{robots_cache}{$domain}{$url};
        }
        
        # robots.txt ๊ฐ€์ ธ์˜ค๊ธฐ
        my $robots_url = "https://$domain/robots.txt";
        my $robot_name = $self->{config}{settings}{user_agent};
        my $rules = WWW::RobotRules->new($robot_name);
        
        my $response = $self->{ua}->get($robots_url);
        if ($response->is_success) {
            $rules->parse($robots_url, $response->decoded_content);
            $self->{robots_cache}{$domain} = $rules;
            
            if ($rules->allowed($url)) {
                INFO("robots.txt: $url ์ ‘๊ทผ ํ—ˆ์šฉ");
                return 1;
            } else {
                WARN("robots.txt: $url ์ ‘๊ทผ ๊ธˆ์ง€");
                return 0;
            }
        } else {
            # robots.txt๊ฐ€ ์—†์œผ๋ฉด ์ ‘๊ทผ ํ—ˆ์šฉ์œผ๋กœ ๊ฐ„์ฃผ
            INFO("robots.txt ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ. ์ ‘๊ทผ ํ—ˆ์šฉ์œผ๋กœ ๊ฐ„์ฃผ");
            return 1;
        }
    }
    
    # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
    sub fetch_page {
        my ($self, $url) = @_;
        
        # robots.txt ํ™•์ธ
        return undef unless $self->check_robots_txt($url);
        
        # ์บ์‹œ ํ™•์ธ
        my $cache_key = md5_hex($url);
        my $cache_file = "$self->{cache_dir}/$cache_key.html";
        my $cache_ttl = $self->{config}{settings}{cache_ttl} || 3600;
        
        if (-f $cache_file && (time - (stat($cache_file))[9]) < $cache_ttl) {
            DEBUG("์บ์‹œ์—์„œ $url ๋กœ๋“œ");
            open my $fh, '<:encoding(utf8)', $cache_file or return undef;
            my $content = do { local $/; <$fh> };
            close $fh;
            return $content;
        }
        
        # ์š”์ฒญ ๋”œ๋ ˆ์ด
        my $delay = $self->{config}{settings}{request_delay} || 3;
        INFO("$delay์ดˆ ๋Œ€๊ธฐ ์ค‘...");
        sleep($delay);
        
        # ์›น ํŽ˜์ด์ง€ ์š”์ฒญ
        INFO("ํŽ˜์ด์ง€ ์š”์ฒญ: $url");
        my $response = $self->{ua}->get($url);
        
        if ($response->is_success) {
            my $content = $response->decoded_content;
            
            # ์บ์‹œ ์ €์žฅ
            open my $fh, '>:encoding(utf8)', $cache_file or do {
                WARN("์บ์‹œ ํŒŒ์ผ์„ ์ €์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!");
                return $content;
            };
            print $fh $content;
            close $fh;
            
            return $content;
        } else {
            ERROR("ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: " . $response->status_line);
            return undef;
        }
    }
    
    # ํ—ค๋“œ๋ผ์ธ ์ถ”์ถœ
    sub extract_headlines {
        my ($self, $site) = @_;
        
        my @all_headlines;
        my $page = $site->{pagination}{start_page} || 1;
        my $max_pages = $site->{max_pages} || 1;
        
        while ($page <= $max_pages) {
            # ํŽ˜์ด์ง€ URL ์ƒ์„ฑ
            my $page_url = $site->{url};
            if ($page > 1 && $site->{pagination}{pattern}) {
                my $pattern = $site->{pagination}{pattern};
                $pattern =~ s/\{page\}/$page/g;
                $page_url .= $pattern;
            }
            
            # ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ
            my $html = $self->fetch_page($page_url);
            last unless $html; # ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ
            
            # HTML ํŒŒ์‹ฑ
            my $tree = HTML::TreeBuilder->new;
            $tree->parse($html);
            
            # ํ—ค๋“œ๋ผ์ธ ์ปจํ…Œ์ด๋„ˆ ์ฐพ๊ธฐ
            my $container_selector = $site->{selectors}{headlines}{container};
            my ($tag, $class) = $container_selector =~ /([a-zA-Z0-9]+)(?:\.([a-zA-Z0-9\-_]+))?/;
            
            my @containers;
            if ($class) {
                @containers = $tree->look_down('_tag', $tag, 'class', qr/$class/);
            } else {
                @containers = $tree->look_down('_tag', $tag);
            }
            
            # ๊ฐ ์ปจํ…Œ์ด๋„ˆ์—์„œ ํ—ค๋“œ๋ผ์ธ ์ •๋ณด ์ถ”์ถœ
            foreach my $container (@containers) {
                my $headline = {};
                
                # ์ œ๋ชฉ ์ถ”์ถœ
                my $title_selector = $site->{selectors}{headlines}{title};
                ($tag, $class) = $title_selector =~ /([a-zA-Z0-9]+)(?:\.([a-zA-Z0-9\-_]+))?/;
                my $title_element;
                
                if ($class) {
                    $title_element = $container->look_down('_tag', $tag, 'class', qr/$class/);
                } else {
                    $title_element = $container->look_down('_tag', $tag);
                }
                
                $headline->{title} = $title_element ? $title_element->as_text : "";
                
                # ๋งํฌ ์ถ”์ถœ
                my $link_selector = $site->{selectors}{headlines}{link};
                ($tag, $class) = $link_selector =~ /([a-zA-Z0-9]+)(?:\.([a-zA-Z0-9\-_]+))?/;
                my $link_element;
                
                if ($class) {
                    $link_element = $container->look_down('_tag', $tag, 'class', qr/$class/);
                } else {
                    $link_element = $container->look_down('_tag', $tag);
                }
                
                $headline->{link} = $link_element ? $link_element->attr('href') : "";
                
                # ์ƒ๋Œ€ URL์„ ์ ˆ๋Œ€ URL๋กœ ๋ณ€ํ™˜
                if ($headline->{link} && $headline->{link} !~ /^https?:\/\//) {
                    my ($base_url) = $site->{url} =~ m{(https?://[^/]+)};
                    $headline->{link} = $base_url . $headline->{link};
                }
                
                # ์š”์•ฝ ์ถ”์ถœ
                my $summary_selector = $site->{selectors}{headlines}{summary};
                ($tag, $class) = $summary_selector =~ /([a-zA-Z0-9]+)(?:\.([a-zA-Z0-9\-_]+))?/;
                my $summary_element;
                
                if ($class) {
                    $summary_element = $container->look_down('_tag', $tag, 'class', qr/$class/);
                } else {
                    $summary_element = $container->look_down('_tag', $tag);
                }
                
                $headline->{summary} = $summary_element ? $summary_element->as_text : "";
                
                # ๋‚ ์งœ ์ถ”์ถœ
                my $date_selector = $site->{selectors}{headlines}{date};
                ($tag, $class) = $date_selector =~ /([a-zA-Z0-9]+)(?:\.([a-zA-Z0-9\-_]+))?/;
                my $date_element;
                
                if ($class) {
                    $date_element = $container->look_down('_tag', $tag, 'class', qr/$class/);
                } else {
                    $date_element = $container->look_down('_tag', $tag);
                }
                
                $headline->{date} = $date_element ? $date_element->as_text : "";
                
                # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
                $headline->{source} = $site->{name};
                $headline->{category} = $site->{category};
                $headline->{scraped_at} = scalar(localtime);
                
                # ๋นˆ ์ œ๋ชฉ ์ œ์™ธ
                next unless $headline->{title};
                
                push @all_headlines, $headline;
            }
            
            $tree->delete;
            
            # ์ตœ๋Œ€ ํ—ค๋“œ๋ผ์ธ ์ˆ˜ ํ™•์ธ
            my $max_headlines = $self->{config}{settings}{max_headlines_per_site} || 50;
            last if @all_headlines >= $max_headlines;
            
            $page++;
        }
        
        return \@all_headlines;
    }
    
    # ์ฃผ์ œ ๋ถ„๋ฅ˜
    sub classify_headlines {
        my ($self, $headlines) = @_;
        
        my $topics = $self->{config}{topics};
        
        foreach my $headline (@$headlines) {
            my $content = lc($headline->{title} . " " . $headline->{summary});
            my @matched_topics;
            
            foreach my $topic (keys %$topics) {
                my $keywords = $topics->{$topic};
                
                foreach my $keyword (@$keywords) {
                    if ($content =~ /\b$keyword\b/i) {
                        push @matched_topics, $topic;
                        last;
                    }
                }
            }
            
            $headline->{topics} = \@matched_topics;
        }
        
        return $headlines;
    }
    
    # ํ—ค๋“œ๋ผ์ธ ์ €์žฅ
    sub save_headlines {
        my ($self, $headlines, $format) = @_;
        $format ||= 'json';
        
        my $timestamp = time();
        my $filename = "data/headlines/headlines_$timestamp.$format";
        
        if ($format eq 'json') {
            open my $fh, '>:encoding(utf8)', $filename or die "ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
            print $fh encode_json($headlines);
            close $fh;
        } elsif ($format eq 'csv') {
            require Text::CSV;
            
            open my $fh, '>:encoding(utf8)', $filename or die "ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!";
            my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
            
            # ํ—ค๋” ์ž‘์„ฑ
            $csv->print($fh, ["์ œ๋ชฉ", "๋งํฌ", "์š”์•ฝ", "๋‚ ์งœ", "์ถœ์ฒ˜", "์นดํ…Œ๊ณ ๋ฆฌ", "์ฃผ์ œ", "์ˆ˜์ง‘์‹œ๊ฐ„"]);
            print $fh "\n";
            
            # ๋ฐ์ดํ„ฐ ์ž‘์„ฑ
            foreach my $headline (@$headlines) {
                $csv->print($fh, [
                    $headline->{title},
                    $headline->{link},
                    $headline->{summary},
                    $headline->{date},
                    $headline->{source},
                    $headline->{category},
                    join(", ", @{$headline->{topics}}),
                    $headline->{scraped_at}
                ]);
                print $fh "\n";
            }
            
            close $fh;
        } else {
            ERROR("์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: $format");
            return 0;
        }
        
        INFO("ํ—ค๋“œ๋ผ์ธ ์ €์žฅ ์™„๋ฃŒ: $filename");
        return $filename;
    }
    
    # ์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ์‹คํ–‰
    sub run {
        my ($self) = @_;
        
        my @all_headlines;
        
        foreach my $site (@{$self->{config}{sites}}) {
            INFO("์‚ฌ์ดํŠธ ์Šคํฌ๋ž˜ํ•‘: $site->{name}");
            
            try {
                my $headlines = $self->extract_headlines($site);
                push @all_headlines, @$headlines;
                INFO("$site->{name}์—์„œ " . scalar(@$headlines) . "๊ฐœ ํ—ค๋“œ๋ผ์ธ ์ถ”์ถœ ์™„๋ฃŒ");
            } catch {
                ERROR("$site->{name} ์Šคํฌ๋ž˜ํ•‘ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: $_");
            };
            
            # ์‚ฌ์ดํŠธ ๊ฐ„ ๋”œ๋ ˆ์ด
            sleep(2);
        }
        
        # ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ™์€ ์ œ๋ชฉ์˜ ๋‰ด์Šค)
        my %seen;
        @all_headlines = grep { !$seen{$_->{title}}++ } @all_headlines;
        
        # ์ฃผ์ œ ๋ถ„๋ฅ˜
        $self->classify_headlines(\@all_headlines);
        
        # ๊ฒฐ๊ณผ ์ €์žฅ
        $self->save_headlines(\@all_headlines, 'json');
        $self->save_headlines(\@all_headlines, 'csv');
        
        return \@all_headlines;
    }
    
    1;

    3. ๋‰ด์Šค ๋ถ„์„ ๋ชจ๋“ˆ (NewsAnalyzer.pm)

    lib/NewsAnalyzer.pm

    package NewsAnalyzer;
    
    use strict;
    use warnings;
    use JSON;
    use Log::Log4perl qw(:easy);
    use File::Glob qw(bsd_glob);
    use Text::CSV;
    use List::Util qw(max);
    
    # ์ดˆ๊ธฐํ™”
    sub new {
        my ($class, %args) = @_;
        
        # ๋กœ๊น… ์„ค์ •
        Log::Log4perl->easy_init({
            level => $args{debug} ? $DEBUG : $INFO,
            file => ">>logs/analyzer.log",
            layout => "%d{yyyy-MM-dd HH:mm:ss} [%p] %m%n"
        });
        
        my $self = {
            data_dir => $args{data_dir} || 'data/headlines',
            output_dir => $args{output_dir} || 'data/analysis',
            debug => $args{debug} || 0
        };
        
        return bless $self, $class;
    }
    
    # ์ตœ์‹  ํ—ค๋“œ๋ผ์ธ ํŒŒ์ผ ์ฐพ๊ธฐ
    sub get_latest_headlines {
        my ($self, $format) = @_;
        $format ||= 'json';
        
        my @files = bsd_glob("$self->{data_dir}/headlines_*.$format");
        @files = sort { $b cmp $a } @files; # ์ตœ์‹  ํŒŒ์ผ์ด ๋จผ์ € ์˜ค๋„๋ก ์ •๋ ฌ
        
        return $files[0] if @files;
        return undef;
    }
    
    # ํ—ค๋“œ๋ผ์ธ ๋กœ๋“œ
    sub load_headlines {
        my ($self, $file) = @_;
        
        return [] unless $file && -f $file;
        
        if ($file =~ /\.json$/) {
            open my $fh, '<:encoding(utf8)', $file or do {
                ERROR("ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!");
                return [];
            };
            
            my $json = do { local $/; <$fh> };
            close $fh;
            
            return decode_json($json);
        } elsif ($file =~ /\.csv$/) {
            my @headlines;
            
            open my $fh, '<:encoding(utf8)', $file or do {
                ERROR("ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!");
                return [];
            };
            
            my $csv = Text::CSV->new({ binary => 1, auto_diag => 1 });
            my $header = $csv->getline($fh);
            $csv->column_names($header);
            
            while (my $row = $csv->getline_hr($fh)) {
                # ์ฃผ์ œ๋ฅผ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
                $row->{topics} = [split(/,\s*/, $row->{topics})];
                push @headlines, $row;
            }
            
            close $fh;
            return \@headlines;
        } else {
            ERROR("์ง€์›ํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: $file");
            return [];
        }
    }
    
    # ํ‚ค์›Œ๋“œ ๋นˆ๋„ ๋ถ„์„
    sub analyze_keyword_frequency {
        my ($self, $headlines) = @_;
        
        my %keyword_count;
        my $stop_words = $self->get_stop_words();
        
        foreach my $headline (@$headlines) {
            my $text = lc($headline->{title} . " " . $headline->{summary});
            
            # ๋‹จ์–ด ์ถ”์ถœ (์•ŒํŒŒ๋ฒณ, ์ˆซ์ž, ํ•œ๊ธ€ ๋“ฑ ํฌํ•จ)
            my @words = $text =~ /([a-z0-9๊ฐ€-ํžฃ]+)/g;
            
            foreach my $word (@words) {
                # ๋ถˆ์šฉ์–ด ๋ฐ ์งง์€ ๋‹จ์–ด ์ œ์™ธ
                next if exists $stop_words->{$word};
                next if length($word) < 3;
                
                $keyword_count{$word}++;
            }
        }
        
        # ๋นˆ๋„์ˆœ ์ •๋ ฌ
        my @sorted_keywords = sort { $keyword_count{$b} <=> $keyword_count{$a} } keys %keyword_count;
        
        # ์ƒ์œ„ 50๊ฐœ๋งŒ ๋ฐ˜ํ™˜
        my @top_keywords = @sorted_keywords[0..min(49, $#sorted_keywords)];
        
        my %result;
        foreach my $keyword (@top_keywords) {
            $result{$keyword} = $keyword_count{$keyword};
        }
        
        return \%result;
    }
    
    # ๋ถˆ์šฉ์–ด ๋ชฉ๋ก
    sub get_stop_words {
        my ($self) = @_;
        
        my @stop_words = qw(
            the and for that with this from have are was not but what been has its
            who which when where why how all any both each more most other some such
            than then too very can just should now also
        );
        
        my %stop_words_hash = map { $_ => 1 } @stop_words;
        return \%stop_words_hash;
    }
    
    # ์ฃผ์ œ๋ณ„ ํ—ค๋“œ๋ผ์ธ ๋ถ„๋ฅ˜
    sub group_by_topic {
        my ($self, $headlines) = @_;
        
        my %topics;
        
        foreach my $headline (@$headlines) {
            foreach my $topic (@{$headline->{topics}}) {
                push @{$topics{$topic}}, $headline;
            }
        }
        
        return \%topics;
    }
    
    # ์ถœ์ฒ˜๋ณ„ ํ—ค๋“œ๋ผ์ธ ์ˆ˜ ๊ณ„์‚ฐ
    sub count_by_source {
        my ($self, $headlines) = @_;
        
        my %sources;
        
        foreach my $headline (@$headlines) {
            $sources{$headline->{source}}++;
        }
        
        return \%sources;
    }
    
    # ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ
    sub save_analysis {
        my ($self, $analysis) = @_;
        
        my $timestamp = time();
        my $filename = "$self->{output_dir}/analysis_$timestamp.json";
        
        open my $fh, '>:encoding(utf8)', $filename or do {
            ERROR("ํŒŒ์ผ์„ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $!");
            return;
        };
        
        print $fh encode_json($analysis);
        close $fh;
        
        INFO("๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ ์™„๋ฃŒ: $filename");
        return $filename;
    }
    
    # ์ „์ฒด ๋ถ„์„ ์‹คํ–‰
    sub run {
        my ($self) = @_;
        
        # ์ตœ์‹  ํ—ค๋“œ๋ผ์ธ ํŒŒ์ผ ์ฐพ๊ธฐ
        my $latest_file = $self->get_latest_headlines('json');
        unless ($latest_file) {
            ERROR("๋ถ„์„ํ•  ํ—ค๋“œ๋ผ์ธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
            return;
        }
        
        INFO("ํ—ค๋“œ๋ผ์ธ ํŒŒ์ผ ๋ถ„์„ ์ค‘: $latest_file");
        
        # ํ—ค๋“œ๋ผ์ธ ๋กœ๋“œ
        my $headlines = $self->load_headlines($latest_file);
        INFO("์ด " . scalar(@$headlines) . "๊ฐœ ํ—ค๋“œ๋ผ์ธ ๋กœ๋“œ ์™„๋ฃŒ");
        
        # ํ‚ค์›Œ๋“œ ๋นˆ๋„ ๋ถ„์„
        my $keyword_frequency = $self->analyze_keyword_frequency($headlines);
        INFO("ํ‚ค์›Œ๋“œ ๋นˆ๋„ ๋ถ„์„ ์™„๋ฃŒ: " . scalar(keys %$keyword_frequency) . "๊ฐœ ํ‚ค์›Œ๋“œ");
        
        # ์ฃผ์ œ๋ณ„ ๋ถ„๋ฅ˜
        my $topics = $self->group_by_topic($headlines);
        INFO("์ฃผ์ œ๋ณ„ ๋ถ„๋ฅ˜ ์™„๋ฃŒ: " . scalar(keys %$topics) . "๊ฐœ ์ฃผ์ œ");
        
        # ์ถœ์ฒ˜๋ณ„ ์นด์šดํŠธ
        my $sources = $self->count_by_source($headlines);
        INFO("์ถœ์ฒ˜๋ณ„ ์นด์šดํŠธ ์™„๋ฃŒ: " . scalar(keys %$sources) . "๊ฐœ ์ถœ์ฒ˜");
        
        # ๋ถ„์„ ๊ฒฐ๊ณผ ์ข…ํ•ฉ
        my $analysis = {
            timestamp => scalar(localtime),
            headline_count => scalar(@$headlines),
            keyword_frequency => $keyword_frequency,
            topic_counts => { map { $_ => scalar(@{$topics->{$_}}) } keys %$topics },
            source_counts => $sources,
            top_headlines => [ @$headlines[0..min(9, $#$headlines)] ]
        };
        
        # ๊ฒฐ๊ณผ ์ €์žฅ
        $self->save_analysis($analysis);
        
        return $analysis;
    }
    
    sub min {
        my ($a, $b) = @_;
        return $a < $b ? $a : $b;
    }
    
    1;

    4. ๋ฉ”์ธ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ (run_scraper.pl)

    run_scraper.pl

    #!/usr/bin/perl
    use strict;
    use warnings;
    use FindBin;
    use lib "$FindBin::Bin/lib";
    use NewsScraper;
    use NewsAnalyzer;
    use Getopt::Long;
    use Time::HiRes qw(time);
    
    # ๋ช…๋ น์ค„ ์˜ต์…˜
    my $debug = 0;
    my $config_file = 'config/sites.json';
    my $analyze = 1;
    
    GetOptions(
        "debug" => \$debug,
        "config=s" => \$config_file,
        "analyze!" => \$analyze
    ) or die("์˜ต์…˜ ์˜ค๋ฅ˜\n");
    
    print "๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ์‹œ์ž‘...\n";
    my $start_time = time();
    
    # ์Šคํฌ๋ž˜ํผ ์ƒ์„ฑ ๋ฐ ์‹คํ–‰
    my $scraper = NewsScraper->new(
        debug => $debug,
        config_file => $config_file
    );
    
    my $headlines = $scraper->run();
    printf("์ด %d๊ฐœ ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘ ์™„๋ฃŒ\n", scalar(@$headlines));
    
    # ๋ถ„์„ ์‹คํ–‰ (์˜ต์…˜์ด ์ผœ์ ธ ์žˆ๋Š” ๊ฒฝ์šฐ)
    if ($analyze) {
        print "์ˆ˜์ง‘๋œ ํ—ค๋“œ๋ผ์ธ ๋ถ„์„ ์ค‘...\n";
        
        my $analyzer = NewsAnalyzer->new(
            debug => $debug
        );
        
        my $analysis = $analyzer->run();
        
        # ๊ฐ„๋‹จํ•œ ๋ถ„์„ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
        if ($analysis) {
            print "\n=== ๋ถ„์„ ๊ฒฐ๊ณผ ์š”์•ฝ ===\n";
            print "์ด ํ—ค๋“œ๋ผ์ธ ์ˆ˜: $analysis->{headline_count}\n";
            
            print "\n์ƒ์œ„ 10๊ฐœ ํ‚ค์›Œ๋“œ:\n";
            my $i = 0;
            for my $keyword (sort { $analysis->{keyword_frequency}{$b} <=> $analysis->{keyword_frequency}{$a} } keys %{$analysis->{keyword_frequency}}) {
                last if $i++ >= 10;
                printf("  %s: %d\n", $keyword, $analysis->{keyword_frequency}{$keyword});
            }
            
            print "\n์ฃผ์ œ๋ณ„ ํ—ค๋“œ๋ผ์ธ ์ˆ˜:\n";
            for my $topic (sort { $analysis->{topic_counts}{$b} <=> $analysis->{topic_counts}{$a} } keys %{$analysis->{topic_counts}}) {
                printf("  %s: %d\n", $topic, $analysis->{topic_counts}{$topic});
            }
            
            print "\n์ถœ์ฒ˜๋ณ„ ํ—ค๋“œ๋ผ์ธ ์ˆ˜:\n";
            for my $source (sort { $analysis->{source_counts}{$b} <=> $analysis->{source_counts}{$a} } keys %{$analysis->{source_counts}}) {
                printf("  %s: %d\n", $source, $analysis->{source_counts}{$source});
            }
        }
    }
    
    my $end_time = time();
    printf("\n์ž‘์—… ์™„๋ฃŒ! ์†Œ์š” ์‹œ๊ฐ„: %.2f์ดˆ\n", $end_time - $start_time);

    5. ์ž๋™ํ™” ์„ค์ • (crontab)

    crontab ์„ค์ •

    # ๋งค์ผ ์˜ค์ „ 6์‹œ์— ๋‰ด์Šค ์Šคํฌ๋ž˜ํผ ์‹คํ–‰
    0 6 * * * cd /path/to/news_scraper && /usr/bin/perl run_scraper.pl >> logs/cron.log 2>&1
    
    # ๋งค์ผ ์˜คํ›„ 6์‹œ์—๋„ ์‹คํ–‰
    0 18 * * * cd /path/to/news_scraper && /usr/bin/perl run_scraper.pl >> logs/cron.log 2>&1
    ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ์ž‘๋™ ํ๋ฆ„ ์„ค์ • ๋กœ๋“œ ๋‰ด์Šค ์Šคํฌ๋ž˜ํ•‘ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋‰ด์Šค ์‚ฌ์ดํŠธ - Tech News - Business Daily - Science Portal ์ €์žฅ ํ˜•์‹ - JSON - CSV - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ„์„ ๊ฒฐ๊ณผ - ํ‚ค์›Œ๋“œ ๋นˆ๋„ - ์ฃผ์ œ๋ณ„ ๋ถ„๋ฅ˜ - ํŠธ๋ Œ๋“œ ์‹œ๊ฐํ™” ์„ค์ • ํŒŒ์ผ - ์‚ฌ์ดํŠธ ๋ชฉ๋ก - ์„ ํƒ์ž ์ •์˜ - ์ฃผ์ œ ํ‚ค์›Œ๋“œ

    ํ”„๋กœ์ ํŠธ ํ™œ์šฉ ๋ฐฉ์•ˆ

    1. ํŠธ๋ Œ๋“œ ๋ชจ๋‹ˆํ„ฐ๋ง

    ํŠน์ • ์‚ฐ์—…์ด๋‚˜ ์ฃผ์ œ์— ๊ด€ํ•œ ๋‰ด์Šค ํŠธ๋ Œ๋“œ๋ฅผ ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด, AI, ๋ธ”๋ก์ฒด์ธ, ๊ธฐํ›„ ๋ณ€ํ™” ๋“ฑ์˜ ์ฃผ์ œ์— ๋Œ€ํ•œ ๋ฏธ๋””์–ด ๊ด€์‹ฌ๋„ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์ง€.

    2. ๊ฒฝ์Ÿ์‚ฌ ๋ชจ๋‹ˆํ„ฐ๋ง

    ํŠน์ • ๊ธฐ์—…์ด๋‚˜ ๋ธŒ๋žœ๋“œ์— ๊ด€ํ•œ ๋‰ด์Šค๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ๊ฒฝ์Ÿ์‚ฌ์˜ ๋™ํ–ฅ์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด. ์ƒˆ๋กœ์šด ์ œํ’ˆ ์ถœ์‹œ, ์ธ์ˆ˜ํ•ฉ๋ณ‘, ๊ฒฝ์˜์ง„ ๋ณ€๊ฒฝ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๋น ๋ฅด๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์ง€.

    3. ๋งž์ถคํ˜• ๋‰ด์Šค ๋‹ค์ด์ œ์ŠคํŠธ

    ๊ด€์‹ฌ ์žˆ๋Š” ์ฃผ์ œ์— ๋Œ€ํ•œ ๋‰ด์Šค๋งŒ ๋ชจ์•„์„œ ๊ฐœ์ธํ™”๋œ ๋‰ด์Šค ๋‹ค์ด์ œ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. ์ด๋ฉ”์ผ๋กœ ์ž๋™ ๋ฐœ์†กํ•˜๊ฑฐ๋‚˜ ์›น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์ง€.

    4. ๊ฐ์„ฑ ๋ถ„์„ ๋ฐ ์—ฌ๋ก  ๋ชจ๋‹ˆํ„ฐ๋ง

    ์ˆ˜์ง‘๋œ ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ์— ๊ฐ์„ฑ ๋ถ„์„์„ ์ ์šฉํ•˜์—ฌ ํŠน์ • ์ฃผ์ œ์— ๋Œ€ํ•œ ๋ฏธ๋””์–ด์˜ ํƒœ๋„๋‚˜ ์—ฌ๋ก  ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์–ด. ์ด๋Š” ๋ธŒ๋žœ๋“œ ํ‰ํŒ ๊ด€๋ฆฌ๋‚˜ ์œ„๊ธฐ ๋Œ€์‘์— ์œ ์šฉํ•ด.

    ํ”„๋กœ์ ํŠธ ํ™•์žฅ ์•„์ด๋””์–ด

    1. ์›น ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€: ์ˆ˜์ง‘๋œ ๋‰ด์Šค์™€ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์›น ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด. Perl์˜ Mojolicious๋‚˜ Dancer2 ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€.
    2. ์•Œ๋ฆผ ์‹œ์Šคํ…œ: ํŠน์ • ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๋‰ด์Šค๊ฐ€ ๋ฐœ๊ฒฌ๋˜๋ฉด ์ด๋ฉ”์ผ์ด๋‚˜ ๋ฉ”์‹ ์ €๋กœ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.
    3. ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ ๊ฐ•ํ™”: Lingua::EN::Tagger๋‚˜ ์™ธ๋ถ€ API๋ฅผ ํ™œ์šฉํ•ด ๋” ์ •๊ตํ•œ ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ์™€ ์ฃผ์ œ ๋ถ„๋ฅ˜๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.
    4. ์‹œ๊ณ„์—ด ๋ถ„์„: ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ํ‚ค์›Œ๋“œ ํŠธ๋ Œ๋“œ ๋ณ€ํ™”๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.
    5. ๋จธ์‹ ๋Ÿฌ๋‹ ํ†ตํ•ฉ: ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋‰ด์Šค ๋ถ„๋ฅ˜๋‚˜ ์ค‘์š”๋„ ์˜ˆ์ธก ๋“ฑ์˜ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ์„ ํ›ˆ๋ จ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด.

    ๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์š”์•ฝ

    ์ด ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ํ”„๋กœ์ ํŠธ๋Š” Perl์˜ ์›น ์Šคํฌ๋ž˜ํ•‘ ๋Šฅ๋ ฅ์„ ์‹ค์ „์—์„œ ํ™œ์šฉํ•˜๋Š” ์ข‹์€ ์˜ˆ์‹œ์•ผ. ์šฐ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ์ˆ ๊ณผ ๊ฐœ๋…์„ ์ ์šฉํ–ˆ์–ด:

    • ์—ฌ๋Ÿฌ ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
    • HTML ํŒŒ์‹ฑ ๋ฐ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
    • ์ •๊ทœํ‘œํ˜„์‹์„ ํ™œ์šฉํ•œ ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ
    • JSON ๋ฐ CSV ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ ์ €์žฅ
    • ๊ฐ„๋‹จํ•œ ํ…์ŠคํŠธ ๋ถ„์„ ๋ฐ ํ‚ค์›Œ๋“œ ์ถ”์ถœ
    • cron์„ ์ด์šฉํ•œ ์ž๋™ํ™”
    • ๋ชจ๋“ˆํ™”๋œ ์ฝ”๋“œ ๊ตฌ์กฐ
    • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊น…
    • robots.txt ์ค€์ˆ˜ ๋“ฑ ์œค๋ฆฌ์  ์Šคํฌ๋ž˜ํ•‘

    ์ด ํ”„๋กœ์ ํŠธ๋Š” ํ™•์žฅ์„ฑ์ด ๋†’์•„์„œ ๋‹ค์–‘ํ•œ ์šฉ๋„๋กœ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ๋” ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์‹ค์ „ ์‘์šฉ ์‚ฌ๋ก€๋กœ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ง์ ‘ ์ ์šฉํ•ด ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ์ถœ๋ฐœ์ ์ด ๋  ๊ฑฐ์•ผ!

    ์ด ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด Perl ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๋ชจ๋“  ํ•ต์‹ฌ ๊ฐœ๋…์„ ์‹ค์ „์—์„œ ํ™œ์šฉํ•ด ๋ดค์–ด. ์ด์ œ ๋„ˆ๋งŒ์˜ ์›น ์Šคํฌ๋ž˜ํ•‘ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•  ์ค€๋น„๊ฐ€ ๋์–ด! ๐Ÿš€

    ๋งˆ์น˜๋ฉฐ: Perl ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์—ฌ์ •์„ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ๐Ÿ

    ์šฐ๋ฆฌ๋Š” Perl์„ ์‚ฌ์šฉํ•œ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ๊ธฐ์ˆ ๊นŒ์ง€ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ์‚ดํŽด๋ดค์–ด. ๋‹จ์ˆœํ•œ ์›น ํŽ˜์ด์ง€ ์š”์ฒญ๋ถ€ํ„ฐ ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ, ์ž๋™ํ™”, ๋ถ„์„๊นŒ์ง€ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์ „์ฒด ๊ณผ์ •์„ ๋‹ค๋ค˜์ง€.

    ๋ฐฐ์šด ๋‚ด์šฉ ์š”์•ฝ

    1. ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ด: Perl๊ณผ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ๊ด€๊ณ„, ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์›๋ฆฌ
    2. ํ™˜๊ฒฝ ์„ค์ •: Perl ์„ค์น˜ ๋ฐ ํ•„์ˆ˜ ๋ชจ๋“ˆ ์„ค์ •
    3. ํ•ต์‹ฌ ๋ชจ๋“ˆ ํ™œ์šฉ: LWP, HTML::Parser, Mojo::DOM ๋“ฑ ์ฃผ์š” ๋ชจ๋“ˆ ์‚ฌ์šฉ๋ฒ•
    4. ์ •๊ทœํ‘œํ˜„์‹ ๋งˆ์Šคํ„ฐ: ๊ฐ•๋ ฅํ•œ ํŒจํ„ด ๋งค์นญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
    5. ๋ณต์žกํ•œ ์›น์‚ฌ์ดํŠธ ์ฒ˜๋ฆฌ: ๋™์  ์ฝ˜ํ…์ธ , ์ธ์ฆ, ๋ด‡ ์ฐจ๋‹จ ์šฐํšŒ ๋“ฑ
    6. ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ฐ€๊ณต: ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ์ฒ˜๋ฆฌ
    7. ์ž๋™ํ™”์™€ ์Šค์ผ€์ค„๋ง: ์ฃผ๊ธฐ์  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™”
    8. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ๋””๋ฒ„๊น…: ์•ˆ์ •์ ์ธ ์Šคํฌ๋ž˜ํ•‘ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
    9. ๋ฒ•์ , ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ: ์ฑ…์ž„๊ฐ ์žˆ๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘
    10. ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‰ด์Šค ํ—ค๋“œ๋ผ์ธ ์ˆ˜์ง‘๊ธฐ ๊ตฌํ˜„

    ๋‹ค์Œ ๋‹จ๊ณ„

    ์›น ์Šคํฌ๋ž˜ํ•‘ ์—ฌ์ •์„ ๊ณ„์†ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฐœ์ „ํ•  ์ˆ˜ ์žˆ์–ด:

    • ๋” ๋งŽ์€ ์›น์‚ฌ์ดํŠธ ํƒ์ƒ‰: ๋‹ค์–‘ํ•œ ๊ตฌ์กฐ์˜ ์›น์‚ฌ์ดํŠธ์—์„œ ์Šคํฌ๋ž˜ํ•‘ ์—ฐ์Šตํ•˜๊ธฐ
    • ๋ฐ์ดํ„ฐ ๋ถ„์„ ์‹ฌํ™”: ์ˆ˜์ง‘ํ•œ ๋ฐ์ดํ„ฐ์— ๊ณ ๊ธ‰ ๋ถ„์„ ๊ธฐ๋ฒ• ์ ์šฉํ•˜๊ธฐ
    • ๋จธ์‹ ๋Ÿฌ๋‹ ํ†ตํ•ฉ: ์Šคํฌ๋ž˜ํ•‘ํ•œ ๋ฐ์ดํ„ฐ๋กœ ์˜ˆ์ธก ๋ชจ๋ธ ํ›ˆ๋ จํ•˜๊ธฐ
    • ๋ถ„์‚ฐ ์Šคํฌ๋ž˜ํ•‘: ์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ ๋Œ€๊ทœ๋ชจ ์Šคํฌ๋ž˜ํ•‘ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ
    • API ๊ฐœ๋ฐœ: ์Šคํฌ๋ž˜ํ•‘ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ž์ฒด API ๋งŒ๋“ค๊ธฐ

    ์›น ์Šคํฌ๋ž˜ํ•‘์€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์˜ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ง€๋งŒ, ํ•ญ์ƒ ์ฑ…์ž„๊ฐ ์žˆ๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ๊ธฐ์–ตํ•ด. ์›น์‚ฌ์ดํŠธ์˜ ์ด์šฉ์•ฝ๊ด€์„ ์กด์ค‘ํ•˜๊ณ , ์„œ๋ฒ„์— ๊ณผ๋„ํ•œ ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š์œผ๋ฉฐ, ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์Šคํฌ๋ž˜ํ•‘ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด.

    ์ด ๊ฐ€์ด๋“œ๊ฐ€ Perl์„ ์‚ฌ์šฉํ•œ ์›น ์Šคํฌ๋ž˜ํ•‘์˜ ์„ธ๊ณ„๋กœ ๋“ค์–ด๊ฐ€๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ. ์ด์ œ ๋„ˆ๋งŒ์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•  ์ค€๋น„๊ฐ€ ๋์–ด! ๐Ÿš€

    ์งˆ๋ฌธ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๊ณต์œ ํ•ด ์ฃผ์„ธ์š”. ํ–‰์šด์„ ๋น•๋‹ˆ๋‹ค! ๐Ÿ˜Š

    ์ถ”๊ฐ€ ์ž๋ฃŒ ๋ฐ ์ฐธ๊ณ  ๋งํฌ ๐Ÿ“š

    Perl ์›น ์Šคํฌ๋ž˜ํ•‘ ๊ด€๋ จ ๋„์„œ

    • "Perl & LWP" by Sean M. Burke
    • "Web Scraping with Perl" by Michael Schrenk
    • "Mastering Perl" by brian d foy

    ์›น ์Šคํฌ๋ž˜ํ•‘ ๊ด€๋ จ ์ปค๋ฎค๋‹ˆํ‹ฐ

    ๋ฒ•์ , ์œค๋ฆฌ์  ๊ฐ€์ด๋“œ๋ผ์ธ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.