Ruby gsub, friends and unrelated stuffs

เราเรียกใช้เมทธอด gsub ของคลาส String (String#gsub) ใน Ruby ได้สองแบบ
จาก API doc:
คำสั่ง => ผลลัพธ์

  1. str.gsub(pattern, replacement) => new_str
  2. str.gsub(pattern) {|match| block } => new_str

ตรง pattern นี่ จะเป็น String ธรรมดา ๆ ก็ได้

เค้าให้ตัวอย่างมาแบบนี้

  • "hello".gsub(/[aeiou]/, '*') #=> "h*ll*"
  • "hello".gsub(/([aeiou])/, '<\1>') #=> "h<e>ll<o>"
  • "hello".gsub(/./) {|s| s[0].to_s + ' '} #=> "104 101 108 108 111 "

แบบ (1) ใช้เป็นแล้ว ก็เหมือน ๆ กับในภาษาอื่น ๆ
ส่วนแบบ (2) ที่ผ่าน code block เข้าเมทธอด ยังไม่เคยลอง .. ก็ลองซะคราวนี้

โจทย์คือ ผมจะแทนตัวอักษร &, ‘, ", < และ > ทั้งหมดในไฟล์ ๆ หนึ่ง
ด้วย XML Entity &amp;, &apos;, &quot;, &lt; และ &gt;

ตอนแรกผมทำแบบนี้


 1: def convert1 (line)
 2:  if line != nil
 3:   line.gsub!(Re_amp,  Amp)
 4:   line.gsub!(Re_apos, Apos)
 5:   line.gsub!(Re_quot, Quot)
 6:   line.gsub!(Re_lt, Lt)
 7:   line.gsub!(Re_gt, Gt)
 8:   return line
 9:  else
10:   return line
11:  end
12: end
13:
14: IO.foreach(ARGV[0]) { |line| puts convert1(line) }

โปรแกรมจะเริ่มทำงานที่บรรทัดสุดท้าย (14) โดยอ่านไฟล์ตามชื่อที่ให้มาทาง command line argument แรก 1
หลังจากนั้น ในแต่ละบรรทัดของไฟล์ ก็จะทำคำสั่ง puts convert1(line) 2
อย่างกรณีนี้ก็คือ ทุก ๆ ครั้งที่อ่าน 1 บรรทัด ก็ให้พิมพ์บรรทัดที่ถูกแปลงแล้วออกมา

เมทธอด foreach ทำให้เราเขียนโค้ดที่อาจจะยาวอย่างน้อยสามสี่บรรทัดในภาษาอื่น ได้เหลือแค่บรรทัดเดียวใน Ruby !

  • IO.foreach(name, sep_string=$/) {|line| block } => nil

อย่าเพิ่งนอกเรื่องไปไกล กลับมาที่ gsub ต่อ …

โอเค จากบรรทัดที่ 14 จะเห็นว่าเราเรียกใช้ฟังก์ชั่น convert1, ซึ่งประกาศไว้ตรงบรรทัดที่ 1-12 (13 เป็นเลขไม่ดี เราขอไม่ใช้ :P)

บรรทัด 3-7 จะเห็นว่าเราเรียก gsub! 3 เป็นชุดเลย
โดยที่ Re_xxx นี่ เป็นออบเจกต์ pattern ที่จะหา &, ‘, … ที่เราเตรียมไว้ล่วงหน้าแล้ว
และพวก Amp, Apos, … ก็เป็น constant 4 ที่มีค่า &amp;, &apos;, …
(ตรงนี้ผมคิดไปเองว่า ถ้าเราเตรียมแพตเทิร์นและสตริงไว้ล่วงหน้า
คงจะช่วยให้ Ruby ไม่ต้องสร้างออบเจกต์แพตเทิร์นทุกครั้งใน “ลูป” foreach (ทุก ๆ บรรทัด)
แต่เนื่องจากไม่รู้ว่า ตัว Ruby runtime ทำงานจริง ๆ ยังไง มี optimization อะไรบ้าง เลยไม่แน่ใจว่ามันจะช่วยจริงรึเปล่า
… เอาว่าช่วยทางใจละกัน อยากเขียนแบบนี้น่ะ)

โค้ดของของแพตเทิร์น (regular expression):


Re_amp  = /&(?![-_a-zA-Z0-9]{1,12};)/  # (skip & in &xxx;)
Re_apos = /\'/
Re_quot = /\"/
Re_lt   = /</
Re_gt   = />/

เอาล่ะ ถึงเรื่องที่จะเล่าละ

ทีนี้ จะเห็นว่า เฮ้ย เราเรียก gsub! ซ้ำ ๆ ตั้งหลายรอบแหน่ะ
1 gsub! ต่อ 1 คู่ (แพตเทิร์น/ตัวอักษรที่จะแทน) ต่อ 1 บรรทัด
ดู ๆ ไป (อย่างเด่นชัด, ตามคำแปลของ LEXiTRON) มันน่าจะมีวิธีที่ .. มีประสิทธิภาพมากกว่านี้สิ

เป็นไปได้รึเปล่า ที่เราจะแทน คู่ (แพตเทิร์น/ตัวอักษร) ได้มากกว่า 1 คู่ ในการเรียก gsub! 1 ครั้ง ?
คำตอบคือ เป็นไปได้ ด้วยการผ่านโค้ดบล็อกเข้า gsub!
โดยให้ตัวโค้ดบล็อกเป็นตัวตัดสินใจเรื่องการแทนที่


 1: Re_escapes = /(&(?![-_a-zA-Z0-9]{1,12};))|(\')|(\")|(<)|(>)/
 2:
 3: def escape (char)
 4:  return case char
 5:   when '&'  then Amp
 6:   when '\'' then Apos
 7:   when '\"' then Quot
 8:   when '< '  then Lt
 9:   when '>'  then Gt
10:   else char
11:  end
12: end
13:
14: def convert2 (line)
15:  if line != nil
16:   line.gsub!(Re_escapes) { |match| match = escape(match) }
17:   return line
18:  else
19:   return line
20:  end
21: end
22:
23: IO.foreach(ARGV[0]) { |line| puts convert2(line) }

ที่โค้ดนี้ จุดที่ convert2 ต่างกับ convert1 ก็คือ ตรงบรรทัดที่ 16
จะเห็นว่า แทนที่เราจะส่ง 2 พารามิเตอร์ — (pattern, replacement)
เราส่ง 1 พารามิเตอร์ กับ 1 บล็อก แทน — (pattern) { block }

โค้ดบล็อก { |match| match = escape(match) } จะถูกเรียกทุกครั้งที่ gsub! พบแพตเทิร์น Re_escapes
โดย match จะเป็นค่าของสิ่งที่พบ
เราก็จัดการแปลง match ด้วยฟังก์ชั่น escape ซะ เท่านั้นก็จบ
เท่ากับว่า เราเรียก gsub! แค่ครั้งเดียว5

ในฟังก์ชั่น escape ก็ไม่มีอะไร, แบบว่าจะโชว์การใช้ case ... when น่ะ
เราสามารถผสม return กับ case ... when ได้ด้วย (แทนที่จะเขียน then return ... ซ้ำ ๆ)

Re_escapes นี่ก็เป็นแพตเทิร์นที่เรายุบรวม Re_amp, … Re_gt เข้าด้วยกัน ไม่มีอะไรมาก

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

อยากรู้ว่าสองวิธีนี้ (convert1 vs convert2) จะให้ประสิทธิภาพต่างกันแค่ไหน
ลองทดสอบง่าย ๆ ด้วย Benchmark#bmbm


require 'benchmark'

...

Benchmark.bmbm do |x|
 x.report("convert1:") { IO.foreach(ARGV[0]) { |line| convert1(line) } }
 x.report("convert2:") { IO.foreach(ARGV[0]) { |line| convert2(line) } }
end

ลองรัน6 wellform-demo.rb etlex.xml

ผลลัพธ์:


Rehearsal ---------------------------------------------
convert1:  23.914000   0.070000  23.984000 ( 24.015000)
convert2:  22.813000   0.130000  22.943000 ( 22.973000)
----------------------------------- total: 46.927000sec

                user     system      total        real
convert1:  23.914000   0.100000  24.014000 ( 24.065000)
convert2:  22.913000   0.090000  23.003000 ( 23.063000)

กับไฟล์ etlex.xml ขนาดประมาณ 18 MB (668,938 บรรทัด)
convert2 เร็วกว่า convert1 ประมาณ 1 วินาที (ประมาณ 4%)

คุ้มมั๊ยเนี่ย ? 😛

ก็ .. ถือว่าคุ้มละกัน เพราะได้เรียน Ruby น่ะ 😀


1 ข้อสังเกต: ตรงนี้จะไม่เหมือน command line argument ในสไตล์ C หรือ Python
ที่ argument แรก (0) จะหมายถึงชื่อโปรแกรมที่กำลังรันอยู่
ใน Ruby ถ้าต้องการรู้ชื่อโปรแกรม ให้เรียกจากโลกาตัวแปร (global variable) $0 กลับ

2 ตรงนี้เราเรียกว่า code block
อย่างบรรทัด 14 นี้คือ เราส่งโค้ดบล็อก {…} เข้าเมทธอด foreach
โดย line (ใน | |) เนี่ย ก็เป็นเหมือน argument ของโค้ดบล็อก
(คิดซะว่า เรากำลังเขียนฟังก์ชั่นอันนึงก็ได้ ทำเหมือน argument ปกติ)
โดย line จะถูกแทนภายในเมทธอดที่เราส่งโค้ดบล็อกเข้าไป
(อันนี้คือตามความเข้าใจจากการทดลอง กลไกภาษา Ruby จริง ๆ อาจจะไม่ใช่แบบนี้ก็ได้ ต้องลองอ่านเอกสารดู) กลับ

3 gsub! ต่างกับ gsub ตรงที่ มันจะเปลี่ยนแปลงสตริงต้นฉบับเลย;
ในขณะที่ gsub จะไม่เปลี่ยนแปลงสตริงต้นฉบับ แต่จะคืนสตริงใหม่ออกมา;
พูดอีกอย่าง: โค้ด x.gsub!(a,b) ให้ผลเหมือน x = x.gsub(a,b)
ใน Ruby เมทธอดที่ลงท้ายด้วย ! มีลักษณะแบบนี้เหมือนกันหมด กลับ

4 by convention (ตามข้อตกลง), ค่าคงที่ (constant) ใน Ruby จะขึ้นต้นด้วยตัวพิมพ์ใหญ่ เช่น Book, Song
อย่างไรก็ตาม ข้อควรระวังคือ ในโปรแกรม เราสามารถแก้ไขค่าคงที่ได้ และ Ruby runtime ก็ไม่ห้าม
จะมีก็แค่ขึ้น warning เท่านั้น แต่โปรแกรมจะทำงานต่อไปตามปกติ
ซึ่งตรงนี้นี่ … อืมม ผมไม่แน่ใจว่าเป็นความคิดที่ดีรึเปล่า
อย่างน้อย คนเขียน Java น่ะไม่น่าจะชอบ กลับ

5 ตรงนี้ บางคนอาจจะเห็นว่า บรรทัดที่ 16 กับ 17 น่าจะยุบรวมกันได้
ข้อควรระวังก็คือ ถ้าจะยุบ อย่าลืมเปลี่ยน gsub! เป็น gsub
return line.gsub(Re_escapes) { |match| match = escape(match) }
เพราะในกรณีที่สตริงไม่มีการเปลี่ยนแปลง, gsub! จะคืนค่า nil
แต่ gsub จะคืนค่าสตริงต้นฉบับ
สิ่งที่เราอยากได้ ในโปรแกรมนี้ คืออย่างหลัง กลับ

6 ภาษาอังกฤษ “long run” แปลว่า ในระยะยาว 😛 กลับ

Thai full-text search in MySQL – Draft Propsal

สถาบันส่งเสริมการจัดการความรู้เพื่อสังคม (สคส.)
กำลังจะให้ทุนเพื่อพัฒนาความสามารถในการค้นหาข้อความภาษาไทย (Thai full-text search)
ในระบบฐานข้อมูลโอเพนซอร์ส MySQL
โดยมีงบประมาณ 200,000 บาท

โดยตอนนี้ มีร่างข้อเสนอสำหรับโครงการแล้ว และคณะทำงานเห็นว่า น่าจะรับฟังความเห็นของสาธารณะเสียหน่อย เกี่ยวกับร่างข้อเสนอนี้

ใครสนใจ ก็ลองอ่านดู แล้วแสดงความคิดเห็นกันได้ที่เว็บบอร์ด LTN ห้อง General ที่กระทู้นี้ครับ — ขอบคุณครับ 🙂

Emdros – a database engine for annotated text

เมื่อคืนวีร์พูดถึง Emdros ว่าน่าสนใจ สำหรับงานฐานข้อมูลทางภาษาศาสตร์ ก็เลยเข้าไปดูเว็บซะหน่อย

Emdros is:

  • an opensource text database engine for storage and retrieval of analyzed or annotated text.
  • applicable especially in corpus linguistics and computational linguistics.
  • equiped with a powerful query-language MQL, based on the Extended MdF mathematical model of text.

A short paper explaninig Emdros.

ข้างบนจะเห็นคำว่า Extended MdF หรือที่ในเว็บ Emdros จะใช้คำว่า EMdF, ชื่อเต็มๆ ของมันคือ Extended Monads dot Feature โดยพัฒนาต่อมาจาก Monads dot Feature (book; review)

More about text database:

ThaiWrap รุ่น 5

(บล็อกเก่า ThaiWrap bookmarklet, Auto thaiWrap())

รายการเปลี่ยนแปลง:

  • ตัวแบ่งคำ เปลี่ยนจาก <WBR> มาใช้ zero-width space (U+200B) แทน เนื่องจาก Opera ไม่รู้จัก <WBR>
  • เพิ่มการตรวจเบราเซอร์ เพื่อข้ามการทำงานทั้งหมด ถ้าใช้ Internet Explorer (เหตุผล: 1. จะได้ไม่เสียเวลา เพราะ IE ตัดคำได้อยู่แล้ว 2. IE แสดงผล zero-width space ไม่ได้)

ตอนนี้ยังเหลือปัญหา เรื่องไม่ทำงานกับเฟรมที่ซ้อนเฟรม ไล่ DOM reference ตะกี้นี้ เจอละว่ามันผิดตรงไหน (เราไปใช้ window.frames ซึ่งมันจะส่งค่ากลับเฉพาะ frames ระดับบนสุดเท่านั้น, คาดว่า. นอกจากนั้น มันยังไม่อยู่ใน spec ด้วย – อันตราย) แต่ยังไม่รู้จะแก้ไง ขอค้นก่อน

ThaiWrap bookmarklet/JavaScript, Release 5