+4

Kanto transit chatbot với Node.js và mongoDb

Chatbotlà khái niệm không mới nhưng gần đây trở thành hot trend khi kết hợp cùng machine learningAI. Mình sẽ có một bài viết cụ thể hơn về kỹ thuật cũng như những cơ hội mà chatbot đem lại trong một bài khác. Trong khuôn khổ bài viết này xin phép mỳ ăn liền chatbot đơn giản mà mình mới làm trong một buổi tối gió và lạnh ở Tokyo.

Lấy cảm hứng từ việc ngày thứ 2 vừa rồi Tokyo có trận tuyết khủng bố nhất 30 năm trở lại đây và chiến thằng của U23 Việt Nam (=))) mình có xây dựng một chatbot đơn giản để gửi tin nhắn cảnh báo mỗi khi tàu điện trên các line (vùng Kanto) mình quan tâm nếu line có thay đổi trạng thái.

Đây cũng được hi vọng là viên gạch đầu tiên cho một chatbot hoàn chỉnh hơn với nhiều chức năng hỗ trợ cá nhân (auto dự báo thời tiết, viết mail report, xem giá cổ phiếu, bitcoin, rss,...) mà mình (nếu có thời gian và không lười) định sẽ xây dựng.

Ý tưởng

  • Flowchart cho chatbot đơn giản này.

    flow-chart

  • Khi người dùng nhập tên line, chatbot sẽ lấy thông tin về line đó, gửi trả lại thông tin và từ đó người dùng sẽ subscribe line này, nếu dữ liệu có thay đổi chatbot sẽ gửi thông báo tới messenger của khách hàng qua việc thực hiện 1 cronjob để liên tục check dữ liệu.

  • Mình chọn facebook messenger platform cùng node.jsmongodb để mọi thứ trở nên đơn giản hơn =))

Ok, đã xong phần ý tưởng và công cụ, bắt tay vào làm.

Dữ liệu đầu vào

  • Đầu tiên chúng ta cần có một nguồn dữ liệu về traffic tàu điện ở vùng Kanto. Mình có thử search traffic API của Tokyo nhưng không tìm được nên mình chọn giải pháp sử dụng dữ liệu từ Yahoo transit. Như vậy cần một html parser để có thể lấy dữ liệu phục vụ cho chatbot.

Facebook chatbot

  • Chúng ta sẽ tạo một chatbot sử dụng Facebook messenger platform, sử dụng test-drive để mọi thứ trở nên đơn giản.

  • Các bạn có thể làm theo đến bước 5. Facebook có recommend sử dụng localtunnel, tuy nhiên mình thấy localtunnel thiếu ổn định và bị delay khá nhiều nên mình chuyển qua dùng một công cụ khác là ngrok. Các bạn có thể down tại đây, giải nén ra và dùng liền.

  • Sau khi chạy server (node app.js) chúng ta sẽ chạy lệnh

    ./ngrok http 5000
    

    cổng local 5000 đã có thể kết nối với facebook thông qua ngrok service.

  • Copy link tại giao diện ngrok (https method) và paste vào facebook test-drive, đến đây chúng ta đã công đoạn setup chatbot.

  • Bây giờ bắt đầu vọc code của fb, dữ liệu đầu vào sẽ là text (message) nên cần sửa hàm receivedMessage để trả về kết quả mong muốn. Bỏ qua các text đặc biệt, trường hợp default của vòng lặp switch chính là phần cần tuỳ biến.

    app.js

    default:
          parseInfo(senderID, subcribeLine, msgFlg) // senderID: fbID của người dùng, subcribeLine: tên line người dùng nhập vào, msgFlg: 0:cronJob, 1: tin nhắn của người dùng.
    

    Input của người dùng được lấy tại app.js

    var messageText = message.text;
    

    và chatbot cần phải tìm ra line chính xác từ đống này

    và chỗ này nữa

Yahoo transit web parser

  • 'nodejs html parser' trên googlecheerio (info) là lựa chọn có vẻ tốt nhất, tiện thể import thêm mongodb và để lưu dữ liệu và node-cron(info) để tạo cron-job cho chatbot.

    npm install cheerio
    npm install mongo
    npm install node-cron
    npm install cron
    

    app.js

    var mongoClient = require('mongodb').MongoClient;
    var url = "mongodb://localhost:27017/";
    var cheerio = require('cheerio');
    var CronJob = require('cron').CronJob;
    
  • Nắng đã có mũ, mưa đã có ô, không biết parse thế nào để ra cái mình cần đã có selectorgadget(info).

    Với extension này mình tìm ra được '#mdAreaMajorLine td:nth-child(1)' là css selector của mục cần lấy, inspect thêm 1 lần thì mình đã lấy đầy đủ các thông tin sau:

    1. tên line (lineName) tại children[0].children[0].data
    2. link info page của line children[0].attribs.href
    3. tình trạng line (lineStatus) tại $('dt')[0].children[1].data hoặc $('dt')[0].children[2].data
    4. thông tin line (lineTrouble) tại $('#mdServiceStatus p')[0].children[0].data
  • Bóc tách được thông tin rồi chúng ta sẽ viết hàm parseInfo để lấy tất cả thông tin của line theo yêu cầu của người dùng. app.js

      function parseInfo(senderID, subcribeLine , msgFlg) {
          request('https://transit.yahoo.co.jp/traininfo/area/4/', function (error, response, html) {
              if (!error && response.statusCode == 200) {
                  var found = false;
                  var $ = cheerio.load(html);
                  $('td:nth-child(1)').each(function (idx, elem) {
                      var lineName = elem.children[0].children[0].data;
                      if (lineName.includes(subcribeLine)) {
                          found = true;
                          request(elem.children[0].attribs.href, function (error, response, html) {
                              if (!error && response.statusCode == 200) {
                                  var $ = cheerio.load(html);
                                  var lineStatus = $('dt')[0].children[1].data || $('dt')[0].children[2].data;
                                  var lineTrouble = $('#mdServiceStatus p')[0].children[0].data
                                  handleRequest(senderID, lineName, lineStatus, lineTrouble, msgFlg);
                              }
                          });
                      }
                  });
                  if (!found){
                      sendTextMessage(senderID, subcribeLine + "の情報がありません。");
                  }
              }
          });
      }
    
  • Tiện tay viết luôn hàm xử lý việc gửi tin nhắn reply cho người dùng

    app.js

      function handleRequest(senderID, lineName, lineStatus, lineTrouble, msgFlg){
          mongoClient.connect(url, function(err, db) {
              if (err) throw err;
              var dbo = db.db("chatbot");
              var query = { userId: senderID, subcribeLine: lineName };
    
              dbo.collection("rosen").find(query).toArray(function(err, result) {
                  if (err) throw err;
                  if (result[0] && result[0].lineStatus == lineStatus && result[0].lineTrouble == lineTrouble) {
                      // line info did not change
                      console.log("Did not change");
                      // if user send the msg (nếu là người dùng gửi thì sẽ gửi lại trạng thái dù không thay đổi)
                      if (msgFlg == 1){
                          var msg = '[' + result[0].subcribeLine.toString() + ']: ' + result[0].lineStatus.toString() + '\n' + result[0].lineTrouble.toString();
                          sendTextMessage(senderID, msg);
                      }
                  } else if (result[0]) {
                      // line info has been changed
                      var newVal = { $set: { lineStatus: lineStatus, lineTrouble: lineTrouble } };
                      dbo.collection("rosen").updateOne(query, newVal, function(err, res) {
                          if (err) throw err;
                          console.log(senderID + lineName + " updated");
                          db.close();
                      });
                      var msg = '[' + lineName.toString() + ']: ' + lineStatus.toString() + '\n' + lineTrouble.toString();
                      sendTextMessage(senderID, msg);
                  } else {
                      // user subcribe to new line
                      var myobj = { userId: senderID, subcribeLine: lineName, lineStatus: lineStatus, lineTrouble: lineTrouble };
                      dbo.collection("rosen").insertOne(myobj, function(err, res) {
                          if (err) throw err;
                          console.log(senderID + lineName + "inserted");
                      });
                      var msg = '[' + lineName.toString() + ']: ' + lineStatus.toString() + '\n' + lineTrouble.toString();
                      sendTextMessage(senderID, msg);
                  }
                  db.close();
              });
          });
      }
    
  • Test thử nào

https://gfycat.com/ifr/UnhealthyWeightyIndusriverdolphin

Cron-job

  • Việc cuối cùng chúng ta cần tạo cronjob để chatbot của chúng ta (bản chất là node.js app) check trạng thái liên tục (mỗi phút một lần) và nếu có thay đổi sẽ gửi thông báo đến cho người dùng

    app.js

      new CronJob('1 * * * * *', function() {
          mongoClient.connect(url, function(err, db) {
              if (err) throw err;
              var dbo = db.db("chatbot");
              dbo.collection("rosen").find({}).toArray(function(err, result) {
                  if (err) throw err;
                  result.forEach(function (res) {
                      parseInfo(res.userId, res.subcribeLine, 0);
                  });
                  db.close();
              });
          });
      }, null, true, 'Asia/Tokyo');
    
  • Test hoạt động lần cuối

https://gfycat.com/ifr/SmartOrdinaryBluebreastedkookaburra

Vậy là chúng ta đã hoàn thành xong một chatbot với chức năng tự động báo cáo tình trạng các line. Hiện tại thì chức năng của chatbot giao thông này vẫn còn rất đơn giản và ở mức tạm dùng được và đã được mình deploy tại https://chatbot.qmau.me (update: đã bị dỡ xuống) . Một số chức năng đơn giản như xem và xoá subscribeLine sẽ được mình hoàn thiện nốt trong thời gian tới.

Để lại một likecomment để buổi tối của mình không lãng phí nhé. :3

Source code của project có tại đây. Feel free to use!

Ps: Tầm này chỉ muốn ở Hà Nội sang chơi với cháu rồi đi ăn bún đậu cùng đồng bào thôi 😦

Refs

Link bài viết gốc có tại blog cá nhân 😄 https://qmau.me/blog/post/kanto-transit-chatbot-voi-yahoo-transit-va-node-js


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí