[PYTHON] Display Disney's waiting time with LINE bot

table of contents

** Bold ** is where I had a hard time

Overview

I tried to make a bot with Python instead of node.js by referring to the following site. Also, the information obtained by scraping was obtained from the same site as the one listed above.

I made a LINE bot that tells me the waiting time of Disney

Since this site is supposed to be easy to use, there are many character strings, but on the contrary, I used the rich menu and Flex Message so that the user can select the desired data. Rich menus, Flex Messages, etc. are not organized, so I looked at the references and went back and forth between various sites, so I hope to put them together.

Created LINE bot

I will paste the completed version first. Please register and use it if you like! To read directly, please click here

The source code is available on GitHub. If you want to see detailed scraping code etc., please click here [https://github.com/ryodisney/disney_wait)

Directory structure

Diagram

I put everything in a folder called disney. Since .git was a hidden file, it does not appear here, but it is in the same hierarchy as deploy.bat.

disney
├  deploy.bat
├  main.py
├  scrape_requests.py
├  makejsonfile.py
├  Procfile
├  runtime.txt
├  requirements.txt
│  
└─templates
        land_theme.json
        recipt.json
        sea_theme.json
        theme_select.json

Introduction of each file

The part that moves the LINE bot will be explained later, so the contents of the configuration file are listed below.

** deploy.bat **: This saves you from having to type every command when you deploy a modified version to Heroku.

deploy.bat


git add . && git commit -m 'Improve' && git push

** Procfile **: Creates a configuration file, Procfile, to teach Heroku how to start the program. After moving to the current directory at the command prompt, enter the command below. At this time, put the one that starts first in the place where main.py is written. The name doesn't have to be main.py.

Procfile


echo web: python main.py > Procfile

** runtime.txt **: The version of Python to use is listed here.

runtime.txt


python-3.7.0

** requirements.txt **: The modules used in Python that have been pip installed are written here. Now you can use these modules on Heroku as well.

requirements.txt


Flask==1.1.1
line-bot-sdk==1.15.0
requests==2.21.0
bs4==0.0.1
lxml==4.4.2
https://github.com/heroku/heroku-buildpack-chromedriver.git
https://github.com/heroku/heroku-buildpack-google-chrome.git

Flow of operation

  1. Have the home button pressed (rich menu described below)
  2. Park selection
  3. Opening check
  4. If the park is open, select the category of waiting time you want to get (rich menu described later)
  5. Scraping → Output with Flex Message recipe

Creating a rich menu

The 6-split part shown in the image below is the rich menu. This time, instead of building it yourself, I will use the functions of LINE Official Manager. I really wanted to use a postback, so I wanted to make it completely myself, but I compromised because I couldn't figure out how to implement it even after reading the reference.

LINE Official Manager After logging in, click the red frame below. If you press the create button there, the following page will be displayed. The title can be anything. It seems to distinguish when you make multiple rich menus. (It seems that you can not switch the rich menu with the same account unless you make it yourself) I think that the display period should be set aside longer. The start date will not come out unless it is before the implementation date. (Of course)

Go down to ** Content Settings **. You can select the number of divisions by pressing ** Select template **. I chose 6 divisions here. If you select ** Upload background image **, it will be a one-sided image, so if you want to attach an image individually to 6 divisions, press ** Create image ** below. You can also choose the reaction when you press it with an action. This time it will lead to the event after that, so I will make it a text. (I don't think I have many opportunities to use others) Here are some points when creating an image. First, you can upload the image by pressing the red circle icon. And the outer frame is framed by the blue circle icon. I think it would be better if we couldn't understand the boundaries without this. With the default thickness, there was a gap, so the edge was just right with a max of 5. Also, press ** Apply ** in the upper right corner after all. If you press it in the middle, it will be saved as a single image in the background, and you will not be able to edit it individually. This rich menu is the result.

The home button written in the flow of operation is the Mickey icon at the bottom center. It is responsible for returning the text "Home" when pressed. The other five are all for selecting a category, so they are different from the home button.

Park selection (button)

After pressing the home button, a button like the one below will appear so that you can select a park. I will roughly describe the processing when the home button is pressed. After this, there is also a postback event, and there is information that I want to save such as which park is selected, so I am using global variables considering the scope of the variable. The process after pressing the home button starts with ** les = "les" **. The important points are the following two points.

--How to display json file --Difference between push and reply

home_button.py


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    global park,genre,area,info_url,target_url,counter,situation

    text = event.message.text
    userid = event.source.user_id

    #At first and reset
    if text == "home":
        #Initialization
        park = "park"
        genre = "genre"
        area = "area"
        info_url = ""
        target_url = ""
        counter = 0
        situation = ""


        les = "les"
        template = template_env.get_template('theme_select.json')
        data = template.render(dict(items=les))


        select__theme_massage = FlexSendMessage(
                alt_text="Theme selection",
                contents=BubbleContainer.new_from_json_dict(json.loads(data))
                )
            
        line_bot_api.push_message(userid, messages=select__theme_massage) 

The first thing I want to write about is TextMessage and FlexSendMessage. These need to be tampered with at the top of the source code copied and pasted with Echolalia. It seems that Event, Action, Message system needs to be imported as shown below. If you get an error, please check if it is described here.

from linebot.models import (
    MessageEvent, TextMessage, PostbackTemplateAction, PostbackEvent, PostbackAction, QuickReplyButton, QuickReply,
    FlexSendMessage, BubbleContainer, CarouselContainer, TextSendMessage
)

View json file

To be honest, I only know how to apply it to the template using jinja2, so I don't understand much. Therefore, if you are a beginner, it will be faster to copy ** les = "les" ** or less.

  1. Create a json file

I used Flex Message Simulator to mess with the finished product and put it into shape for the time being.

```json:theme_select.json
{
"type": "bubble",
"hero": {
  "type": "image",
  "url": "https://secured.disney.co.jp/content/disney/jp/secured/dcc/tokuten/bf-tdr-prk-tckt/_jcr_content/par/dcc_hero_panel_image/image1.img.jpg/1474355301452.jpg ",
  "size": "full",
  "aspectRatio": "20:13",
  "aspectMode": "cover"
},
"body": {
  "type": "box",
  "layout": "vertical",
  "contents": [
    {
      "type": "text",
      "text": "Please select a park",
      "weight": "bold",
      "size": "lg"
    }
  ]
},
"footer": {
  "type": "box",
  "layout": "vertical",
  "spacing": "sm",
  "contents": [
    {
      "type": "button",
      "style": "link",
      "height": "sm",
      "action": {
        "type": "postback",
        "label": "land",
        "data": "land"
      }
    },
    {
      "type": "button",
      "style": "link",
      "height": "sm",
      "action": {
        "type": "postback",
        "label": "C",
        "data": "sea"
      }
    },
    {
      "type": "spacer",
      "size": "sm"
    }
  ],
  "flex": 0
}

} ``` The content of "action" is - type - label - data

"Type" is the form of data exchange, "label" is what is written on the button (in this case, "land", "sea"), and "data" is the data to be received. It seems that images and sounds are also included in "data". For more information, please read Reference. (Supplement) Select "type" of "action": {} according to the purpose. If you want it to appear as a message when the other party presses it, you should use "type": message.

Important!

To save the json file, create a folder called templates in the same hierarchy and save it in that folder! It seems that it is reading from there.

  1. Substitute in the code below Substitute in the character string part.

    template = template_env.get_template('theme_select.json')
    
  2. When implementing a carousel (horizontal slide one), change to the code below Change from Bubble Container to Carousel Container.

    select__theme_massage = FlexSendMessage(
            alt_text="Theme selection",
            contents=CarouselContainer.new_from_json_dict(json.loads(data))
            )

Difference between push and reply

You can push multiple times for one event, but reply seems to be possible only once. So if you reply, you will not be able to send any more messages after that. For example, assuming that scraping will take some time, if you want to display "Processing" when receiving a message from the user and then display the result in a json file, push "Processing" and push the json file. It will be solved by replying. Also, push must prepare userid and message. Look at button.py above and imitate it.

push.py


line_bot_api.push_message(userid, messages=select__theme_massage) 

reply.py


line_bot_api.reply_message(
    event.reply_token,
    FlexSendMessage(
        alt_text="Result display",
        contents=BubbleContainer.new_from_json_dict(json.loads(data))
    )
) 

Opening check

The datetime is used to display "closed" except for the opening hours. Below are the two codes. The first is to receive the park selection data in a postback, check the business hours, and reply to the user with the return value. The second is a code that confirms business hours. (bonus)

** Note: ** If you don't set Heroku's time zone to Japan, datetime will be in the US time zone. For time zone setting → this article

postback_park.py


@handler.add(PostbackEvent)
def handle_postback(event):
    global park,genre,area,info_url,target_url,counter,situation
    area = ""

    post_data = event.postback.data
    userid = event.source.user_id

    if post_data == "land" or post_data == "sea":
        park = post_data
        if park == "land":
            #Links such as opening hours and weather
            info_url = "https://tokyodisneyresort.info/index.php?park=land"
            park_ja = "land"
        
        elif park == "sea":
            #Links such as opening hours and weather
            info_url = "https://tokyodisneyresort.info/index.php?park=sea"
            park_ja ="C"

        #Check opening hours
        business_hour = Scrape_day(info_url)
        situation = Check_park(business_hour)

        if situation == "close":
            print("close")
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text="The park is closed")
                )
                
        elif situation == "open":
            park_message = TextSendMessage(text= str(park_ja) + "Is selected\n categories from the menu below\n Please select")
            line_bot_api.push_message(userid, messages=park_message)

Emptying the area is an error avoidance assuming that you accidentally press the button multiple times.

check_park.py


#Check if it's opening time now
def Check_park(business_hour):
    #Check the current time and date
    dt_now = dt.now()

    #Today's date
    year = int(dt_now.year)
    month = int(dt_now.month)
    day = int(dt_now.day)

    #Division of opening hours
    open_time = business_hour.split("~")[0]
    if open_time.split(":")[0] == "":
        return "close"

    else:
        open_hour = int(open_time.split(":")[0])
        open_minute = int(open_time.split(":")[1])

        #Division of closing time
        close_time = business_hour.split("~")[1]
        close_hour = int(close_time.split(":")[0])
        close_minute = int(close_time.split(":")[1])

        #datetime
        open_datetime = dt(year,month,day,open_hour,open_minute)
        close_datetime = dt(year,month,day,close_hour,close_minute)


        if open_datetime < dt_now < close_datetime:
            return "open"

        else:
            return "close"

The argument business_hour is a string of business hours scraped from the site.

Select the waiting time category you want to get if the park is open

As a result of selecting a park, if it is open, proceed to the next step. Press the rich menu you just created and select which category you want to see the latency. At this time, the selected category is displayed as text on the screen. The only way to avoid this is to make your own rich menu.

Scraping → Output with Flex Message recipe

As mentioned above, the entire code is published on github, so I will not describe scraping the waiting time in particular. This section describes how to use Flex Message's recipe when outputting the acquired value. If the number of strings to be output is small or if you don't care about the shape, the push or reply described above is sufficient, so you can skip this step.

I will play with the json file this time as well, but I will do three main things.

--Embed variable in part --Embed items that change from time to time --File initialization

I edited the recipe of Flex Message Simulator and created the following json file.

recipt.json


{
    "type": "bubble",
    "styles": {
    "footer": {
        "separator": true
    }
    },
    "body": {
    "type": "box",
    "layout": "vertical",
    "contents": [
        {
        "type": "text",
        "text": "Waiting time",
        "weight": "bold",
        "color": "#1DB446",
        "size": "sm"
        },
        {
        "type": "text",
        "text": "theme",
        "weight": "bold",
        "size": "xl",
        "margin": "md"
        },
        {
        "type": "separator",
        "margin": "xxl"
        },
        {
        "type": "box",
        "layout": "vertical",
        "margin": "xxl",
        "spacing": "sm",
        "contents": [
            
        ]
        }
    ]
    }
}

Embed variables in part

I want to change the part that says "text": "theme" to "text": "acquired characters", so perform the following processing.

set_json.py


def Send_area(area):
    json_file = open('templates/recipt.json', 'r',encoding="utf-8-sig")
    json_object = json.load(json_file)
    json_object["body"]["contents"][1]["text"] = str(area)
    #writing
    new_json_file = open('templates/recipt.json', 'w',encoding="utf-8")
    json.dump(json_object, new_json_file, indent=2,ensure_ascii=False)

As a procedure

  1. First, read the above json file in a writable form
  2. Go to where the "text": "theme" is by specifying the elements of the complex list (may be easy to find in some editors)
  3. And if you assign a variable to the contents of "text", you can assign a variable to a part. It's like that.

Embed items that change from time to time

This was solved by embedding a variable in a box with a fixed shape and inserting it additionally.

new_json.py


def Make_jsonfile(attraction,info):
    json_file = open('templates/recipt.json', 'r',encoding="utf-8-sig")
    json_object = json.load(json_file)

    new =   {
                "type": "box",
                "layout": "vertical",
                "margin": "xxl",
                "spacing": "sm",
                "contents": [
                {
                    "type": "box",
                    "layout": "horizontal",
                    "contents": [
                    {
                        "type": "text",
                        "text": str(attraction),
                        "size": "sm",
                        "color": "#555555",
                        "flex": 0
                    },
                    {
                        "type": "text",
                        "text": str(info),
                        "size": "md",
                        "color": "#111111",
                        "align": "end"
                    }
                    ]
                }
            ]
        }

    json_object["body"]["contents"][3]["contents"].append(new)

    new_json_file = open('templates/recipt.json', 'w',encoding="utf-8")
    json.dump(json_object, new_json_file, indent=2,ensure_ascii=False)

It's complicated and difficult to understand, but the important thing is to append (new). The empty contents at the bottom of the above recipe.json is in list format, so you can append to it even if you don't know the number of data to display in advance. In new_json.py, the attraction name and waiting time are embedded in variables, and a box that summarizes them is inserted in contents.

File initialization

Of course, if you repeat append without initializing, the information so far will remain. Before creating the recipt, it is initialized by overwriting the above recipt.json.

Summary and future issues

Although it was my first post, it has become quite long. This time, I focused on the rich menu and Flex Message that I had a hard time, and I hope it will be helpful to someone. The challenges that can be raised are

--If you can get information from the official website, more items can be displayed (though I think it is strict in terms of security). ――I want to create my own rich menu and use the rich menu itself dynamically (when I press the rich menu, a new rich menu appears instead of a button)

there is. In particular, there are few Python articles about the second rich menu, so if anyone who happens to read this article is familiar with it, I would be grateful if you could post it.

Recommended Posts

Display Disney's waiting time with LINE bot
Monitor web page updates with LINE BOT
I made a stamp substitute bot with line
Create a LINE BOT with Minette for Python
LINE BOT with Python + AWS Lambda + API Gateway
I made a LINE Bot with Serverless Framework!
Serverless LINE bot made with IBM Cloud Functions
Make a LINE WORKS bot with Amazon Lex
[AWS] I made a reminder BOT with LINE WORKS
I made a household account book bot with LINE Bot
Make a morphological analysis bot loosely with LINE + Flask
Display line numbers in vim editor (with default settings)
I made a LINE BOT with Python and Heroku
[Super easy] Let's make a LINE BOT with Python.
[LINE Messaging API] Create parrot return BOT with Python
Make LINE BOT (Echolalia)
LINE BOT if ~ stumbled
3D display with plotly
Taskbar display with tqdm
Display TOPIX time series
I made a LINE BOT that returns parrots with Go
Make a LINE bot with GoogleAppEngine / py. Simple naked version
Create a machine learning app with ABEJA Platform + LINE Bot
Let's execute the command on time with the bot of discord
Until I return something with a line bot in Django!
[AWS] I made a reminder BOT with LINE WORKS (implementation)