๐Ÿฆ„

๋กœ์ŠคํŠธ์•„ํฌ ๋Œ€๊ธฐ์—ด ์นด์นด์˜คํ†ก ์•Œ๋ฆผ๋ด‡ ๊ฐœ๋ฐœ๊ธฐ - 2

์„œ๋น„์Šค ๋งํฌ : (https://pf.kakao.com/_dcPGj)
Github ๋งํฌ : (https://github.com/suitelab/lostark-wait-notifier)

์ด๋ฏธ์ง€

๊ธฐ์กด ์„œ๋น„์Šค๊ฐ€ loaq์ธก์˜ ์š”์ฒญ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋กค๋งํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜์—ˆ๋‹ค.
~~์„œ๋น„์Šค๊ฐ€ ์ฃผ๊ฑฐ๋ฒ„๋ ค๋”ฐ..~~
์„œ๋น„์Šค์˜ ์นœ๊ตฌ์ˆ˜๋Š” ์ ์  ๋Š˜์–ด๊ฐ€๋Š”๋ฐ ์ œ๋Œ€๋กœ ์šด์˜ํ•˜์ง€ ๋ชปํ•ด์„œ ์ฃ„์†ก์Šค๋Ÿฌ์šธ ๋”ฐ๋ฆ„์ด์—ˆ๊ณ , ์ด๋ ‡๊ฒŒ ์ˆ˜์š”๊ฐ€ ๋งŽ์€ ์„œ๋น„์Šค๋ฅผ ๊ผญ ๋‹ค์‹œ ์šด์˜ํ•ด๋ด์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์„ ๊ฐ–๊ณ ์žˆ์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์„œ๋น„์Šค๋ฅผ ์ค‘๋‹จํ•˜๊ณ  ์žˆ๋Š” ์™€์ค‘์— (https://rubystarashe.github.io/lostark/)(์ดํ•˜ ๋Œ€๊ธฐ์—ด์„œ๋น„์Šค)๋ฅผ ์šด์˜ํ•˜๊ณ  ๊ณ„์‹ 
๋ฃจ๋น„์Šคํƒ€๋‹˜์ด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์„ ํ˜ผ์พŒํžˆ ํ—ˆ๋ฝํ•ด์ฃผ์…”์„œ ๋‹ค์‹œ ์šด์˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์ƒํ•˜์˜€๋‹ค.

  1. ๋Œ€๊ธฐ์—ด ์„œ๋น„์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋กค๋งํ•œ๋‹ค.
  2. ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜์—ฌ DB์— ์ €์žฅํ•œ๋‹ค.
  3. ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  4. ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์‹œ ๋งˆ๋‹ค DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ kakao ํ”Œ๋Ÿฌ์Šค์นœ๊ตฌ api๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ํ•œ๋‹ค.

1. ๋Œ€๊ธฐ์—ด์„œ๋น„์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํฌ๋กค๋งํ•œ๋‹ค.

~~ํฌ๋กค๋Ÿฌ์˜ ๊ฒฝ์šฐ selenium๋ฅผ ์ž์ฃผ ์จ๋ดค์–ด์„œ ๊ณ ๋ คํ•ด๋ดค์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋Š” selenium ํŠน์„ฑ์ƒ request๋‹น ๋งค๋ฒˆ ๋น ๋ฅด๊ฒŒ ํฌ๋กค๋ง ํ•ด์•ผํ•˜๋Š” ์ด๋ฒˆ ์„œ๋น„์Šค์—๋Š” ์–ด์šธ๋ฆฌ์ง€ ์•Š๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.~~
๋ผ๊ณ  1ํŽธ์— ์ ์—ˆ์ง€๋งŒ ๋Œ€๊ธฐ์—ด์„œ๋น„์Šค๊ฐ€ ๋ Œ๋”๋ง์„ ํ†ตํ•ด ์ƒ์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” DOM๊ตฌ์กฐ๋ฅผ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.
๊ทธ๋ž˜์„œ selenium์„ ํ†ตํ•ด ๋ธŒ๋ผ์šฐ์ง•์„ ํ•˜๊ณ  ๊ธ์–ด์˜จ DOM์„ BeautifulSoup๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

๋ฐ”๋กœ ๋Œ€๊ธฐ์—ด์„œ๋น„์Šค์˜ ๊ตฌ์กฐ๋ฅผ ํŒŒ์•…ํ•ด๋ณด์ž.

๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ธฐ ์ „์˜ ๋ชจ์Šต

<body data-n-head="">
    <div id="__nuxt">
        <style>
            #nuxt-loading {
                visibility: hidden;
                opacity: 0;
                position: absolute;
                left: 0;
...

๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋œ ํ›„์˜ ๋ชจ์Šต

<div id="__nuxt">
...
  <div class="box">
    <div class="item">
      <span class="data name">์•ˆํƒ€๋ ˆ์Šค</span>
      <span class="data queue" style="opacity: 1; color: yellow;">9590</span>
    </div>
    <div class="item">
      <span class="data name">์•„ํฌํˆฌ๋ฅด์Šค</span>
      <span class="data queue" style="opacity: 1; color: yellow;">7839</span>
    </div>
...

ํฌ๋กค๋งํ•˜๊ธฐ ์ข‹๊ฒŒ ๊ตฌ์กฐ๊ฐ€ ์งœ์—ฌ์ ธ์žˆ๋‹ค.
soup.select() ๋ฉ”์„œ๋“œ๋กœ span tag์˜ data name ํƒœ๊ทธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ์„œ๋ฒ„์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ,
data queue ํƒœ๊ทธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ์„œ๋ฒ„์˜ ๋Œ€๊ธฐ์—ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.
๊ทธ๋Ÿฌ๋ฉด ๋ฐ”๋กœ ๊ตฌํ˜„ํ•ด๋ณด์ž.

for i in range(server_count):
    server = soup.select('span.data.name')[i].text
    queue = soup.select('span.data.queue')[i].text

    items.append({"server": server, "queue": queue})

2. ํฌ๋กค๋งํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜์—ฌ DB์— ์ €์žฅํ•œ๋‹ค.

์›๋ž˜ DB๊นŒ์ง€ ์“ฐ๊ณ ์‹ถ์ง€ ์•Š์•˜์ง€๋งŒ selenium์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋‹ˆ ํ•œ๋ฒˆ ํฌ๋กค๋งํ•˜๋Š”๋ฐ ํ‰๊ท  4.5์ดˆ๋ผ๋Š” ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ๋‹ค. ์นดํ†ก๋ด‡์—๊ฒŒ ๋Œ€๊ธฐ์—ด ์š”์ฒญํ•˜๊ณ  ์•ฝ 5์ดˆ๊ฐ„ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋œ๋‹ค๋ฉด ๋งค์šฐ ๋”์ฐํ•  ๊ฒƒ์ด๋‹ค.
๊ทธ๋ž˜์„œ ๊ตฌ์ƒํ•œ ์ƒ๊ฐ์€ selenium ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋„์›Œ๋†“๊ณ  ์ฃผ๊ธฐ์ ์œผ๋กœ ํฌ๋กค๋ง์„ ํ•˜์—ฌ DB๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค.

DB๋Š” ๋งŽ์ด ์“ฐ์ด๋Š” MySql์„ ์‚ฌ์šฉํ•˜์˜€๊ณ  ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ด๋ฏธ์ง€

server table์—๋Š” ์ตœ์ดˆ์— ์„œ๋ฒ„๋ฆฌ์ŠคํŠธ๋ฅผ ์ €์žฅํ•ด๋‘๊ณ  ํฌ๋กค๋ง ํ• ๋•Œ๋งˆ๋‹ค queue table์— insertํ•˜๊ฒŒ ํ•˜์˜€๋‹ค.

def insert_queue_query(self, queues):
    for i, queue in enumerate(queues):
        sql_query = f"INSERT INTO queue(date_time, server_id, queue) VALUES (now(), {i + 1}, {queue['queue']});"
        self.cursor.execute(sql_query)

    self.conn.commit()
    print('{0}๊ฐœ์˜ ์ •๋ณด ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: {1}'.format(len(queues),  str(datetime.now())))

๊ทธ๋ฆฌ๊ณ  table์— ์‹œ๊ฐ„๋‹น ์•ฝ 10,000๊ฑด์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ฌ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜์–ด ํผํฌ๋จผ์Šค๋ฅผ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค ์ผ๋ถ€๋ฅผ ์‚ญ์ œํ•˜๊ฒŒ ํ•˜์˜€๋‹ค.
๋งŒ์•ฝ ํ†ต๊ณ„๋“ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์ด๋ถ€๋ถ„์€ ๋‹ค์‹œ ๊ณ ๋ คํ•ด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

def delete_data(self):
    sql = """
    DELETE FROM queue
    ORDER BY date_time ASC LIMIT 9000;
    """
    self.cursor.execute(sql)

    self.conn.commit()
    print('9000๊ฐœ์˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ: {0}'.format(str(datetime.now())))

Crawler class์˜ __init__๋ถ€๋ถ„์—์„œ ํฌ๋กฌ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— db์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ• ๋•Œ (~~๋Š๋ฆฐ~~)ํฌ๋กฌ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์‹คํ–‰ํ•  ํ•„์š”๋Š” ์—†์—ˆ๋‹ค.

DbTools class ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ select_only ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‘ ์œผ๋กœ์จ ์กฐํšŒ๋งŒ ์‚ฌ์šฉํ•  ๊ฐ์ฒด์˜ ๊ฒฝ์šฐ ๋น ๋ฅธ ์†๋„๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค.

class DbTools:
    def __init__(self, select_only = False):
        self.conn = pymysql.connect(host=config.DATABASE_CONFIG["host"],
                                    user=config.DATABASE_CONFIG["user"],
                                    db=config.DATABASE_CONFIG["db"],
                                    password=config.DATABASE_CONFIG["password"],
                                    charset=config.DATABASE_CONFIG["charset"])
        self.cursor = self.conn.cursor()
        self.crawler = None

        if not select_only:
            self.crawler = Crawler()

3. ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์Šค์ผ€์ค„๋Ÿฌ๋Š” ์ฒ˜์Œ ์จ๋ดค์ง€๋งŒ ์‚ฌ์šฉ๋ฒ•์ด ๊ฐ„๋‹จํ•ด๋ณด์ด๋Š” schedule ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜์˜€๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์งง์€ ์ฝ”๋“œ๋กœ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์™„์„ฑ๋˜์—ˆ๋‹ค.

์„œ๋ฒ„์™€ ๋ณ„๊ฐœ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋Œ๋ฉด์„œ 2์ดˆ๋งˆ๋‹ค ํฌ๋กค๋งํ•˜๊ณ  4000์ดˆ(์•ฝ 1์‹œ๊ฐ„)๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ 9000๊ฑด์„ ์‚ญ์ œํ•˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋œ ๊ฒƒ์ด๋‹ค.

# schedule.py
db = DbTools(select_only=False)

def insert_schedule():
    db.save_data()

def delete_schedule():
    db.delete_data()

if __name__ == "__main__":
    print("scheduler is running! {}".format(datetime.now()))

    schedule.every(2).seconds.do(insert_schedule)
    schedule.every(4000).seconds.do(delete_schedule)
    while True:
        schedule.run_pending()
        time.sleep(1)

4. ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์‹œ ๋งˆ๋‹ค DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ kakao ํ”Œ๋Ÿฌ์Šค์นœ๊ตฌ api๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ํ•œ๋‹ค.

kakao api ์‚ฌ์šฉ๋ถ€๋ถ„์€ ๊ธฐ์กด๊ณผ ํฌ๊ฒŒ ๋ณ€ํ•œ๊ฒŒ ์—†์ง€๋งŒ message_button ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์•˜๋‹ค.
(~~๋ฌผ๋ก  ๋ธ”๋กœ๊ทธ ํ™๋ณด๋ผ๋Š” ์‚ฌ๋ฆฌ์‚ฌ์š•์„ ์ฑ„์šฐ๊ธฐ ์œ„ํ•ด์„œ~~)

data_send["message"].update({'message_button': {'label': '๊ฐœ๋ฐœ์ž ์œค์˜ด๋ฏ€ ๋ธ”๋กœ๊ทธ', 'url': 'http://suitee.me'}})
์ด๋ฏธ์ง€

๊ฐœ๋ฐœ์ž ์œค์˜ด๋ฏ€ ๋ธ”๋กœ๊ทธ ๋ผ๋Š” ๋ฒ„ํŠผ์ด ์•„์ฃผ ์ž˜ ์ƒ์„ฑ๋˜์—ˆ๋‹ค.

5. ๊ธฐํƒ€

  • server๋Š” AWS EC2์˜ t3.micro ์ธ์Šคํ„ด์Šค๋กœ Ubuntu๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

  • GUI๋ฅผ ์„ค์น˜ํ•˜์ง€ ์•Š์€ ๊ด€๊ณ„๋กœ putty๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์žˆ๋Š”๋ฐ ๋‹น์—ฐํžˆ putty๋ฅผ ์ข…๋ฃŒํ•˜๋ฉด putty๋กœ ์‹คํ–‰ํ•˜๊ณ  ์žˆ๋˜ ํ”„๋กœ๊ทธ๋žจ์€ ์ข…๋ฃŒ๊ฐ€ ๋œ๋‹ค.
    ๊ทธ๋ž˜์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋ฅผ ๋Œ๋ฆฌ๋Š” ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์›๋ž˜ node.js์šฉ์œผ๋กœ npm์—์„œ ์ œ๊ณต๋˜๋Š” forever๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

  • ์„ค์น˜ ํ›„ ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ช…๋ น์–ด๋ฅผ ์ž‘์„ฑํ•˜๋ฉด python ์„œ๋น„์Šค์ง€๋งŒ ์•„์ฃผ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.

$ forever start -c python3 kakao.py
$ forever start -c python3 scheduler.py
  • ๋กœ์ŠคํŠธ์•„ํฌ ์ ๊ฒ€๊ณต์ง€์‚ฌํ•ญ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ๊ณ ๋ ค์ค‘์ด๋‹ค. ๋น ๋ฅธ์‹œ์ผ๋‚ด์— ์„œ๋น„์Šค๊ฐ€ ์ œ๊ณต๋  ์ˆ˜๋„ ์žˆ์„๊ฒƒ ๊ฐ™๋‹ค.

  • ์„ฌ์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ธฐ๋Šฅ๋„ ๊ณ ๋ ค์ค‘์ด๋‹ค.

  • ์ด๋ฒˆ ์ฃผ๋ง๋ถ€ํ„ฐ ๋กœ์ŠคํŠธ์•„ํฌ๊ฐ€ ์„œ๋ฒ„๋ฅผ ๋Œ€๋Œ€์ ์œผ๋กœ ์ฆ์„คํ•œ๋‹ค๊ณ  ํ•œ๋‹ค. ์šธ์–ด์•ผ ํ• ์ง€ ์›ƒ์–ด์•ผ ํ• ์ง€โ€ฆ ๋‚ด ์„œ๋น„์Šค์˜ ์กดํ ์—ฌ๋ถ€๊ฐ€ ๋‹ฌ๋ ค ์žˆ์„๋“ฏ ์‹ถ๋‹ค. ๋ฌผ๋ก  ๋Œ€๊ธฐ์—ด์ด ์—†์œผ๋ฉด ๋”์šฑ ํ–‰๋ณตํ•  ๊ฒƒ ๊ฐ™๋‹ค :)

๐Ÿ›ด

yoon.homme
yoon.homme

๊ธฐ์ˆ ๊ณผ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜์˜ ํž˜์ด ์„ธ์ƒ์„ ๋ฐ”๊พผ๋‹ค๊ณ  ๋ฏฟ์Šต๋‹ˆ๋‹ค.

ํŽธ๋ฆฌํ•œ ์„ธ์ƒ์œผ๋กœ ๋‚˜์•„๊ฐ€๊ธฐ ์œ„ํ•ด ๊ณ ๋ฏผํ•˜๊ณ  ๊ฐœ๋ฐœํ•ฉ๋‹ˆ๋‹ค.