Python Web Scraping part 3 - เทคนิคการ extract ข้อมูลด้วย XPath
Tags: python, tutorial, webdev, datascience
Published: 2020-04-12 05:35:39

มาต่อกันกับ series Python Web Scraping part 3 นะครับ หลังจากที่ใน [part ที่แล้ว](https://dev.to/copypasteengineer/python-web-scraping-part-2-chrome-s-code-inspector-3ok6) เราได้เรียนรู้วิธีการแกะ 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/รายชื่อเทศบาลตำบลในประเทศไทย](https://th.wikipedia.org/wiki/รายชื่อเทศบาลตำบลในประเทศไทย) กันเหมือนเดิมนะครับ โดยเป้าหมายคือ **หา XPath สำหรับ extract รายชื่อเทศบาลตำบลทั้งหมด** ซึ่งใน [part ที่แล้ว](https://dev.to/copypasteengineer/python-web-scraping-part-2-chrome-s-code-inspector-3ok6) เราใช้ Chrome Inspector ในการแกะ XPath ของ **"Element" หนึ่ง ๆ** มาได้แบบง่าย ๆ แล้วนะครับ ในตอนนี้เรากำลังจะหา XPath ที่สามารถ extract ข้อมูลทั้งหมดได้ **(คือครอบคลุมทุก element ไม่ใช่แค่ 1 element)**

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

สำหรับท่านที่ยังไม่เข้าใจว่าจะเอา sample XPath ตรงนี้มาได้อย่างไร **กรุณาอ่าน [part 2](https://dev.to/copypasteengineer/python-web-scraping-part-2-chrome-s-code-inspector-3ok6) ด่วน ๆ เลยครับ!** หรือถ้ามีข้อสงสัย สามารถคอมเม้นถามด้านล่าง หรือในเพจ [Copy Paste Engineer](https://www.facebook.com/CopyPasteEng/) ได้เลยนะครับ

ด้านล่างนี้เป็นตัวอย่างที่ผม sample ออกมา
```xpath
 (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>`
```xpath
//*[@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)`)
```xpath
(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)`)
```xpath
(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 มีร่วมกันก่อน ก็คือ
```xpath
//*[@id="mw-content-text"]/div/table
```
ทีนี้ ก็จะเริ่มใส่ condition ตามข้อสังเกตุของเราข้างต้นแล้วนะครับ 
**1\.** เราต้องการเลือกตารางตั้งแต่ตารางที่ 3 ขึ้นไปเท่านั้น วิธีการคือให้ใส่ condition เข้าไป ว่า **position ของ `<table>` เนี่ย ต้องมากกว่าหรือเท่ากับ 3 ครับ** ซึ่งเขียนเป็น condition ได้ว่า `position() >= 3` ก็จะได้ XPath เป็น
```xpath
//*[@id="mw-content-text"]/div/table[position() >= 3]
```
**2\.** ตามด้วย `<tbody>` และ `<tr>` ก็เติมเข้าไปตรง ๆ ได้เลยครับ
```xpath
//*[@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 เป็น
```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 ที่เป็นไปได้มาให้เองครับ
```xpath
//*[@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 นั้นออกมา
```xpath
//*[@id="mw-c.../tbody/tr/td[2 <= position() and position() <= 3]//a//text()
```

ได้ XPath สุดท้ายเป็น
```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](https://dev.to/copypasteengineer/python-web-scraping-part-1-python-49ce) นั่นเองครับ**

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

ซึ่งจริง ๆ แล้ว **ผู้เขียนเองก็ไม่ได้จำคำสั่งของ XPath ได้ทั้งหมดจริง ๆ หรอกครับ** เมื่อจำเป็นต้องใช้ที ก็ search หาจาก internet เอา โดยผู้เขียนพบว่า **เว็บ _w3school_ ได้ทำรายการของคำสั่งต่าง ๆ พร้อมคำอธิบายเอาไว้ได้ดีและครบถ้วนแล้ว ทุก ๆ ท่านสามารถนำไปใช้เพื่ออ้างอิงขณะที่เขียนได้เลยครับ [link นี้เลยครับ](https://www.w3schools.com/xml/xpath_intro.asp)**

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

FB Page: [Copy Paste Engineer](https://www.facebook.com/CopyPasteEng)

<center>**- ขอบคุณที่อ่านครับ -**</center>

**part อื่น ๆ ใน series** 
\- [part 1: การดูดข้อมูลเบื้องต้น ด้วย Python](https://dev.to/copypasteengineer/python-web-scraping-part-1-python-49ce) 
\- [part 2: Chrome's Code Inspector](https://dev.to/copypasteengineer/python-web-scraping-part-2-chrome-s-code-inspector-3ok6) 
\- [part 3: เทคนิคการ extract ข้อมูลด้วย XPath](https://dev.to/copypasteengineer/python-web-scraping-part-3-extract-xpath-18h)
\- [part 4: ทำไมถึง scrape บางเว็บไม่ได้??? 7 เทคนิคง่าย ๆ ให้ scrape เว็บส่วนใหญ่ได้ลื่นปรื๊ด](https://dev.to/copypasteengineer/python-web-scraping-part-4-scrape-7-scrape-4ko4)
\- [part 5: วิธีเลียนแบบการรับส่งข้อมูลของเว็บเป้าหมาย](https://dev.to/copypasteengineer/python-web-scraping-part-5-47nf)