PyMOTM: Beautiful Soup 4 (Part II)
This post hasn't been updated for 8 years
Beautiful Soup 4
Mục đích: Parse HTML, XML và Website scraping
Hôm nay chúng ta tiếp tục phần II của module Beautiful Soup 4 của Python trong series PyMOTM nhé. Ở phần I mình đã giới thiệu sơ qua về module này như cách cài đặt module, các parser cho module và giới thiệu về các loại object của Beautiful Soup 4. Giờ chúng ta sẽ đi tiếp sang phần Navigating the tree của Beautiful Soup 4 nhé.
Navigating the tree
Chúng ta vẫn sẽ sử dụng lại nguyên liệu của phần I để thực nghiệm luôn nhé !
Going down
Các thẻ (tag) HTML có thể chứa chuỗi (string) hay các tag con khác bên trong nó. Các tag này được gọi là children tag. Beautiful Soup cung cấp cho chúng ta nhiều cách khác nhau để di chuyển tới và duyệt các thẻ (không phải là string bởi vì string thì không thể có children ) này. OK, chúng ta đi tìm hiểu chi tiết hơn cho phần Going down này nhé
Di chuyển bằng tên tag
Cách đơn giản nhất để bạn di chuyển tới 1 tag nào đó là bạn sử dụng ngay tên tag của nó. Ví dụ, mình muốn lấy tag title
:
(Pdb) html_dom.head.title
<title>Document</title>
(Pdb) html_dom.title
<title>Document</title>
Nếu như tag đó có nhiều hơn 1, thì bạn sẽ nhận được tag đầu tiên mà BS4 gặp.
(Pdb) html_dom.body.p
<p>Item 001</p>
(Pdb) html_dom.p
<p>Item 001</p>
Để lấy tất cả, chúng ta có thể sử dụng method find_all()
(nó sẽ được giới thiệu trong phần Searching the tree nhé) để lấy tất cả những thẻ có tên là tham số đầu tiên của method:
(Pdb) html_dom.find_all('a')
[<a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>]
.contents
và .children
.contents
chứa danh sách các tag con mà nó có:
(Pdb) html_dom.head.contents
[u'\n ', <meta charset="unicode-escape"/>, u'\n ', <title>Document</title>, u'\n ', <link href="css.css" rel="stylesheet" type="text/css"/>, u'\n']
(Pdb) html_dom.head.title.contents
[u'Document']
Một string thì không thể chứa .contents
(Pdb) title = html_dom.title.contents[0]
(Pdb) title
u'Document'
(Pdb) title.contents
*** AttributeError: 'NavigableString' object has no attribute 'contents'
Trong trường hợp này, thay vì lấy danh sách bằng cách dùng .contents
thì bạn có thể duyệt nội dung của nó bằng thằng .children
:
(Pdb) for child in html_dom.title.children: print(child)
Document
.descendants
.contents
và .children
chỉ lấy ra danh sách các tag con mà bản thân nó chứa trong đó. Như ví dụ trên thì nó sẽ trả về danh sách các thẻ con hiện thời. Còn .descandants
sẽ trả về cho bạn 1 danh sách đệ quy các đời con cháu của nó.
(Pdb) for child in html_dom.head.descendants: print(child)
<meta charset="utf-8"/>
<title>Document</title>
Document
<link href="css.css" rel="stylesheet" type="text/css"/>
(Pdb)
Bạn có thắc mắc tại sao lại có nhiều dấu newline ở ví dụ trên không? Có phải mình viết nhầm hay là quên không format lại cho dễ nhìn hơn? Không, nếu bạn xem lại ví dụ của .contents
và .children
thì bạn sẽ thấy trong danh sách nó trả về sẽ có cả \n
. Vậy làm sao để loại bỏ nó ra khỏi danh sách? Chả lẽ phải kiểm tra một vài điều kiện rồi mới in ra? Bạn sẽ có câu trả lời trong phần .strings
và stripped_strings
nhé ^^!
.string
Nếu 1 tag mà chỉ chứa một con, và con đó là 1 NavigableString
, thì bạn có thể lấy ra bằng .string
(Pdb) html_dom.title.string
u'Document'
Nếu tag đó chứa 1 children là 1 tag khác, thì khi bạn sử dụng .string
, nó cũng sẽ trả ra cho bạn nội dung của thẻ con đó. Mình sẽ viết 1 ví dụ khác nhé. Vì trong nguyên liệu của chúng ta không có thẻ nào chỉ chứa 1 thẻ con kèm string cả
(Pdb) html_str = BeautifulSoup('<head><title>Lorem ipsum dolor</title></head>', 'html5lib')
(Pdb) html_str.head
<head><title>Lorem ipsum dolor</title></head>
(Pdb) html_str.head.string
u'Lorem ipsum dolor'
Nếu như thẻ đó có chứa nhiều hơn 1 thẻ con khác, thì .string
sẽ được trả về là None
. Chúng ta dùng luôn nguyên liệu đã chuẩn bị từ trước nhé. Nó đủ điều kiện để sử dụng
(Pdb) print(html_dom.head.string)
None
.strings
và .stripped_strings
Nếu bạn muốn lấy tất cả các string trong một tag (bao gồm cả các tag con - recursive), bạn có thể sử dụng .strings
:
(Pdb) for str in html_dom.body.strings: str
u'\n '
u'\n '
u'\n '
u'\n '
u'Item 001'
u'\n '
u'Price: 01$'
u'\n '
u'Buy'
u'\n '
u'\n '
u'\n '
u'Item 002'
u'\n '
u'Price: 02$'
u'\n '
u'Buy'
u'\n '
u'\n '
u'\n '
u'Item 003'
u'\n '
u'Price: 03$'
u'\n '
u'Buy'
u'\n '
u'\n '
u'\n '
u'Item 004'
u'\n '
u'Price: 04$'
u'\n '
u'Buy'
u'\n '
u'\n '
u'\n '
u'Item 005'
u'\n '
u'Price: 05$'
u'\n '
u'Buy'
u'\n '
u'\n '
u'\n '
u'\n\n'
Lần này mình không sử dụng print
để mọi người có thể thấy rõ các phần tử \n
. Và cũng quay lại ví dụ của .descendants
, mình cũng có nhắc đến việc làm sao để loại bỏ những phần tử \n
này ra khỏi danh sách kết quả, đúng không? Vâng, chúng ta sẽ sử dụng .stripped_strings
nhé
(Pdb) for str in html_dom.body.stripped_strings: str
u'Item 001'
u'Price: 01$'
u'Buy'
u'Item 002'
u'Price: 02$'
u'Buy'
u'Item 003'
u'Price: 03$'
u'Buy'
u'Item 004'
u'Price: 04$'
u'Buy'
u'Item 005'
u'Price: 05$'
u'Buy'
Vâng, với .stripped_strings
thì BS4 sẽ bỏ qua những dấu newline (\n
) cho chúng ta. Vậy là đã xong phần Going down. Chúng ta sẽ qua phần Going up nhé.
Going up
Bất kể 1 tag hay 1 string nào cũng đều chứa parent tag. Để lấy parent tag, bạn có thể sử dụng attribute .parent
.
.parent
Trong ví dụ dưới đây, mình sẽ vào tag title
rồi thử lấy parent của nó là gì nhé
(Pdb) title = html_dom.title
(Pdb) title
<title>Document</title>
(Pdb) title.parent
<head>\n <meta charset="unicode-escape"/>\n <title>Document</title>\n <link href="css.css" rel="stylesheet" type="text/css"/>\n</head>
(Pdb) title.parent.name
u'head'
(Pdb)
Vậy parent của tag html
là gì? Tên nó là document
. Nhưng chính thức nó là thằng bs4.BeautifulSoup
(Pdb) html = html_dom.html
(Pdb) html.parent.name
u'[document]'
(Pdb) type(html.parent)
<class 'bs4.BeautifulSoup'>
.parents
Ở trên, chúng ta mới chỉ lấy được parent của tag hiện tại. Vậy nếu chúng ta muốn lấy nhiều cấp parent của tag đó thì sao? Chả lẽ chúng ta cứ .parent
rồi lại .parent
?
(Pdb) title.parent.name
u'head'
(Pdb) title.parent.parent.name
u'html'
Nếu thế này thì bất tiện quá nhỉ? Rất may, BS4 hỗ trợ chúng ta thuộc tính .parents
để làm việc này
(Pdb) p_first = html_dom.p
(Pdb) p_first
<p class="title">Item 001</p>
(Pdb) for parent in p_first.parents: parent.name
u'div'
u'div'
u'body'
u'html'
u'[document]'
Vâng, ở ví dụ trên, mình đã lấy tag p
đầu tiên mà thằng BS4 nhìn thấy. Rồi từ đấy thử lấy ngược lên xem nó nằm trong những thẻ nào. Và nhìn kết quả, chúng ta cũng thấy rõ lợi ích của thuộc tính .parents
rồi nhỉ ?
Hết đi lên rồi lại đi xuống, giờ chúng ta sẽ chuyển qua phần đi ngang nhé )!
Going sideways
Xem lại file bs4.html
, chúng ta sẽ thấy các tag div có class item
ngang hàng nhau và cùng thuộc tag div cha có class là items-list
. Chúng được gọi là các tag anh em (siblings). Chúng ta cùng đi tìm hiểu làm sao để duyệt các tag có mối quan hệ này nhé ^^!
.next_sibling
và .previous_sibling
Bạn có thể sử dụng .next_sibling
và .previous_sibling
để di chuyển giữa các element. Để ví dụ, chúng ta sẽ lấy tag p đầu tiên trong document (<p class="title">Item 001</p>
). Sau đó sử dụng .next_sibling
chú ta sẽ được tag p kế tiếp nó (<p class="price">Price: 01$</p>
). Thế nếu chúng ta sử dụng .previous_sibling
thì sao? Vì đằng sau nó không có gì. Chúng ta sẽ nhận đc 1 giá trị None
! Trên thực tế thì là vậy, nhưng trong BS4, .next_sibling
của tag p đầu tiên lại là \n
chứ không phải là tag p kế tiếp như chúng ta nhìn vào nội dung file bs4.html
. Để lấy được tag p kế tiếp, chúng ta cần phải gọi 2 lần .next_sibling
(Pdb) first_item = html_dom.p
(Pdb) first_item
<p class="title">Item 001</p>
(Pdb) first_item.next_sibling
u'\n '
(Pdb) first_item.next_sibling.next_sibling
<p class="price">Price: 01$</p>
(Pdb) first_item.previous_sibling
u'\n '
(Pdb) print(first_item.previous_sibling.previous_sibling)
None
.next_siblings
và .previous_siblings
Bạn cũng có thể lấy tất cả các tag anh em của tag hiện tại bằng các sử dụng .next_siblings
hoặc .previous_siblings
.
(Pdb) first_item = html_dom.p
(Pdb) first_item
<p class="title">Item 001</p>
(Pdb) for next_item in first_item.next_siblings: next_item
u'\n '
<p class="price">Price: 01$</p>
u'\n '
<p><a href="#">Buy</a></p>
u'\n '
(Pdb) for prev_item in first_item.previous_siblings: prev_item
u'\n '
(Pdb)
Going back and forth
Xét lại file bs4.html
, khi một HTML parse làm việc, nó sẽ sinh ra 1 danh sách các sự kiện của tag như: mở tag <html>
, mở tag <head>
, mở và đóng tag <meta>
, mở tag <title>
, thêm nội dung cho tag title
, đóng tag <title>
, ... BS4 hỗ trợ chúng ta xây dựng lại quá trình parse 1 document đó. Chúng ta cùng đi tìm hiểu nhé.
.next_element
và .previous_element
Thuộc tính .next_element
và .previous_element
nhìn chung khá giống với .next_sibling
và .previous_sibling
, nhưng nó khác 1 điểm là .next_elements
và .previous_element
sẽ duyệt tất cả các tags con (bao gồm cả string). Chúng ta sẽ tạo ra 1 document đơn giản để xem quá trình của nó nhé:
<html><head><meta charset="UTF-8"><title>Document</title></head><body></body></html>
(Pdb) html_str = BeautifulSoup('<html><head><meta charset="UTF-8"><title>Document</title></head><body></body></html>', 'html5lib')
(Pdb) head_tag = html_str.head
(Pdb) head_tag
<head><meta charset="unicode-escape"/><title>Document</title></head>
Nếu chúng ta gọi head_tag.next_element
, nếu giống như .next_sibling
thì chúng ta phải nhận đc là tag body
, nhưng như mình đã nói ở trên, .next_element
và .previous_element
sẽ duyệt cả các tags con trong document để mô tả lại quá trình parse document của 1 HTML parser. Nên thay vì nhận đc tag body
, chúng ta sẽ nhận được tag con của head trước:
(Pdb) head_tag.next_element
<meta charset="unicode-escape"/>
(Pdb) head_tag.next_sibling
<body></body>
Bạn đã thấy sự khác biệt rồi chứ ?
.next_elements
và .previous_elements
Giống như với .next_element
và .previous_element
, nhưng 2 thuộc tính này giúp bạn nhận về 1 danh sách các tag. 2 thuộc tính này mô tả rõ nhất quá trình parse document của HTML parser
(Pdb) for tag in head_tag.next_elements: tag
<meta charset="unicode-escape"/>
<title>Document</title>
u'Document'
<body></body>
Đến đây là đã kết thúc phần Navigating the tree
và bài viết cũng khá dài rồi. Nên mình xin tạm dừng tại đây nhé. Hẹn các bạn trong phần tiếp theo, mình sẽ giới thiệu tiếp về phần Searching the tree
để làm sao tìm được những tag mình mong muốn, chúng ta sẽ có cái nhìn rõ hơn về việc Website scraping
!
All Rights Reserved