Python Web Scraping #3: ใช้ Beautiful Soup ในการอ่านข้อมูลในหน้า Website

Nior Atthakorn
5 min readJun 7, 2020

--

จากตอนที่แล้ว เราได้ใช้ Requests ร่วมกับ Pandas ในการทำ Web Scraping มาแล้ว ซึ่งก็ยังคงมีข้อจำกัดอยู่ว่าข้อมูลที่ต้องการจะต้องอยู่ในรูปแบบตารางเท่านั้น ซึ่งในหลายๆ ครั้งข้อมูลที่เราต้องการก็ไม่ได้อยู่ในรูปแบบที่เราต้องการนั้น

ในบทความนี้เราจึงจะใช้ Beautiful Soup ซึ่งเป็น library สำหรับการ parse HTML ให้อยู่ในรูปแบบที่เข้าถึงได้ง่ายขึ้น มาช่วยแก้ปัญหาที่ตัว Pandas สามารถแกะได้เฉพาะข้อมูลที่เป็นตารางเท่านั้น และให้ Pandas ไปเป็นปลายทางของข้อมูล (เป็นตารางที่เก็บข้อมูลหลังจากทำเสร็จแล้ว) แทน จะมีวิธีการทำยังไง ก็อ่านต่อด้านล่างนี้เลย Let’s Go!

ดูใน GitHub หรือ ลองเล่นใน Google Colab

ก่อนหน้านี้เราใช้ function get ของ Requests ในการดึงข้อมูล HTML จาก website จากนั้นให้ function read_html ของ Pandas จัดการดึงตารางออกมาจาก HTML ที่ได้ แต่ถ้าข้อมูลส่วนที่เราต้องการไม่ใช่ตารางหล่ะ ?

ถ้ายังจำกันได้ จริงๆ แล้ว Pandas เป็น library ที่ถูกสร้างขึ้นมาเพื่อใช้ในการ manipulate data เป็นหลัก แต่ก็มีความสามารถที่จะนำมาใช้ scrape ได้ตามบทความในตอนที่ 1 และ 2 ด้วยความสามารถในส่วนของการ read data ของ Pandas

แต่ถ้า data ของเราไม่ได้มาในรูปแบบของตารางโดยตรง (อาจจะไม่เป็นตารางเลย หรือเรามองเป็นตารางแต่ HTML ที่ผู้เขียน website นั้นใช้ไม่ใช่รูปแบบของตาราง) เราก็จะไม่สามารถใช้ Pandas ในการ Scrape ได้แล้ว ในบทความนี้จึงจะให้ Pandas กลับไปทำหน้าที่ manipulate data เหมือนเดิม

แล้วเราจะใช้อะไรมาจัดการแทน ? เดาไม่ยากครับ ก็ใช้ Beautiful Soup มาช่วยในการ parse ตัว HTML ที่ requests ดึงมาแทน Pandas แล้วนำไปเก็บใน DataFrame ของ Pandas เพื่อที่จะนำไป manipulate ต่อได้ง่ายๆ นั่นเองงงง

ในเมื่อ Pandas ดึงข้อมูลได้แค่ตารางจากหน้า website ก็ให้ Beautiful Soup มาดึงข้อมูลแบบอื่นๆ แล้วค่อยจัดการด้วย Pandas ต่อ

ขั้นตอนที่ 1 ก็เหมือนๆ เดิมครับ import library ที่จะใช้ ในที่นี้จะใช้ 3 ตัว

  • Requests -> ดึงข้อมูลหน้า website
  • Beautiful Soup -> parse HTML ให้เราหา component ต่างๆ ได้ง่าย (และยืดหยุ่น) ขึ้น
  • Pandas -> manipulate data ที่ได้
import requests
from bs4 import BeautifulSoup
import pandas as pd

ขั้นตอนที่ 2 ใส่ url ของ website ที่เราต้องการ scrape เหมือนเดิม ในที่นี้จะใช้ website Meta tier list ของเกม LoR ของ mobalytics ต้องขออนุญาตและขอขอบคุณมา ณ ที่นี้ด้วยนะครับ -/\-

url = 'https://lor.mobalytics.gg/meta-tier-list'

ขั้นตอนที่ 3 ให้เราใช้ function get ของ Requests ในการดึงข้อมูลหน้า website โดยใส่ parameter เป็น url ที่เราได้กำหนดไว้ในขั้นตอนที่แล้ว (เหมือนบทความที่แล้วเป๊ะๆ เลย)

response = requests.get(url)

ขั้นตอนที่ 4 สร้าง object BeautifulSoup โดยใส่ argument เป็น HTML text (response.text เหมือนเดิมที่ได้กล่าวไปแล้วในขั้นตอนที่ 4 ในบทความที่ 2) คู่กับ parser ที่จะใช้ ซึ่งในที่นี้เราต้องการ parse HTML ก็ใช้ "html.parser" เลยยยย (สำหรับเรื่อง parser สามารถดูเพิ่มเติมได้ที่นี่เลยครับ)

soup = BeautifulSoup(response.text, 'html.parser')

ก่อนจะไปถึงขั้นตอนที่ 5 เรามาดูหน้าตาของ website ที่เราต้องการจะ scrape สักนิดนึงว่าเป็นยังไง และเราต้องการดึงส่วนไหนออกมา

ภาพที่ 1: หน้า website ที่เราต้องการจะ scrape

ในบทความนี้เราจะลองดึง

  • Tier ของ Deck (ตัวอักษรที่มุมบนซ้ายของแต่ละช่อง)(กรอบสีแดง)
  • ชื่อ Deck (ตัวอักษรตรงกลางของแต่ละช่อง)(กรอบสีเขียว)

ในการจะดึงข้อมูลจากหน้า website เราต้องเริ่มจากการหา tag ที่ข้อมูลของเราอยู่ให้ได้ก่อน เพื่อจะได้ทำการดึง tag นั้นพร้อมข้อมูลที่เราต้องการออกมาได้

โดย ก่อนจะหา tag เราต้องเปิดหน้า HTML Elements ขึ้นมาดูก่อน (ใครที่ไม่มีพื้นฐาน HTML เลย อาจจะงงๆ นิดนึง) สำหรับใน Google Chrome สามารถทำได้โดยการคลิกขวา แล้วเลือก Inspect (กรอบสีแดงตามภาพที่ 2)

ภาพที่ 2: คลิกขวาแล้วเลือก Inspect

จากนั้นจะมีหน้าต่างที่เป็น HTML Elements ขึ้นมาทางด้านล่าง ดังภาพที่ 3 (ของใครขึ้นด้านข้างก็ไม่ต้องตกใจไปครับ เป็น HTML Elements แบบนี้เหมือนกันก็พอครับ)

ภาพที่ 3: หน้า browser หลังจากเปิดหน้าต่าง HTML Elements

จากหน้านี้เราสามารถนำเมาส์ไปชี้ที่ tag ทางด้านล่าง เพื่อดูว่า tag นั้นครอบคลุมอะไรบ้างได้ ดังภาพที่ 4

ภาพที่ 4: Highlight ส่วนที่ตรงกับ tag ที่เราเอา mouse ชี้อยู่

ในภาพที่ 4 จะเห็นว่าพอลองเอาเมาส์ชี้ไปที่ tag ในกรอบสีแดง จะมี highlight ขึ้นที่หน้า website ตรงกรอบสีเขียว เราก็จะใช้เทคนิคตรงนี้ในการหาส่วนที่เราต้องการ เพื่อนำไปใช้ในการ scrape ได้

หลังจากเรารู้เทคนิคแล้ว ก็ได้เวลาไปต่อข้อ 5 กันจริงๆ แล้ว goๆ

ขั้นตอนที่ 5 เนื่องจาก Tier ของ Deck (ตัวอักษรมุมบนซ้ายของช่อง) กับชื่อ Deck (ตัวอักษรตรงกลางของช่อง) มีสิ่งที่เหมือนกันคือ อยู่ใน “ช่อง” เดียวกัน ดังนั้นในขั้นตอนที่ 5 นี้เราจะทำการหาช่องก่อน ให้เราลองเลื่อนเมาส์ในหน้า HTML Elements ไปเรื่อยๆ จนเจอช่องที่เราต้องการนะครับ

HTML Elements จะมีลักษณะเป็น hierarchy นะครับ อย่างตอนนี้เรารู้ tag ที่ highlight ตัวอักษรตรงกลางกรอบที่เราต้องการแล้ว เราก็ค่อยๆ ขยับขึ้น (ขยับไปหา tag ที่มี hierarchy สูงกว่า) แล้วดูว่าได้กรอบที่ต้องการหรือยัง โดยอย่าลืมว่ากรอบที่เราต้องการต้องครอบคลุมตัวอักษรตรงกลางและตัวอักษรตรงมุมบนซ้ายด้วย

ภาพที่ 5: Highlight ช่องแรกที่เราต้องการ

พอเจอกรอบแล้วอย่าลืมลองเลื่อนๆ ดู tags ที่อยู่ด้านในด้วยนะครับ ว่ามีครบที่เราต้องการจริงๆ มั้ย อย่างในรูปที่ 5 นี้ผมเช็คแล้วว่ามีครบที่เราต้องการแน่นอน

ในที่นี้เรารู้แล้วว่า ข้อมูลที่เราต้องการนั้นอยู่ใน tag “div” แต่ว่า tag “div” นั้นมีเยอะเหลือเกิน พร้อมทั้งอยู่ในหลายชั้นเหลือเกิน การดึงข้อมูลจาก tag “div” ทั้งหมดนั้นอาจจะไม่ค่อยสะดวกนัก แต่ไม่ต้องตกใจไป ค่อยๆ ดูไปก่อน

เริ่มจากการลองยุบ tag “div” ที่เราเจอนี้ลงก่อน (ดังภาพที่ 6) เพื่อจะได้ลองดูว่า กรอบอื่นๆ ที่เราต้องการเหมือนกันเนี่ย มีความเหมือน/ต่างอย่างไรกับ tag นี้บ้าง

ภาพที่ 6: tag ของแต่ละช่องมีส่วนที่เหมือนกัน

จะเห็นว่า แต่ละกรอบที่เราต้องการนั้น มีสิ่งหนึ่งที่เหมือนกันคือ มีชื่อ class ที่เหมือนกัน เราจะใช้ตรงนี้เป็นตัวช่วยในการดึงข้อมูลที่เราต้องการกัน

ให้เราใช้ method .find_all ของ BeautifulSoup object ในการหา tag ที่มีลักษณะตามที่เราต้องการทั้งหมด โดยหลักๆ แล้วเราจะใส่ argument กัน 2 ตัว นั่นคือ

  • name: ชื่อของ tag ที่เราจะดึงข้อมูล
  • attrs: dictionary ที่มี key เป็นชื่อของ attribute และ value เป็นค่าของ attribute นั้นๆ

อ่านแล้วอาจจะยังงง ลองดูตัวอย่างที่เรากำลังจะทำนี้ครับ ชื่อของ tag ของเราคือ "div" แล้ว attrs ของเรามี 1 คู่ คือมี key เป็น "class" และ value เป็น "meta-archetypecomponent__MetaArchetypeWrapper-sc-10x0k3h-0 grJXaq" นั่นเองครับ

results = soup.find_all('div', {'class': 
'meta-archetypecomponent__MetaArchetypeWrapper-sc-10x0k3h-0 grJXaq'})

ผลลัพธ์ที่เราได้จาก .find_all นั้น จะเป็น list ของ tag (ครอบคลุมถึง tag ย่อยๆ ทั้งหมด) ที่ตัว method สามารถหาเจอนะครับ

ทีนี้หลังจากเราได้ผลลัพธ์แล้ว เราลองมาดูกันสักนิดว่าเราได้จำนวนของกรอบเท่ากับในหน้าเว็บหรือไม่ ณ วันที่เขียนบทความนี้ มีกรอบทั้งหมด 18 กรอบ เราก็ลองใช้ function len ดูว่าผลลัพธ์ของเรามีเท่ากันหรือไม่

In  [6]: len(results)
Out [6]: 18

สวยงามมมม ผลลัพธ์ก็ได้ตรงตามหน้า website พอดี

ขั้นตอนที่ 6 หลังจากได้กรอบทั้งหมดมาแล้ว เราจะต้องเอา tag ที่ได้ของแต่ละกรอบ มาวน loop เพื่อหา Tier ของ Deck (ตัวอักษรมุมบนซ้ายของช่อง) และชื่อ Deck (ตัวอักษรตรงกลางของช่อง) ออกมา แต่ก่อนจะวน loop เราจะลองเอา tag แรกของเรามาหาก่อน จะได้รู้ว่าใน loop จะต้องดึงยังไง

6.1 เริ่มจากการดึงผลลัพธ์ตัวแรกออกมา

first_result = results[0]

ลองหา tag ของตัวอักษรมุมบนซ้าย โดยการคลิกขวาแล้วเลือก Inspect อีกครั้งที่ตัวอักษรนั้น หน้า HTML Elements ข้างล่างจะเด้งไปหาส่วนที่เราต้องการเอง จะได้ผลลัพธ์ดังภาพ

ภาพที่ 7: tag ของตัวอักษรมุมบนซ้าย

จะเห็นว่าตัวอักษรที่เราต้องการนั้นอยู่ใน tag “span” และมี class เป็น “tier-hex-imagestyles__TierLabel-sc-13fs6l0–2 bTGiWh” เราก็ลองใช้ method .find_all เหมือนเดิมก่อน เพื่อลองดูว่าหาเจอไหม และมีอะไรอย่างอื่นติดมาไหม

In  [8]: first_result.find_all('span', 
{'class': 'tier-hex-imagestyles__TierLabel-sc-13fs6l0-2 bTGiWh'})
Out [8]: [<span class="tier-hex-imagestyles__TierLabel-sc-13fs6l0-2 bTGiWh">S</span>]

จะเห็นว่า มีแค่ tag ที่เราต้องการเพียงอย่างเดียวเลย เราสามารถเปลี่ยนมาใช้ method .find เฉยๆ แทน .find_all ได้ ก็จะได้ผลลัพธ์เป็น tag แรกที่เจอตามรูปแบบที่เราค้นหา (แต่ในที่นี้มี tag เดียวที่เราต้องการอยู่แล้ว ไม่มีปัญหาที่จะใช้ .find เฉยๆ)

นอกจากนี้เราต้องการแค่เพียงตัวอักษรที่อยู่ข้างใน (ในกรอบแรกนี้คือตัว “S”) เพียงอย่างเดียว ไม่ได้ต้องการทั้ง tag ยาวๆ เราจะใช้ attribute .text ในการดึงข้อความข้างในออกมา ทำให้ได้ผลลัพธ์ตามนี้

In  [9]: first_result.find('span', 
{'class': 'tier-hex-imagestyles__TierLabel-sc-13fs6l0-2 bTGiWh'}).text
Out [9]: 'S'

ในส่วนของข้อความตรงกลางช่อง เราก็ใช้วิธีเดียวกันกับตัวอักษรมุมบนซ้าย จะได้ผลลัพธ์ดังนี้

In [10]: first_result.find('h3').text
Out[10]: 'Heimer Control'

สังเกตว่าตอนใช้ method .find ในครั้งนี้ผมไม่มีการใส่ attribute แต่อย่างที่บอกไปก่อนหน้านี้แล้วว่า เราจะใส่/ไม่ใส่ยังไงก็ได้ ถ้าเท่าที่เราใส่ทำให้เราสามารถเข้าถึงข้อมูลที่เราต้องการได้ เท่านั้นก็เพียงพอแล้ว (ก็ในข้อบนๆ จริงๆ ก็สามารถใส่ attribute "id" ได้ด้วยนี่นา แต่ไม่มีประโยชน์ในการหา ก็ไม่ใส่เหมือนกันแหละคร้าบ)

6.2 ทีนี้หลังจากเราได้แล้วว่า

  • ส่วนของ Tier ของ Deck (ตัวอักษรมุมบนซ้ายของช่อง) สามารถหาได้โดยเอา tag ของช่องนั้นๆ มา .find('span', {'class': 'tier-hex-imagestyles__TierLabel-sc-13fs6l0-2 bTGiWh'}).text
  • ส่วนของชื่อ Deck (ตัวอักษรตรงกลางของช่อง) สามารถหาได้โดยเอา tag ของช่องนั้นๆ มา .find('h3').text

เราก็มาทำการ loop เพื่อเก็บข้อมูลลง DataFrame กัน แต่ก่อนจะ Loop ให้เราสร้าง DataFrameเปล่าๆ ขึ้นมา เพื่อรอการเพิ่มข้อมูลจาก loop กัน

df = pd.DataFrame(columns=['Deck Tier', 'Deck Name'])

หลังจากนั้นก็ทำการ loop เพื่อดึงข้อมูลที่เราต้องการมาใส่ใน DataFrame เลยยยยย

# loop tag ของแต่ละช่องที่เราหาได้
for tag in results:

# สร้าง dictionary เปล่าเพื่อเก็บข้อมูลช่องที่กำลังดู
temp = {}

# หา Tier ของ Deck (ตัวอักษรมุมบนซ้ายของช่อง) แล้วเก็บค่าใน dict
temp['Deck Tier'] = tag.find('span',
{'class': 'tier-hex-imagestyles__TierLabel-sc-13fs6l0-2 bTGiWh'}).text

# หาชื่อ deck แล้วเก็บค่าใน dict
temp['Deck Name'] = tag.find('h3').text

# นำ dict ที่มีไปเพิ่มข้อมูลใน DataFrame ที่สร้างไว้
df = df.append(temp, ignore_index=True)

หลังจาก loop เสร็จเรียบร้อยแล้ว ก็มาดูผลลัพธ์กัน

df
ภาพที่ 8: ผลลัพธ์ที่ได้

ดูดีมีชาติตระกูลลลล

เป็นไงกันบ้างครับกับการใช้ Beautiful Soup มาช่วย Requests ทำ Web Scraping ก็ยังง่ายเหมือนเดิมใช่ไหมหล่ะครับ ทีนี้เราก็เหมือนจะ Scrape website ไหนก็ได้แล้ว เย่ๆ

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

ส่วนใครยังไม่ได้อ่าน 2 ตอนก่อนหน้านี้ หรืออ่านแล้วลืมแล้ว สามารถย้อนอ่านได้ที่นี่เลย:

สำหรับวันนี้ขอตัวก่อนนะคร้าบ บายยยย

--

--

Nior Atthakorn

โปรแกรมบ้าง ดนตรีบ้าง ปนๆ กันไป