[네이버 블로그 크롤링] Easy Writing Golden Sentences

Joey Hong·2020년 9월 30일
0

크롤링

목록 보기
5/5
post-custom-banner

크롤링 소개

크롤링 목적

  • EBS Easy Writing 라디오는 매일 영어 5문장을 다룬다.
  • 이를 노션 페이지에 입력해두고 학습한다.
  • 블로그들이 이 문장들을 포스팅하고있어 이를 크롤링을 하고자 한다.

🚨 노션에 바로 입력하려했으나 아직 성공하지 못했다.

블로그 선정

수많은 블로그들 중 하루도 빼먹지 않고 Easy Writing 문장들을 포스팅되는 블로그들을 찾아봤다.

후보1

  • 후보1은 게시글 맨 아래에 문장이 나와 위의 내용 길이에 따라 selector도 변한다는 단점이 있다. 게다가 한글 문장은 게시글 맨 위에 나오거나, 어떤 경우는 문장들 앞에 번호가 매겨지거나 문장들 사이에 공백이 생기는 등 작성법에 일관성이 너무 없어 배제했다.

후보2

  • 후보2는 게시글 가장 위에 문장이 온다는 점에서 크롤링하기 좋지만 가끔 게시글 맨 위에 제목이 오거나 내용에 관련된 링크가 추가되는 등 역시나 일관성이 조금 부족해 배제했다.

후보3

  • 후보3은 거의 대부분의 경우 일관성있게 게시글 상단의 항상 같은 구간에 문장이 배치된다. 문장 앞의 번호나 문장 사이의 공백여부도 항상 일정해서 크롤링하기 가장 적합했으므로 후보3을 선택했다.
  • 하지만 후보3은 내용에 따라 오타가 많다는 치명적인 문제점이 있다. 크롤링을 해보는 것에 의의를 두고 후보3을 크롤링 해보았다.

크롤링 형태

result 폴더

  • 크롤링하 결과는 result/result.md에 입력한다
  • result 폴더가 없는 경우 생성해준다
fs.readdir('result', (err) => {
	if (err) {
		console.log('result 폴더가 없어 생성합니다')
		fs.mkdirSync('result');		//폴더 생성
	}
});

result.md

  1. 라디오 방송 날짜
  2. 번호 매겨진 한글 5문장
  3. 한/영 사이 공백 한 줄
  4. 번호 매겨진 영어 5문장
  5. 마지막에 공백 한 줄

크롤링 방식

1. 블로그 접속

  • 후보3은 여러 게시판을 가지고 있어 Easy Writing글만 있는 게시판으로 접속해준다
  • 가끔 페이지 이동 후 selector를 찾지 못하는 경우를 방지하기 위해 waitUntil: 'networkidle0' 옵션을 추가해줬다.
await page.goto('http://blog.naver.com/PostList.nhn?blogId=magpie777&from=postList&categoryNo=48', {
	waitUntil: 'networkidle0'
});	//페이지 접속

2. 게시글 링크 수집

  • 이동한 Easy Writing 게시판은 하루치 라디오 내용을 담은 게시글들을 담고있다

  • 매주 주말에 크롤링을 한다면 차례대로 금, 목, 수, 화, 월 게시글 링크를 가져올 수 있다.

    • 월화수목금 순서를 하기 위애 (6-i)번부터 가져온다 (i=0; i++)
      🎯 네이버 블로그는 내용이 iframe에 감싸져있지만 직접 추출한 링크로 이동시 iframe에 감싸져있지 않다. (iframe찾을 필요X)
for (let i = 1; i <= 5; i++) {
	const link = await page.$eval(
		`.blog2_categorylist tr:nth-child(${6 - i}) a`,
		element => element.href
    	);		// 이동한 페이지에서 링크 가져오기
	const pageI = await browser.newPage();	//pageI 라는 새로운 페이지 열기
	await pageI.goto(link);		//가져온 링크로 이동

	...
}

iframe

위의 경우는 iframe을 사용할 필요가 없었지만 필요한 경우를 위해 다루는 법을 공유한다

  • iframe은 페이지 안에 있는 또 다른 페이지라고 생각하면 된다.
  • 따라서 이 페이지에 있는 selector는 바로 접근할 수 없다.
    • element selector로 내용을 선택하면 그제서야 selector가 나타난다

iframe 내의 내용 다루는 법

const iframe = page.frames().find(frame => frame.name() === 'mainFrame');
const link = await iframe.$eval(`selector`, element => element.href);	
  1. 조건을 만족하는 iframe을 찾는다
    • 위에서는 'mainFrame'이라는 이름을 가진 iframe을 찾아보았다
  2. 페이지 내 그 iframe을 새로운 이름으로 저장 후 이것을 page처럼 사용하면 된다
    • 위에서는 iframe이라는 이름으로 저장했다
    • iframe내에 있는 selector에 접근해서 링크를 빼왔다.

3. 문장 크롤링

링크 수집하는 for문 안에서 문장 가져오는 for문을 돌린다

  • result배열에 문장을 하나씩 넣는다
  • result[0]에는 라디오 방송 날짜를 적어줬다
  • result[1] ~ [5]에는 한글 문장을 삽입한다
  • result[6]은 빈줄을 삽입한다
  • result[7] ~ [11]에는 영어 문장을 삽입한다
  • result[12]에도 빈줄을 삽입한다
const result = [];					//문자 넣을 배열 생성
result[0] = [`${month}${date}${days[i - 1]}`];	//첫줄에 날짜
for (const [j, n] of nb.entries()) {
  const text = await pageI.$$eval(		//문장 가져오기
    `.se-module.se-module-text p:nth-child(${n})`, 
    element => element[0].textContent
  );
  if (text != '') {
    if ((n - 1) < 5)	//1번부터 4번 문장 (한글)
      result[n - 1] = [`${(n - 1)%5}. ${text}`];
    
    else if (7 <= (n - 1)  && (n - 1) <= 10)	//1번부터 4번 문장(영어)
      result[n - 1] = [`${(n - 2)%5}. ${text}`]
    
    else if ((n - 1) == 5 || (n - 1) == 11)		//5번 문장(한글/영어)
      result[n - 1] = [`5. ${text}`];
    
    else if (n - 1 == 6)		//한글과 영어문장 사이 빈줄 삽입
      result[n - 1] = [''];
  }
  result.push(['']);		//마지막에 빈줄 삽입
}

5. 파일에 입력

배열을 문자열로 변환 stringify

배열을 텍스트로 바꾸기 위해 stringify를 사용해준다

  • 다만 stringify를 이용해 문자열로 변환시 문장 앞뒤에 쌍따옴표 " 가 랜덤으로 붙는다
  • 따라서 replace를 사용해 문
    자열 내 쌍따옴표를 모두 제거해주었다
    • 문장 내에 필요할 수도 있는 쌍따옴표도 제거되어버린다는 단점이 있다..
str += stringify(result).replace(/\"/g, '');	//배열을 문자열로 변환 후 따옴표 제거

파일에 입력 writeFileSync

  • result.md에 입력해준다
fs.writeFileSync('result/result.md', str);	//문자열을 result/result.md에 입력 

블로그 크롤링 완성 코드


const fs = require('fs');
const puppeteer = require('puppeteer');
const axios = require('axios');
const stringify = require('csv-stringify/lib/sync');


fs.readdir('result', (err) => {
  if (err) {
    console.log('result 폴더가 없어 생성합니다')
    fs.mkdirSync('result');
  }
});


const crawler = async () => {
  try{
    const browser = await puppeteer.launch({ 
      headless: false,
    });	//브라우저 띄우기
    const page = await browser.newPage();	//페이지 띄우기
    await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36');
    await page.goto('http://blog.naver.com/PostList.nhn?blogId=magpie777&from=postList&categoryNo=48', {
      waitUntil: 'networkidle0'
    });	//페이지 접속
			
    
    //문자열 가져올 때 돌릴 숫자 배열
    let nb = [];
    for (let i = 2; i <= 12; i++)
      nb.push(i);
    
    //날짜 입력시 필요한 배열
    const days = ['(월)', '(화)', '(수)', '(목)', '(금)'];
    const months = ['Month', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

    
    let str = '';
    //게시판의 게시글 링크 가져오기
    for (let i = 1; i <= 5; i++) {
      const link = await page.$eval(`.blog2_categorylist tr:nth-child(${6 - i}) a`, element => element.href);	
      const pageI = await browser.newPage();
      await pageI.goto(link);

      
      //블로그의 제목에서 날짜 따오기
      //[22 September]와 같은 형식으로 되어있다
      let month = '';
      let date = '';
      let k = 0;
      let title = await pageI.$$eval('.se-fs-.se-ff-', element => element[0].textContent);
      while (title[k] != ' ')
        k++;
      date = title.substring(1, k);
      let l = k;
      while (title[l] != ']')
        l++;
      let monthstr = title.substring(k + 1, l); 
      month = months.indexOf(monthstr);

      
      //문자열 가져오기
      const result = [];
      result[0] = [`${month}${date}${days[i - 1]}`];
      for (const [j, n] of nb.entries()) {
        const text = await pageI.$$eval(`.se-module.se-module-text p:nth-child(${n})`, element => element[0].textContent);
        
        //문자열을 result에 입력
        if (text != '') {
          if ((n - 1) < 5)
            result[n - 1] = [`${(n - 1)%5}. ${text}`];
          else if (7 <= (n - 1)  && (n - 1) <= 10)
            result[n - 1] = [`${(n - 2)%5}. ${text}`]
          else if ((n - 1) == 5 || (n - 1) == 11)
            result[n - 1] = [`5. ${text}`];
          else if (n - 1 == 6)
            result[n - 1] = [''];
        }
        result.push(['']);
      }
      await pageI.close();
      //배열을 문자열로 변환 후 쌍따옴표 제거
      str += stringify(result).replace(/\"/g, '');

    }
    //문자열을 파일에 저장
    fs.writeFileSync('result/result.md', str);
    await page.close();		//페이지 닫기
    await browser.close();	//브라우저 닫기
  } catch (e) {
    console.error(e);
  }
}

crawler();
profile
개발기록
post-custom-banner

0개의 댓글