Author: kanakorn.h

  • วิธีรัน Jupyter Notebook, ปิด browser แล้วระบบส่ง LINE บอกพร้อมภาพ เมื่อเสร็จแล้ว ค่อยกลับมาดูผล

    ปัจจุบัน งานด้าน Data Science ก็มักจะใช้ Jupyter Notebook เพราะสะดวกในการทดลอง ทดสอบ ทำทีละบรรทัด ดูผล ปรับแต่งไปได้เรื่อย ๆ แถมสามารถซื้อ Server ส่วนกลาง ลงทุน GPU แล้วใช้พร้อม ๆ กันหลาย ๆ คนทั้งทีมก็ได้

    ปัญหาอยู่ตรงที่ การสร้าง Model มักจะใช้เวลานาน (มาก) แล้ว Jupyter มันเป็น Web-based จะปิด Browser ก็ได้ แต่หลายคนคงเคยเจอว่า พอกลับมาเปิด URL เดิม ก็ไม่เห็นผลที่รันแล้ว

    จริง ๆ แล้วคือ Jupyter Notebook นั้น ทำงานต่อไป จนเสร็จแล้วแหล่ะ แต่ เว็บ Browser น่าจะไม่สามารถต่อ Session ได้ (หวังว่าจะนึกภาพออก) ที่สำคัญ มันรันนานมาก เสร็จเมื่อไหร่ก็ไม่รู้ แล้วจะรู้ได้อย่างไรว่าเสร็จ เสร็จแล้วผลเป็นอย่างไร

    ในบทความนี้ จะนำเสนอวิธีการที่ทำให้

    1. สั่งรันงานที่ใช้เวลานาน (ขอยกตัวอย่าง 100 วินาที) แล้วปิด Browser ไปทำอย่างอื่นต่อได้เลย
    2. เมื่อระบบทำงานเสร็จ จะแจ้ง LINE Notify พร้อมแนบภาพ Graph ผลลัพธ์ ส่งมาด้วย
    3. พอกลับมาเปิด Browser กลับมาใน Jupyter Notebook เดิม สามารถดูผลการรันอื่น ๆ ได้อีกครั้งเหมือนกับไม่ได้ปิด Browser
    ภาพรวม

    เริ่มต้นจาก Import

    import sys
    from IPython.display import clear_output
    import logging
    import sys
    import requests
    import datetime,time,pytz
    import random

    Function ในการส่ง LINE Notify

    เป็นส่วนของ LINE TOKEN และ function “jobdone” ไว้ส่ง LINE Notify สามารถกำหนดข้อความ และกำหนดภาพที่จะแนบได้

    LINE_TOKEN="YOUR-LINE-TOKEN"
    
    def jobdone(LINE_TOKEN, message="Done!", img=None):
        notify_url = 'https://notify-api.line.me/api/notify'
        header={
            'Authorization': "Bearer " + LINE_TOKEN
        }        
        data=({
            'message': message
        })
    
        files = {'imageFile': open(img, 'rb')} if img else None
        session = requests.Session()
        response = session.post(
            notify_url,
            headers=header,
            files=files,
            data=data
        ).json()
        if files:
            files['imageFile'].close()

    Function สำหรับ Plot

    รับข้อมูล และ สร้างภาพเพื่อประกอบการส่ง Line

    import matplotlib.pyplot as plt
    def plot_something(data, resultfilename):
        fig, ax = plt.subplots()
        ax.plot(data)
        ax.grid()
        fig.savefig(resultfilename)
        plt.show()

    ส่วนที่ใช้เวลานาน

    %%capture capture_output
    resultfilename="result.png"
    start_time=time.time()
    
    data=[]
    for i in range(100):
        data.append(random.random()*100)
        time.sleep(1)
    plot_something(data, resultfilename)
    
    duration=time.time()-start_time
    now=datetime.datetime.now(pytz.timezone('Asia/Bangkok')).strftime("%Y-%m-%d %H:%M:%S")
    jobdone(LINE_TOKEN, f'{now} เสร็จแล้วนะโว้ย [{duration:.2f} sec.]', img=resultfilename)

    ตรงนี้ต้องอธิบายเพิ่ม

    ใน Jupyter จะมีสิ่งที่เรียกว่า “magic cell” ในที่นี้คือ

    %%capture capture_output

    โดยที่ %%capture คือคำสั่งที่จะเก็บผลลัพธ์ทุกอย่าง ที่ส่งออกจาก stdout, stderr รวมถึงภาพด้วย ในคำสั่งข้างต้น จะส่งออกไปยัง capture_output ซึ่งจะนำไปใช้ในภายหลัง

    ต่อมา จะจำลองการทำงานที่ยาวนาน คือ random ค่าระหว่าง 1-100 เอาไปใส่ใน array “data” จากนั้น ก็ sleep 1 วินาที รวมแล้ว ขั้นตอนนี้จะทำงาน 100 วินาที หรือ 1 นาที 40 วินาที (ในการทำงานจริง ยาวนานกว่านั้นมาก ๆ)

    เมื่อเสร็จแล้ว plot_something จะเอาข้อมูล data ไป plot แล้วแสดงผล และ save เป็นไฟล์ตามที่กำหนด คือ result.png

    คำสั่งต่อมา เป็นการแสดง วันเวลาปัจจุบัน โดยแสดงเป็น Timezone ของไทย

    สุดท้าย function jobdone กำหนดข้อความ และภาพที่จะส่ง

    Let’s go!

    รอไป 1 นาที 40 วินาที ก็จะได้ LINE Notify (อ่านขั้นตอนการได้มาซึ่ง LINE Token ได้จาก วิธีแจ้งเตือนจาก Google Forms เข้า LINE )

    กลับมาใน Jupyter Notebook

    ใน cell ใหม่ ใช้คำสั่ง

    capture_output()

    ก็จะแสดงผลสิ่งที่ดำเนินการไป ระหว่างนั้นได้ครับ

    หวังว่าจะเป็นประโยชน์ครับ

  • [บันทึกกันลืม] วิธีตั้งค่า DBeaver ให้สามารถใช้ Memory ได้สูงขึ้น

    พอดีใช้ MacBook RAM 16 GB และใช้งาน DBeaver เป็น Database Tool รุ่น Community ดีอ่ะ มันฟรี Run บน Java ทำให้ใช้งาน Cross platform ได้

    ส่วนคนในสถานศึกษา สามารถใช้งานรุ่น Pro ได้นะ ใช้ Email ของมหาวิทยาลัยไปขอ Academic Licence ได้ ใช้งานได้เหมือนรุ่น Enterprise แต่ก็ต้องขอใช้ปีต่อปี ข้อดีคือ มีพวก Visual Query Builder, Support NoSQL และ ติดต่อกับพวก Big Data ได้ ลองใช้งานดู ถ้าถูกใจใช้ถาวร ก็ซื้อเค้าเถอะ ดีจริง ๆ ดีกว่าไปใช้ขอ Crack เสี่ยงภัยมาก

    แต่ส่วนตัว ใช้รุ่น Community Edition ก็รู้สึกเพียงพอแล้ว ใช้ต่อไป

    มีปัญหาอยู่นิดเดียวคือ ค่า Default ของการใช้ Java Virtual Machine เนี่ย มันตั้ง configuration มาให้ใช้แค่ 1 GB ทำให้เวลาเขียน Query ที่บางทีต้อง Fetch ข้อมูลมาไว้ใน Local Memory บ้าง ก็จะขึ้น Error

    java.lang.OutOfMemoryError: Java heap space ”

    (RAM 16 GB นะโว้ย เต็มไรกัน)

    ซึ่งไปลองค้นหามา พบว่า เราสามารถตั้งค่าได้ โดยแก้ไขไฟล์ dbeaver.ini

    สำหรับคนใช้ MacOS ทำงี้นะครับ

    ไปที่ Finder > Application หาคำว่า DBeaver
    คลิกที่เมนู Show Package Contents
    Contents > Eclipse > dbeaver.ini
    ใช้ Editor ตามใจปรารถนา แก้ตรง -Xmx1024m เป็น สัก 1/4 หรือ 1/2 ของ RAM ก็ดี มากไป เครื่อง Hang ได้ ผมตั้งไว้ 4GB ก็แก้เป็น -Xmx4096m แล้ว Save

    ก็เท่านี้ ปิด เปิดใหม่ คราวนี้ เปิด Database เขียน Query ได้ตามสะดวกครับ

  • วิธี Word Wrap ใน Jupyter Notebook / Jupiter Lab

    มีคำถามมา หาคำตอบเจอ คิดว่าเป็นประโยชน์​เลยบันทึกไว้

    ปัญหาของคนใช้ Jupyter Notebook / Jupyter Lab คือ ถ้ามีโค๊ดยาว ๆ จะไม่ขึ้นบันทัดใหม่ให้ จริง ๆ ต้องบอกว่า ไม่ Word Wrap ตามภาพที่ 1

    ภาพที่ 1: Jupyter Notebook / Jupyter Lab ไม่ Word Wrap

    ไปค้นหามา พบข้อมูลจาก Alex Ioannides ตอบที่ https://stackoverflow.com/questions/48202340/enable-word-wrap-in-jupyterlab-code-editor

    เค้าแนะนำให้เอาแก้ไขโดยใส่สิ่งนี้ใน Settings

    {
       "codeCellConfig": {
          "lineWrap": "wordWrapColumn",
          "wordWrapColumn": 80
       }
    }

    แต่มันอยู่ตรงไหนหล่ะ

    มันอยู่ตรงนี้นะ Settings > Advanced Settings Editor

    แล้วก็คลิกที่ Notebook

    จากนั้นเอาโค๊ดข้างต้นไปแปะที่ User Preferences แล้วกดปุ่ม Save ด้านขวามือบน (รูปแผ่น Disk)

    ผล

    หวังว่าจะเป็นประโยชน์นะครับ

  • [บันทึกกันลืม] วิธีแก้ปัญหาข้อมูลประเภท timedelta64[ns] ใน Pandas ไม่สามารถเขียนลงฐานข้อมูล field ที่เป็น Time ได้

    เหตุ:

    import pymysql
    import pandas as pd
    import sqlalchemy
    
    import time
    import datetime
    
    conn = pymysql.connect(
            host="source.database.server",
            port=3306,
            user='username',
            password='password',
            database='databasename',
        )
    
    sql="""
       SELECT *
       FROM   table1
    """
    df1=pd.read_sql(sql,conn)
    df1.dtypes

    ผลคือ:

    .
    .
    .
    ref_visit_date                            object
    ref_visit_time                   timedelta64[ns]
    .
    .
    .
    

    ซึ่ง ref_visit_time นั้น ใน MySQL/MariaDB ใช้ชนิดเป็น Time

    เช่น 08:00:00 หมายถึง 8 นาฬิกา อะไรทำนองนั้น

    ปัญหา:

    อยู่ที่ตอนเอา Dataframe นี้ ไป Write ใส่อีก Table นึง

    host="destination.database.server"
    port=int(3306)
    user='user'
    password='password'
    dbname='databasename'
    db_string = f"mysql+pymysql://{user}:{password}@{host}:{port}/{dbname}"
    
    destination_conn = sqlalchemy.create_engine(db_string)
    
    df.to_sql('table2'
           , con=destination_conn
           , if_exists='append'
           , index=False
           , method='multi'
           , chunksize=10000)

    ซึ่ง table2 นั้น มีโครงสร้างเหมือนกับ table1 เลย โดยเฉพาะ ref_visit_time มีชนิดเป็น Time เช่นกัน

    แต่ถ้า run code นี้จะได้ Error/Warning ว่า

    UserWarning: the 'timedelta' type is not supported, and will be written as integer values (ns frequency) to the database.

    เหตุเพราะ แทนที่จะเก็บเป็น Time มันดันเปลี่ยนเป็น Integer ที่เป็นจำนวน Nanosecond แทนนั่นเอง

    วิธีแก้ไข

    ต้องตรวจสอบว่า มี column ใน destination table มีชนิดเป็น Time (timedelta64[ns]) ให้เอาไปบวกกับเวลาเริ่มต้น แล้ว เปลี่ยนชนิดเป็น Time แทน ดังนี้

    for column in df:
        if df[column].dtype == 'timedelta64[ns]':
            df[column]=df[column].apply(lambda x: (datetime.datetime.min+x).time())

    เราก็จะได้ข้อมูลที่ถูกต้องกลับมา

    ตามนั้นครับ

  • Zoom Developer – การใช้ Webhook เพื่อรายงานเหตุการณ์ต่าง ๆ ในห้อง Zoom

    เช่นเดียวกับ Enterprise-grade อื่น ๆ Zoom ก็มี API ให้เราพัฒนาโน่นนี่ได้ บทความนี้จะมาแนะนำวิธีการ Webhook เพื่อรายงาน ผ่าน LINE Notify เมื่อมีคนเข้ามาในห้อง เช่นใช้เพื่อแจ้งคนบน Host ว่า มีคนเข้ามาแล้วนะ เป็นต้น

    แต่ก่อนอื่น ต้องเข้าใจว่า Zoom Account มี หลายรูปแบบ โดยเข้าไปที่เว็บ

    https://zoom.us/profile

    1. Basic Account อันนี้ หลายคนคงจะใช้อยู่ ที่ถูกลิมิต ไม่ให้ใช้งานเกิน 40 นาที — อันนี้ จะเห็นคำว่า BASIC อยู่ในหน้า Profile ดังรูปที่ 1
    รูปที่ 1. Basic Account

    2. LICENSED Account แบบนี้ สร้างเอง จ่ายตังค์เอง นักเลงพอ ก็จะประชุมได้ไม่จำกัดเวลา คนเข้าได้พร้อมกัน 100 คน จะมีคำว่า LICENSED ดังรูปที่ 2

    รูปที่ 2. Licensed Account

    3. แต่มีอีกแบบ ไฮโซกว่า คือแบบ Corporate (มั้ง) แบบว่า ในองค์กรซื้อ ข้อแตกต่างกับแบบที่ 2 คือ ประชุมไม่จำกัดเวลา และ ผู้เข้าร่วมได้สูงสุด 300 คน ดังรูปที่ 3

    รูปที่ 3. Corporate Licensed Account

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

    https://marketplace.zoom.us/develop/create

    สำหรับ Basic Account เค้าให้เราเป็น Admin จึงสร้างได้ครบทุกชนิด ดังรูปที่ 4

    รูปที่ 4. Basic Account สร้างได้หมดทุกอย่าง

    สำหรับ Licensed Account แบบจ่ายตังค์เอง เค้าให้เราเป็น Admin จึงสร้างได้ครบทุกชนิด ดังรูปที่ 5

    รูปที่ 5. Licensed Account สร้างได้หมดทุกอย่าง

    ส่วน Corporate Licensed Account … ก็น่าเสียดาย ที่ Admin เค้าให้เราเป็นแค่ Member จึงไม่สามารถใช้งานได้ครบถ้วนได้ เสียดายตังค์ เอ๊ย เสียดายจัง ดังรูปที่ 6

    รูปที่ 6. Corporate Licensed Account สร้างได้เฉพาะ OAuth กับ Chatbot เพราะเค้าให้เราเป็นแค่ Member

    ก็ … แล้วแต่นะ

    มาสร้าง Zoom Webhook กันเถอะ

    Requirement คือ เราเป็น Host เปิดห้อง Zoom เอาไว้ ซึ่งจะเริ่มประชุมเวลา 10:00 แต่ต้องการทราบว่า มีใครเข้ามาก่อนเวลา ให้ ส่ง Line Notify ไปแจ้ง Host ให้ทราบ

    Zoom Webhook จะทำหน้าที่เฝ้าห้องให้ แล้วเมื่อเกิด Event ที่กำหนด ก็จะส่ง POST request ไปยัง URL ที่กำหนดไว้ ดังนั้น สิ่งแรกคือ เราจะมาสร้าง Google Cloud Function ด้วยภาษา Python เพื่อรับ POST Request นั้น

    (Prerequisite: ท่านสามารถใช้ Google Cloud Platform และ LINE Notify ได้แล้ว)

    Google Cloud Function

    1. ไปที่ https://console.cloud.google.com/functions/add จะได้หน้าตาประมาณนี้ (รูปที่ 7)
      กรอก Function name, Region (แนะนำให้ใช้ Singapore), ให้ Trigger ด้วย HTTP Protocol และ Allow unauthenticated invocation. จากนั้นคลิก Save แล้วก็ Next
    รูปที่ 7. สร้าง Google Cloud Function
    1. เลือก Runtime เป็นภาษา programming ที่ต้องการ ในที่นี้จะเลือก Python แล้วคลิกที่ปุ่ม ​Enable API ตามรูปที่ 8.
    รูปที่ 8. เลือก Python แล้ว Enable API (สำหรับการใช้งานครั้งแรก)
    1. ใส่ Code ดัง รูปที่ 9 (สามารถ Copy จาก gist : https://gist.github.com/nagarindkx/fe36a26bd867d1ae627ad0d69f6e6b4d)

    ในบรรทัดที่ 3: เดี๋ยวเราจะเอากลับมาใส่ในภายหลัง

    ในบรรทัดที่ 4: ต้องไปสร้าง LINE Notify Token ก่อน สามารถทำตามวิธีการนี้ได้ วิธีแจ้งเตือนจาก Google Forms เข้า LINE

    ในบรรทัดที่ 7 และ 8: เราจะกลับมาสลับ Comment ในภายหลัง

    รูปที่ 9. Python code สำหรับรับการ Alert จากฝั่ง Zoom Webhook

    สุดท้าย แก้ไขไฟล์ requirements.txt เพิ่มบรรทัดใหม่ แล้วเขียนคำว่า

    requests

    เข้าไป ตรงนี้จะเป็น packages ไว้ใช้ติดต่อเพื่อส่ง LINE Notify ดังรูปที่ 10.

    จากนั้นคลิกปุ่ม DEPLOY

    รูปที่ 10. แก้ไขไฟล์ requirements.txt เพิ่ม requests package เข้าไปด้วย เพื่อให้สามารถเรียก LINE Notify API ได้

    ก่อนจะทำขั้นตอนถัดไป ให้คลิกที่ Tab TRIGGER แล้ว copy Trigger URL ไว้ ดัง รูปที่ 11

    รูปที่ 11. copy Trigger URL เก็บไว้ก่อน

    Zoom Webhook Only

    ต่อไป กลับมาที่ Zoom App Marketplace (https://marketplace.zoom.us/develop/create)

    คลิกที่ ปุ่ม Create ใน Webhook Only

    ระบบจะเด้งให้ตั้งชื่อ App Name ดังรูปที่ 12.

    ให้ตั้งชื่อตามสมควร แล้วคลิกปุ่ม Create

    รูปที่ 12. ตั้งชื่อ App Name

    ตั้งค่าต่าง ๆ ดังรูปที่ 13. แล้วคลิกปุ่ม Continue

    รูปที่ 13. กรอข้อมูลต่าง ๆ

    ต่อไป เป็นขั้นตอนการเฝ้า Event หรือ Event Subscriptions

    คลิก Switch

    คลิกปุ่ม Add new event subscription

    กรอกข้อมูล Subscription Name

    แล้วเอา Trigger URL จาก รูปที่ 11. ใส่ใน Event notification endpoint URL

    ดังรูปที่ 14.

    รูปที่ 14. ระบุ Subscription Name และ Event notification endpoint URL

    จากนั้น คลิกปุ่ม Add events

    เลือก Event types ที่ต้องการ ในที่นี้ เป็น Participant joined meeting before host ดังรูปที่ 15

    จากนั้น คลิกปุ่ม Done แล้ว Save

    รูปที่ 15. เลือก Participant joined meeting before host

    จากนั้น ระบบจะให้ Verification Token มา ให้ copy เก็บไว้ เพื่อเอาไปใส่ใน Google Cloud Function ต่อไป ดังรูปที่ 16.

    จากนั้นคลิกปุ่ม Continue เป็นอันเรียบร้อย

    รูปที่ 16. Verification Token เพื่อเอาไปใส่ใน Google Cloud Function

    สุดท้าย กลับไปที่ Google Cloud Function

    • กลับเอา Verification Token ไปใส่ใน zoom_verification_token
    • เอา LINE Notify Token ของจริง ไปใส่ใน access_token
    • Uncomment บรรทัดที่ 7
    • Comment บรรทัดที่ 8

    แล้วคลิก Deploy

    ทดสอบใช้งาน

    สร้างห้อง Zoom แบบไม่ต้องมี Waiting Room ลองให้คนอื่นเข้าไปในห้องก่อน Host

    Zoom Webhook จะส่ง HTTP POST ไปยัง Google Cloud Function โดยจะใส่ค่าลงไปใน Header “authorization”

    ถ้า ค่าดังกล่าว ตรงกับ zoom_verification_token ระบบก็จะส่ง LINE Notify ออกไป

    หวังว่าจะเป็นประโยชน์ครับ

  • [บันทึกกันลืม] Pandas Dataframe กับการขึ้นบรรทัดใหม่ ชิดซ้าย และทำให้คำที่ค้นหา เป็นตัวหนาสีแดง

    from IPython.display import display, HTML
    finding='text to find'
    display(HTML(df1.to_html()
                 .replace('\\r\\n','<br>')
                 .replace('<td>','<td style="text-align: left; vertical-align: top;">')
                 .replace('<th>','<th style="text-align: left;">')
                 .replace(f'{finding}',f'<b style="color:red;">{finding}</b>')
                )
           )

    กันลืมไง สั้น ๆ

  • [บันทึกกันลืม] วิธีย้ายทั้ง Folder จาก Google Drive ไปยัง Shared Drives ขององค์กร

    จากที่ Google Apps for Education หรือ Google Workspace for Education ปัจจุบัน ซึ่งจากเดิม ชูนโยบาย Unlimited Storage มาเป็น 100 GB ต่อ คน และ 100 TB ต่อ องค์กร — เพียงพอ ต่อการใช้งานเพื่อการศึกษาจริง ๆ ถ้าจัดการให้ดี

    เหตุแห่งการเปลี่ยนแปลง

    ผมว่า ก็เรา ๆ นี่แหล่ะ ใช้พื้นที่เค้าแบบไม่ใช่เพื่อการศึกษา ต้องยอมรับก่อน แล้วเรามาแก้ไขกัน

    • มีบางคนในองค์กร เอาไปเก็บไฟล์หนัง ไม่เกี่ยวกับการศึกษา จริงหรือไม่ รู้อยู่แก่ใจ, เรื่องนี้ Super Admin ขององค์กรตรวจสอบได้ ด้วย Google Vault จัดการให้เด็ดขาด (Super Admin รายงานได้ ผู้มีอำนาจต้องตัดสินใจ … กล้าไม๊ ค่อยมาดูกัน)
    • บางองค์กรไม่มีมาตรการควบคุมบัญชี สร้างมากเกินความจำเป็น แล้วบางคนเอาบัญชีไปขายใน Lazada, Shopee จริงไม่จริง รู้อยู่แก่ใจ
    • บางคน เอาแต่ประโยชน์ส่วนตน ได้สิทธิ์ แต่เอาพื้นที่ Shared Drives ไปปล่อยให้ เว็บโป๊ เว็บหนังซีรี่ ใช้พื้นที่ซะงั้น

    ก็เป็นธรรมดา Google ลงทุนทรัพยากร แต่ไม่ได้ใช้ให้ถูกตามวัตถุประสงค์ ก็ต้องมีการควบคุม งานส่วนตัว ใช้ My Drive, งานของหน่วยงาน ใช้ Shared Drives จบ

    (บางคนบอกว่า ย้ายไป Microsoft OneDrive ก็ได้วะ 5TB … ทำเหมือนเดิม เดี๋ยวก็จะได้ผลเหมือนเดิม)

    ผมได้เล่าไว้บ้างแล้ว เกี่ยวกับการใช้พื้นที่ของ Shared Drives

    มาดูวิธีแก้ปัญหากันดีกว่า

    • จากเดิม คนในสำนักงานก็จะชอบใช้ My Drive แล้ว Share ให้คนในหน่วยงานใช้กัน ก็เข้าใจได้ เพราะตอนแรกไม่มี Shared Drives เคยทำอย่างไรก็จะทำอย่างนั้น ไม่ชอบเปลี่ยนแปลง ถึงเวลาต้องเปลี่ยนแล้ว คือ งานส่วนรวม เอาไปเก็บใน Shared Drive
    • เอาหล่ะ สมมุติว่า เราเคยใช้ Folder หนึ่ง เก็บสารพัดสิ่งที่เกี่ยวกับงานจริง ๆ แต่มันอยู่ใน My Drive (พื้นที่ส่วนตัว) สมมุติใช้ Folder ชื่อ parent ในนั้นมี Sub folder ย่อย และมีไฟล์ย่อย ๆ ซ้อน ๆ ลงไปจำนวนมาก
    parent folder ซึ่งเป็นงานจริง ๆ และมีไฟล์ และ folder ย่อย ๆ ลงไปจำนวนมาก
    • แนะนำว่า แต่ละหน่วยงาน เช่น ระดับภาควิชา หรือ ระดับหน่วยงาน ไปสร้างพื้นที่ไว้บน Shared Drives

    ปัญหาคือ … การย้ายไฟล์จาก​ Google Drives ไปยัง Shared Drives นั้น ทำได้ในระดับไฟล์ ไม่สามารถย้ายทั้ง Folder ได้

    ใครใคร่ย้าย ก็เลือกไฟล์ แล้วคลิกขวา Move to ไป

    แต่กรณี ของผมมีไฟล์ ที่สร้างจากอุปกรณ์ ขนาด 400-800 kb จำนวน …. 60 GB คิดคร่าว ๆ 2 ไฟล์ เป็น 1 MB ก็ราว ๆ 60 x 1024 x 2 = 122,880 ไฟล์

    กรณีย้ายไฟล์จำนวนมากใน Folder เดียวกัน ไปไว้บน​ Shared Drives

    ผมสร้าง Google Apps Script ไว้ที่ https://gist.github.com/nagarindkx/70f254a21c5cad7f964e762ab8b2e733

    function moveFilesFromDriveToShareDrive() {
      /*
      Created by: Kanakorn Horsiritham
      Digital Innovation and Data Analytics (DIDA)
      Faculty of Medicine
      Prince of Songkla University
      Hatyai, Songkhla
      THAILAND
      */
      sourceFS=DriveApp.getFolderById('GoogleDrive-Folder-ID').getFiles()
      destinationFS=DriveApp.getFolderById('SharedDrives-Folder-ID')
      
      while(sourceFS.hasNext()){
        f=sourceFS.next()
        f.moveTo(destinationFS) 
      }
    }

    สิ่งที่ท่านต้องทำคือ

    1. เปิด Google Drive ต้นทางบน Web Browser แล้วดูที่ URL แล้ว Copy เฉพาะ GoogleDrive-Folder-ID เช่น

      https://drive.google.com/drive/folders/GoogleDrive-Folder-ID

      เก็บไว้ก่อน

    2. เปิด Shared Drives ปลายทาง แล้วทำเหมือนกัน

    3. จากนั้น สร้าง Google Apps Script ใน​ Google Drives

    4. ตั้งชื่อ Project ว่า movefile1

    5. copy code ข้างต้นมาใส่ จากนั้นแก้ไข GoogleDrive-Folder-ID และ SharedDrives-Folder-ID จากข้อ 1,2 แล้วกดปุ่ม Save

    6. ถ้าไฟล์ไม่มากนัก ก็ เลือก moveFilesFromDriveToShareDrive กดปุ่ม Run

    ถ้าทำครั้งแรก ระบบอาจจะถามอย่างนี้ ก็ให้เลือก Review Permissions แล้ว Next Next เลือก Account ไป (ขั้นนี้แล้ว ไม่อธิบายรายละเอียดแล้วนะครับ)

    จากนั้นก็รอไป Script จะทำงานต่อเนื่อง ยาวนานไม่เกิน 1800 วินาที หรือ 30 นาที ผลที่ได้คือ เฉพาะ Files เท่านั้น ที่จะย้ายไปอยู่ใน Shared Drives แต่ใจเย็น ๆ มีวิธีพาไปทั้งยวง รอแป๊บ

    7. แต่ถ้ามีไฟล์จำนวนมาก จนครบ 30 นาทีแล้ว ก็ยังไม่หมด ก็ต้องอาศัย Trigger เพื่อสั่งให้ทำทุก ๆ 1 ชั่วโมง ดังนี้
    เลื่อน Cursor ไปทางซ้ายมือ คลิกที่ Triggers

    คลิกปุ่ม Add Trigger

    เลือกตามนี้ แล้ว Save

    จากนั้นก็รอไป ระบบจะทำจนหมดเวลา 30 นาที รออีก 1 ชั่วโมง แล้วก็ทำต่อไปเรื่อย ๆ

    อันนี้ ของผม

    ยังอีกยาวนาน

    กรณีย้ายทั้ง Folder ไปไว้บน​ Shared Drives

    UPDATE!!!!

    เลือก Folder ต้นทาง และ ปลายทาง จากนั้น copy/move ได้เลย โดย ที่จะสร้างโครงสร้าง Folders และ Files ที่ Shared Drives ตามเดิมจากต้นทาง

    ผมทำ Script ไว้ที่ https://gist.github.com/nagarindkx/e2897455b2ede2ae168f8cd5ccf16982

    function copyFilesFromDriveToShareDrive() {
      /*
      Created by: Kanakorn Horsiritham
      Digital Innovation and Data Analytics (DIDA)
      Faculty of Medicine
      Prince of Songkla University
      Hatyai, Songkhla
      THAILAND
      */
      
      sourceID='GoogleDrive-Folder-ID'
      destinationID='SharedDrives-Folder-ID' 
       
      copyFolder(sourceID, destinationID)
      //moveFolder(sourceID, destinationID)
    
    }
    
    function copyFolder(sourceID, destinationID){
      sFD = DriveApp.getFolderById(sourceID)
      dFD = DriveApp.getFolderById(destinationID)
      
      files = sFD.getFiles()
      while(files.hasNext()) {
        f=files.next()
        if (! dFD.getFilesByName(f.getName()).hasNext()) {
          f.makeCopy(f.getName(), dFD)
          console.log('copy file: ' + f.getName())
        } else {
          Logger.log( "file: " + f.getName() + ' exist')
        }
        
      }
    
      folders=sFD.getFolders()
      nextlevel=[]
      while(folders.hasNext()) {
        fd = folders.next()
        tmpDestinationFolder = dFD.getFoldersByName(fd.getName())
        if (! tmpDestinationFolder.hasNext()) {
          new_folder = dFD.createFolder(fd.getName())
          Logger.log( "create folder: " + fd.getName())
        } else {
          new_folder = tmpDestinationFolder.next()
          Logger.log( "folder: " + fd.getName() + ' exist')
        }
        
        nextlevel.push([fd.getId(), new_folder.getId()])
      }
      nextlevel.forEach(function(r){
        copyFolder(r[0], r[1])
      })
    }
    
    
    function moveFolder(sourceID, destinationID){
      sFD = DriveApp.getFolderById(sourceID)
      dFD = DriveApp.getFolderById(destinationID)
      
      files = sFD.getFiles()
      while(files.hasNext()) {
        f=files.next()
        if (! dFD.getFilesByName(f.getName()).hasNext()) {
          console.log('file: ' + f.getName())
          f.moveTo( dFD)
          console.log('move file: ' + f.getName())
        } else {
          Logger.log( "file: " + f.getName() + ' exist')
        }
      }
    
      folders=sFD.getFolders()
      nextlevel=[]
      c=0
    
      if (folders.hasNext()) {
        while(folders.hasNext()) {
          fd = folders.next()
          tmpDestinationFolder = dFD.getFoldersByName(fd.getName())
          if (! tmpDestinationFolder.hasNext()) {
            new_folder = dFD.createFolder(fd.getName())
          } else {
            new_folder = tmpDestinationFolder.next()
            Logger.log( "folder: " + fd.getName() + ' exist')
          }
          nextlevel.push([fd.getId(), new_folder.getId()])
          c=c+1
        }
        nextlevel.forEach(function(r){
          moveFolder(r[0], r[1])
        })
      } else {
        sFD.setTrashed(true)
      }
      
    }
  • [บันทึกกันลืม] แก้ปัญหารัน Shell Script กับมือทุกอย่างสมบูรณ์ แต่พอใช้ cron แล้วพัง

    เรา “จำกันมา” ว่า การเขียน Shell Script นั้น ทำด้วยมืออย่างไร ก็เขียนลงไปในไฟล์ แล้วเอา Shell เช่น Bash ไป Run แล้วเอาไปตั้งในไฟล์ /etc/cron.d/mycron

    ปัญหาคือ

    บางทีเขียนด้วยภาษา Script เช่น python, R แล้วบางที ก็อยากจะเพิ่มภาษาไทยเข้าไป

    ตามสูตร ก็ต้องใช้คำสั่ง locale ตรวจสอบ

    LANG=en_US.UTF-8
    LANGUAGE=en_US.UTF-8
    LC_CTYPE="en_US.UTF-8"
    LC_NUMERIC="en_US.UTF-8"
    LC_TIME="en_US.UTF-8"
    LC_COLLATE="en_US.UTF-8"
    LC_MONETARY="en_US.UTF-8"
    LC_MESSAGES="en_US.UTF-8"
    LC_PAPER="en_US.UTF-8"
    LC_NAME="en_US.UTF-8"
    LC_ADDRESS="en_US.UTF-8"
    LC_TELEPHONE="en_US.UTF-8"
    LC_MEASUREMENT="en_US.UTF-8"
    LC_IDENTIFICATION="en_US.UTF-8"
    LC_ALL=en_US.UTF-8

    ก็จะพบว่า เป็น UTF-8 สวยงาม

    เขียนใส่ข้อมูลที่ประมวลผลมา ก็จะได้เป็นภาษาไทยสวยงาม

    สวัสดีชาวโลก

    แต่ พอเอาไปใส่ Shell Script ผลออกมาเป็นตัวอักษรบนตัวเลข ซึ่งนั่นคือ UTF-8 HEX code

    <U+0E2B><U+0E32><U+0E14><U+0E43><U+0E2B><U+0E0D><U+0E48>

    เหตุของปัญหา

    ก็ลองเอา คำสั่ง locale ไปใส่ใน Shell Script แล้วดูผลในไฟล์พบว่า

    LANG=
    LANGUAGE=
    LC_CTYPE="POSIX"
    LC_NUMERIC="POSIX"
    LC_TIME="POSIX"
    LC_COLLATE="POSIX"
    LC_MONETARY="POSIX"
    LC_MESSAGES="POSIX"
    LC_PAPER="POSIX"
    LC_NAME="POSIX"
    LC_ADDRESS="POSIX"
    LC_TELEPHONE="POSIX"
    LC_MEASUREMENT="POSIX"
    LC_IDENTIFICATION="POSIX"
    LC_ALL=

    สรุปคือ locale ตอนใช้ cron เป็น Default อย่างนี้ (พอดีทำงานบน docker ที่ base image มาจาก debian)

    วิธีแก้ไขปัญหา

    google สิครับ รออะไร ไปพบข้อมูลจาก https://www.logikdev.com/2010/02/02/locale-settings-for-your-cron-job/

    คือ ให้ใส่

    SHELL=/bin/bash
    LANG=en_US.UTF-8
    LANGUAGE=en
    LC_CTYPE=en_US.UTF-8

    ลงไปใน cron ด้วย อย่างเช่น

    SHELL=/bin/bash
    LANG=en_US.UTF-8
    LANGUAGE=en
    LC_CTYPE=en_US.UTF-8
    */3 * * * * /bin/bash /code/somescript.sh

    แล้วก็อย่าลืม

    /etc/init.d/cron restart

    เป็นอันเรียบร้อย

    หวังว่าจะมีประโยชน์ครับ

  • [บันทึกกันลืม] วิธีซ่อน choice ที่ไม่จำเป็น ด้วย jQuery

    ปัญหามีอยู่ว่า ในการเขียน Web Application ด้วย django แบบฟอร์มที่ซับซ้อนหน่อย จะมีประมาณว่า ถ้าเลือกข้อนี้ ให้ขึ้นคำถามอีกข้อขึ้นมาถาม

    เช่น ถ้าเลือกข้อ “รพ.สงขลานครินทร์” ให้ขึ้นคำถาม “ลักษณะการทำงาน” ซึ่งก็ไม่ได้ยากอะไร

    แต่ว่า คำตอบของข้อนี้ ในกรณีนี้ จะต้องบังคับตอบ แต่ ถ้าเลือก “สถานพยาบาลอื่น ๆ” จะต้องไม่บังคับตอบ

    ปัญหาคือ เจ้า Choice ที่ เมื่อไม่จำเป็นต้องตอบ มันต้องไม่มีค่า แต่ตอนที่มันต้องใช้ตอบ (อย่างในภาพ) มันควรจะหายไป แต่มันไม่หายไป (เข้าใจยากหน่อยนะ)

    ลองใช้

    $('#id_q2_1_work_3').hide();

    ก็ไม่หาย

    วิธีแก้คือ ไป hide label

    $("label[for='id_q2_1_work_3']").hide()

    เวิร์คเลย

    จบ