본문 바로가기

프로젝트

[크롤링] 네이버 뉴스 댓글 API 분해해보기

네이버 뉴스 댓글의 더보기 버튼

 네이버 뉴스 댓글은 "더보기 버튼"이 존재해서 댓글을 특정 개수 단위로 가져올 수 있다. 이것만 보면 페이지가 동적으로 동작하므로 셀레니움 기반으로 데이터를 가져오는 프로젝트가 많이 있었다. 처음에는 나도 댓글 수집을 셀레니움 기반으로 해야 하나 고민을 했다.

 나는 가능한 한 네이버 댓글을 셀레니움 없이 크롤링 할 수 있기를 원했다. 셀레니움은 브라우저를 직접 조작하는 방식이라 단순 http 통신보다 속도가 느리기 때문이다. 진행 중인 프로젝트에서 하루의 특정 시간마다 뉴스 데이터를 수집 및 가공하여 머신러닝을 돌려야 하는데 데이터 양이 상당할 수도 있다는 점, 사용되는 서버 환경이 기껏해야 EC2 프리티어 수준에서 조금 좋은 수준일 것이라고 예상하는 점을 고려하면 데이터를 최대한 짧은 시간 내에 읽어 처리하는 편이 좋겠다고 생각했다.

 아무튼 이러한 이유로 셀레니움이 아닌 단순 http 방식으로 처리할 수 있는 방법이 있지 않을까 싶은 마음에 신나게 구글링을 진행했는데, 다행히도 방법이 존재했다.

https://hoonzi-text.tistory.com/4

 요약하면 네이버 뉴스 댓글의 '더보기'를 클릭했을 때 수행하는 HTTP 요청을 보고 사용된 API를 추정하는 것이다. 해당 글을 보고 실제로 버튼을 눌러봤는데, HTTP 요청을 통해 API 정보를 알 수 있었다.

HTTP 요청을 통해 파악한 api 주소

 다만 저 글이 작성될 당시에 비해 API가 업데이트 된 것인지는 몰라도, 첫 페이지를 제외하고는 가져올 수 없는 문제가 있었다. URL 상에서도 일부 쿼리가 추가되어 있기 때문에 어차피 프로젝트에 사용할 겸 분석해봤다.

 현재 분석한 내용은 정확하지 않을 수 있다.


반환 데이터 구조

대략적으로 다음과 같다.

{
  "success": true,
  "code": "1000",
  "message": "요청을 성공적으로 처리하였습니다.",
  "lang": "ko",
  "country": "KR",
  "result": {
    "commentList": [ // 엄청 많은 정보 객체 ],
    "pageModel": { 
      "page": 2,
      "pageSize": 20,
      "indexSize": 10,
      "startRow": 21,
      "endRow": 40,
      "totalRows": 125,
      "startIndex": 20,
      "totalPages": 7,
      "firstPage": 1,
      "prevPage": 1,
      "nextPage": 3,
      "lastPage": 7,
      "current": null,
      "threshold": null,
      "moveToLastPage": false,
      "moveToComment": false,
      "moveToLastPrev": false
    },
    "morePage": {
      "prev": "1000002000002062yn0kqv6gnj",
      "next": "1000001000001062ymmrtwcsu9",
      "start": "100005j00005t062ykkbk8lqan",
      "end": "0zik0za000001062ykmaw67vhl"
    },
    "count": {
      "comment": 127,
      "reply": 25,
      "exposeCount": 127,
      "delCommentByUser": 6,
      "delCommentByMon": 0,
      "blindCommentByUser": 0,
      "blindReplyByUser": 0,
      "total": 152
    },
    "sort": "FAVORITE",
  },
  "date": "2023-08-17T11:48:19+0000"
}
  • success: 성공 여부를 반환한다.
  • code: 성공하면 "1000" 값을 가진다
  • message: 요청 메시지를 반환한다.
  • lang, country: 언어 / 나라 정보
  • result: 가장 필요한 정보가 모여 있는 부분
    • commentList: 댓글 객체에 대한 리스트
    • pageModel: 페이지와 관련된 정보. 페이지 번호, 사이즈 등 정보와 관련되어 있음.
    • morePage: 이전 / 다음 페이지 탐색에 필요한 페이지 Id를 담고 있는 객체.
    • count: 댓글 개수 관련 정보
  • sort: 댓글 정렬 기준. ex) 순공감순 / 최신순 ...
  • date: 요청한 시간

위 객체 정보에서 특이한 점은 pageModel / morePage가 분리되어있다는 점이다.

pageModel은 단순히 페이지를 관리하기 위한 목적의 객체이다. 현재 요청이 몇번째 페이지에 해당하는지, 총 페이지 개수는 몇개이고 이전 / 다음 페이지 번호는 얼마인지 정도의 정보를 가지고 있다. 중요한 점은 pageModel의 페이지 번호만으로는 다음 페이지를 가져오지 못한다는 점이다.

### forTest
GET https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=news&pool=cbox5&_cv=20230810113455&lang=ko&country=KR&objectId=news005%2C0001631566&pageSize=100&indexSize=10&pageType=more&page=1 HTTP/1.1
Referer: https://n.news.naver.com/article/comment/005/000163156
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/98.0.4758.102
# 페이지 시작은 보통 1 만약 아니면 보정 필요
### forTest
GET https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=news&pool=cbox5&_cv=20230810113455&lang=ko&country=KR&objectId=news005%2C0001631566&pageSize=100&indexSize=10&pageType=more&page=2 HTTP/1.1
Referer: https://n.news.naver.com/article/comment/005/000163156
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/98.0.4758.102

위 두 HTTP GET 요청은 마지막 페이지 번호만 다르다. 그러나, 결과는 아래와 같이 동일하다.

첫번째 요청을 보내 받은 응답 body
두번째 요청을 보내 받은 응답 body
첫번째 요청(좌)과 두번째 요청(우)의 페이지 모델 차이

위 결과를 보면 알 수 있듯 URL의 page 쿼리는 단지 페이지 모델에만 영향을 줄 뿐, 실제 페이지 데이터를 탐색하는 부분에는 영향을 주지 않는다. 첫번째 페이지는 1로 지정되어 있는 것으로 보인다.

morePage는 각 페이지의 Id 값을 포함한 객체이다. 첫번째 / 마지막 / 이전 / 다음 페이지에 대한 Id 정보를 가지고 있으며, 이 값을 기준으로 다음 페이지를 순회하고 있다. 현재 페이지가 마지막 페이지인 경우 "next" === "end"가 된다.

네이버 댓글 API 사용 요점은 pageModel 및 morePage 객체에 담긴 값을 기반으로 다음 페이지를 탐색하는 것이다. pageModel 자체는 탐색에 큰 역할을 수행하지 않지만, 상태를 관리할 때 도움이 될 수 있다.


요청 URL 구조

기사 URL: https://n.news.naver.com/article/005/0001631566

GET https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=news&pool=cbox5&lang=ko&country=KR&objectId=news005%2C0001631566&pageSize=20&indexSize=10&page=2&pageType=more&moreParam.next=1000000000000062ymz0kpdng9 HTTP/1.1 
Referer: https://n.news.naver.com/article/comment/005/0001631566 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/98.0.4758.102

https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json 경로에 요청을 보낸다. 원래는 지금보다 훨씬 많은 쿼리들이 존재하나, 어느 정도 꼭 필요해 보이는 것만 남겼다.

헤더에 Referer은 원래 기사를 의미한다. 웃기는 부분은 현재 글 쓴 시점에는 동일한 기사 주소가 아니어도 정상적으로 댓글을 가져올 수 있다. 아마 네이버 뉴스라면 다 가져올 수 있는게 아닐까? 싶다.

User-Agent는 유저의 요청으로 인식하도록 구성한 부분이다. 네이버 기사 본문의 경우 해당 옵션이 없으면 뉴스 본문을 제외한 부분만 제공한다.

  • ticket=news: 뉴스에 대한 댓글을 의미하는 것 같다. 생략하면 데이터를 가져오는데 실패한다.
  • pool=cbox5: ticket 과 관련된 변수로 보이며, 생략하면 데이터를 가져오는데 실패한다.
  • lang=ko: 언어 지정에 사용된다. 'ko'라면 "message"가 한글로 나온다. 생략하면 데이터를 가져오는데 실패한다.
  • country=KR: 나라를 지정한다. 생략해도 큰 문제는 없었다.
  • objectid=news[number]/[id값]: 가져올 댓글의 Id 값을 의미한다. 기사 본문 URL 기준 article 뒷부분이 일종의 Id 값으로 작용하고 있다.
  • pageSize=20: 한번에 가져올 데이터의 양을 의미한다. 늘릴 수 있다.
  • indexSize=10: 정확한 의미는 모르겠으나, 중요해보여서 빼지 않았다. 빼도 동작하기는 한다.
  • page: 현재 페이지 번호를 의미한다. 이 값이 있어야 페이지 모델이 정상적으로 동작할 수 있다.
  • pageType=more: 이 값이 있어야만 다음 페이지 탐색을 진행한다.
  • moreParam.next=id값: id값에 해당하는 페이지를 요청한다. 아무런 값을 지정하지 않으면 1페이지.
  • sort: 정렬 기준을 의미한다. 일부 댓글 페이지는 아래 속성 중 일부를 의도적으로 가리는데, 이게 API 수준에서는 숨겨진 정렬 기준을 선택할 수 있다. 예를 들어 순공감순이 없는 기사 댓글에 대해 sort=favorite을 설정하면 순공감순으로 댓글을 가져온다.
    • 순공감순: favorite
    • 최신순: new
    • 공감비율순: relative
    • 답글순: reply
    • 과거순: old

네이버 정렬 기준

위 쿼리에서 페이지 탐색 시 변경되는 값은 page와 moreParam.next밖에 없다. page는 페이지 모델 동작에 이용되고, moreParam.next는 ID를 기반으로 페이지를 가져올 때 사용한다. 이때 다음 페이지의 ID는 이전 페이지를 요청할 때 응답된 result.morePage.next에 담겨 있으므로, 병렬적으로 모든 페이지를 가져오는 등의 동작은 어려울 것 같다.


페이지 탐색 방법

 첫 페이지의 경우 moreParam.next 쿼리를 생략, page=1로 지정한다. 페이지 탐색에 성공하면 응답 메시지에 담긴 result.morePage.next 을 통해 다음 페이지의 Id를 알 수 있으므로, 해당 값을 이용하여 다음 페이지를 탐색한다. 다음 페이지는 moreParam.next=[이전 페이지 탐색으로 얻은 id] 정도로 얻을 수 있을 것 같다.

 첫 페이지를 탐색하면 페이로드의 pageModel을 통해 총 페이지 수를 알 수 있다. 해당 숫자를 기록해둘 수도 있지만, 페이지를 탐색하는 과정에서 댓글이 엄청 늘어나서 페이지 자체가 증가할 수 있으므로, 현재 페이지 번호와 마지막 페이지 번호를 비교하는 방식으로 처리하는 것이 좋을 것 같다. result.pageModel.page === result.pageModel.lastPage일 때 댓글 페이지 탐색을 종료하면 될 것 같다. 아래를 보면 page == lastPage이므로 탈출 조건이라고 볼 수 있다.

"result": {
  "pageModel": {
    "page": 1,
    "pageSize": 100,
    "indexSize": 10,
    "startRow": 1,
    "endRow": 38,
    "totalRows": 38,
    "startIndex": 0,
    "totalPages": 1,
    "firstPage": 1,
    "prevPage": 0,
    "nextPage": 0,
    "lastPage": 1,
    "current": null,
    "threshold": null,
    "moveToLastPage": false,
    "moveToComment": false,
    "moveToLastPrev": false
  }
}

위 정보를 기반으로 다음에는 기사 + 댓글 목록을 가져오는 프로그램을 만들어 봐야겠다.

https://github.com/blaxsior/lambda-deploy-test/blob/master/news_crawler/src/crawling/comments.ts

 분석한 내용을 기반으로 댓글을 가져오도록 구현한 자바스크립트 기반 코드이다. 본인이 진행하는 프로젝트에서는 모든 댓글을 가져오는 기능이 필요하지 않게 되어 주석 처리해두었으나, 모든 댓글을 전부 가져와야 하는 경우 getNewsComments 함수에 지정된 주석 처리를 제거하면 정상적으로 데이터를 가져올 수 있다.