+3

PyMOTM: Beautiful Soup 4 (Part II)

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.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.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.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 .stringsstripped_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.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.previous_sibling

Bạn có thể sử dụng .next_sibling.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.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.previous_element

Thuộc tính .next_element.previous_element nhìn chung khá giống với .next_sibling.previous_sibling, nhưng nó khác 1 điểm là .next_elements.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.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.previous_elements

Giống như với .next_element.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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí