본문 바로가기
카테고리 없음

네이버 자동포스팅 소스코드 참고

by 하늘정보 2025. 1. 6.
반응형
더보기

요약

1. Selenium과 Requests 비교

  • Selenium은 웹 자동화 도구로 매우 강력하지만, 성능과 유지보수의 한계가 있음.
  • Requests는 보다 효율적이고 가벼운 대안으로, 직접 HTTP 요청을 만들어 처리함.
  • Requests로 구현하려면 HTTP 요청의 동작을 철저히 분석하고 재현해야 함.

2. 목표: Requests로 네이버 블로그 글 작성 자동화

준비 과정

  1. 네트워크 요청 분석:
    • 개발자 도구 → 네트워크 탭을 활용해 서버와 주고받는 데이터를 확인.
    • 로그인, 글쓰기 등의 요청에서 필요한 데이터를 추출.
  2. 로그인 요청 분석:
    • 네이버 로그인에는 복잡한 값들이 필요하므로 Selenium 사용을 권장.
  3. 글쓰기 요청 분석:
    • "RabbitWrite.naver" 요청을 통해 데이터 페이로드와 헤더 정보를 추출.
    • 헤더에서 User-Agent, Referer, Cookie, Content-Type 등을 포함.

주요 데이터 요소

  • Document ID: 글 작성 요청 시 사용되는 고유 값.
  • Component ID: 글 제목과 본문의 고유 값.
  • Editor Source: 블로그 글 작성 시 필요한 인증 토큰.

3. 데이터 추출 과정

핵심 데이터 확보

  1. service_config 호출:
    • Se-Authorization 헤더와 함께 GET 요청으로 Document ID를 얻음.
  2. Editor Source:
    • PostWriteFormManagerOptions.naver 요청에서 Editor Source를 추출.

최종 데이터 구조

  • documentModel과 populationParams라는 두 개의 주요 데이터 구조를 사용하여 요청 구성.
  • 필요한 ID와 값을 동적으로 생성하여 요청에 포함.

4. 최종 코드 및 구현

  • 필요한 모든 데이터를 추출한 후 RabbitWrite.naver로 POST 요청을 보냄.
  • headers와 data를 구성해 요청.
  • 요청이 성공하면 블로그에 글이 작성됨.

5. 결과

  • Requests로 네이버 블로그 글 작성 자동화에 성공.
  • 성공 여부는 서버 응답에서 확인.

주요 교훈

  • Requests로 복잡한 웹 자동화를 구현하려면 철저한 분석과 데이터 추출 과정이 필수.
  • 네이버처럼 복잡한 인증 체계와 데이터 구조를 사용하는 플랫폼에서는 헤더와 페이로드 분석이 중요.

 

 

파이썬에 입문을 하고나면 셀레니움을 자주 사용하게 된다. (아님말고)

 

웹 자동화를 쉽게 할 수 있고, 안되는게 거의 없어서 아주아주 유용하고 좋긴한데 ...? !

 

여러가지 단점들이 존재하고 ..... 권태기에 빠지게 된다.

 

 

 

그렇게 새로운 사랑을 하게 되는데..... 그 대상이 바로 ..... requests !!!!!!!!!!!!!!!!!

 

전애인 (셀레니움)의 단점들을 전부 카바 한다. 후후

 

 

 

 

 

다만, 완벽해서인지 까다롭고 어렵다 ^^ ...

 

 

 

 

 

 

아무튼 블로그 첫포스팅에서 셀레니움을 사용해서 블로그 글을 작성했었는데 

 

1편:

 

[Python] 네이버 블로그 자동 포스팅 만들기 - 1

프로그램 개발 의뢰가 들어왔다.... 바로 GPT를 이용해 네이버 블로그에 자동으로 글을 올려야 한다는 의뢰 .... !!  코딩을 전문적으로 배운적이 없고 경력도 없어서 어떤게 정석인진 모르겠으

myworld1004.tistory.com

 

2편:

 

[Python] 네이버 블로그 자동 포스팅 만들기 - 2

1편:  [Python] 네이버 블로그 자동 포스팅 만들기 - 1프로그램 개발 의뢰가 들어왔다.... 바로 GPT를 이용해 네이버 블로그에 자동으로 글을 올려야 한다는 의뢰 .... !!  코딩을 전문적으로 배운적

myworld1004.tistory.com

 

3편:

 

[Python] 네이버 블로그 자동 포스팅 만들기 - 3 / openai api

1편: [Python] 네이버 블로그 자동 포스팅 만들기 - 1프로그램 개발 의뢰가 들어왔다.... 바로 GPT를 이용해 네이버 블로그에 자동으로 글을 올려야 한다는 의뢰 .... !!  코딩을 전문적으로 배운적

myworld1004.tistory.com

 

 

 

이번엔 requests로 블로그 글을 작성해보려고 한다.

 

 

 

 

 

 

 

 

 

먼저 임포트. 

우리가 사용할 requests 모듈을 불러와 준다.

 

 

import requests

 

 

 

 

 

그 다음 다른걸 하기전에 먼저 웹에서 어떤 행동들을 할때 서버와 무슨 정보를 주고 받는지를 알아내야한다.

 

탐정이 되는것.

그게 바로 requests에서의 핵심이다.

 

 

 

 

 

대부분의 사건단서는 현장에 있는 법.

 

네이버 로그인 페이지로 들어가보자.

 

 

 

 

네이버 로그인창

 

 

일단 개발자 도구 -> 네트워크 탭을 누른 상태로 로그인을 해보자

(개발자 도구 키는법은 다들 아시죠 .. ?)

 

 

 

 

로그인 버튼을 누르면 어떤 이상한 문자들이 우측 네트워크탭에 주르르르륵 뜰텐데

 

 

 

네이버 로그인 네트워크탭

 

 

로그인버튼을 누르자마자 서버에 요청한 항목인

nidlogin.login을 클릭해서 상세정보를 확인해보자.

 

딱 봐도 이름부터 로그인.로그인 인게 로그인과 관련된 네트워크 요청일 확률이 매우높다.

 

 

 

 

네트워크 페이로드

 

 

 

 

payload탭에는 이 네트워크요청을 할 때 어떤 값들을 포함해서 요청을 했는지를 알려준다.

네이버 로그인에는 위와 같은 보기만해도 머리가 아픈 값들을 매우 많이 포함해서 보내고 있다.

 

 

 

 

requests로 네이버로그인을 구현하려면 저 값들이 다 어디서 오는지 찾아내고, 어떻게 생성되는지 확인하고

그걸 그대로 구현해낸 뒤 requests를 해야 한다.

 

 

 

 

그러니까 우리는.

네이버 로그인은 그냥 셀레니움을 쓰자.

 

 

 

 

https://blog.naver.com/블로그이름?Redirect=Write&

 

 

 

위 링크로 가보자.

 

글쓰기 창이 나올텐데

 

 

 

블로그글쓰기

 

 

제목과 본문을 대충 써놓고 발행을 해보자.

물론 개발자도구, 네트워크 탭을 켜 놓은 상태여야한다 !

 

 

 

 

 

 

블로그네트워크

 

 

 

발행버튼을 누른 순간 가장 먼저 나온애들중

 

RabbitWrite.naver가 눈에 띈다. 이름에 write가 들어갔으니 글을 쓸때 중요한 요청인가보다.

 

 

 

 

 

 

페이로드를 봤더니 아니나 다를까, 글쓸때 필요했던 정보들을 가공해서 서버에 전송하는 게 틀림없다.

 

 

 

 

 

 

 

헤더탭

 

 

 

헤더탭도 확인하자.

서버에 requests를 때릴때 헤더탭에 있는 정보를 넘겨주지 않으면

프로그램이라고 판단하고 거부하는 경우가 많고,

 

어떤 요청들은 헤더에 토큰을 넣어서 보내거나 하는 경우가 있어서 꼭 살펴봐야한다.

 

 

 

 

다행히 블로그글을 쓸때는 어떤 비밀스런 값들을 헤더에 넣고 보내지 않아도 되는거 같다.

 

 

우리가 필요한건

 

User-Agent, Referer, Cookie, Content-Type

이 4개다.

 

 

 

 

 

 

url은 Request Url, 메소드는 아래 POST다.

 

 

 

{
    "documentId": "",
    "document": {
        "version": "2.8.0",
        "theme": "default",
        "language": "ko-KR",
        "id": "01J0AES1E2798A61B1K77YV6W1",
        "di": {
            "dif": false,
            "dio": [{
                "dis": "N",
                "dia": {
                    "t": 0,
                    "p": 0,
                    "st": 74,
                    "sk": 11
                }
            }, {
                "dis": "N",
                "dia": {
                    "t": 0,
                    "p": 0,
                    "st": 74,
                    "sk": 11
                }
            }]
        },
        "components": [{
            "id": "SE-2db1acb9-6090-4823-830c-5bdda26959a0",
            "layout": "default",
            "title": [{
                "id": "SE-8f71fff0-a222-4ffb-94a6-37906c870c46",
                "nodes": [{
                    "id": "SE-43692b55-c4c4-485a-831f-4173c87b14de",
                    "value": "제목스",
                    "@ctype": "textNode"
                }],
                "@ctype": "paragraph"
            }],
            "subTitle": null,
            "align": "left",
            "@ctype": "documentTitle"
        }, {
            "id": "SE-01c5f6c7-a5e3-4863-bc5e-614c39add7de",
            "layout": "default",
            "value": [{
                "id": "SE-32743609-0626-4275-ab62-341b38adc42b",
                "nodes": [{
                    "id": "SE-28d2d6a5-56cd-441a-896f-92e05e84a7de",
                    "value": "본문스",
                    "@ctype": "textNode"
                }],
                "@ctype": "paragraph"
            }],
            "@ctype": "text"
        }]
    }
}

 

 

우리가 보내야할 data값중 일부이다.

처음 document의 id값과 components안에 있는 id 값들이 수상하다.

뭔가 변수들일 느낌이 팍 온다.

 

 

 

 

두번째 id값은 uuid에 SE-를 붙인거 같고,

첫번째 id값은 아마 글쓰기 요청 이전에 생성된 값 같다.

이런건 보통 이전 네트워크 요청 흔적들에서 찾아낼 수 있다.

 

 

 

 

{
    "configuration": {
        "openType": 2,
        "commentYn": true,
        "searchYn": true,
        "sympathyYn": true,
        "scrapType": 2,
        "outSideAllowYn": true,
        "twitterPostingYn": false,
        "facebookPostingYn": false,
        "cclYn": false
    },
    "populationMeta": {
        "categoryId": 1,
        "logNo": null,
        "directorySeq": 14,
        "directoryDetail": null,
        "mrBlogTalkCode": null,
        "postWriteTimeType": "now",
        "tags": "",
        "moviePanelParticipation": false,
        "greenReviewBannerYn": false,
        "continueSaved": false,
        "noticePostYn": false,
        "autoByCategoryYn": false,
        "postLocationSupportYn": false,
        "postLocationJson": null,
        "prePostDate": null,
        "thisDayPostInfo": null,
        "scrapYn": false,
        "autoSaveNo": 1718337971504
    },
    "editorSource": "AkX6PyIpc6c+bp5MRWKyfg=="
}

 

이것도 우리가 보내야할 데이터인데

autoSaveNo, editorSource 이 두개가 범인같다.

 

이것도 역시 이전 네트워크 요청들에서 찾아낼 수 있다.

 

 

 

 

 

{
    "configuration": {
        "openType": 2,
        "commentYn": true,
        "searchYn": true,
        "sympathyYn": true,
        "scrapType": 2,
        "outSideAllowYn": true,
        "twitterPostingYn": false,
        "facebookPostingYn": false,
        "cclYn": false
    },
    "populationMeta": {
        "categoryId": 1,
        "logNo": null,
        "directorySeq": 14,
        "directoryDetail": null,
        "mrBlogTalkCode": null,
        "postWriteTimeType": "now",
        "tags": "",
        "moviePanelParticipation": false,
        "greenReviewBannerYn": false,
        "continueSaved": false,
        "noticePostYn": false,
        "autoByCategoryYn": false,
        "postLocationSupportYn": false,
        "postLocationJson": null,
        "prePostDate": null,
        "thisDayPostInfo": null,
        "scrapYn": false,
        "autoSaveNo": 1718337971504
    },
    "editorSource": "AkX6PyIpc6c+bp5MRWKyfg=="
}

 

이것도 우리가 보내야할 데이터인데

autoSaveNo, editorSource 이 두개가 범인같다.

 

이것도 역시 이전 네트워크 요청들에서 찾아낼 수 있다.

네이버 블로그에 requests를 이용해서 게시물을 쓰고 싶었다.
 
사실 계속 해봐도 
 

{"isSuccess":false,"result":{"errorCode":"invalid parameter","errorMessage":""}}

 
 
이거만 리턴을 받아서 ....................
 
 
 
휴... 티스토리 글 빨리 써야하는데 ㅡㅡㅡ .. 하던 오늘
 
 
 

Success = True 반환

 
 
 
글 작성에 성공했다 ... !!!
 
블로그 확인해보니 실제로 글이 잘 작성되어 있었다.
 
 
 
 
 
 
 
autoSaveNo, editorSource, document의 id값과 components안에 있는 id 값
 
두번째 id값은 uuid에 SE-를 붙인거 같고,
첫번째 id값은 아마 글쓰기 요청 이전에 생성된 값 같다.
 
 
 
1편에서 중요할거 같다는 정보들이였다.
 
생각보다 안 중요한애들도 있었다..
 
 
 

data = {
    "blogId": 블로그아이디(네이버아이디X),
    "documentModel": documentModel,
    "populationParams": populationParams
}

 
 
우리가 보낼 데이터 형식은 위와 같다.
 
 

documentModel = {
        "documentId": "",
        "document": {
            "version": "2.8.0",
            "theme": "default",
            "language": "ko-KR",
            "id": "의문의값",
            "components": [
                {
                    "id": "SE-e1ed2dc4-208e-47f0-b7f6-c69aafd4ca33",
                    "layout": "default",
                    "title": [
                        {
                            "id": "SE-9ed3e56c-433e-42a7-921a-b578de4cdb5f",
                            "nodes": [
                                {
                                    "id": "SE-65c8b39a-7278-4fde-bf5b-836e7e0ab508",
                                    "value": "vvv",
                                    "@ctype": "textNode"
                                }
                            ],
                            "@ctype": "paragraph"
                        }
                    ],
                    "subTitle": None,
                    "align": "left",
                    "@ctype": "documentTitle"
                },
                {
                    "id": "SE-7f83e8c8-b8c2-4a3b-b3c4-57fd8b7890c9",
                    "layout": "default",
                    "value": [
                        {
                            "id": "SE-e991ad7d-5627-450f-b0f9-9d9017a21ed2",
                            "nodes": [
                                {
                                    "id": "SE-287ea9fd-04af-41d3-b902-5df1a8c5908c",
                                    "value": "zz",
                                    "@ctype": "textNode"
                                }
                            ],
                            "@ctype": "paragraph"
                        }
                    ],
                    "@ctype": "text"
                }
            ],
            "di": {
                "dif": False,
                "dio": [
                    {
                        "dis": "N",
                        "dia": {"t": 0, "p": 0, "st": 18, "sk": 3}
                    },
                    {
                        "dis": "N",
                        "dia": {"t": 0, "p": 0, "st": 18, "sk": 3}
                    }
                ]
            }
        }
    }

 
documentModel의 구조는 이러한데, 우린 저기 의문의 값을 먼저 구해보자.
 
 
 
 
 

의문의값 있는곳

 
 
의문의값은 service_config
 

https://platform.editor.naver.com/api/blogpc001/v1/service_config

 
위 주소에서 받아올 수 있다 .
 
 
 
 
 

id값

 
 
위에 빨간색 해놓은 부분이 바로 의문의값인데, 
 
 
 
 
 
 
 

 
 
그냥 Get 요청이다. 파라미터도 없고 아무것도 없다.
 
 
의외로 간단하네 했지만 전편에서도 그랬듯 헤더탭을 자세히 봐야한다.
 
 
 
 
 
 

인증값

 
 
 
 
평소에는 없던 Se-Authorization 이라는 항목도 헤더에 넣어서 보내줘야하는걸 확인 했다.
 
 
 
Authorization 는 인증, 허가 이런 뜻인데 저게 있어야만 요청이 될 거 같다는 느낌이 확 온다.
 
 
 
 
 

Authorization

 
 
 
저 값을 그대로 복사해서 컨트롤 F 해서 찾아보면 이 값이 어디서부터 시작됐는지 알 수 있다.
( 이렇게해서 알게되면 진짜 럭키비키고 .... 없는 경우도 허다하다 )
 
 
 
 
 
 
 

토큰

 
 
 
service_config가 호출되기 전 PostWriteFormSeOptions.naver 라는 요청에서 token이 발급되는데
 
그 토큰 값이 service_config를 호출할때 필요한 Se-Authorization 값이라는걸 우린 알게되었다.
 
 
 
 
 
 

def token():
    url = 'https://blog.naver.com/PostWriteFormSeOptions.naver'
    param = {
        'blogId': 블로그아이디,
        'categoryNo': 카테고리넘버
    }
    header = {
        'Referer': 'https://blog.naver.com/PostWriteForm.naver?blogId=블로그아이디&Redirect=Write&categoryNo=카테고리넘버&redirect=Write&widgetTypeCall=true&from=section&topReferer=https%3A%2F%2Fsection.blog.naver.com%2FBlogHome.naver%3FdirectoryNo%3D0%26currentPage%3D1%26groupId%3D0&trackingCode=blog_sectionhome_pc&directAccess=false',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Whale/3.27.254.15 Safari/537.36'
    }

    a = r.get(url=url,headers=header,params=param,cookies=cookies)
    return a.json()['result']['token']

def service_config():
    url = 'https://platform.editor.naver.com/api/blogpc001/v1/service_config'
    header = {
        'Referer': 'https://blog.naver.com/PostWriteForm.naver?blogId=블로그아이디&Redirect=Write&categoryNo=카테고리넘버&redirect=Write&widgetTypeCall=true&from=section&topReferer=https%3A%2F%2Fsection.blog.naver.com%2FBlogHome.naver%3FdirectoryNo%3D0%26currentPage%3D1%26groupId%3D0&trackingCode=blog_sectionhome_pc&directAccess=false',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Whale/3.27.254.15 Safari/537.36',
        'Se-Authorization': token()
    }

    a = r.get(url=url,headers=header,cookies=cookies)
    return a.json()['editorInfo']['id']

 
 
 
 
 
 
service_config를 호출하면 id값이 나오게된다 !
 
 
 
 
 
자 의문의 값은 구했고
 
 
 
 

populationParams = {
        "configuration": {
            "openType": 2,
            "commentYn": True,
            "searchYn": True,
            "sympathyYn": True,
            "scrapType": 2,
            "outSideAllowYn": True,
            "twitterPostingYn": False,
            "facebookPostingYn": False,
            "cclYn": False
        },
        "populationMeta": {
            "categoryId": 1,
            "logNo": None,
            "directorySeq": 0,
            "directoryDetail": None,
            "mrBlogTalkCode": None,
            "postWriteTimeType": "now",
            "tags": "첫글",
            "moviePanelParticipation": False,
            "greenReviewBannerYn": False,
            "continueSaved": False,
            "noticePostYn": False,
            "autoByCategoryYn": False,
            "postLocationSupportYn": False,
            "postLocationJson": None,
            "prePostDate": None,
            "thisDayPostInfo": None,
            "scrapYn": False
        },
        "editorSource": 의문의값
}

 
 
다음은 populationParams에 의문의값을 구해보자.
이건 블로그에서 발행을 누르면 나오는 작은 화면(?)에 설정값들이다.
 
아무튼 에디터 소스를 구해보자.
 
 
 
 

에디터소스

 
 
 
 
에디터 소스는 아까 슬쩍 들여본 PostWriteFormSeOptions.naver의 바로 다음 호출에서 찾을 수 있다.
 
 
 
 
 
 

def editorSource():
    url = 'https://blog.naver.com/PostWriteFormManagerOptions.naver'
    param = {
        'blogId': 블 로 그 아 디,
        'categoryNo': 카테고리넘버
    }
    header = {
        'Referer': 'https://blog.naver.com/PostWriteForm.naver?blogId=블로그아디&Redirect=Write&categoryNo=1&redirect=Write&widgetTypeCall=true&from=section&topReferer=https%3A%2F%2Fsection.blog.naver.com%2FBlogHome.naver%3FdirectoryNo%3D0%26currentPage%3D1%26groupId%3D0&trackingCode=blog_sectionhome_pc&directAccess=false',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Whale/3.27.254.15 Safari/537.36'
    }
    a = r.get(url=url,headers=header,params=param,cookies=cookies)
    return a.json()['result']['formView']['editorSource']

 
사실 url만 다르고 token구하는거랑 똑같다.
 
 
 
 
자 의문의 값들 다 구했다.
 
 
 

def post():
    url = "https://blog.naver.com/RabbitWrite.naver"

    headers = {
        "content-type": "application/x-www-form-urlencoded",
        'Referer': 'https://blog.naver.com/PostWriteForm.naver?blogId=블로그아이디&Redirect=Write&categoryNo=1&redirect=Write&widgetTypeCall=true&topReferer=https%3A%2F%2Fwww.naver.com%2F&trackingCode=naver_etc&directAccess=false',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Whale/3.27.254.15 Safari/537.36'
    }

    data = {
        "blogId": 블로그아이디,
        "documentModel": '{"documentId":"","document":{"version":"2.8.0","theme":"default","language":"ko-KR","id":"' + service_config() + '","components":[{"id":"SE-e1ed2dc4-208e-47f0-b7f6-c69aafd4ca33","layout":"default","title":[{"id":"SE-9ed3e56c-433e-42a7-921a-b578de4cdb5f","nodes":[{"id":"SE-65c8b39a-7278-4fde-bf5b-836e7e0ab508","value":"ㅇ","@ctype":"textNode"}],"@ctype":"paragraph"}],"subTitle":null,"align":"left","@ctype":"documentTitle"},{"id":"SE-7f83e8c8-b8c2-4a3b-b3c4-57fd8b7890c9","layout":"default","value":[{"id":"SE-e991ad7d-5627-450f-b0f9-9d9017a21ed2","nodes":[{"id":"SE-287ea9fd-04af-41d3-b902-5df1a8c5908c","value":"ㅇ","@ctype":"textNode"}],"@ctype":"paragraph"}],"@ctype":"text"}],"di":{"dif":false,"dio":[{"dis":"N","dia":{"t":0,"p":0,"st":18,"sk":3}},{"dis":"N","dia":{"t":0,"p":0,"st":18,"sk":3}}]}}}',
        "populationParams": '{"configuration":{"openType":2,"commentYn":true,"searchYn":true,"sympathyYn":true,"scrapType":2,"outSideAllowYn":true,"twitterPostingYn":false,"facebookPostingYn":false,"cclYn":false},"populationMeta":{"categoryId":1,"logNo":null,"directorySeq":0,"directoryDetail":null,"mrBlogTalkCode":null,"postWriteTimeType":"now","tags":"첫글","moviePanelParticipation":false,"greenReviewBannerYn":false,"continueSaved":false,"noticePostYn":false,"autoByCategoryYn":false,"postLocationSupportYn":false,"postLocationJson":null,"prePostDate":null,"thisDayPostInfo":null,"scrapYn":false},"editorSource":"' + editorSource() + '"}',
        "productApiVersion": "v1"
    }

    a = r.post(url=url, headers=headers, data=data, cookies=cookies)
    print(a.text)

 
 
 
 
 
 
 

성공

 

 

반응형

/* */