DEV Community

CopyPasteEngineer
CopyPasteEngineer

Posted on • Updated on

Python Web Scraping part 3 - เทคนิคการ extract ข้อมูลด้วย XPath

มาต่อกันกับ series Python Web Scraping part 3 นะครับ หลังจากที่ใน part ที่แล้ว เราได้เรียนรู้วิธีการแกะ XPath ของแต่ละ element ออกมาจาก web browser แบบง่าย ๆ กันแล้ว แต่ทีนี้ในกรณีที่ข้อมูลที่เราสนใจมีหลายตำแหน่งมาก เราคงไม่อยากเสียเวลานั่ง extract แต่ละ element ด้วยมือทั้งหมดใช่ไหมครับ (เพราะถ้าจะทำด้วยมือทั้งหมด ก็ก๊อปตัวข้อมูลมาตรง ๆ เลยสะดีกว่า 555)

ท่านที่พอจะมีประสบการณ์การทำเว็บมาบ้าง พอเห็น XPath สัก 1 หรือ 2 อัน ก็อาจจะเดาได้แล้ว ว่า XPath ที่เหมาะสม ที่น่าจะครอบคลุมทุก ๆ element นั้น ควรจะเป็นอะไร แต่ถึงจะเดาไม่ออกก็ไม่เป็นไรครับ เพราะอย่างบาง website ที่ซับซ้อนขึ้นหน่อย ต่อให้มีประสบการณ์ก็คงเดาไม่ออกในทีแรกเช่นกัน

สำหรับในบทความนี้ก็จะสอนวิธีหนึ่งที่สามารถทำได้ เพื่อหา XPath ที่ "น่าจะ" ครอบคลุมข้อมูลทั้งหมดที่เราต้องการมานะครับ ซึ่งเป็นวิธีที่ผู้เขียนเองก็ใช้งานอยู่จริงในการ scrape website ในหลาย ๆ ครั้งครับ (ที่บอกว่า "น่าจะ" เนี่ย เพราะว่าในความเป็นจริงไม่มีใครรู้หรอกครับ ว่า website เป้าหมายนี้ ถูกเขียน หรือถูกสร้างมาอย่างไร แต่ด้วยวิธีดังกล่าว+หลักของสถิติ เราก็จะมั่นใจได้ในระดับหนึ่งว่า XPath ของเรามัน "น่าจะ" ดีเพียงพอครับ)

วิธีหนึ่งที่ผมทำก็คือ sample XPath ออกมาดู เอามา สำรวจ เพิ่มอีกเยอะ ๆ ก็จะช่วยให้เห็นภาพมากขึ้นครับ จากนั้นเราก็จะใช้ syntax ของ XPath ในการ select และระบุ condition เพื่อให้ครอบคลุมกรณีที่เรา sample ออกมา ให้มากที่สุดครับ

Get Started!

ทบทวนสักเล็กน้อย ใน part นี้เราก็ยังอยู่กับเว็บ Wikipedia https://th.wikipedia.org/wiki/รายชื่อเทศบาลตำบลในประเทศไทย กันเหมือนเดิมนะครับ โดยเป้าหมายคือ หา XPath สำหรับ extract รายชื่อเทศบาลตำบลทั้งหมด ซึ่งใน part ที่แล้ว เราใช้ Chrome Inspector ในการแกะ XPath ของ "Element" หนึ่ง ๆ มาได้แบบง่าย ๆ แล้วนะครับ ในตอนนี้เรากำลังจะหา XPath ที่สามารถ extract ข้อมูลทั้งหมดได้ (คือครอบคลุมทุก element ไม่ใช่แค่ 1 element)

1. Sample XPath มาเพิ่ม

อย่างในเว็บเป้าหมาย ถ้าลอง scroll เลื่อนขึ้นลงดู จะเห็นว่ามันมีหลายตาราง เพราะฉะนั้น เพื่อให้เราได้เห็นรูปแบบที่หลากหลาย แล้วก็ครอบคลุมข้อมูลทั้งหมดมากที่สุด ก็ควรจะหยิบ sample ออกมาจากหลาย ๆ ที่ กระจาย ๆ กันหน่อยนั่นเองครับ เช่น เอามาจากตาราง 1 บ้าง จากตาราง 8 บ้าง จากตารางสุดท้ายบ้าง หรือจากหัวตารางบ้าง จากท้ายตารางบ้าง ปน ๆ กันไป โดยหลักสถิติแล้วยิ่งเราสุ่มมามากเท่าไหร่ เราก็จะยิ่งมั่นใจได้ว่าเราได้เห็น pattern ที่เป็นไปได้ทั้งหมดแล้วมากขึ้นนั่นเองครับ (จะเรียกว่าหลักสถิติหรือดวงก็ได้นะครับ 555)

สำหรับท่านที่ยังไม่เข้าใจว่าจะเอา sample XPath ตรงนี้มาได้อย่างไร กรุณาอ่าน part 2 ด่วน ๆ เลยครับ! หรือถ้ามีข้อสงสัย สามารถคอมเม้นถามด้านล่าง หรือในเพจ Copy Paste Engineer ได้เลยนะครับ

ด้านล่างนี้เป็นตัวอย่างที่ผม sample ออกมา

 (1) //*[@id="mw-content-text"]/div/table[3]/tbody/tr[1]/td[3]/b/a    (ตาราง 1 บรรทัด 1)
 (2) //*[@id="mw-content-text"]/div/table[3]/tbody/tr[2]/td[3]/b/a    (ตาราง 1 บรรทัด 2)
 (3) //*[@id="mw-content-text"]/div/table[3]/tbody/tr[10]/td[3]/b/a   (ตาราง 1 บรรทัด 10)
 (4) //*[@id="mw-content-text"]/div/table[4]/tbody/tr[3]/td[3]/b/a    (ตาราง 2 บรรทัด 1)
 (5) //*[@id="mw-content-text"]/div/table[4]/tbody/tr[30]/td[2]/b/a   (ตาราง 2 บรรทัด 28)
 (6) //*[@id="mw-content-text"]/div/table[7]/tbody/tr[25]/td[2]/b/a/b (ตาราง 5 บรรทัด 23)
 (7) //*[@id="mw-content-text"]/div/table[10]/tbody/tr[41]/td[2]/b/a  (ตาราง 8 บรรทัดสุดท้าย)
 (8) //*[@id="mw-content-text"]/div/table[24]/tbody/tr[3]/td[3]/a/b   (ตาราง 22 บรรทัด 1)
 (9) //*[@id="mw-content-text"]/div/table[76]/tbody/tr[3]/td[3]/b/a   (ตารางสุดท้าย บรรทัด 1)
(10) //*[@id="mw-content-text"]/div/table[76]/tbody/tr[37]/td[2]/b/a  (ตารางสุดท้าย บรรทัดสุดท้าย)

จะเห็นว่า XPath จริง ๆ ของข้อมูลเรา มันไม่ได้ตรงไปตรงมาเสมอไปนะครับ อย่างเช่นตัวเลขของ table และ tr ใน XPath ของแต่ละ sample นั้น ก็อาจจะไม่ได้สัมพันธ์กับเลขตารางหรือเลขบรรทัดของข้อมูลเสมอไป หรือบางทีก็มี element b บ้าง บางทีก็ไม่มี ซึ่งในเหตุการณ์เช่นนี้สามารถพบเจอได้จริง ในเว็บทั่ว ๆ ไปนะครับ โดยอาจเกิดจาก developer ที่เขียนโค้ดไม่สัมพันธ์กันเอง หรืออย่างในกรณีของ Wikipedia นี้ คืออาจเกิดจาก user ซึ่งเป็นผู้เขียนบทความได้ใช้โครงสร้างของข้อมูลที่แตกต่างกันนั่นเองครับ

2. สำรวจ XPath

ในขั้นตอนนี้ เราจะทำการวิเคราะห์ XPath ที่เรา sample ออกมา แล้วก็ตั้งข้อสังเกตุถึง จุดที่เหมือน และ จุดที่แตกต่าง สำหรับแต่ละตัวอย่างนะครับ ซึ่งอาจจะต้องใช้ความรู้ HTML เล็กน้อยนะครับ แต่จะพยายามอธิบายให้เข้าใจง่ายที่สุดนะครับ

เมื่อเรา sample ออกมาดูเยอะ ๆ แล้วก็จะเห็นครับว่ามันมีส่วนที่เหมือนกันอยู่ คือ ขึ้นต้นด้วย element <table>

//*[@id="mw-content-text"]/div/table

ซึ่ง <table> ก็หมายถึงตัวตารางแต่ละตารางนั่นเองครับ

และจะสังเกตุได้จาก XPath ที่ได้มา ว่าข้อมูลที่เราต้องการ
1. จะเริ่มต้นที่ ตารางที่ 3 (เพราะในตารางที่ 1 บรรทัดที่ 1 ใช้ table[3])
2. แล้วตามด้วย <tbody>
3. ตามด้วย <tr> คือแต่ละ row ของ table นั้น ๆ
4. จากนั้นก็ <td> คือแต่ละ column ของ row นั้น ๆ ซึ่งอาจจะเป็น column ที่ 2 หรือ column ที่ 3 ก็ได้ (เช่น sample (4) และ (5))

(4) //*[@id="mw-content-text"]/div/table[4]/tbody/tr[3]/td[3]/b/a    (ตาราง 2 บรรทัด 1)
(5) //*[@id="mw-content-text"]/div/table[4]/tbody/tr[30]/td[2]/b/a   (ตาราง 2 บรรทัด 28)

5. แล้วก็อาจจะมี หรือไม่มี <b> ก็ได้ (กรณีที่ไม่มี <b> ดู sample ที่ (8))

(8) //*[@id="mw-content-text"]/div/table[24]/tbody/tr[3]/td[3]/a/b   (ตาราง 22 บรรทัด 1)

6. ตามด้วย <a> คือข้อความที่เป็น link
7. แล้วก็อาจจะมี หรือไม่มี <b> ก็ได้

นี่ก็เป็นข้อสังเกตุทั้งหมดที่เราได้จาก sample ของเรานะครับ

3. สร้าง XPath (จริง ๆ สักที 555)

ต่อไป จากข้อสังเกตุทั้ง 7 ข้อ ด้านบน เราก็ได้ทราบแล้วว่า elements ที่เราต้องการ จะมี XPath ลักษณะอย่างไร ทีนี้เรานำข้อสังเกตุทั้งหมดมารวมกัน เพื่อสร้าง XPath ขึ้นมา 1 อัน ที่สามารถครอบคลุม cases ที่กล่าวมาได้ทั้งหมด! โดยใช้การเขียน condition มาเพื่อช่วยเลือก elements ที่ต้องการนะครับ อันดับแรกก็เริ่มจาก XPath ที่ทุก sample มีร่วมกันก่อน ก็คือ

//*[@id="mw-content-text"]/div/table

ทีนี้ ก็จะเริ่มใส่ condition ตามข้อสังเกตุของเราข้างต้นแล้วนะครับ
1. เราต้องการเลือกตารางตั้งแต่ตารางที่ 3 ขึ้นไปเท่านั้น วิธีการคือให้ใส่ condition เข้าไป ว่า position ของ <table> เนี่ย ต้องมากกว่าหรือเท่ากับ 3 ครับ ซึ่งเขียนเป็น condition ได้ว่า position() >= 3 ก็จะได้ XPath เป็น

//*[@id="mw-content-text"]/div/table[position() >= 3]

2. ตามด้วย <tbody> และ <tr> ก็เติมเข้าไปตรง ๆ ได้เลยครับ

//*[@id="mw-content-text"]/div/table[position() >= 3]/tbody/tr

3. ตามด้วย <td> แต่เราจะเอาเฉพาะ column ที่ 2 หรือ 3 เท่านั้น วิธีหนึ่งที่ทำได้ก็คือ ใส่ condition ว่า position ของ <td> เนี่ย ต้อง >= 2 และ <= 3 ซึ่งเขียน condition ออกมาได้ว่า 2 <= position() and position() <= 3 จึงได้ XPath เป็น

//*[@id="mw-c.../tbody/tr/td[2 <= position() and position() <= 3]

4. อย่างที่สังเกตุว่าบาง sample ก็มี <b> ขึ้นก่อนแล้วตามด้วย <a> (/b/a/)
หรือบาง sample ก็เป็น <a> ขึ้นก่อน แล้วตามด้วย <b> (/a/b/)
แล้วยังมี <b> แล้ว <a> แล้ว <b> อีก (/b/a/b/)
โชคดีที่ใน XPath เราสามารถละได้ โดยการเขียนเป็น //a// จะหมายถึงว่าจะมี element อะไรขึ้นก่อน <a> ก็ได้ (กี่ elements ก็ได้ หรือจะไม่มีก็ได้) แล้วตามด้วย elements อะไรก็ได้ (กี่ elements ก็ได้ หรือจะไม่มีก็ได้) แล้วตัว engine ก็จะหาทุก ๆ elements ที่เป็นไปได้มาให้เองครับ

//*[@id="mw-c.../tbody/tr/td[2 <= position() and position() <= 3]//a//

note: ซึ่งการละใน XPath นี้ ต้องระวังในการใช้สักเล็กน้อยนะครับ อย่าง //a// อาจหมายถึง /b/a ก็ได้ หรือ /b/a/b ก็ได้ หรืออาจหมายถึง /b/div/a/div/b/a/b/div ก็ได้ คือในส่วนที่ละ จะแทนเป็นอะไรก็ได้นั่นเองครับ

5. สุดท้ายตามด้วย text() คือต้องการเอาเฉพาะข้อความที่อยู่ใน element นั้นออกมา

//*[@id="mw-c.../tbody/tr/td[2 <= position() and position() <= 3]//a//text()

ได้ XPath สุดท้ายเป็น

//*[@id="mw-content-text"]/div/table[position() >= 3]/tbody/tr/td[2 <= position() and position() <= 3]//a//text()

ซึ่งเป็น XPath ที่เรามั่นใจว่าและครอบคลุมทุกกรณีที่เรา sample ออกมา และ(หวังว่า)จะครอบคลุมข้อมูลที่เราต้องการจริง ๆ ทั้งหมด

และเป็น XPath ที่เราก็ได้นำไปใช้จริงแล้วในการ extract รายชื่อเทศบาลตำบล ใน บทความ part ที่ 1 นั่นเองครับ

Summary

สำหรับการสร้าง XPath นั้น วิธีการที่นำเสนอไปก็เป็นหนึ่งในวิธีที่สามารถทำได้ง่าย ๆ ซึ่งเราจะสามารถมั่นใจได้ในระดับหนึ่งว่า XPath ที่ได้มานั้น ครอบคลุมข้อมูลส่วนใหญ่ โดยทำการ sample ออกมาให้มากขึ้น และ sample ในหลาย ๆ จุดที่แตกต่างกัน กระจาย ๆ กันไป แต่อาจจะต้องอาศัยความรู้ในเรื่อง syntax และคำสั่งของ XPath สักเล็กน้อยสำหรับการเขียน condition เพื่อ select element ต่าง ๆ นะครับ

ซึ่งจริง ๆ แล้ว ผู้เขียนเองก็ไม่ได้จำคำสั่งของ XPath ได้ทั้งหมดจริง ๆ หรอกครับ เมื่อจำเป็นต้องใช้ที ก็ search หาจาก internet เอา โดยผู้เขียนพบว่า เว็บ w3school ได้ทำรายการของคำสั่งต่าง ๆ พร้อมคำอธิบายเอาไว้ได้ดีและครบถ้วนแล้ว ทุก ๆ ท่านสามารถนำไปใช้เพื่ออ้างอิงขณะที่เขียนได้เลยครับ link นี้เลยครับ

และถ้ามีเรื่องไหนที่สนใจเพิ่มเติมสามารถ comment เอาไว้ได้นะครับ

FB Page: Copy Paste Engineer

- ขอบคุณที่อ่านครับ -

part อื่น ๆ ใน series
- part 1: การดูดข้อมูลเบื้องต้น ด้วย Python
- part 2: Chrome's Code Inspector
- part 3: เทคนิคการ extract ข้อมูลด้วย XPath
- part 4: ทำไมถึง scrape บางเว็บไม่ได้??? 7 เทคนิคง่าย ๆ ให้ scrape เว็บส่วนใหญ่ได้ลื่นปรื๊ด
- part 5: วิธีเลียนแบบการรับส่งข้อมูลของเว็บเป้าหมาย

Top comments (0)