본문 바로가기
IT

싸이월드 클럽 네이버카페로 이전

by aerobody 2020. 2. 1.
반응형

 

cyworld-club-exporter-master.zip
0.04MB

https://bab2min.tistory.com/547

 

[PHP] 네이버 카페 API를 이용한 싸이월드 클럽 => 네이버 카페 이전 후기

불과 10여 년 전까지만 해도 대학생 과 커뮤니티 100 중 99는 싸이월드 클럽을 사용했습니다. 과연 싸이월드의 전성기라고 할 수 있었죠. 하지만 시대가 변해 SNS가 등장하고 싸이월드가 망하면서 싸이클럽 역시..

bab2min.tistory.com

 

[PHP] 네이버 카페 API를 이용한 싸이월드 클럽 => 네이버 카페 이전 후기

 

과 10여 년 전까지만 해도 대학생 과 커뮤니티 100 중 99는 싸이월드 클럽을 사용했습니다. 과연 싸이월드의 전성기라고 할 수 있었죠. 하지만 시대가 변해 SNS가 등장하고 싸이월드가 망하면서 싸이클럽 역시 쇠락의 길로 접어들었습니다. 지금은 대부분의 대학생 커뮤니티가 페이스북을 기반으로 하고 있다고 알고있으며, 일부 카페를 사용한다고 하는데요, 페이스북 그룹의 특징 상 게시물을 카테고리화해서 분류하기가 어렵고, 과거 자료를 열람하는 것이 매우 불편합니다. 그래서 과 커뮤니티를 네이버 카페로 옮기기로 했습니다.

싸이월드 클럽 크롤러

문제는 02년부터 최근까지 10년 넘게 축적된 싸이클럽의 데이터를 어떻게 카페로 옮길 것인가! 였습니다. 만 개가 넘는 게시물을 어떻게 손으로 직접 옮길수는 없고, 프로그램을 이용해야할텐데 딱히 좋은 방법이 없더라구요. 다행히도 예전에 monoless 형님이 만들어 공개하준 싸이클럽 추출기(https://github.com/monoless/cyworld-club-exporter)가 있어서 이걸 포크해서 기능을 개선해서 https://github.com/bab2min/cyworld-club-exporter 를 만들었습니다. node js 기반으로 작성된 코드고, 이를 실행하여 싸이클럽에서 게시물은 JSON 형태로 가져오고 첨부된 파일과 사진을 모두 다운받아 저장했습니다.

 

네이버 카페 API

이제 네이버 카페에 자동으로 이 게시물을 올려야하는데, 이는 네이버에서 제공하는 카페 API를 이용하기로 했습니다. 사실 카페 API가 좀 부실하긴 합니다. 제공하는게 카페 가입과 글쓰기인데, 글쓰기는 말머리 선택이나 첨부파일 삽입 등이 안되고, 댓글도 달 수가 없습니다. 어쩔 수 없이 게시물 내에 댓글이나 기타 메타데이터 정보를 다 꾸겨넣는 형태로 이전을 하기로 결정했습니다.

네이버 API를 사용하려면 웹 서버가 필요하기에 PHP 위에서 돌리기로 하고 코드를 작성했습니다. 당연히 버전은 PHP 7.0입니다. 따라서 그 이하 버전의 PHP에서는 아래 코드가 정상적으로 작동되지 않을 수도 있습니다.

 

 

먼저 인증은 OAuth2.0 방식으로 수행됩니다. 제일 처음 인증을 요청하려면 다음과 같이 하면 됩니다.

<request.php>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

<?php

class OAuthRequest

{

    var $client_id;

    var $redirect_url;

    var $state;

    var $session;

    var $authorize_url = "https://nid.naver.com/oauth2.0/authorize";

 

    function __construct( $client_id, $redirect_url) {

        $this->client_id = $client_id;

        $this->redirect_url = $redirect_url;

    }

    function start_session (){

        session_start();

    }

    function generate_state() {

        $mt = microtime();

        $rand = mt_rand();

        $this->state = md5( $mt . $rand );

    }

    function set_state() {

        $this->generate_state();

        $_SESSION['state'] = $this->state;

    }

    function get_request_url() {

        return $this->authorize_url . "?response_type=code&client_id=" . $this->client_id . "&state=" . $this->state . "&redirect_url=" . urlencode($this->redirect_url);

    }

}

 

$request = new OAuthRequest( CLIENT ID, CALLBACK URL);

$request->start_session();

$request->set_state();

$request->get_request_url();

header('Location: '. $request->get_request_url() );

?>

 

request.php에 접속하면 네이버 로그인 창이 연결됩니다. 로그인이 성공하면 해당 앱의 작동을 허용할것인지 물어보구요, 여기서 승락을 하면 callback url로 넘어가게 됩니다.

 

callback url 측에서는 다음과 같이 처리해주면 되구요.

<callback.php>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

<?php

  $client_id = CLIENT ID;

  $client_secret = CLIENT SECRET;

  $code = $_GET["code"];

  $state = $_GET["state"];

  $redirectURI = urlencode(CALLBACK URL);

  $url = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=".$client_id."&client_secret=".$client_secret."&redirect_uri=".$redirectURI."&code=".$code."&state=".$state;

  $is_post = false;

  $ch = curl_init();

  curl_setopt($ch, CURLOPT_URL, $url);

  curl_setopt($ch, CURLOPT_POST, $is_post);

  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

  $headers = array();

  $response = curl_exec ($ch);

  $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

  curl_close ($ch);

  if($status_code == 200) {

    session_start();

    $_SESSION['acc'] = json_decode($response);

    print_r($response);

  } else {

    echo "Error 내용:".$response;

    session_destroy();

  }

?>

 

넘겨받은 code와 state를 이용해 다시 네이버에 요청해 access_token을 받게 됩니다. 이 access_token이 있어야 카페 API를 비롯한 모든 로그인 API에 접속가능하게 됩니다.

(위의 code들은 네이버 개발자 페이지에서 가져와서 수정했습니다.)

 

 

multipart/form-data로 이미지 첨부를 처리하는게 까다롭더라구요. 카페 API를 편리하게 이용하기 위해 클래스를 만들었습니다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

<?php

class NaverCafe{

    private $access_token = "";

    function __construct($access_token) {

        $this->access_token = $access_token;

    }

     

    static function encodeInput($input) {

        return urlencode(str_replace('"', '\"', $input));

    }

     

    function write($clubid, $menuid, $subject, $content, $images = []) {

        $url = "https://openapi.naver.com/v1/cafe/".$clubid."/menu/".$menuid."/articles";

        $postfields = array("subject"=>urlencode($subject), "content"=>self::encodeInput($content), "metoo"=>"false");

        $ch = curl_init();

         

        static $disallow = array("\0", "\"", "\r", "\n");

         

        foreach ($postfields as $k => $v) {

            $k = str_replace($disallow, "_", $k);

            $body[] = implode("\r\n", array(

                "Content-Disposition: form-data; name=\"{$k}\"",

                "",

                filter_var($v),

            ));

        }

 

        foreach ($images as $v) {

            switch (true) {

                case false === $v = realpath(filter_var($v)):

                case !is_file($v):

                case !is_readable($v):

                    continue;

            }

            $data = file_get_contents($v);

            $body[] = implode("\r\n", array(

                "Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"",

                "Content-Type: application/octet-stream",

                "",

                $data,

            ));

        }

         

        do {

            $boundary = "---------------------" . md5(mt_rand() . microtime());

        } while (preg_grep("/{$boundary}/", $body));

         

        array_walk($body, function (&$part) use ($boundary) {

            $part = "--{$boundary}\r\n{$part}";

        });

         

        $body[] = "--{$boundary}--";

        $body[] = "";

         

        curl_setopt_array($ch, array(

            CURLOPT_POST       => true,

            CURLOPT_POSTFIELDS => implode("\r\n", $body),

            CURLOPT_URL => $url,

            CURLOPT_HTTPHEADER => array(

                "Authorization: Bearer ".$this->access_token,

                "Expect: 100-continue",

                "Content-Type: multipart/form-data; boundary={$boundary}", // change Content-Type

            ),

            CURLOPT_RETURNTRANSFER => 1

        ));

        $response = curl_exec($ch);

        curl_close ($ch);

        return json_decode($response);

    }

     

};

?>


access_token을 이용해 NaverCafe 클래스를 생성하고, write 메서드를 호출해서 카페에 게시물을 작성하는것이죠. write의 파라메터 중 $images는 첨부할 이미지 파일들의 경로를 배열에 넣어주어야합니다. 첨부할 이미지가 없다면 빈 배열을 넣어주면 됩니다.

첨부파일은... 아직 업로드할 방법을 찾아내지 못했네요. 일단 이미지의 비중이 크니 이미지부터 업로드시키고 나중에 생각해보렵니다.

JSON으로 받은 게시물은 MariaDB에 SQL로 입력하고, 첨부 사진/파일들은 서버에 업로드했습니다. 첨부 파일이 3기가가 넘는 관계로 업로드에 4시간이 넘게 걸렸어요... 네이버 카페로 이전된 게시물은 데이터베이스 측에서 체크하도록 하고, 오류가 발생한 게시물은 오류 내용까지 넣어두도록 했습니다. 

문제는..... 네이버의 게시물 작성 정책이었는데, 하루에 ID/IP당 게시물 작성 갯수를 200개로 한도를 두었습니다. 그리고 짧은 시간에 더 자주 업로드할수록 제한을 강하게 거는듯하구요. 1만개가 넘는 게시물을 이렇게 업로드하려면 50일이 넘게 걸려요. 또 access_token 만료기한이 1시간이기 때문에 1시간마다 재인증을 해줘야하니, 프로그램을 계속 돌리려해도 하루에 한 번씩, 최소 50번은 재인증을 해줘야하구요. 일단 다중계정으로 해결해보고자 합니다. 과연 이 작업이 언제 끝날지 심히 걱정되긴 하네요...ㅋㅋㅋㅋ

 

https://studyforus.com/share/621104

 

[Python] 싸이월드 미니홈피 백업 스크립트 - Study For Us

안녕하세요?humit 님의 도움으로 싸이월드 미니홈피 백업 스크립트를 허접하게나마 완성했습니다.원래 의도는 최대한 selenium을 사용하지 않고 작성하는 것이었지만'더 보기'를 클릭하는 부분 등 여러 부분에서 java...

studyforus.com

 

안녕하세요?

humit 님의 도움으로 싸이월드 미니홈피 백업 스크립트를 허접하게나마 완성했습니다.

원래 의도는 최대한 selenium을 사용하지 않고 작성하는 것이었지만

'더 보기'를 클릭하는 부분 등 여러 부분에서 javascript가 사용된 관계로 부득이 selenium에 많은 부분을 의존하였습니다.

짧은 시간에 완성하다보니 부족한 점이 많은 스크립트이지만

싸이월드 도메인 만료를 앞두고 급히 필요하신 분들이 계실 것 같다는 생각에 일단 올립니다.

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

from bs4 import BeautifulSoup

from selenium import webdriver

import urllib, time, os

import urllib.request

from urllib.parse import unquote

 

driver = webdriver.Firefox(executable_path='geckodriver')

driver.implicitly_wait(15)

waiting_time = 3 # time.sleep()의 대기시간을 지정합니다.

 

def parse():

    html = driver.page_source

    soup = BeautifulSoup(html, 'html.parser')

    return soup

 

def naming(filename, extension):

    full_filename = filename + extension

    add_number = 1

    while os.path.isfile(path + '\\' + full_filename) == True: # 파일명이 중복되는 경우를 처리합니다.

        full_filename = filename + '-' + str(add_number) + extension

        add_number += 1

    full_path = path + '\\' + full_filename

    return full_path

 

url = '미니홈피 URL을 입력하세요!!!'

driver.get(url)

time.sleep(waiting_time)

if driver.current_url == 'http://www.cyworld.com/error/error.html' or driver.title == '싸이홈 | 에러메세지':

    print("Page doesn't exist.")

    quit()

 

# '더보기'를 마지막까지 반복 실행합니다.

while True:

    soup = parse()

    button = soup.find('p', attrs = {'class' : 'btn_list_more'})

    if button['style'] == '':

        driver.execute_script("getPostList('more','');")

        time.sleep(1)

    else:

        break

 

# 미니홈피의 작성자 이름으로 폴더를 생성합니다.

path = os.getcwd() + '\\' + soup.select_one('input#homenm')['value']

if not os.path.exists(path):

    print('New directory "%s" was created.' % path)

    os.mkdir(path)

 

count, count_image, count_swf = 0, 0, 0

while True:

    try:

        driver.execute_script("viewDetail(" + str(count) + ",'Y');") # 게시글로 이동합니다.

        time.sleep(waiting_time)

        frame = driver.find_element_by_xpath('/html/body/div[1]/article[2]/iframe')

        driver.switch_to.frame(frame) # 게시글의 프레임으로 이동합니다.

        time.sleep(1)

        soup = parse()

        date = soup.select_one('div.view1 p') # 태그에 작성일이 들어있습니다.

        # 게시글을 작성한 날짜를 추출하여 파일명으로 지정합니다.

        unwanted = date.find('strong')

        unwanted.extract() # p 태그 내부의 strong 태그(글 작성자)를 제거합니다.

        filename = date.text.replace('\n', '').split()[0].replace('.', '-')

        if soup.select_one('figure img') != None: # 이미지 파일을 처리합니다.

            try:

                image = soup.select_one('figure img')

                extension = '.' + image['src'].split('.')[-1] # 확장자를 추출합니다.

                full_path = naming(filename, extension)

                urllib.request.urlretrieve(unquote(image['src']), full_path)

                if os.path.getsize(full_path) < 1024: # 1kb 이하의 파일인 경우 삭제합니다.

                    os.remove(full_path)

                else:

                    count_image += 1

            except:

                pass

        if soup.select_one('div.webPage object param') != None: # swf 파일을 처리합니다.

            try:

                image = soup.select_one('div.webPage object param')

                extension = '.' + image['value'].split('.')[-1] # 확장자를 추출합니다.

                full_path = naming(filename, extension)

                req = urllib.request.Request(image['value'])

                req.add_header('Referer', url)

                sourcecode = urllib.request.urlopen(req).read()

                with open(full_path, 'wb') as f:

                    f.write(sourcecode)

                if os.path.getsize(full_path) < 1024: # 1kb 이하의 파일인 경우 삭제합니다.

                    os.remove(full_path)

                else:

                    count_swf += 1

            except:

                pass

        driver.switch_to.default_content() # 원래의 프레임으로 돌아갑니다.

        time.sleep(1)

        count += 1

    except:

        break

 

print('Total downloaded images : %d files' % count_image)

print('Total downloaded flashes : %d files' % count_swf)

driver.quit()

 

참고로 예전에 올려놓은 이미지가 뜨지 않는 경우도 있더군요.

(이미지 링크가 깨진 것인지, 싸이월드 측에서 백업하는 과정에서 파일 자체가 삭제된 것인지는 모르겠습니다)

이런 경우에 이미지 파일이 흰색으로 뜨는 경우가 있어서 파일 사이즈를 확인한 후 삭제하도록 처리했습니다.

(사이즈보다는 RGB로 판단하는 것이 정확하겠지만, swf 파일의 경우에 어떻게 처리해야 되는지 모르겠네요)

그리고 78~80행에서 swf 파일을 다운로드 하려면 referer가 필요한데 이 부분에서 막혀서 고생했네요.

이 부분에 결정적 도움을 주신 humit 님께 다시 한 번 감사드립니다.

최대한 안정적으로 구동하게 하려고 노력했는데 그 결과 테스트 결과 무난하게 구동되지만

스크립트 자체만 놓고보면 전반적으로 여러 군데에 군더더기가 있네요 ㅠㅠ

함수로 처리해서 좀 더 간결하게 대략 80행 정도로 작성하면 좋겠지만, 제가 실력도 없고 요새 시간이 부족했습니다.

이것으로 추억의 싸이월드도 굿바이네요~

추억을 오래 간직하려면 역시 아마존 라이트세일을 활용해야 되는 것일까요? ㅎㅎ

그럼 스포어 회원님들께서도 감기 조심하시고 굿밤 되세요! ^-^

 

https://hurderella.tistory.com/126

 

파이썬(Python) - 게시물 페이지에서 이미지를 다운받아 저장하기.

포스팅하기에 앞서.. 6월부터 조금조금씩 쓰기 시작해서 벌써 11월이다. 나의 게으름에 이마를 탁친다.. 이제 여덟번째 포스팅... 조금만 더 부지런 떨어도 일주일에 1개, 한달에 4개, 두달이면 다될 것을 5개월이..

hurderella.tistory.com

 

비타맘 : 네이버 포스트

아름답고 건강하게, 비타맘

m.post.naver.com

 

베지사 스파클링 프리미엄 : 비타맘

VegeSA Sparkling 뷰티푸드 베타카로틴 명일엽 유산균 식이섬유다이어트 채식 기미 기미주근개제거 이너뷰티 신선초 효소

smartstore.naver.com

 

쉭앤칙 하지원 퀸즈밤 라벤다 티트리 멀티밤 : 비타맘

쉭앤칙 하지원 퀸즈밤 라벤다 티트리 멀티밤

smartstore.naver.com

 

컷앤블럭 식욕억제제 칼로리컷팅제 2주 3주 4주 복부 단기간 다이어트 보조제 식품 : 비타맘

[비타맘] Beauty & Health by vitamam 아름답고 건강하게, 비타맘

smartstore.naver.com

 

키즈플러스업 성장기 어린이 멀티비타민미네랄 3개월분 코코아맛 비타민A 비타민B12 아연 : 비타맘

키즈플러스업 성장기 어린이 멀티비타민미네랄 비타민A 비타민B12 아연 꼭 필요한 영양소만 골라 담았습니다. 아이들이 먼저 찾는 맛있고 건강한 코코아맛 츄어블 비타민.

smartstore.naver.com

 

어린이홍삼 비타진생키즈 (30포) : 비타맘

어린이홍삼 어린이건강식품 어린이건강 컴파운드k 컴파운드케이 진세노사이드 초등학생 냠냠맛있는 면역강화 키즈홍삼

smartstore.naver.com

 

비티진 사포닌 한뿌리 (10g x 30포) : 비타맘

[비타맘] Beauty & Health by vitamam 아름답고 건강하게, 비타맘

smartstore.naver.com

 

컴파운드케이 비티진 더씨케이 오리지널 : 비타맘

컴파운드케이 컴파운드k 원기회복 면역력홍삼 사포닌 진세노사이드 명절선물 시댁선물 설날선물 추석선물

smartstore.naver.com

 

반응형