การระบุเส้นทางด้วย XPath แบบ Advance

Mr.Rukpong
Arcadia Software Development
4 min readJan 2, 2019

จากที่เราเคยเขียน XPath แบบพื้นฐานกันไปแล้ว หรือใครยังไม่เคยอ่าน การเขียน XPath แบบพื้นฐาน สามารถเข้าไปตามลิ้งนี้ได้ครับ

วันนี้เราจะมาเริ่มเขียนแบบ Advance กันครับ

  • XPath ที่เราเคยเขียนกันไป คือ Absolute Path ที่จะระบุ Path ไปแบบตรงๆ เลยเช่น
//*[@id="footer"]/div[1]/div/p[1]
  • XPath ที่เราจะมาเขียนในบทความนี้คือ Relative Path ที่จะระบุ Path แบบมีความสัมพันธ์ต่อกัน อาจจะได้ใช้งานในหลายๆ กรณีอย่างเช่น
//*[@class="des-add-op"]//parent::li[@class="user3"]

XPath ที่มี Parent ต่างกัน แต่ตัวเองเป็นตัวเดียวกัน

หรือ

//*[@class="search"]//child::p[@class="search_user"]

XPath ที่มี Child ต่างกัน แต่ตัวเองเป็นตัวเดียวกัน

ถึงจะดูซับซ้อนที่จะอธิบายอย่างละเอียดในบทความนี้ครับ

อันดับแรกต้องทำใจการเลย การเขียน XPath แบบ Relative Path เราจะต้องเข้าใจความสัมพันธ์ของ XPath เอง ซึ่งเราไม่สามารถ get XPath อัตโนมัติได้แล้ว

เรามาเริ่มจากการรู้จักวิธีการเขียน XPath เพิ่มเติม เพื่อให้เขียน XPath ได้ Advance และทำให้เราสะดวกมากขึ้น

  1. contains

การใช้คำสั่ง contains เพื่อค้นหา XPath ที่มีข้อความที่เราต้องการอยู่ในส่วนที่เราสนใจ เช่น

//*[contains(text(),'ไทย')]

และผู้ช่วยของเรา XPath Helper (Extension สำหรับ Chrome) จะช่วย Highlight Element จาก XPath ที่เราเขียนอยู่ วิธีใช้สามารถกลับไปอ่านบทความ การเขียน XPath แบบพื้นฐาน ได้ครับ

จากตัวอย่างข้างต้น //*[contains(text(),‘ไทย’)] เพื่อระบุว่า text ของ Element นั้น จะต้องเป็นคำว่า ไทย Element ทุกตัวที่มีคำว่า ไทย จะถูก Highlight

2. and / or

//*[@class="FPdoLc VlcLAe"]//input[@type="submit" and @name="btnK"]

จาก XPath ตัวอย่างที่เขียนไว้ข้างบน จะได้ Element เจาะจงแค่ตัวเดียว

และจะสังเกตเห็นว่า ถ้าเราเขียน XPath โดยไม่ระบุ and name ลงไป

//*[@class="FPdoLc VlcLAe"]//input[@type="submit"]

เราจะได้ Element สองตัว ที่มี Attribute type=“submit” ทั้งหมด

แต่ถ้าเราต้องการระบุชัดเจนลงไปอีก สามารถใช้ and เพื่อช่วยชี้ XPath ให้ชัดลงไปว่าต้องมี Attribute type=“submit” และ Attribute name=“btnK” ด้วย

** And หรือ Or สามารถเลือกใช้ได้แล้วแต่กรณี And อาจจะใช้ในกรณีที่ต้องการระบุ Element แบบเจาะจง ส่วน Or อาจจะระบุ Element ที่มี Attribute เหมือนกัน หลายๆ ตัวได้

3. starts-with

//span[starts-with(@id,'fs')]

จากตัวอย่างจะสังเกตเห็นว่าใน class footer ของ Page จะมี id อยู่สองตัวคือ fsr, fsl แต่เราต้องการ Element ทั้งสองตัว เมื่อเราใช้ starts-with และระบุ Attribute ที่เราสนใจ //span[starts-with(@id,’fs’)] หมายถึงต้องการเลือก id ที่ขึ้นต้นด้วย fs ทั้งหมด ก็จะได้ Element id ทั้ง fsr และ fsl

เมื่อเรารู้วิธีการเขียน XPath เพิ่มเติมด้วยการใช้คำสั่งช่วยไปแล้ว ทีนี้เรามาเริ่ม การเขียน XPath แบบ Relative Path กันครับ

ซึ่งจะเรียกวิธีการเขียน XPath แบบนี้ได้ว่า XPath Axes Methods การเขียน XPath แบบอ้างอิง

เป็นเหมือนความสัมพันธ์แบบเครือญาติ เริ่มอธิบายไปทีละตำแหน่งเลยครับ โดยการใช้ตัวอย่าง Page เดียวกันเลย ดังนี้

<html>
<body>
<h1>Title Page</h1>
<li class="subtitle">description title</li>
<div class="search">
<p class="search_user">ค้นหาข้อมูล Users</p>
<input></input>
<button>ค้นหา</button>
</div>
<br>
<br>
<div class=content-page>
<li class="user1">name 1
<p class="des-add">address 1</p>
</li>
<li class="user2">name 2
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
<p class="des-add-op2">address 3</p>
</li>
<li class="user3">name 3
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
</li>
<li class="user4">name 4
<p class="des-add">address 1</p>
</li>
<li class="user5">name 5
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
</li>
</div>
<br>
<br>
<div class="search">
<p class="search_page">ค้นหาข้อมูลภายใน Page</p>
<input></input>
<button>ค้นหา</button>
</div>
</body>
</html>
  1. following (ตามหลัง)
//*[@class="content-page"]//following::div

following คือคำสั่งเพื่อค้นหา XPath ที่อยู่ถัดจากเครื่องหมาย following::

ในที่นี้หมายถึงต้องการหา tag div ที่อยู่ถัดไปจาก XPath //*[@class=“content-page”]

จะได้ Element div ทั้งชุดที่อยู่ถัดจาก XPath //*[@class=“content-page”] นั่นก็คือ

<div class="search">
<p class="search_page">ค้นหาข้อมูลภายใน Page</p>
<input></input>
<button>ค้นหา</button>
</div>

2. preceding (ก่อนหน้า)

//*[@class="content-page"]//preceding::div

preceding คือคำสั่งเพื่อค้นหา XPath ที่อยู่ก่อนหน้าเครื่องหมาย preceding::

ถ้าสังเกตจะเห็นว่า XPath เขียนคล้ายกับ following:: เลย แต่เปลี่ยนเป็นคำสั่ง preceding:: แทน เพื่อค้นหา XPath ที่มี tag div ที่อยู่ก่อนหน้า XPath //*[@class=“content-page”] จะได้ Element ทั้งชุดดังนี้

<div class="search">
<p class="search_user">ค้นหาข้อมูล Users</p>
<input></input>
<button>ค้นหา</button>
</div>

3. parent (พ่อแม่)

เราเข้าสู่ความสัมพันธ์แบบเครือญาติกันจริงๆ จังๆ แล้ว เริ่มต้นที่ parent (พ่อแม่)

หมายถึง ต้องการ หา XPath ที่เป็น parent ของ Element ที่เราสนใจอยู่ เช่น

//*[@class="des-add-op"]//parent::li[@class="user3"]

หมายถึง ต้องการหา XPath ที่เป็น parent ของ //*[@class=“des-add-op”] ซึ่งจากในตัวอย่าง class=“des-add-op” มีอยู่หลายตัว แต่ parent แต่จากกัน ถ้าเราระบุ XPath ที่เป็น parent ชัดเจน เราก็จะได้ตัวที่ต้องการ

จาก XPath ดังกล่าว จะทำให้เราได้ //*[@class=“des-add-op”] ของ parent li[@class=“user3”] ดังนี้(ตัวที่เน้นตัวหนาไว้)

<li class="user3">name 3
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
</li>

4. child (ลูก)

//*[@class="search"]//child::p[@class="search_user"]

child จากตรงข้ามกับ parent ต้องการหาความสัมพันธ์ XPath ที่เป็น child (ลูก) ของตัวเอง เช่น XPath ตัวอย่าง ต้องการหา child ของ //*[@class=“search”] ที่มี XPath เป็น p[@class=“search_user”] จะได้ Element ดังนี้

<div class="search">
<p class="search_user">ค้นหาข้อมูล Users</p>
<input></input>
<button>ค้นหา</button>
</div>

5. ancestor (บรรพบุรุษ)

ทีนี้เราเข้าสู่ความสัมพันธ์ ที่สูงกว่า parent แล้วครับ นั่นก็คือ ancestor (บรรพบุรุษ) เราจะสามารถหา XPath ที่เป็น ancestor (บรรพบุรุษ) ของ Element ตัวที่เราสนใจได้ครับ เช่น

//*[@class="des-add-op2"]//ancestor::div

จากตัวอย่าง XPath หมายถึงต้องการหา ancestor ของ XPath //*[@class=“des-add-op2"] จะได้ XPath นี้

//div[@class="content-page"]

** แต่ถ้าเราใช้ tag parent::

//*[@class="des-add-op2"]//parent::div

เราจะไม่ได้ Element ตัวใดเลย เพราะว่า parent ของ //*[@class=“des-add-op2”] ไม่มี tag div

6. descendant (ลูกหลาน)

tag descendant:: จะตรงกันข้ามกับ ancestor คือ จะหา descendant (ลูกหลาน) ของ Element ตัวที่เราสนใจ เช่น

//*[@class="content-page"]//descendant::p

หมายถึง ต้องการหา descendant (ลูกหลาน) ของ Element //*[@class=“content-page”] ในที่นี้ จะได้ tag p ที่อยู่ภายใต้ XPath ของ //*[@class=“content-page”] ทั้งหมด 9 Element (ตัวที่เน้นตัวหนาทั้งหมด)

<div class=content-page>
<li class="user1">name 1
<p class="des-add">address 1</p>
</li>
<li class="user2">name 2
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
<p class="des-add-op2">address 3</p>

</li>
<li class="user3">name 3
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>

</li>
<li class="user4">name 4
<p class="des-add">address 1</p>
</li>
<li class="user5">name 5
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>

</li>
</div>

7. self (ตัวเอง)

//*[@class="user2"]//self::p[@class="des-add-op"]

tag self:: หมายถึง ต้องการค้นหา XPath ที่อยู่ใน scope ของตัวเอง ในกรณี XPath ตัวอย่างจะได้ Element

<li class="user2">name 2
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
<p class="des-add-op2">address 3</p>
</li>

** ถ้าเปลี่ยน XPath ไปอยู่ใน scope //*[@class=“ content-page”]

//*[@class="content-page"]//self::p[@class="des-add-op"]

จะได้ Element ที่มี XPath p[@class=”des-add-op”] 3 Element

8. following-sibling (พี่น้อง)

//*[@class="user2"]/p[@class="des-add"]//following-sibling::p

tag following-sibling:: จะแสดง Element ที่เกี่ยวข้องแบบ sibling (พี่น้อง) นั่นก็คือ XPath ที่อยู่ใน scope ชั้นเดียวกัน เช่นตัวอย่างข้างต้น Xpath //*[@class=“user2"]/p[@class=“des-add”] จะได้ Element ดังนี้

<li class="user2">name 2
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
<p class="des-add-op2">address 3</p>
</li>

เมื่อใส่ tag following-sibling::p จะหา tag p ที่อยู่ในระดับชั้นเดียวกัน จะได้ Element 2 ตัวคือ

<li class="user2">name 2
<p class="des-add">address 1</p>
<p class="des-add-op">address 2</p>
<p class="des-add-op2">address 3</p>

</li>

แล้วการเขียนแบบ Relative Path มันมีข้อดียังไงล่ะ?

  • เพื่อทำให้เราสามารถหา Element ที่ tag เหมือนกัน แต่อยู่ภายใต้ XPath กันคนละที่ได้
  • หรือสามารถกรอง Element ที่สนใจจาก Parent ที่เหมือนกัน หรือ Child ที่เหมือนกันได้
  • ยืดหยุ่นกว่า ทำไมหา Element ได้เสมอ เมื่อมันมีความสัมพันธ์เหมือนที่เราเขียน XPath ไว้ แต่ละมีข้อเสียตรงที่เราจะไม่รู้ระดับชั้นของ XPath จริงๆ (เห็นแต่ความสัมพันธ์ของ XPath)

☆ ยกตัวอย่างการมอง Element เป็นรูปธรรม เช่น หนังสือ A เล่มหนึ่ง (Element ตัวหนึ่ง) อยู่ในกล่องกระดาษ และหนังสือ A อีกเล่มหนึ่ง อยู่ในกล่องไม้

  • Absolute Path จะระบุว่า หนังสือ A ในกล่องกระดาษ (เริ่มมองจากกล่องกระดาษ > หนังสือ)
  • Relative Path จะระบุว่า หนังสือ A ที่อยู่ในกล่องกระดาษ (เริ่มมองจากหนังสือ > และระบุว่าอยู่ที่ไหน)

** แต่ Relative Path สามารถหาได้ว่า หนังสือ A อยู่ในกล่องอะไรบ้าง ก็ได้

ความหมายจะไม่ต่างกัน อยู่ที่ว่าจะมองจากมุมไหน?

การเลือกใช้วิธีเขียน XPath แต่ละแบบขึ้นอยู่กับว่า ต้องการเลือก Element แบบไหน สนใจ Element แบบไหน

  • ต้องการระบุ Path แบบตรงไปตรงมา ก็เขียนแบบ Absolute Path
  • ต้องการระบุ Path แบบมีความสัมพันธ์ ก็เขียนแบบ Relative Path

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

--

--