เรื่องเล่า myujikkii.com และ music48voter.com ฉบับปี 2020

สำหรับเว็บ music48voter.com ตัวเว็บใช้บริการ DigitalOcean ด้วยเหตุผลการเข้าถึงได้ดีจากนอกประเทศ ราคาไม่แพง รวมทั้ง 6 อย่างที่เห็น จ่ายเดือนละ 30USD โดยให้ B/W ค่อนข้างเยอะ สเปคก็ค่อนข้างดี เร็วใช้ได้ (ปีก่อนมีปัญหา ตปท เข้าได้ยาก เพราะระบบอยู่ในไทย)

ใช้ VM จำนวน 4 ตัว เป็น LBx1 (HAProxy), Webx2 (docker-compose),DBx1 (MariaDB+Redis) ส่วนไฟล์สลิปไปฝากที่บริการ Spaces คล้ายๆ S3 ของ AWS เก็บแยกไปต่างหาก

เว็บพัฒนาบน .NET Core 3.1 (C#) ต่อยอดจากปีก่อน (ปีที่แล้ว .NET Core 2.0) docker image build บน Docker Hub
เหตุผลที่มี Webx2 เพราะกลัว deploy แก้ไขจะได้ไม่ล่มแบบปีก่อน ทำ auto deploy ร่วมด้วย (ปีก่อนแก้หรือ deploy แล้วเว็บจะ down แป็บนึง เพราะ start/stop Kestrel manual) และเหตุผลที่ไม่ใช้ K8s เพราะเครื่องมันจะเยอะขึ้นอีกตัว บวกกับขี้เกียจ (จบ ?)

myujikkii.com อันนี้อีกทีมนึงทำ ซึ่งด้านการ sync ยอด progress ดึงจาก music48voter อีกที (cache 5 นาที) ทีม myujikkii ก็จะได้ไม่ปวดหัวกับระบบหลังบ้าน เลยรวมที่เดียว และ myujikkii เอา cloudflare ทำ proxy cache ด้วยเพื่อให้โหลดเร็วขึ้นประหยัด B/W

ระบบหลังบ้านในการตรวจสลิป มีทีมตรวจแล้วแจ้งสถานะ approve/reject แล้วส่งอีเมลแจ้งสมาชิกในเว็บ (ปีก่อนไม่มีอีเมลแจ้ง) ถือว่าโอเค เพราะทีมหลังบ้านไม่ต้องคอยแจ้งหรือลุ้นว่าเค้าจะกลับมาแก้ไขไหม และทำให้ทีมงานเช็คยอดง่ายขึ้น (ตัวอีเมลใช้บริการ mailgun ส่งอีเมลฟรี 5,000 ฉบับ)

ส่วนฝั่งโค้ดโหวตก็เหมือนปีก่อน และสุดท้าย last word ตอนแรกจะใช้ Google Form แต่คิดว่าลำบากตรงเอาข้อความมาแปะ และตรวจยอด เลยเขียนเพิ่มให้ทีมงานบริการจัดการได้จากจุดเดียว โดยข้อมูลโดเนทและข้อความก็อยู่ฐานข้อมูลเดียวกันทั้งหมด เอาเวลาไปโฟกัสเรื่องยอดกับบิ้วโหวตดีกว่า

สุดท้าย ปีนี้เราพยายามให้แฟนคลับ ไม่ว่าจะเป็นสมาชิกที่โหวตเอง โดเนทกับกิจกรรม หรือรู้สึกยุ่งยากแต่อยากโดเนทแบบไม่ระบุตัวตน ก็ทำได้หมด เราพยายามให้ทุกคนมีส่วนรวมกับกิจกรรมให้เยอะที่สุด

หวังว่าจะสนุกกับกิจกรรมของทีมงานที่ทำกันทุกคน ^^

เบื้องหลัง music48voter.com

เว็บ music48voter.com เป็นเว็บลูกของ music48project.com สร้างเพิ่มเติมขึ้นมาเพื่อเป็นช่วยในการรวบรวมคะแนนเพื่อวัดผลในการดำเนินกิจกรรมของกลุ่มแฟนคลับ Music BNK48 ต่อไป

ตัวระบบก็ปั่น “backend กึ่ง front-end” ช่วงปีใหม่ 3-4 วัน แม้ว่าจะทำเก็บมาเรื่อย ๆ แต่ว่ามันแค่โครง ๆ เป็นไอเดียในระดับ user management แล้วลองของเรื่อง .NET Core อีกหนึ่งตัว (มีอีกหลายตัวที่กำลังคิดว่าจะย้ายมาใช้ เพราะมันมีอนาคตกว่า) โดยจากการทำโครงการเล็ก ๆ ตัวนี้ เจ้า .NET Core ตั้งแต่ 2.x มาเป็นต้นมา มันเขียนสะดวก และง่ายขึ้นมาก ความสามารถต่าง ๆ ตาม .NET Framework เกือบทันแล้ว (บางจุดก็ใช้แรงหน่อย แต่รวม ๆ ก็พอไหว) เลยใช้แทน PHP ที่หลัง ๆ ยุ่งกับมันน้อยลงเยอะ

ตัวเว็บนี้ใช้ .NET Core version 2.1 ใช้ร่วมกับฐานข้อมูล MySQL version 5.7 ซึ่งตัวเดิม ๆ ที่ new project มามันไม่รองรับหรอก แต่เราใช้ Class provider อย่าง Pomelo MySql ที่เป็น Class provider ที่ on top บน Entity Framework Core มาทำงานแทน ทำให้ไม่ต้องเขียนคำสั่ง SQL จริง ๆ แต่ทำผ่าน LINQ และ ORM แทนซึ่งจบในตัว

สำหรับ background woker ใช้ Hangfire ตัว Free ก็เพียงพอ สำหรับใช้พวกส่งอีเมล เพื่อจัดคิวในการส่ง และแก้ปัญหาเรื่อง blocking UI คือกดแล้วรู้เลยว่าสมัครแล้ว ส่วนส่งอีเมลยืนยันการสมัครที่เราไปต่อ 3rd party SMTP ภายนอกที่มักทำงานช้า ในระดับหลักเกือบวินาที มันทำให้ฝั่งเว็บค้างไปแป็บนึงมาช่วยให้มันไม่ต้องรอจุดนั้น แถมยังลดปัญหา waiting ที่ thread ช่วงส่งเมล ส่วนในหน้า Ranking ตัว report เนี่ย มันใช้พลังในการ summary เยอะ ก็ใช้ schedule woker ที่อยู่ในตัว Hangfire อยู่แล้วทำ ให้มันทำงานทุก ๆ 5 – 10 นาทีเพื่อสร้างข้อมูลใหม่ใน report table แทน แล้วเวลาแสดงผลก็ให้มาดึงที่ตัวนี้แทน การแสดงผลตารางดังกล่าวจะได้ไม่หนักฐานข้อมูล

ในส่วนของ Front-end Lib ก็ใช้ Bootstrap 4 เอาง่าย ๆ โดยจะมึนหัวนิดนึงตอน upgrade มาใช้ เพราะตัว default ของ .NET Core 2.1 คือ Bootstrap 3 ก็เลยต้องใช้ bower มาช่วยในการจัดการตรงนี้ แก้ default style ที่มันค้างใน Framework อยู่เยอะพอสมควร แต่แลกกับการทำงานดีง่ายกว่ามาก สำหรับทีมออกแบบหน้าเว็บ ที่เป็นอีกทีมหนึ่ง ที่เค้าใช้ Bootstrap 4 เข้ามาช่วยกันแก้ไข และตบแต่งเพิ่มเติมให้ออกมาสวยงามขึ้นอีก ทั้ง layout และ stylesheet ถ้าใครติดตามตัวเว็บช่วงนั้นจะเห็นว่าเราปรับปรุงตลอดเวลา ยิ่งช่วงวันท้าย ๆ เรามีการปรับโทนสีของเว็บใหม่ทั้งหมด เพื่อให้เข้ากับแนวทางการทำกิจกรรมช่วงสุดท้าย

ตัว Binary ตอน deploy ขึ้นทำงานบน CentOS 7 ทั้งฝั่ง App และ Database โดยตัว App วิ่งผ่าน HAProxy ที่ทำหน้าที่ reverse proxy อีกที (เป็น best practice ที่ควรทำ เพื่อสำหรับ HA และทำ hot deploy ได้)

สำหรับขั้นตอนการตรวจสอบโค้ดโหวตในฝั่งแอปก็ไปแกะเอามาจาก javascript ของฝั่งเว็บโหวตเพื่อเอามาตรวจสอบอีกรอบก่อนเอาลงฐานข้อมูลเพื่อลดงานหลักบ้าน ส่วนงานตรวจสอบอีกชั้นว่าโหวต หรือไม่ก ็ทำระบบหลังบ้านให้ทีมงานมาตรวจสอบจาก screen recorder อีกที ซึ่งยุ่งยากหน่อย แต่มันเป็นทางเดียว เพราะจะให้แฟนคลับแต่ละคนมากรอก username และ password เพื่อให้เราไปดึง token ผ่าน OAuth2 แล้วเอาไปดึงข้อมูลจาก API ออกจากเว็บโหวตโดยตรงคงมีไม่กี่คนจะมาใช้

สำหรับตัวเนื้อหาจะมีทีมงานอีกทีมลงมาช่วยกันหลายสิบคน เพื่อคอยค้นหากิจกรรมของแฟนคลับมาลงในหน้า Activity เพื่อเป็นจุดรวมข้อมูล และรวมถึงหน้าข้อมูล-รายละเอียดที่เป็นภาษาญี่ปุ่นที่มีการเพิ่มเติมเข้ามาในช่วงหลัก เพื่อช่วยให้แฟนคลับที่เป็นคนญี่ปุ่นได้เข้าร่วมทำกิจกรรมได้

มีเรื่องฮา ๆ และตื่นเต้นคือ ตอนเปิดให้สมัครวันแรก คนเข้ามาเว็บค่อนข้างเยอะมาก หลักพันกว่าคน แล้วตอนนั้นหน้าลงทะเบียนดันมี bug เกิดขึ้น เลยต้อง hot deploy ใหม่ในช่วงนั้นไป ทำเอาคนสมัครตอนนั้นบ่นกัน timeline ไหม้เลย (ToT)/~~~

แล้วระหว่างการเปิดให้ใช้งานก็มีปัญหาอยู่เรื่อย ๆ เพราะระบบจัดการเว็บบางจุดก็มี bug อยู่ แต่ด้วยความที่งานส่วนตัวก็มี ทำให้บางจุดก็ไม่ได้แก้ หรือแก้ข้อมูลโดยตรงผ่านตัวจัดการฐานข้อมูลตรง ๆ ไปแทน ส่วนหลัก ๆ ที่เจอเยอะ คือลงทะเบียนแล้วไม่ได้ยืนยันการลงทะเบียนที่อยู่ในอีเมลที่ส่งไปยังอีเมลตอนสมัคร ทำให้เข้าใช้งานไม่ได้ ซึ่งเหตุผลที่ต้องยืนยันอีเมลด้วย เพราะต้องใช้เพื่อเป็นช่องทางติดต่อที่ต้องใช้งานได้จริง เพื่อรับของตอนกิจกรรมสิ้นสุดลง เลยจำเป็นต้องมีขั้นตอนนี้

อันนี้บันทึกเท่าที่คิดออก ณ ตอนนี้ ไม่รู้ตกหล่นอะไรไหมนะ ԅ(¯﹃¯ԅ)

เปรียบเทียบรูปโปสเตอร์เลือกตั้งที่เป็นรูปแถมจากซีดีเธียร์เตอร์ AKB48 52nd Single「Teacher Teacher」และรูปที่ปริ๊นต์จากร้านอัดรูป “Kamera no Kitamura” ที่ญี่ปุ่น

เปรียบเทียบรูปโปสเตอร์เลือกตั้งที่เป็นรูปแถมจากซีดีเธียร์เตอร์ AKB48 52nd Single「Teacher Teacher」และรูปที่ปริ๊นต์จากร้านอัดรูป “Kamera no Kitamura” ที่ญี่ปุ่น ที่ได้รับลิขสิทธิ์เพื่อปริ๊นต์ ภาพอย่างถูกต้องจาก AKS

ความละเอียดของภาพไม่หนีกันเลย โดยดูจากตรงเส้นผมที่จะมาเป็นเส้นที่ชัดมากทั้งสองแบบ

โทนสว่างและมืดของภาพ ภาพจากซีดีจะมืดกว่านิด ๆ ภาพที่ปริ๊นต์เองจะสว่างกว่าเล็กน้อย (ไม่จับผิดนี่มองไม่ออก)

ด้านล่างของรูปจากซีดีจะใส่ที่มาว่ามาจากซีดี AKB48 Teacher Teacher ซึ่งมีขนาดใหญ่กว่าจากการปริ้นต์ที่จะบอกแค่บริษัทที่ถือลิขสิทธิ์ ทำให้พื้นที่ในการวางรูปโปสเตอร์ของรูปแถมจากซีดีเล็กกว่าเล็กน้อย

ด้านหลังรูป ในส่วนรูปแถม จะมีลายน้ำ AKB48 Teacher Teacher ส่วนรูปปริ้นต์จะเป็นกระดาษอัดรูปที่เราคุ้นเคยกัน คือบอกยี่ห้อกระดาษ แต่มีลายน้ำระบุชื่อไฟล์รูป และหมายเลขลำดับรูปที่สั่งปริ้นต์ออกมาแต่ละรูปจะมีลำดับเลขรันไปเรื่อยๆ

โครงการ Music We Choose You เราส่งรหัสโหวตเลือกตั้งด้วยวิธีอัตโนมัติอย่างไร

ก่อนอ่านผมอยากทำความเข้าใจก่อนว่าผมเป็นเพียงหนึ่งในทีมงาน Music We Choose You กว่าหลายสิบชีวิตเท่านั้น มีทีมงานส่วนอื่น ๆ ที่ต้องอ่านบัตรโหวต และกรอกด้วยมือโหวตลงไปบนเว็บเองอีกหลายพันโหวต ที่ไม่ได้เกิดจากกระบวนการนี้ และใน blog นี้ อาจไม่ได้กล่าวถึงชื่อทีมงานหลังบ้านเป็นรายคนได้ เพราะเพิ่งได้เจอหน้ากันก็ตอนวันลุ้นผลคะแนนเสียงเลือกตั้ง และบางคนที่เป็นทีมงานฝั่งญี่ปุ่นก็ยังไม่เจอหน้ากันจนวันนี้ แต่ทุกคนทำงานอย่างหนัก เพื่อให้ได้ซึ่งรหัสโหวตเลือกตั้งมาเข้ากระบวนการ และสุดท้ายคือผู้สนับสนุนเงินที่ร่วมกันโดเนทเข้าโครงการเพื่อให้มีเงินจำนวนมากในการจัดหารซีดี และรหัสโหวตเลือกตั้งมา นั้นหมายความว่า 18,502 คะแนน เกิดจากความร่วมแรงร่วมใจกันของทีมงาน และผู้สนับสนุนทุกคน ผมไม่ขอรับเครเดิตเหล่านี้ไว้เพียงคนเดียว ถ้ามีรายชื่อที่แน่ชัด เดี่ยวจะนำลงมาใส่ให้อีกครั้ง

ในส่วนของกระบวนการนี้ไม่ได้เป็นสิ่งใหม่อะไร แต่เป็นการสังเกต และมีต้นแบบจากคลิปจากแฟนคลับฝั่งญี่ปุ่นที่ได้ลงแนวทางปฏิบัติไว้ เรานำมาปรับปรุงและเปลี่ยนแปลงในหลาย ๆ ส่วนเพื่อให้สอดคล้องกับการทำงานในทีมอีกรอบ (ใน blog นี้บางขั้นตอนเรามี source code เปิดเผยให้ลองนำไปศึกษาต่อด้วย)

ฉะนั้นการเล่าเรื่องของกระบวนการในส่วนนี้จึงเป็นเหมือนบันทึกความทรงจำ เป็นข้อมูลอ้างอิงในอนาคตสำหรับนำไปปรับปรุงกระบวนการ และสำหรับบุคคลที่สนใจ

ตัวอย่างกระบวนการ และสิ่งที่ผมนำเป็นต้นแบบจากฝั่งญี่ปุ่น

การเขียนใน blog นี้จะมี 2 ช่วง คือช่วงก่อนประกาศผลด่วน กับช่วงหลังประกาศผลด่วน ซึ่งผมพยายามแทรกช่วงก่อนประกาศผลด่วนเพื่อให้รู้ว่าก่อนประกาศผลด่วน เราทำอะไร และนำมาปรับปรุงให้ดีขึ้นอย่างไรหลังจากประกาศผลด่วนเพื่อให้ส่งรหัสโหวตให้ได้ทัน เพราะทีมงานก็เสียดายคะแนนอีกหลายร้อยคะแนนที่ส่งขึ้นไปช่วงผลด่วนไม่ทัน เนื่องจากเรายังประสานงานระหว่างทีมกันไม่ดี ตัวชุดโปรแกรมที่ทำงานในช่วงนั้นยังเจอปัญหาติดขัด และรวมไปถึงเว็บรับรหัสโหวต AKB48 ล่มช่วงประมาณตี 2-5 ในช่วงก่อนปิดรับผลด่วน จนทำให้เราโหวตช่วงผลด่วนไปได้ไม่ตามเป้าที่วางไว้

การติดต่อประสานงานระหว่างทีมงานหลัก ๆ คือใช้ LINE และ Discord สำหรับ LINE จะใช้เพื่อสื่อสารแบบเร่งด่วน และสื่อสารกับทีมฝั่งญี่ปุ่น ส่วน Discord จะใช้ประสานงานช่วงส่งรหัสโหวต และพูดคุยแก้ไขรหัสโหวตในช่วงกลางคืนตั้งแต่ประมาณ 3 ทุ่มถึงตี 2 (มีบางวันตี 3) เหตุผลที่เราใช้เพราะการสื่อสารด้วยเสียงมันไม่ต้องพิมพ์ เพราะมือพิมพ์รหัสโหวต หรือแก้ไขข้อมูลกันอยู่ การพิพม์ LINE ทำให้งานส่วนแก้ไขรหัสโหวตหยุดชะงักได้ แล้วคุณภาพเสียงของ Discord ค่อนข้างดีไม่ค่อยเจอดีเลย์อีกด้วย

เริ่มด้วยการแสกนบัตรโหวตด้วย Fujitsu ScanSnap iX500 ซึ่งเป็น Sheet Fed Scanner ยอดนิยมมากสำหรับงานแบบนี้ โดยเราแสกนออกมาเป็น JPEG ไฟล์ โดยเหตุผลที่เราไม่แสกนออกมาเป็น PDF เพราะ ไฟล์รูปภาพบริหารจัดการง่ายกว่า หรือนำไปเปิดไฟล์เพื่ออ่านกับอุปกรณ์หลากหลาย

ScanSnap iX500_006
ScanSnap iX500_006 @ flickr

เมื่อได้ไฟล์รูปภาพ ทีมงานฝั่งญี่ปุ่น-ไทยตกลงกันว่าจะอัพโหลดรูปภาพผ่าน Google Drive จากญี่ปุ่นมาไทยโดยการแชร์ Folder กลางตัวหนึ่ง เพื่อใช้สำหรับการส่งไฟล์รูปภาพบัตรโหวต และเอาไว้ค้นหาสำหรับกรอกทบทวน-แก้ไขรหัสโหวตในกรณีที่นำไฟล์รูปดังกล่าวไป OCR (Optical character recognition – การแปลงไฟล์ภาพเอกสารที่เป็นรูปภาพ ให้เป็นไฟล์ข้อความตัวอักษรโดยอัตโนมัติ) แล้วไม่ได้รหัสโหวตออกมาอย่างถูกต้อง โดยเราจัดแบ่ง Folder ตามรอบที่ส่งมาจากฝั่งญี่ปุ่น โดยคนที่กำหนดชื่อเป็นฝั่งญี่ปุ่น เพราะต้องให้ทางนั้นกำหนดรหัสอ้างอิงที่เข้าใจง่ายสำหรับคนทำงานฝั่งต้นทางที่มีจำนวนแผ่น และต้องดูแลจัดการบัตรโหวตจำนวนมาก เพราะจำนวนคนฝั่งญี่ปุ่นน้อยกว่าฝั่งไทยมาก เราต้องคิดถึงการจัดการหน้างานฝั่งนั้นเป็นหลัก

ในโครงการนี้ในส่วนของการโหวต เรามีทีมงาน 2 ส่วน คือทีมกรอกมือ และกดโหวต แล้วนำรหัสโหวตที่โหวตเสร็จแล้วมาใส่ใน Google Sheets ส่วนอีกทีมคือทีมที่ผมลงมาช่วยเป็นส่วนหนึ่งของทีมงาน ซึ่งทำผ่าน OCR โดยจุดรวมของรหัสโหวตทั้ง 2 ทีมนี้ คือใช้ Google Sheets ตัวหนึ่ง เพื่อใช้สำหรับทำงานร่วมกันเพื่อแชร์ข้อมูลรหัสโหวต และการแก้ไขข้อมูล โดยทั้ง 2 ทีมมีคนกว่า 10 คนช่วยกันดู ช่วยกันแก้ไขรหัสโหวต เราเก็บรหัสโหวตต่างๆ พร้อมกับชื่อไฟล์รูปจากใน Google Drive เพื่อช่วยอ้างอิงย้อนกลับไปยังรูปต้นฉบับที่รหัสโหวตนั้นถูกกรอกมา เพราะหากรหัสโหวตนั้นใช้ไม่ได้ด้วยเหตุผลใด ๆ เราสามารถกลับไปค้นหาได้ง่าย

ในรอบส่งรหัสโหวตก่อนประกาศผลด่วนเมื่อเราได้รับการยืนยันรูปที่จะทำ OCR ได้แล้ว เราเอารูปเหล่านั้นไปเข้า OCR ทันที แล้วพบว่าความเร็วและข้อผิดพลาดมีเยอะพอสมควร ในระดับความผิดพลาด ~35% และความเร็วควรจะเร็วได้มากกว่านี้

แต่หลังจากรอบผลด่วนเราปรับปรุงกระบวนการเพิ่มด้วยการใช้โปรแกรมที่ชื่อ XnView เข้ามาเพื่อทำ resize/crop แบบ batch (GitHub – Source Code) ในส่วนของรูปก่อนส่งเข้า OCR เพื่อให้ตัว OCR มันอ่านเฉพาะจุดที่จำเป็นต้องอ่าน และได้ผลตอบกลับที่เร็วขึ้น โดยรูปหมื่นกว่ารูปผ่านกระบวนการ batch นี้ เพื่อช่วยเรื่องลดความผิดพลาดจาก OCR ให้ได้มากที่สุด และยังลดจำนวนข้อมูลที่จะต้องส่งขึ้นระบบของ Google Cloud Vision API ได้ด้วย เราเคยจับเวลา รูปบัตรโหวตกว่า 2,000 รูป เราใช้เวลาไม่ถึง 15 นาที โดยนับเวลาตั้งแต่ resize/crop รูปบัตรโหวตจำนวนดังกล่าว มาจนได้ตัวเนื้อ Text กลับออกมาจาก Google Cloud Vision API กว่า 1,800 รายการ ซึ่งเกือบทั้งหมดที่สามารถนำไปส่งรหัสโหวตเข้าระบบได้ทันที ระดับความผิดพลาดลดต่ำลงเหลือ ~20% นั้นทำให้เราได้ประโยชน์จากการใช้ไฟล์รูปภาพแทน PDF เพราะเราสามารถหาชุดโปรแกรมที่สามารถทำการ resize/crop ได้ง่ายกว่า

รูปภาพต้นฉบับ

จะได้รูปภาพที่โดน resize/crop แบบด้านล่าง

 

สำหรับ OCR ที่เราใช้คือ Google Cloud Vision API  ด้วยเหตุผลตั้งต้นดังนี้

  1. การหา OCR Library แบบ Offline ไม่น่าจะคุ้มค่า เพราะคิดบนฐานค่าใช้จ่ายหากซื้อ License หรือหากใช้ Open source ก็ต้องแน่ใจว่าจะใช้งานได้จริง เราลองผิดลองถูกจุดนี้ไม่ได้มากนัก คำตอบของโจทย์จึงตกมาที่ Cloud Service แทน
  2. การเชื่อมต่อกับ Google Sheets API บน Credential ตัวเดียวกันได้ ทำให้การพัฒนาส่วนนี้เร็วขึ้น

แต่สุดท้ายในเหตุผลข้อที่ 2 ไม่ได้ถูกนำไปใช้หลังจากวันประกาศผลด่วน แม้จุดประสงค์คือส่งรหัสโหวตขึ้น Google Sheets ให้ทีมงานคนอื่นเห็นรหัสโหวตชุดนั้นร่วมกันได้ทันที แต่เจอปัญหาใหญ่คือติด rate limit API และทำเรื่องของ request จากทีม support ของ Google แล้วการตอบสนองไม่ทันต่อการใช้งาน (รอตอบกลับ 3 วัน) ผมจึงตัดสินใจย้ายไปใช้ฐานข้อมูลอย่าง MariaDB บน Cloud แทน แล้วเอารหัสโหวตที่ได้จาก OCR ใส่ย้อนหลับไปบน Google Sheets ด้วยมือ (Google Sheets API – Usage Limits = 500 requests per 100 seconds per project, and 100 requests per 100 seconds per user.)

ในขั้นตอน OCR จนได้รหัสโหวตมานี้ ช่วยลดจำนวนงานหลังบ้านที่จะต้องกรอกรหัสโหวตลงใน Google Sheets ได้เยอะมาก โดยเราเหลือกำลังคนไว้แก้รหัสโหวตที่ผิดพลาดประมาณ 15-20% ที่เหลือแทนได้

ในส่วนของ Script ที่เราใช้ส่งรูปขึ้น Google Cloud Vision API นั้น เราใช้ PHP ที่เขียนแบบ CLI เพื่อส่งไฟล์รูปข้างต้นขึ้น Google Cloud Vision API เพื่อให้ได้ Text ออกมา แล้วใช้ Regular expression กรองรหัสโหวตตาม pattern ที่กำหนดออกมา เป็นไฟล์แบบ CSV ไฟล์ (GitHub – Source Code)

เหตุผลที่เราใช้ CSV เพราะมันสามารถ import เข้าฐานข้อมูลผ่าน HeidiSQL เครื่องมือจัดการฐานข้อมูล MariaDB (ใช้งานกับ MySQL หรือ SQL Server ก็ได้) ได้ทันที และยังสามารถเปิดแก้ไขบน Spreadsheet อย่าง Microsoft Excel หรือ Google Sheets ได้ด้วย

โดยตัว HeidiSQL สามารถ export ตัวข้อมูลย้อนกลับมาเป็น CSV เพื่อนำไปทำงานบน Google Sheets ได้ทันที ทำให้เราไม่จำเป็นต้องใช้ Script อีกตัววิ่งส่งข้อมูลไปอัพเดทผ่าน Google Sheets API ตลอดเวลา และจุดสำคัญ การได้ CSV ไฟล์ดังกล่าว สามารถเอาไฟล์ CSV นั้นไปใช้กับโปรแกรมส่งรหัสโหวตอัตโนมัติที่เราเตรียมไว้ได้ด้วย

บางคนอาจจะคิดว่าทำไมไม่ใช้ฐานข้อมูลกลางเชื่อมต่อกันไปเลย ทำไมใช้ CSV โยนให้โปรแกรมส่งรหัสโหวตอัตโนมัติ?
– เหตุผลคือเรื่องกระจายเครื่องไปหลาย ๆ เครื่องเพื่อส่งรหัสโหวต ตัวโปรแกรมดังกล่าวติดตั้งง่ายไม่ซับซ้อนมาก และคัดลอกตัว Project และ CSV ของรหัสโหวต ปรับค่านิดหน่อย ก็เริ่มต้นทำงานได้แล้ว การเชื่อมฐานข้อมูลกลางอาจจะต้องทำ whitelist IP ฯลฯ อีกหลายตัวเพื่อป้องกันโดน hack อีกชั้น ซึ่งยุ่งยากเกินไป

สำหรับตัวโปรแกรมที่ใช้ในการส่งรหัสโหวตแบบอัตโนมัตินั้น ที่ชื่อ Katalon Studio พระเอกของงานนี้ ซึ่งเป็นเครื่องมือ automation testing solution ของนักพัฒนาซอฟต์แวร์ที่ โดยอ่านข้อมูลรหัสโหวตจากไฟล์ CSV ไปส่งลงฟอร์มของเว็บ AKB48

ตัว script ภายในเขียนด้วย groovy (GitHub – Source Code) ดักจับผลการส่งรหัสโหวตมี 4 สถานะดังนี้

  1. OK – รูปซ้ายบน
    เราดัก Keyword “投票完了” ที่หมายถึง Vote completed
    สถานะลงคะแนนโหวตสำเร็จ เก็บรวบรวมไปทำข้อที่ 2 อีกทีตอนที่ไม่มีรหัสโหวตส่งเข้าระบบเพื่อบันทึกวัน-เวลา และยืนยันผลอีกรอบ
  2. Already – รูปขวาบน
    เราดัก Keyword “入力したシリアルナンバーは既に投票済みです。” ที่หมาย The serial number you have entered has already been voted.
    เป็นสถานะที่บอกว่ารหัสโหวตนี้ถูกใช้งานกับผู้ลงเลือกตั้งคนนี้ โดยมีเวลากำกับ
    โดยเราจะต้องนำผลของหน้านี้ใช้ Regular expression เอาชุดข้อมูลวันและเวลาที่โหวตออกมาใส่ลงฐานข้อมูลด้วยเพื่อยืนยันวัน-เวลา
  3. Error – รูปขวาล่าง
    เราดัก Keyword “エラーが発生しました。” ซึ่งหมายถึง An error occurred.
    เกิดจากเว็บ AKB48 เกิดข้อผิดพลาด โดยถ้าเราได้ข้อความตัวนี้ คือโหวตไม่เข้า และเท่าที่เจอ คือรหัสโหวตใช้งานได้ เราต้องเอาไปเข้ากระบวนการสำหรับส่งรหัสโหวตเข้าเว็บอีกรอบ
  4. Failed – รูปซ้ายล่าง
    เราดัก Keyword “入力されたシリアルナンバーは無効であるか既に投票済みです。” ที่หมายถึง The serial number entered is invalid or already voted.
    ตรงนี้จะมี 2 ส่วน คือรหัสโหวตผิดจริง ๆ หรือรหัสโหวตถูกนำไปใช้กับผู้สมัครคนอื่นแล้ว หากรหัสโหวตถูกต้องโดยเทียบกับบัตรโหวต แสดงว่ารหัสโหวตใบนั้นถูกนำไปโหวตกับผู้สมัครคนใดคนหนึ่งที่ไม่ใช่คนที่เรากำลังโหวตอยู่ วิธีต่อไปคือเอารหัสดังกล่าวไปกรอกให้กับผู้สมัครคนอื่น ๆ ถ้าคนไหนขึ้นสถานะแบบข้อที่ 2. ก็คือรหัสโหวตนั้นถูกใช้โหวตผู้สมัครคนนั้นไปก่อนหน้านี้

รูปแบบหน้าทั้ง 4 หน้าที่จะเจอเมื่อเราส่งผลโหวต


จากการใช้ Katalon Studio เราจะเก็บผลการทำงานทั้ง HTML ไฟล์ และ Screenshot ของหน้าผลการโหวตเป็น JPEG ด้วย เพื่อใช้ในการตรวจสอบต่อไปหลังจบรอบโหวต

ในช่วงก่อนประกาศผลด่วนเราไม่ได้ตรวจสอบเคสในข้อที่ 3. และ 4. แยกออกจากกัน ทำให้เราพลาดคะแนนบางส่วนไปเพราะเว็บรับรหัสโหวต AKB48 ไม่ได้เสถียรตลอดเวลา จะมีการส่ง “An error occurred.” ออกมาอยู่เรื่อยๆ แม้แต่ช่วงวันท้าย ๆ ก็ยังไม่ได้แก้ไขให้ดีขึ้น มีอยู่หลายช่วงที่เราส่งรหัสโหวตไปกว่า 1,000 รายการ แล้วโดนตีตกด้วย An error occurred. อยู่เกือบ 600 รายการ และสุดท้ายทั้งหมดที่โดนตีตกเป็นรหัสโหวตที่ใช้งานได้ทุกตัว ทำให้เราต้องคัดแยกเพื่อให้ทีมหลังบ้านที่อ่านรหัสโหวตผิดพลาดไม่ต้องไปอ่านข้อมูลที่เกิดจากตัวเว็บ AKB48 แจ้งความผิดพลาดทั้ง ๆ ที่รหัสโหวตไม่ผิดอีก เพื่อลดงานฝั่งคนแก้ไขรหัสโหวตลง

เมื่อรหัสโหวตทุกส่วนส่งครบหมดจะเหลือเฉพาะรหัสโหวตที่โหวตได้ และรหัสโหวตที่โหวตไม่ได้ เราจะเอา log ของการส่งรหัสโหวตครั้งนั้นมาอ่านด้วย PHP ที่เป็น script ตัวอ่าน log แล้วเข้าไป flag สถานะในฐานข้อมูลตามแต่ละรหัสโหวตที่มี เพื่อบอกว่าโหวตสำเร็จหรือไม่ แล้วทำการสรุปรหัสโหวตที่โหวตได้ และรหัสโหวตที่โหวตไม่ได้ แยก Worksheet บน Google Sheets ออกจากกัน โดยในส่วนของ Worksheet ที่ใส่รหัสโหวตที่ส่งแล้วผิดพลาด จะมีทีมงานอีกส่วนเข้ามาช่วยกันอ่านไฟล์รูปบัตรโหวตและกรอกรหัสโหวตที่ถูกต้องลงไปแทน แล้วเมื่อจบการแก้ไขในแต่ละ Worksheet จะรวบรวมแล้วใช้โปรแกรมอัตโนมัติส่งเข้าเว็บโหวตอีกรอบ และในส่วนรหัสโหวตที่โหวตเรียบร้อย จะมีรอบสำหรับทบทวนวัน-เวลาที่โหวตเพื่อยืนยันรหัสโหวตว่าเมื่อโหวตเข้าไปแล้ว

โดยในขั้นตอนทบทวนวัน-เวลา เราจะใช้ script ส่งรหัสโหวตชุดเดิมส่งรหัสโหวตที่ต้องการวัน-เวลาส่งเข้าไปที่เว็บ AKB48 จนได้สถานะตามข้อที่ 2 แล้วใช้ script อีกชุดเข้าไปอ่านวัน-เวลาในไฟล์ HTML ที่บันทึกไว้ เพื่อนำไปอัพเดทลงฐานข้อมูลเมื่ออัพเดทเสร็จครบทั้งสถานะและวัน-เวลาโหวต เราจะเอาผลทั้งหมดกลับขึ้นไปที่ Google Sheets เพื่อสรุปผลการโหวตในแต่ละรอบเพื่อปิดงานรอบนั้น (ในขั้นตอนทบทวนวัน-เวลานี้เราเอารหัสโหวตที่กรอกด้วยมือไปก่อนหน้านี้ของอีกทีมหนึ่ง มาเข้ากระบวนการนี้ด้วยเช่นกัน)

นั้นหมายความว่า ทุก ๆ รหัสโหวตเราส่งเข้าเว็บ AKB48 เราจำเป็นต้องส่งรหัสโหวตเข้าเว็บโหวตอย่างน้อย 2 ครั้ง แต่ผลดีคือ มันช่วยทำให้เราสรุปยอดในแต่ละวันว่าเราส่งรหัสโหวตเข้าไปได้เท่าไหร่ และช่วยให้เราเก็บคะแนนที่ตกหล่นกลับมาโหวตได้อีกพอสมควร

ตัวอย่างการทำงานในการส่งรหัสโหวตด้วย Katalon Studio

สำหรับหน้ารายงานรหัสโหวต และจำนวนโหวตที่ Actual Vote ช่วงแรกเราใช้ Firebase Realtime Database ซึ่งทำการ Sync ข้อมูลกับ Google Sheetes อีกที ช่วงแรกข้อมูล Realtime มาก กดอัพเดทจำนวนคะแนนฝั่งหน้าเว็บรายงานขึ้นคะแนนใหม่ทันที แต่ด้วยความที่เราไม่ได้ optimize อะไรเลย (คือไม่มีเวลาทำ) สุดท้ายมันก็ไม่ได้ Realtime ตลอด เจออาการ Push ไม่ทำงาน เพราะเราไม่ได้ Index ข้อมูล มันเลยหยุด Push เพราะด้วยจำนวนของ node (รหัสโหวต) เริ่มเยอะขึ้น แถมเราเจอ charge ค่า B/W ของ Firebase เยอะตามไปด้วย เลยเปลี่ยนแผนใหม่เป็นการใช้ไฟล์ JSON data แบบอัพมือแทน โดยก็แค่ Sync จาก Google Sheets ไป export เป็นไฟล์ JSON data เดี่ยว ๆ แล้วฝั่งทีมเว็บส่วนรายงานผลแค่เอาไฟล์ไปวางที่เดิมก็จบงาน ลดค่าใช้จ่ายไปได้เยอะมาก แต่ก็มาหนักฝั่ง client ซึ่งก็เป็นเรื่องที่ช่วยไม่ได้ เพราะเอาเร็วเข้าว่า แล้วช่วง 2-3 วันสุดท้ายเราไม่ได้รายงานคะแนนสุดท้าย เพราะคิดว่ารอสรุปยอดทีเดียวหลังปิดโหวตดีกว่า เพื่อให้ทีมงานทุกส่วนได้โฟกัสกับการหาบัตรโหวต และส่งรหัสโหวตเข้าระบบ AKB48 ให้ได้จนหมดโดยไม่ตกหล่น และยืนยันผลให้พร้อมทุกอย่าง เพราะเราพลาดแบบรอบผลด่วนอีกไม่ได้แล้ว

ในส่วนของเครื่องคอมฯ ที่ส่งรหัสโหวตนั้น เราใช้ทั้งหมด 3 ตัวทำงานสลับกัน มี 1 ตัวทำงานตลอด 24 ชั่วโมง 5 วันติดต่อกันเพื่อโหวตเข้าระบบ และทบทวนผลการโหวตออกมาเป็นวัน-เวลาที่โหวตเพื่อให้แน่ใจว่าคะแนนทุกคะแนนที่ถืออยู่จะเข้าระบบจริง ๆ ซึ่งหากจำนวนคะแนนในมือเยอะกว่านี้ อาจส่งไม่ทัน เพราะจากที่ลองทดสอบค่าเฉลี่ยนในการส่งโหวต สามารถทำความเร็วได้ที่ 600-800 รหัสโหวตต่อชั่วโมง (อย่าลืมว่าเราต้องเก็บตกจากเหตุการณ์เจอหน้าเว็บ Error และทวนวัน-เวลาโหวตอีก) แต่เราก็ได้เตรียมแผนรองรับไว้แล้ว ด้วยการเตรียมเช่า Cloud VPS ที่อยู่ญี่ปุ่นเพิ่มเติมสำหรับส่งรหัสโหวตเพิ่มเติมได้ทันที (คิดว่าภายใน 4-6 ชั่วโมงน่าจะสร้างเครื่อง และพร้อมส่งรหัสโหวตได้อย่างน้อย 4 ตัว หรือเพิ่มความเร็วเป็น 2,000-3,000 โหวตต่อชั่วโมง)

สรุปว่าโครงการนี้เน้นถึกเยอะมาก script อะไรต่าง ๆ บางส่วนก็เผา และ workaround กันแบบสุด ๆ เนื่องจากรีบ และไม่มีเวลามาคิดเรื่อง seamless มากนักในบางขั้นตอน (เพราะงานประจำก็ต้องทำ คะแนนที่ต้องส่งก็เยอะขึ้นกว่าที่คาดหวังไว้ด้วย ต้องเผื่อเวลาตกหล่นไว้อย่างน้อย 1-2 วัน) ตามตารางกราฟด้านล่าง

ช่วงวันประกาศผลเลือกตั้ง ค่อนข้างลุ้นมาก เพราะตอนที่ประกาศแล้วผ่านคะแนนของโครงการไปแล้ว แล้วน้องยังไม่ได้ประกาศชื่อ โดนแซวว่า “พี่ฟอร์ดทำโปรแกรมโหวตผิดคนป่าวพี่” ทีมงานก็ใจคอไม่ดี ผมก็ใจคอไม่ดี เพราะลุ้นมาก แล้วเราทบทวนรหัสโหวตและคะแนนกันเยอะมาก

แล้วสุดท้ายก็เป็นแบบวิดีโอข้างล่างเนี่ยแหละครับ

หากมีอะไรอัพเดทหรือตกหล่น เดี่ยวค่อย ๆ อัพเดทใน blog ตอนนี้เพิ่มเติมอีกที เพราะตอนนี้นึกออกมาได้ประมาณนี้

ทิ้งท้าย ด้านล่างคือสรุป Workflow การทำงานในช่วง 7 วันสุดท้ายของการโหวต

 

 

 

ว่าด้วยการจัดเวลาไลฟ์ของเมมเบอร์ BNK48 ใน VOOV

ถ้าได้ตาม 48G ฝั่งญี่ปุ่นไลฟ์ใน Showroom มาก่อน จะหงุดหงิดกับการจัดเวลาไลฟ์ของน้อง ๆ BNK48 ใน VOOV อยู่พอสมควร เพราะฝั่งญี่ปุ่น การไลฟ์ใน 1 วันของเมมเบอร์บางคนก็มากัน 1-2 รอบก็มี บางคนเดือนนึงจะมาสักครั้ง หรืออาทิตย์นึงเจอกันครั้งนึงก็มี หรือว่าง่าย ๆ จัดสรรเวลากันแบบมาเรื่อยๆ มากันตลอด ไม่ใช่หายไปทั้งวง ผมตามอยู่เกือบ 10 คน วันนึงต้องได้ดู Showroom สักคน แล้วเวลาที่เมมเบอร์ฝั่งญี่ปุ่นไลฟ์เหมือนจะไม่ได้กำหนดระยะเวลาตายตัวว่า 1 ชั่วโมง บางคนยาว 2-3 ชั่วโมงก็มี บางคนมา 10-15 นาทีก็มี แล้วก็มักเจอไลฟ์พร้อม ๆ กันหลายคน ยิ่งช่วงหัวค่ำของที่โน้นนะ เปิด Showroom เข้าไป เจอ 5-10 คนนี่เรื่องปรกติมาก (อันนี้เกิดจากประสบการณ์ที่ได้ติดตาม แน่นอนว่าดีลธุรกิจจริงๆ เป็นอย่างไรก็อีกเรื่อง)

อย่างวันก่อนเปิดตัวโปสเตอร์เลือกตั้ง ระหว่างั้น ก็มีเมมเบอร์ญี่ปุ่น เค้าก็ไลฟ์ซ้อนกับรายการนั้นไม่ได้หยุดไลฟ์แต่อย่างใด แล้วช่วงที่มีไลฟ์เลือกตั้ง มักมีเมมเบอร์เปิดไลฟ์ใน Showroom ไปพร้อม ๆ กับแฟนคลับ พูดคุยกับแฟนคลับไป ดูไลฟ์เลือกตั้งไปด้วยก็มี

จากสิ่งที่พบเจอมา แล้วเอามาเทียบกับฝั่ง BNK48 ก็จะเห็นว่า หากสามารปรับเปลี่ยนให้ไลฟ์แบบรับผิดชอบตัวเอง จัดเวลาเอง มีกลยุทธในการไลฟ์เพื่อดึงฐานแฟนคลับ ดูว่าเวลาไหนเหมาะสมของแต่ละคน มีการนัดแฟนคลับว่าวันนี้วันนั้น ผมว่าการไลฟ์จะดูน่าติดตามกว่านี้ ยิ่งตอนนี้มีรุ่น 1 และ 2 คิดว่าจำนวนเมมเบอร์ก็น่าจะเยอะพอที่จะปรับเปลี่ยนรูปแบบการจัดเวลาในการไลฟ์ได้บ้าง อีกทั้งผลดีก็ตกอยู่กับน้องๆ เอง โดยเฉพาะรุ่น 2 ที่ยังไม่มีฐานแฟนคลับมากนัก เพราะน้องๆ ได้สร้างฐานแฟนคลับได้ง่ายมากขึ้น แล้วไม่จำเป็นต้องวันละ 1 ชั่วโมงพอดี แต่อาจจะนิดๆ หน่อยๆ แต่เก็บให้ครบเดือนละ 1-2 ชั่วโมง ก็น่าจะเป็นทางออก (ถ้าไลฟ์ชนกันก็ไม่น่าจะมีผลอะไรเพราะ VOOV ดูย้อนหลังได้อยู่ดี)

แล้วยิ่งช่วงนี้มีเลือกตั้งเมมเบอร์ฝั่งญี่ปุ่นเค้าไลฟ์กันเยอะมาก มากันทุกวันเลยก็มีบางคน ก็ออกจะเสียดายโอกาศหลายๆ อย่างเหมือนกันนะ

ตัวอย่างกลยุทธในการไลฟ์เพื่อดึงฐานแฟนคลับ ที่น่าสนใจของฝั่งญี่ปุ่น “1 Year Miracle โอนิชิ โมโมกะ เด็กสาวตี5ครึ่ง ปาฏิหาริย์ที่เกิดจากความพยายามในอีกรูปแบบหนึ่ง