Ruby Programming 8 Web Crawling 한국어 정보의 전산 처리 2017. 5. 24.
Web Crawling의 요소 기술 웹에 접속하여 웹문서 읽어오기 웹문서 분석 날짜 처리 반복/재귀 알고리즘 open-uri나 net/http 라이브러리 이용. 웹문서 분석 xml/html parser 라이브러리(예: nokogiri)를 이용할 수도 있으나 간단한 분석일 때는 scan 등의 함수로 정규표현을 검색하여 처리할 수도 있음. 날짜 처리 url이 날짜를 바탕으로 할 때는 Date 클래스로 날짜 처리. 반복/재귀 알고리즘 수많은 웹페이지를 방문할 때에는 iterative 또는 recursive 알고리즘을 사용해야 함.
조선일보 스포츠 기사 제목 읽어오기 조선일보 홈페이지 ‘메뉴 전체보기’를 클릭하여 ‘스포츠 - 전체’를 선택 날짜를 클릭 http://news.chosun.com/svc/list_in/se_list.html?catid=G1 날짜를 클릭 http://news.chosun.com/svc/list_in/list_title.html?catid=G1&indate=20170524 하루당 스포츠 웹페이지가 여러 페이지로 이루어져 있고 한 페이지당 20개의 기사 요약이 제시됨. 각 기사의 제목만 추출하고자 함. 조선일보 홈페이지 html 파일의 인코딩은 euc-kr이므로 iconv를 이용하여 출력 결과를 utf-8로 변환하여 출력
기사의 html 구조 html 소스 코드를 보면, 각 기사는 다음과 같은 구조로 되어 있음. 붉은색 부분만 추출하고자 함. <dl class="list_item"> <dt><a href="http://news.chosun.com/site/data/html_dir/2017/05/24/2017052400394.html">[줌인]김성근 감독 형식은 자진사퇴, 내용은 경질?</a></dt> <dd class="thumb"><a href="http://news.chosun.com/site/data/html_dir/2017/05/24/2017052400394.html"><img src="http://image.chosun.com/sitedata/thumbnail/201705/24/2017052400382_0_thumb.jpg" alt=""></a></dd> <dd class="desc"><a href="http://news.chosun.com/site/data/html_dir/2017/05/24/2017052400394.html">김성근 한화 이글스 감독이 자진사퇴했고, 한화 구단은 사의를 수용하며 이상군 투수코치를 감독대행에 임명했다.김 감독은 지난 21일 대전 삼성 라이온즈전을 마..</a></dd> <dd class="date_author"> <span class="date">2017.05.24 (수)</span> | <span class="author"> 스포츠조선=박재호 기자 </span> </dd> </dl>
Ruby script를 바깥쪽부터 작성함 사용자로부터 시작 날짜와 끝 날짜를 입력받아, 각 날짜의 조선일보 스포츠 웹페이지를 읽어옴. require 'date' require 'net/http' begin_date = Date.new(ARGV[0].to_i, ARGV[1].to_i, ARGV[2].to_i) end_date = Date.new(ARGV[3].to_i, ARGV[4].to_i, ARGV[5].to_i) Net::HTTP.start("news.chosun.com") do |http| #조선일보 홈페이지에 접속 begin_date.upto(end_date) do |date| #시작 날짜부터 끝 날짜까지 d = date.to_s.gsub("-","") #날짜를 문자열로: 20170523 url = "/svc/list_in/list_title.html?catid=G1&indate=#{d}" #해당 날짜의 스포츠 기사 웹 주소 e = find_end_page(http, url, 1) #해당 날짜의 스포츠 기사의 페이지 수 알아냄 (1..e).each do |i| #1페이지부터 끝 페이지까지 search(http, url+"&pn=#{i}") #search 함수 호출 end
find_end_page 함수 @end_pattern = %r|<li><a style="text-decoration:none;cursor:default" class="current">[0-9]+</a></li><li><a style="text-decoration:none;cursor:default">| #마지막 페이지의 특징을 정규표현으로 포착함. def find_end_page(http, url, n) #스포츠 기사가 몇 페이지까지 있는지 알아내는 함수 str = http.get(url+"&pn=#{n}").body #n번째 페이지를 읽어옴 if not str =~ @end_pattern #마지막 페이지가 아니면 find_end_page(http, url, n+1) #find_end_page 함수 재귀 호출 else #마지막 페이지이면 return n #n을 반환하고 함수 종료 end
search 함수 @pattern = %r|<dt><a href="http://news.chosun.com/site/data/html_dir/([0-9]{4}/[0-9]{2}/[0-9]{2})/[0-9]{13}\.html">(.*)</a></dt>| def search(http, url) puts url #url 출력 str = http.get(url).body #url의 웹 문서를 불러옴 str.scan(@pattern).each do |date, title| #웹 문서에서 정규표현(pattern)을 찾음. #괄호 친 두 부분을 date, title이라 지칭 print date, "\t", title, "\n" #date와 title 출력 end
위의 코드의 문제 해당 날짜의 스포츠 웹페이지가 몇 페이지까지 있는지 알아내는 함수(find_end_page)에서도 각 웹페이지의 문서를 읽어오고 각 웹문서를 읽어와서 제목을 추출하는 함수(search)에서도 각 웹페이지의 문서를 읽어오게 되어 있어서 같은 일을 2번 반복하는 꼴이 됨. 페이지 수만큼 반복하는 iterative algorithm을 적용하는 셈. 각 웹페이지 문서를 1번만 읽어오면서 과제를 수행하기 위해서는 recursive algorithm이 필요함. 날짜와 일련번호를 가지고 url을 만드는 일은 search 함수에게 맡기는 게 나음. (그러면 calling code가 더 깔끔해짐.)
recursive algorithm을 적용한 calling code Net::HTTP.start("news.chosun.com") do |http| begin_date.upto(end_date) do |date| search(http, date, 1) #첫째 페이지에 대해 search 함수 호출 end
search 함수 def search(http, date, n) d = date.to_s.gsub("-","") #날짜를 문자열로: 20170523 url = "/svc/list_in/list.html?catid=G1&indate=#{d}&pn=#{n}" puts url #url 출력 str = http.get(url).body str.scan(@pattern).each do |date, title| print date, "\t", title, "\n" #date와 title 출력 end search(http, date, n+1) if not str =~ @end_pattern #마지막 페이지가 아니면 n을 1 증가시켜 함수 재귀 호출
동아일보 동아일보 홈페이지, 스포츠, 최신기사, 날짜별로 들어가 보면, 전반적인 구조는 조선일보와 비슷함. http://news.donga.com/List/Sports/?ymd=20170524&m= 한 페이지에 20개의 기사씩 제시되고 url에 첫 기사의 일련번호(1,21,41,...)와 날짜가 포함됨. http://news.donga.com/List/Sports/?p=21&prod=news&ymd=20170524&m= 조선일보와 비슷하게 search 함수를 재귀호출하되 이 함수의 parameter에 일련번호도 포함시켜야 함.
동아일보용 calling 코드 @pattern = %r|<div class='articleList'><div class='rightList'><a href='http://news.donga.com/List/Sports/[0-9]/[0-9]{2}/([0-9]{8})/[0-9]+/[0-9]'><span class='tit'>(.+?)</span>| @end_pattern = %r|</a><strong>[0-9]+</strong><a href='\?p=none\&prod=news\&ymd=[0-9]{8}\&m=' class='right'>| Net::HTTP.start("news.donga.com") do |http| begin_date.upto(end_date) do |date| search(http, date, 1) #search 함수 호출 end
동아일보용 search 함수 def search(http, date, n) d = date.to_s.gsub("-","") #날짜를 문자열로: 20170524 url = "/List/Sports/?p=#{n}&prod=news&ymd=#{d}&m=" puts url #url 출력 str = http.get(url).body #url의 웹 문서를 불러옴 str.scan(@pattern).each do |date, title| print date, "\t", title, "\n" #date와 title 출력 end search(http, date, n+20) if not str =~ @end_pattern #마지막 페이지가 아니면 search 함수 재귀 호출