Ver2.0 ๐ Perl๋ก ๋ฐฐ์ฐ๋ ์น ์คํฌ๋ํ ์์ ์ ๋ณต: ์ด๋ณด์๋ ์ฝ๊ฒ ๋ฐ๋ผํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ชจ๋ ๊ฒ ๐

๐ Perl๋ก ๋ฐฐ์ฐ๋ ์น ์คํฌ๋ํ ์์ ์ ๋ณต: ์ด๋ณด์๋ ์ฝ๊ฒ ๋ฐ๋ผํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ชจ๋ ๊ฒ ๐
์๋ ? ์ค๋์ ํ๋ก๊ทธ๋๋ฐ ์ธ๊ณ์ ์จ์ ๋ณด์ ๊ฐ์ ์ธ์ด์ธ Perl์ ์ด์ฉํ ์น ์คํฌ๋ํ ๊ธฐ๋ฒ์ ๋ํด ํจ๊ป ์์๋ณผ ๊ฑฐ์ผ. ์น์์ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ๋ถ์ํ๋ ๊ธฐ์ ์ ์์ฆ ์ ๋ง ํซํ ์คํฌ์ด์ง! ๋ฐ์ดํฐ ๋ถ์๊ฐ๋ถํฐ ๋ง์ผํฐ, ์ฐ๊ตฌ์๊น์ง ๋ค์ํ ๋ถ์ผ์์ ํ์ฉ๋๊ณ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์น ์คํฌ๋ํ ๊ด๋ จ ์ฌ๋ฅ์ด ์ธ๊ธฐ ์๋ค๋ ์ฌ์ค, ์๊ณ ์์์ด? ๐
์ด ๊ธ์์๋ Perl์ด๋ผ๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ ์น ์คํฌ๋ํ์ A๋ถํฐ Z๊น์ง ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ค๋ช ํด ์ค๊ฒ. ์ฝ๋ ์์ ์ ์ค์ ํ๊น์ง ๋ชจ๋ ๋ด์์ผ๋ ๋๊น์ง ๋ฐ๋ผ์ ๋ณด์! ๐
๐ ๋ชฉ์ฐจ
- Perl๊ณผ ์น ์คํฌ๋ํ์ ๋ง๋จ: ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ
- Perl ์ค์น๋ถํฐ ์ฒซ ์คํฌ๋ํ ์ฝ๋๊น์ง
- ํ์ Perl ๋ชจ๋ ๋ง์คํฐํ๊ธฐ: LWP, HTML::Parser, Mojo::DOM
- ์ ๊ทํํ์์ผ๋ก ๋ฐ์ดํฐ ์ถ์ถํ๊ธฐ
- ๋ณต์กํ ์น์ฌ์ดํธ ์คํฌ๋ํ ์ ๋ต
- ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ๊ฐ๊ณตํ๊ธฐ
- ์๋ํ์ ์ค์ผ์ค๋ง: ์ฃผ๊ธฐ์ ๋ฐ์ดํฐ ์์ง
- ์๋ฌ ์ฒ๋ฆฌ์ ๋๋ฒ๊น ๊ธฐ๋ฒ
- ์น ์คํฌ๋ํ์ ๋ฒ์ , ์ค๋ฆฌ์ ๊ณ ๋ ค์ฌํญ
- ์ค์ ํ๋ก์ ํธ: ๋ด์ค ํค๋๋ผ์ธ ์์ง๊ธฐ ๋ง๋ค๊ธฐ
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๋ก ์น ์คํฌ๋ํ์ ์์ํ๊ธฐ ์ํ ๊ธฐ๋ณธ ๊ฐ๋ ์ ์ดํดํ์ผ๋, ๋ค์ ์น์ ์์๋ ์ค์ ํ๊ฒฝ ์ค์ ๋ถํฐ ์์ํด ๋ณผ๊ฒ!
2. Perl ์ค์น๋ถํฐ ์ฒซ ์คํฌ๋ํ ์ฝ๋๊น์ง ๐ ๏ธ
Perl ์ค์นํ๊ธฐ
๋จผ์ Perl์ ์ค์นํด์ผ ์น ์คํฌ๋ํ์ ์์ํ ์ ์์ด. ์ด์์ฒด์ ๋ณ ์ค์น ๋ฐฉ๋ฒ์ ์์๋ณด์!
๐ช Windows์์ ์ค์นํ๊ธฐ
Windows์์๋ Strawberry Perl์ด๋ ActivePerl์ ์ค์นํ๋ฉด ๋ผ.
- Strawberry Perl ๋ค์ด๋ก๋์์ ์ค์น ํ์ผ์ ๋ฐ์ ์คํ
- ์ค์น ๋ง๋ฒ์ฌ์ ์๋ด์ ๋ฐ๋ผ ์งํ
- ์ค์น ์๋ฃ ํ ๋ช
๋ น ํ๋กฌํํธ์์
perl -v๋ช ๋ น์ด๋ก ์ค์น ํ์ธ
๐ macOS์์ ์ค์นํ๊ธฐ
macOS์๋ ๊ธฐ๋ณธ์ ์ผ๋ก Perl์ด ์ค์น๋์ด ์์ง๋ง, ์ต์ ๋ฒ์ ์ ์ฌ์ฉํ๋ ค๋ฉด:
- Homebrew๋ฅผ ์ด์ฉ:
brew install perl - ๋๋ Perl ๊ณต์ ์ฌ์ดํธ์์ ๋ค์ด๋ก๋
- ํฐ๋ฏธ๋์์
perl -v๋ก ์ค์น ํ์ธ
๐ง Linux์์ ์ค์นํ๊ธฐ
๋๋ถ๋ถ์ Linux ๋ฐฐํฌํ์๋ Perl์ด ๊ธฐ๋ณธ์ผ๋ก ์ค์น๋์ด ์์ด. ์๋ค๋ฉด:
- Ubuntu/Debian:
sudo apt-get install perl - CentOS/RHEL:
sudo yum install perl - ํฐ๋ฏธ๋์์
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";
}
๐ ์ฝ๋ ์ค๋ช
LWP::UserAgent: ์น ๋ธ๋ผ์ฐ์ ์ฒ๋ผ ๋์ํ์ฌ ์น ํ์ด์ง๋ฅผ ์์ฒญํ๊ณ ์๋ต์ ๋ฐ๋ ๋ชจ๋HTML::TreeBuilder: HTML์ ํ์ฑํ์ฌ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ๋ง๋ค์ด์ฃผ๋ ๋ชจ๋- User-Agent๋ฅผ ์ค์ ํ์ฌ ๋ด์ด ์๋ ์ผ๋ฐ ๋ธ๋ผ์ฐ์ ์ฒ๋ผ ๋ณด์ด๊ฒ ํจ
- ์น ํ์ด์ง๋ฅผ ์์ฒญํ๊ณ ์๋ต์ ๋ฐ์
- HTML์ ํ์ฑํ์ฌ ์ ๋ชฉ ํ๊ทธ๋ฅผ ์ฐพ์ ๋ด์ฉ์ ์ถ๋ ฅ
์ด ์ฝ๋๋ฅผ ์คํํ๋ฉด example.com ์น์ฌ์ดํธ์ ์ ๋ชฉ์ด ์ถ๋ ฅ๋ ๊ฑฐ์ผ. ๊ฐ๋จํ์ง? ์ด๊ฒ ์น ์คํฌ๋ํ์ ๊ธฐ๋ณธ ์๋ฆฌ์ผ. ์น ํ์ด์ง๋ฅผ ๊ฐ์ ธ์์ โ ํ์ฑํ๊ณ โ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ๋ ๊ณผ์ ์ด์ง.
๐ก ์ด๋ณด์ ํ
์ฝ๋๋ฅผ ์ ์ฅํ ๋๋ .pl ํ์ฅ์๋ฅผ ์ฌ์ฉํด. ์๋ฅผ ๋ค์ด my_scraper.pl๋ก ์ ์ฅํ๊ณ , ํฐ๋ฏธ๋์์ perl my_scraper.pl ๋ช
๋ น์ผ๋ก ์คํํ ์ ์์ด.
Windows์์๋ ํ์ผ์ ๋๋ธํด๋ฆญํด๋ ์คํํ ์ ์์ง๋ง, ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ธฐ ์ ์ ์ฐฝ์ด ๋ซํ ์ ์์ผ๋ ๋ช ๋ น ํ๋กฌํํธ์์ ์คํํ๋ ๊ฒ ์ข์.
์ด์ ๊ธฐ๋ณธ์ ์ธ ์น ์คํฌ๋ํ ์ฝ๋๋ฅผ ์์ฑํด ๋ดค์ผ๋, ๋ค์ ์น์ ์์๋ ๋ ๊ฐ๋ ฅํ 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 | ์์กด์ฑ ๋ง์, ์ค์น ๋ณต์กํ ์ ์์ | ์ต์ ์น์ฌ์ดํธ, ๋ณต์กํ ์ ํ ์์ |
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";
}
์ ๊ทํํ์ ๋๋ฒ๊น ๋ฐ ์ต์ ํ
์ ๊ทํํ์์ ๊ฐ๋ ฅํ์ง๋ง ๋๋ฒ๊น ํ๊ธฐ ์ด๋ ค์ธ ์ ์์ด. 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";
}
โ ๏ธ ์ ๊ทํํ์ ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
- HTML ํ์ฑ์ ์ ๊ทํํ์๋ง ์ฌ์ฉํ๋ฉด ์์์น ๋ชปํ ๊ฒฐ๊ณผ๊ฐ ๋์ฌ ์ ์์ด. ๊ฐ๋ฅํ๋ฉด HTML ํ์ฑ ๋ชจ๋๊ณผ ํจ๊ป ์ฌ์ฉํด.
- ๋ณต์กํ ์ ๊ทํํ์์ ์ฑ๋ฅ ์ ํ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด. ํ์ํ ๊ฒฝ์ฐ
qr//๋ก ๋ฏธ๋ฆฌ ์ปดํ์ผํด์ ์ฌ์ฉํด. - ๋๋ฌด ํ์์ ์ธ(greedy) ํจํด์ ์๋ํ์ง ์์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด.
.*?์ ๊ฐ์ ๋นํ์์ (non-greedy) ํจํด์ ์ ์ ํ ์ฌ์ฉํด. - ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ์ ๊ทํํ์ ๋์ ์ ์ฉ ํ์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์.
์ ๊ทํํ์์ 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);
โ ๏ธ ์ค๋ฆฌ์ ์คํฌ๋ํ ์ฃผ์์ฌํญ
๋ด ์ฐจ๋จ ๊ธฐ์ ์ ์ฐํํ๋ ๊ฒ์ ์น์ฌ์ดํธ์ ์ด์ฉ์ฝ๊ด์ ์๋ฐํ ์ ์์ด. ํญ์ ๋ค์ ์ฌํญ์ ๊ณ ๋ คํด:
- ์น์ฌ์ดํธ์ robots.txt ํ์ผ์ ํ์ธํ๊ณ ์ค์ํ๊ธฐ
- ๊ณผ๋ํ ์์ฒญ์ผ๋ก ์๋ฒ์ ๋ถ๋ด์ ์ฃผ์ง ์๊ธฐ
- ๊ฐ๋ฅํ๋ฉด ๊ณต์ API ์ฌ์ฉํ๊ธฐ
- ๊ฐ์ธ์ ๋ณด๊ฐ ํฌํจ๋ ๋ฐ์ดํฐ๋ ์ ์ ํ ์ฒ๋ฆฌํ๊ธฐ
์ฌ๋ฅ๋ท๊ณผ ๊ฐ์ ์๋น์ค๋ฅผ ์ด์ฉํ ๋๋ ์ด์ฉ์ฝ๊ด์ ํ์ธํ๊ณ ์ค์ํ๋ ๊ฒ์ด ์ค์ํด!
ํ์ด์ง๋ค์ด์ ์ฒ๋ฆฌํ๊ธฐ
๋ง์ ์น์ฌ์ดํธ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ ํ์ด์ง๋ก ๋๋์ด ์ ๊ณตํด. ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์์งํ๋ ค๋ฉด ํ์ด์ง๋ค์ด์ ์ ์ฒ๋ฆฌํด์ผ ํด.
ํ์ด์ง๋ค์ด์ ์ฒ๋ฆฌ ์ ๋ต
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";
}
}
์ค์ ์์ : ์์ ๋ฏธ๋์ด ๋ฐ์ดํฐ ์์ง
์ด์ ์ค์ ์์ ๋ฏธ๋์ด ์ฌ์ดํธ์์ ๋ฐ์ดํฐ๋ฅผ ์์งํ๋ ์์ ๋ฅผ ์ดํด๋ณผ๊ฒ. ์ฌ๋ฌ ์ ๋ต์ ์กฐํฉํด์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด์ผ.
์์ ๋ฏธ๋์ด ๋ฐ์ดํฐ ์์ง ์์
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;
๐ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ํ ๊ฐ์ด๋
- SQLite: ๊ฐ๋จํ ํ๋ก์ ํธ, ๋ก์ปฌ ์ ์ฅ, ์ค์ ๊ฐํธ
- MySQL/MariaDB: ์ค๊ฐ ๊ท๋ชจ ํ๋ก์ ํธ, ๋ค์ค ์ฌ์ฉ์ ์ ๊ทผ
- PostgreSQL: ๋ณต์กํ ์ฟผ๋ฆฌ, ๋์ฉ๋ ๋ฐ์ดํฐ, ๊ณ ๊ธ ๊ธฐ๋ฅ ํ์ ์
- 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";
๋ฐ์ดํฐ ๋ด๋ณด๋ด๊ธฐ ๋ฐ ๊ณต์
์์งํ๊ณ ๋ถ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฅธ ์ฌ๋๋ค๊ณผ ๊ณต์ ํ๊ฑฐ๋ ๋ค๋ฅธ ์์คํ ์์ ํ์ฉํ๋ ค๋ฉด ์ ์ ํ ํ์์ผ๋ก ๋ด๋ณด๋ด๋ ๊ฒ์ด ์ค์ํด.
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. ๊ธฐ๋ณธ ์์ ๋ง๋ค๊ธฐ
- ์ค๋ฅธ์ชฝ ํจ๋์์ '๊ธฐ๋ณธ ์์ ๋ง๋ค๊ธฐ'๋ฅผ ํด๋ฆญ
- ์์ ์ด๋ฆ๊ณผ ์ค๋ช ์ ๋ ฅ (์: "์ ํ ๋ฐ์ดํฐ ์คํฌ๋ํ")
- ํธ๋ฆฌ๊ฑฐ ์ค์ (๋งค์ผ, ๋งค์ฃผ, ๋งค์ ๋ฑ)
- ์์ ์ ํ์ผ๋ก 'ํ๋ก๊ทธ๋จ ์์'์ ์ ํ
- ํ๋ก๊ทธ๋จ/์คํฌ๋ฆฝํธ์ ๋ฐฐ์น ํ์ผ ๊ฒฝ๋ก ์ ๋ ฅ
- '๋ง์นจ'์ ํด๋ฆญํ์ฌ ์์ ์์ฑ ์๋ฃ
์ฌ๋ฌ ์ฌ์ดํธ ์คํฌ๋ํ ์๋ํ
์ฌ๋ฌ ์น์ฌ์ดํธ์์ ๋ฐ์ดํฐ๋ฅผ ์์งํด์ผ ํ๋ค๋ฉด, ์ค์ ํ์ผ์ ์ฌ์ฉํด ์คํฌ๋ํ ๋์์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ข์.
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();
์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ์๋ฆผ ์ค์
์๋ํ๋ ์คํฌ๋ํ ์์คํ ์์๋ ์ค๋ฅ ์ฒ๋ฆฌ์ ์๋ฆผ ๊ธฐ๋ฅ์ด ์ค์ํด. ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ ๋ ์ฆ์ ์ ์ ์๋๋ก ์ค์ ํด ๋ณด์.
์ด๋ฉ์ผ ์๋ฆผ ๊ธฐ๋ฅ ์ถ๊ฐ
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;
}
# ๋๋จธ์ง ์คํฌ๋ํ ์ฝ๋...
}
๐ก ์๋ํ ๋ชจ๋ฒ ์ฌ๋ก
- ์ ์ง์ ๋น๋ ์ฆ๊ฐ: ์ฒ์์๋ ๋ฎ์ ๋น๋๋ก ์์ํด์ ์์ ์ฑ์ ํ์ธํ ํ ๋น๋๋ฅผ ๋์ฌ๊ฐ์ธ์.
- ์ค๋ฅ ์ฌ์๋ ๋ก์ง: ์ผ์์ ์ธ ๋คํธ์ํฌ ๋ฌธ์ ์ ๋๋นํด ์ฌ์๋ ๋ก์ง์ ๊ตฌํํ์ธ์.
- ๋ก๊ทธ ์ํ: ๋ก๊ทธ ํ์ผ์ด ๋๋ฌด ์ปค์ง์ง ์๋๋ก ๋ก๊ทธ ์ํ(rotation) ์ค์ ์ ์ถ๊ฐํ์ธ์.
- ๋ฆฌ์์ค ๋ชจ๋ํฐ๋ง: ๋์คํฌ ๊ณต๊ฐ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ฑ์ ๋ชจ๋ํฐ๋งํ์ธ์.
- ๋ฐฑ์ ์ ๋ต: ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ๋ฐฑ์ ํ์ธ์.
์๋ํ๋ ์น ์คํฌ๋ํ์ ์ง์ ํ ๊ฐ์น๋ฅผ ์คํํ๋ ๋จ๊ณ์ผ. ํ ๋ฒ ์ค์ ํด ๋์ผ๋ฉด ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ๋ถ์ํ ์ ์์ด. ๋ค์ ์น์ ์์๋ ์คํฌ๋ํ ์ค ๋ฐ์ํ ์ ์๋ ๋ค์ํ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํ๊ณ ๋๋ฒ๊น ํ๋ ๋ฐฉ๋ฒ์ ๋ ์์ธํ ์์๋ณผ๊ฒ! ๐ ๏ธ
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("์น์ฌ์ดํธ ๊ตฌ์กฐ ํ์ธ ์๋ฃ. ์คํฌ๋ํ ๊ณ์ ์งํํฉ๋๋ค.");
# ์ ์ ์คํฌ๋ํ ์งํ
}
์ฑ๋ฅ ์ต์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ์คํฌ๋ํํ ๋๋ ์ฑ๋ฅ ์ต์ ํ๋ ์ค์ํด.
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;
}
}
๐ ๋๋ฒ๊น ๋ฐ ์ค๋ฅ ์ฒ๋ฆฌ ์ฒดํฌ๋ฆฌ์คํธ
- ์์ธ ์ฒ๋ฆฌ: ๋ชจ๋ ์ธ๋ถ ํธ์ถ(HTTP ์์ฒญ, ํ์ผ ์์ ๋ฑ)์ ์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ
- ๋ก๊น : ์์ธํ ๋ก๊ทธ ๊ธฐ๋ก์ผ๋ก ๋ฌธ์ ์ถ์ ๊ฐ๋ฅํ๊ฒ ์ค์
- ์ฌ์๋ ๋ก์ง: ์ผ์์ ์ธ ์ค๋ฅ์ ๋ํ ์ฌ์๋ ๋ฉ์ปค๋์ฆ ๊ตฌํ
- HTML ์ ์ฅ: ๋๋ฒ๊น ์ ์ํด ์๋ณธ HTML ์ ์ฅ ๊ธฐ๋ฅ ์ถ๊ฐ
- ๊ตฌ์กฐ ๋ณ๊ฒฝ ๊ฐ์ง: ์น์ฌ์ดํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ์ ๊ฐ์งํ๋ ๋ก์ง ๊ตฌํ
- ์๋ฆผ ์์คํ : ์ค์ ์ค๋ฅ ๋ฐ์ ์ ์๋ฆผ ๊ธฐ๋ฅ ์ค์
- ์ฑ๋ฅ ์ต์ ํ: ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋๊ณผ ์๋ ์ต์ ํ
๊ฒฌ๊ณ ํ ์ค๋ฅ ์ฒ๋ฆฌ์ ๋๋ฒ๊น ์์คํ ์ ์ฅ๊ธฐ์ ์ผ๋ก ์์ ์ ์ธ ์น ์คํฌ๋ํ ์๋ฃจ์ ์ ๊ตฌ์ถํ๋ ๋ฐ ํ์์ ์ด์ผ. ๋ค์ ์น์ ์์๋ ์น ์คํฌ๋ํ์ ๋ฒ์ , ์ค๋ฆฌ์ ์ธก๋ฉด์ ์ดํด๋ณผ๊ฒ! ๐งโโ๏ธ
9. ์น ์คํฌ๋ํ์ ๋ฒ์ , ์ค๋ฆฌ์ ๊ณ ๋ ค์ฌํญ โ๏ธ
์น ์คํฌ๋ํ์ ๊ฐ๋ ฅํ ๋๊ตฌ์ง๋ง, ๋ฒ์ , ์ค๋ฆฌ์ ๊ฒฝ๊ณ ๋ด์์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ค์ํด. ์ด๋ฒ ์น์ ์์๋ ์น ์คํฌ๋ํ์ ํ ๋ ๊ณ ๋ คํด์ผ ํ ๋ฒ์ , ์ค๋ฆฌ์ ๋ฌธ์ ๋ค์ ์์๋ณผ๊ฒ.
์น ์คํฌ๋ํ์ ๋ฒ์ ์ธก๋ฉด
1. ์ด์ฉ์ฝ๊ด(Terms of Service)
๋๋ถ๋ถ์ ์น์ฌ์ดํธ๋ ์ด์ฉ์ฝ๊ด์ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ํ ๊ท์ ์ ํฌํจํ๊ณ ์์ด. ๋ง์ ์ฌ์ดํธ๊ฐ ๋ช ์์ ์ผ๋ก ์คํฌ๋ํ์ ๊ธ์งํ๊ฑฐ๋ ์ ํํ๊ณ ์์ง.
๊ถ์ฅ์ฌํญ: ์คํฌ๋ํํ๋ ค๋ ์น์ฌ์ดํธ์ ์ด์ฉ์ฝ๊ด์ ๋ฐ๋์ ํ์ธํ๊ณ , ํ์ฉ๋๋ ๋ฒ์ ๋ด์์๋ง ๋ฐ์ดํฐ๋ฅผ ์์งํด.
2. ์ ์๊ถ๋ฒ
์น์ฌ์ดํธ์ ์ฝํ ์ธ ๋ ์ ์๊ถ๋ฒ์ ๋ณดํธ๋ฅผ ๋ฐ์ ์ ์์ด. ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ์ฌ์ฌ์ฉํ๋ ๊ฒ์ ์ ์๊ถ ์นจํด์ ํด๋นํ ์ ์์ง.
๊ถ์ฅ์ฌํญ: ๊ณต๊ฐ ๋ฐ์ดํฐ๋ ์ฌ์ค ์ ๋ณด๋ง ์์งํ๊ณ , ์ฐฝ์๋ฌผ(๊ธฐ์ฌ, ๋ฆฌ๋ทฐ ๋ฑ)์ ํ๊ฐ ์์ด ๋ณต์ ํ์ง ์๋ ๊ฒ์ด ์ข์. ํ์ํ๋ค๋ฉด ์ ์๊ถ์์ ํ๊ฐ๋ฅผ ๊ตฌํด.
3. ์ปดํจํฐ ์ฌ๊ธฐ ๋ฐ ๋จ์ฉ ๋ฐฉ์ง๋ฒ(CFAA)
๋ฏธ๊ตญ์ CFAA์ ์ ์ฌํ ๋ฒ๋ฅ ๋ค์ ๊ถํ ์์ด ์ปดํจํฐ ์์คํ ์ ์ ๊ทผํ๋ ๊ฒ์ ๊ธ์งํ๊ณ ์์ด. ์น์ฌ์ดํธ๊ฐ ๋ช ์์ ์ผ๋ก ์คํฌ๋ํ์ ๊ธ์งํ๋๋ฐ๋ ๊ณ์ํ๋ค๋ฉด ์ด ๋ฒ๋ฅ ์๋ฐ์ ํด๋นํ ์ ์์ง.
๊ถ์ฅ์ฌํญ: ์น์ฌ์ดํธ๊ฐ ์คํฌ๋ํ์ ๋ช ์์ ์ผ๋ก ๊ธ์งํ๋ ๊ฒฝ์ฐ, ํด๋น ์ฌ์ดํธ์ ๋ฐ์ดํฐ ์์ง์ ํผํ๋ ๊ฒ์ด ์์ ํด.
4. ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฒ
๊ฐ์ธ ์๋ณ ์ ๋ณด(PII)๋ฅผ ์์งํ๋ ๊ฒ์ GDPR(์ ๋ฝ), CCPA(์บ๋ฆฌํฌ๋์) ๋ฑ ๊ฐ์ธ์ ๋ณด ๋ณดํธ๋ฒ์ ์ ์ฉ์ ๋ฐ์ ์ ์์ด.
๊ถ์ฅ์ฌํญ: ๊ฐ์ธ ์ ๋ณด๋ ์์งํ์ง ์๋ ๊ฒ์ด ๊ฐ์ฅ ์์ ํด. ๊ผญ ํ์ํ๋ค๋ฉด ๊ด๋ จ ๋ฒ๋ฅ ์ ์ค์ํ๊ณ , ๋ฐ์ดํฐ ์ฃผ์ฒด์ ๋์๋ฅผ ์ป์ด์ผ ํด.
์น ์คํฌ๋ํ์ ์ค๋ฆฌ์ ์ธก๋ฉด
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. ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์
์์งํ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ ๊ฒ์ธ์ง๋ ์ค๋ฆฌ์ ๊ณ ๋ ค์ฌํญ์ด์ผ.
๊ถ์ฅ์ฌํญ: ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๊ฒ ์ฌ์ฉํ๊ณ , ์ถ์ฒ๋ฅผ ๋ช ์ํ๋ฉฐ, ์๋ณธ ์ฌ์ดํธ์ ๋น์ฆ๋์ค ๋ชจ๋ธ์ ํด์น์ง ์๋๋ก ํด.
์์ ํ ์น ์คํฌ๋ํ์ ์ํ ๊ฐ์ด๋๋ผ์ธ
- API ์ฐ์ : ๊ฐ๋ฅํ๋ฉด ๊ณต์ API๋ฅผ ์ฌ์ฉํด. API๋ ์น์ฌ์ดํธ ์์ ์๊ฐ ๋ช ์์ ์ผ๋ก ๋ฐ์ดํฐ ์ ๊ทผ์ ํ์ฉํ ๋ฐฉ๋ฒ์ด์ผ.
- ์ด์ฉ์ฝ๊ด ํ์ธ: ์คํฌ๋ํํ๋ ค๋ ์น์ฌ์ดํธ์ ์ด์ฉ์ฝ๊ด์ ๋ฐ๋์ ์ฝ๊ณ ์ค์ํด.
- robots.txt ์ค์: ์น์ฌ์ดํธ์ robots.txt ํ์ผ์ ๋ช ์๋ ๊ท์น์ ๋ฐ๋ผ.
- ์์ฒญ ์ ํ: ๊ณผ๋ํ ์์ฒญ์ ๋ณด๋ด์ง ์๋๋ก ์์ฒญ ๊ฐ ์ ์ ํ ๋๋ ์ด๋ฅผ ๋๊ณ , ํ์ํ ๋ฐ์ดํฐ๋ง ์์งํด.
- ์ ์ ๊ณต๊ฐ: ์ฌ์ฉ์ ์์ด์ ํธ ๋ฌธ์์ด์ ๋ด์ ์ด๋ฆ๊ณผ ์ฐ๋ฝ์ฒ ์ ๋ณด๋ฅผ ํฌํจ์์ผ.
- ์บ์ฑ ํ์ฉ: ๊ฐ์ ํ์ด์ง๋ฅผ ๋ฐ๋ณตํด์ ์์ฒญํ์ง ์๋๋ก ์บ์ฑ์ ์ฌ์ฉํด.
- ๊ฐ์ธ์ ๋ณด ๋ณดํธ: ๊ฐ์ธ ์๋ณ ์ ๋ณด๋ ์์งํ์ง ์๊ฑฐ๋, ์์ง ์ ๊ด๋ จ ๋ฒ๋ฅ ์ ์ค์ํด.
- ์ถ์ฒ ํ์: ์์งํ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋๋ ์ถ์ฒ๋ฅผ ๋ช ์ํด.
- ๊ฒฝ์ ๊ณ ๋ ค: ๊ฒฝ์์ฌ์ ํต์ฌ ๋น์ฆ
- ๊ฒฝ์ ๊ณ ๋ ค: ๊ฒฝ์์ฌ์ ํต์ฌ ๋น์ฆ๋์ค ๋ชจ๋ธ์ ํด์น๋ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ง ์์.
- ๋ฒ๋ฅ ์๋ฌธ: ๋๊ท๋ชจ ์์ ์ ์คํฌ๋ํ ํ๋ก์ ํธ๋ฅผ ์์ํ๊ธฐ ์ ์ ๋ฒ๋ฅ ์๋ฌธ์ ๊ตฌํด.
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ํ๋ก์ ํธ ํ์ฉ ๋ฐฉ์
1. ํธ๋ ๋ ๋ชจ๋ํฐ๋ง
ํน์ ์ฐ์ ์ด๋ ์ฃผ์ ์ ๊ดํ ๋ด์ค ํธ๋ ๋๋ฅผ ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ ์ ์์ด. ์๋ฅผ ๋ค์ด, AI, ๋ธ๋ก์ฒด์ธ, ๊ธฐํ ๋ณํ ๋ฑ์ ์ฃผ์ ์ ๋ํ ๋ฏธ๋์ด ๊ด์ฌ๋ ๋ณํ๋ฅผ ์ถ์ ํ ์ ์์ง.
2. ๊ฒฝ์์ฌ ๋ชจ๋ํฐ๋ง
ํน์ ๊ธฐ์ ์ด๋ ๋ธ๋๋์ ๊ดํ ๋ด์ค๋ฅผ ์์งํ์ฌ ๊ฒฝ์์ฌ์ ๋ํฅ์ ํ์ ํ ์ ์์ด. ์๋ก์ด ์ ํ ์ถ์, ์ธ์ํฉ๋ณ, ๊ฒฝ์์ง ๋ณ๊ฒฝ ๋ฑ์ ์ ๋ณด๋ฅผ ๋น ๋ฅด๊ฒ ์ ์ ์์ง.
3. ๋ง์ถคํ ๋ด์ค ๋ค์ด์ ์คํธ
๊ด์ฌ ์๋ ์ฃผ์ ์ ๋ํ ๋ด์ค๋ง ๋ชจ์์ ๊ฐ์ธํ๋ ๋ด์ค ๋ค์ด์ ์คํธ๋ฅผ ๋ง๋ค ์ ์์ด. ์ด๋ฉ์ผ๋ก ์๋ ๋ฐ์กํ๊ฑฐ๋ ์น ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ ๊ณตํ ์ ์์ง.
4. ๊ฐ์ฑ ๋ถ์ ๋ฐ ์ฌ๋ก ๋ชจ๋ํฐ๋ง
์์ง๋ ๋ด์ค ํค๋๋ผ์ธ์ ๊ฐ์ฑ ๋ถ์์ ์ ์ฉํ์ฌ ํน์ ์ฃผ์ ์ ๋ํ ๋ฏธ๋์ด์ ํ๋๋ ์ฌ๋ก ๋ณํ๋ฅผ ์ถ์ ํ ์ ์์ด. ์ด๋ ๋ธ๋๋ ํํ ๊ด๋ฆฌ๋ ์๊ธฐ ๋์์ ์ ์ฉํด.
ํ๋ก์ ํธ ํ์ฅ ์์ด๋์ด
- ์น ์ธํฐํ์ด์ค ์ถ๊ฐ: ์์ง๋ ๋ด์ค์ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ฃผ๋ ์น ๋์๋ณด๋๋ฅผ ๋ง๋ค ์ ์์ด. Perl์ Mojolicious๋ Dancer2 ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ๊ตฌํํ ์ ์์ง.
- ์๋ฆผ ์์คํ : ํน์ ํค์๋๊ฐ ํฌํจ๋ ๋ด์ค๊ฐ ๋ฐ๊ฒฌ๋๋ฉด ์ด๋ฉ์ผ์ด๋ ๋ฉ์ ์ ๋ก ์๋ฆผ์ ๋ณด๋ด๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด.
- ์์ฐ์ด ์ฒ๋ฆฌ ๊ฐํ: Lingua::EN::Tagger๋ ์ธ๋ถ API๋ฅผ ํ์ฉํด ๋ ์ ๊ตํ ์์ฐ์ด ์ฒ๋ฆฌ์ ์ฃผ์ ๋ถ๋ฅ๋ฅผ ๊ตฌํํ ์ ์์ด.
- ์๊ณ์ด ๋ถ์: ์๊ฐ์ ๋ฐ๋ฅธ ํค์๋ ํธ๋ ๋ ๋ณํ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด.
- ๋จธ์ ๋ฌ๋ ํตํฉ: ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ๋ด์ค ๋ถ๋ฅ๋ ์ค์๋ ์์ธก ๋ฑ์ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ ํ๋ จ์ํฌ ์ ์์ด.
๐ ํ๋ก์ ํธ ์์ฝ
์ด ๋ด์ค ํค๋๋ผ์ธ ์์ง๊ธฐ ํ๋ก์ ํธ๋ Perl์ ์น ์คํฌ๋ํ ๋ฅ๋ ฅ์ ์ค์ ์์ ํ์ฉํ๋ ์ข์ ์์์ผ. ์ฐ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ์ ๊ณผ ๊ฐ๋ ์ ์ ์ฉํ์ด:
- ์ฌ๋ฌ ์น์ฌ์ดํธ์์ ๋ฐ์ดํฐ ์์ง
- HTML ํ์ฑ ๋ฐ ๋ฐ์ดํฐ ์ถ์ถ
- ์ ๊ทํํ์์ ํ์ฉํ ํ ์คํธ ์ฒ๋ฆฌ
- JSON ๋ฐ CSV ํ์์ ๋ฐ์ดํฐ ์ ์ฅ
- ๊ฐ๋จํ ํ ์คํธ ๋ถ์ ๋ฐ ํค์๋ ์ถ์ถ
- cron์ ์ด์ฉํ ์๋ํ
- ๋ชจ๋ํ๋ ์ฝ๋ ๊ตฌ์กฐ
- ์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ๋ก๊น
- robots.txt ์ค์ ๋ฑ ์ค๋ฆฌ์ ์คํฌ๋ํ
์ด ํ๋ก์ ํธ๋ ํ์ฅ์ฑ์ด ๋์์ ๋ค์ํ ์ฉ๋๋ก ํ์ฉํ๊ฑฐ๋ ๋ ๋ณต์กํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด. ์น ์คํฌ๋ํ์ ์ค์ ์์ฉ ์ฌ๋ก๋ก ๋ฐฐ์ด ๋ด์ฉ์ ์ง์ ์ ์ฉํด ๋ณผ ์ ์๋ ์ข์ ์ถ๋ฐ์ ์ด ๋ ๊ฑฐ์ผ!
์ด ํ๋ก์ ํธ๋ฅผ ํตํด Perl ์น ์คํฌ๋ํ์ ๋ชจ๋ ํต์ฌ ๊ฐ๋ ์ ์ค์ ์์ ํ์ฉํด ๋ดค์ด. ์ด์ ๋๋ง์ ์น ์คํฌ๋ํ ํ๋ก์ ํธ๋ฅผ ์์ํ ์ค๋น๊ฐ ๋์ด! ๐
๋ง์น๋ฉฐ: Perl ์น ์คํฌ๋ํ์ ์ฌ์ ์ ๋ง๋ฌด๋ฆฌํ๋ฉฐ ๐
์ฐ๋ฆฌ๋ Perl์ ์ฌ์ฉํ ์น ์คํฌ๋ํ์ ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ ๊ธฐ์ ๊น์ง ๊ด๋ฒ์ํ๊ฒ ์ดํด๋ดค์ด. ๋จ์ํ ์น ํ์ด์ง ์์ฒญ๋ถํฐ ๋ณต์กํ ๋ฐ์ดํฐ ์ถ์ถ, ์๋ํ, ๋ถ์๊น์ง ์น ์คํฌ๋ํ์ ์ ์ฒด ๊ณผ์ ์ ๋ค๋ค์ง.
๋ฐฐ์ด ๋ด์ฉ ์์ฝ
- ๊ธฐ๋ณธ ๊ฐ๋ ์ดํด: Perl๊ณผ ์น ์คํฌ๋ํ์ ๊ด๊ณ, ์น ์คํฌ๋ํ์ ์๋ฆฌ
- ํ๊ฒฝ ์ค์ : Perl ์ค์น ๋ฐ ํ์ ๋ชจ๋ ์ค์
- ํต์ฌ ๋ชจ๋ ํ์ฉ: LWP, HTML::Parser, Mojo::DOM ๋ฑ ์ฃผ์ ๋ชจ๋ ์ฌ์ฉ๋ฒ
- ์ ๊ทํํ์ ๋ง์คํฐ: ๊ฐ๋ ฅํ ํจํด ๋งค์นญ์ผ๋ก ๋ฐ์ดํฐ ์ถ์ถ
- ๋ณต์กํ ์น์ฌ์ดํธ ์ฒ๋ฆฌ: ๋์ ์ฝํ ์ธ , ์ธ์ฆ, ๋ด ์ฐจ๋จ ์ฐํ ๋ฑ
- ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ๊ฐ๊ณต: ๋ค์ํ ํ์์ผ๋ก ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ์ฒ๋ฆฌ
- ์๋ํ์ ์ค์ผ์ค๋ง: ์ฃผ๊ธฐ์ ๋ฐ์ดํฐ ์์ง ์๋ํ
- ์ค๋ฅ ์ฒ๋ฆฌ์ ๋๋ฒ๊น : ์์ ์ ์ธ ์คํฌ๋ํ ์คํฌ๋ฆฝํธ ์์ฑ
- ๋ฒ์ , ์ค๋ฆฌ์ ๊ณ ๋ ค์ฌํญ: ์ฑ ์๊ฐ ์๋ ์น ์คํฌ๋ํ
- ์ค์ ํ๋ก์ ํธ: ๋ด์ค ํค๋๋ผ์ธ ์์ง๊ธฐ ๊ตฌํ
์น ์คํฌ๋ํ์ ๋ฐ์ดํฐ ์์ง์ ๊ฐ๋ ฅํ ๋๊ตฌ์ง๋ง, ํญ์ ์ฑ ์๊ฐ ์๊ฒ ์ฌ์ฉํด์ผ ํ๋ค๋ ์ ์ ๊ธฐ์ตํด. ์น์ฌ์ดํธ์ ์ด์ฉ์ฝ๊ด์ ์กด์คํ๊ณ , ์๋ฒ์ ๊ณผ๋ํ ๋ถ๋ด์ ์ฃผ์ง ์์ผ๋ฉฐ, ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ณดํธํ๋ ๋ฐฉ์์ผ๋ก ์คํฌ๋ํํ๋ ๊ฒ์ด ์ค์ํด.
์ด ๊ฐ์ด๋๊ฐ Perl์ ์ฌ์ฉํ ์น ์คํฌ๋ํ์ ์ธ๊ณ๋ก ๋ค์ด๊ฐ๋ ๋ฐ ๋์์ด ๋์๊ธธ ๋ฐ๋ผ. ์ด์ ๋๋ง์ ๋ฐ์ดํฐ ์์ง ํ๋ก์ ํธ๋ฅผ ์์ํ ์ค๋น๊ฐ ๋์ด! ๐
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๊ณต์ ํด ์ฃผ์ธ์. ํ์ด์ ๋น๋๋ค! ๐
์ถ๊ฐ ์๋ฃ ๋ฐ ์ฐธ๊ณ ๋งํฌ ๐
Perl ์น ์คํฌ๋ํ ๊ด๋ จ ๋์
- "Perl & LWP" by Sean M. Burke
- "Web Scraping with Perl" by Michael Schrenk
- "Mastering Perl" by brian d foy
์ ์ฉํ Perl ๋ชจ๋ ๋ฌธ์
์น ์คํฌ๋ํ ๊ด๋ จ ์ปค๋ฎค๋ํฐ
- PerlMonks - Perl ํ๋ก๊ทธ๋๋ฐ ์ปค๋ฎค๋ํฐ
- Stack Overflow - Perl ์น ์คํฌ๋ํ ์ง๋ฌธ
- Reddit Perl ์ปค๋ฎค๋ํฐ
๋ฒ์ , ์ค๋ฆฌ์ ๊ฐ์ด๋๋ผ์ธ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.