Hướng dẫn beautifulsoup trong python

Thư viện Beautiful Soup

Bản gốc

Show

BeautifulSoup là một thư viện Python dùng để lấy dữ liệu ra khỏi các file HTML và XML. Nó hoạt động cùng với các parser (trình phân tích cú pháp) cung cấp cho bạn các cách để điều hướng, tìm kiếm và chỉnh sửa trong parse tree (cây phân tích được tạo từ parser). Nhờ các parser này nó đã giúp các lập trình viên tiết kiệm được nhiều giờ làm việc.

Hướng dẫn beautifulsoup trong python

Các hướng dẫn này minh họa tất cả các tính năng chính của Beautiful Soup 4, với các ví dụ cụ thể. Chúng tôi chỉ cho bạn thấy thư viện tốt cho việc gì, hoạt động ra sao, sử dụng như thế nào, khiến nó làm những điều mà bạn muốn và xử lý ra sao khi nó không thực hiện đúng ý tưởng mà bạn đã vạch ra.

Các ví dụ trong tài liệu này hoạt động như nhau trong Python 2.7 và Python 3.2.

Bạn có thể tìm kiếm tài liệu cho Beautiful Soup 3. Nếu vậy, bạn nên biết rằng Beautiful Soup 3 không còn được phát triển nữa, và Beautiful Soup 4 được khuyến nghị cho tất cả các dự án mới. Nếu bạn muốn tìm hiểu thêm về sự khác biệt giữa hai phiên bản Beautiful Soup này, xem phần Porting code to BS4.

Bắt đầu nhanh

Ở đây có một chuỗi HTML, thứ mà ta sẽ sử dụng làm ví dụ trong suốt tài liệu này. Nó là một đoạn trong chuyện Alice in Wonderland:

html_doc = """
The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.

...

"""

Cho chuỗi trên vào BeautifulSoup constructor nó sẽ tạo ra một BeautifulSoup object, đại diện cho tài liệu dưới dạng cấu trúc dữ liệu lồng nhau:


from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')

print(soup.prettify())
# 
#  
#   
# The Dormouse's story
#   
#  
#  
#   

# #     The Dormouse's story # #  

#  

# Once upon a time there were three little sisters; and their names #     were # # Elsie # # , # # Lacie # # and # # Tillie # # ; and they lived at the bottom of a well. #  

#  

# ... #  

#  #

Dưới đây là một số cách đơn giản để điều hướng trong cấu trúc dữ liệu trên:

soup.title
# The Dormouse's story

soup.title.name
# u'title'

soup.title.string
# u'The Dormouse's story'

soup.title.parent.name
# u'head'

soup.p
# 

The Dormouse's story

soup.p['class'] # u'title' soup.a # Elsie soup.find_all('a') # [Elsie, # Lacie, # Tillie] soup.find(id="link3") # Tillie

Một công việc khá phổ biến là lấy ra tất cả URL tìm thấy trong thẻ của trang:


for link in soup.find_all('a'):
    print(link.get('href'))
# http://example.com/elsie
# http://example.com/lacie
# http://example.com/tillie

Một công việc phổ biến khác là lấy ra tất cả văn bản có trong trang:

print(soup.get_text())
# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...

Những thứ ở trên có phải là những thứ mà bạn cần? Nếu đúng vậy thì hãy đọc tiếp.

Cài đặt Beautiful Soup

Nếu bạn đang sử dụng một phiên bản Debian hay Ubuntu Linux nào đó gần đây, bạn có thể cài đặt Beautiful Soup bằng system package manager:

$ apt-get install python-bs4 (cho Python 2)

$ apt-get install python3-bs4 (cho Python 3)

Beautiful Soup 4 được phát hành thông qua PyPI, vì thế nếu bạn không thể cài đặt nó bằng system package manager, bạn có thể cài đặt nó bằng easy_install hoặc pip. Tên của package là beautifulsoup4, và nó hoạt động như nhau trên cả hai phiên bản Python2 và Python3. Hãy chắc rằng bạn sử dụng đúng phiên bản cho pip hoặc easy_install cho phiên bản Python của bạn (chúng có thể được đặt tên là pip3 và easy_install3 tương ứng nếu bạn sử dụng thêm cả Python3).

Tạo món Soup

Để bóc tách hay parse một tài liệu HTML, ta cần cho nó vào hàm BeautifulSoup() – hàm này sẽ trả về một BeautifulSoup object. Bạn có thể truyền cho nó một File object đang mở hoặc một chuỗi html.


from bs4 import BeautifulSoup

with open("index.html") as fp:
soup = BeautifulSoup(fp)

soup = BeautifulSoup("data")

Đầu tiên, tài liệu được chuyển đổi thành Unicode, và các phần tử HTML được chuyển đổi sang ký tự Unicode:

BeautifulSoup("Sacré bleu!")
Sacré bleu!

Soup được băm bằng parser tốt nhất hiện đang có. Ta có các parser và các cách sử dụng của nó.

lxml, html.parser, xml, html5lib

Các loại Object cần quan tâm

BeautifulSoup sẽ biến một tài liệu HTML phức tạp thành một cây object mà ở đó ta chỉ thao tác với các object bao gồm: Tag, NavigableString, BeautifulSoup, Comment.

Tag

Một Tag object tương ứng với một tag (còn được gọi là một thẻ hoặc một phần tử - element) trong tài liệu html hay xml:

soup = BeautifulSoup('Extremely bold')
tag = soup.b
type(tag)
#

Một Tag có chứa nhiều thuộc tính và phương thức, ta sẽ tìm hiểu sâu hơn trong phần Điều hướng trong cây và Tìm kiếm trong cây. Hiện tại chúng ta chỉ cần quan tâm đến hai thuộc tính quan trọng nhất của một Tag đó là Name và Attributes.

Name

Tag nào cũng có tên, ta có thể lấy nó ra thông qua thuộc tính .name:

tag.name
# u'b'

Nếu bạn thay đổi tên của Tag, thay đổi này sẽ được lưu lại trên tài liệu HTML được băm bởi BeautifulSoup:

tag.name = "blockquote"
tag
# 
Extremely bold

Attributes

Một tag có thể chứa nhiều thuộc tính. Ví dụ như tag có thuộc tính id và giá trị là boldest bằng cách xử lý tag như một dict, bạn có thể dễ dàng lấy ra giá trị của một thuộc tính khi có được tên của thuộc tính đó.

tag['id']
# u'boldest'

Bạn có thể truy cập dict trực tiếp bằng .attrs :

tag.attrs
# {u'id': 'boldest'}

Bạn có thể thêm, sửa, xóa các thuộc tính của tag. Một lần nữa, ta xử lý tag như một dict thông thường:

tag['id'] = 'verybold'
tag['another-attribute'] = 1
tag
# 

del tag['id']
del tag['another-attribute']
tag
# 

tag['id']
# KeyError: 'id'
print(tag.get('id'))
# None

Thuộc tính có nhiều giá trị

Trong HTML4 định nghĩa một vài thuộc tính mà trong nó có thể chứa một lúc nhiều giá trị. Trong HTML5 thì đã loại bỏ một vài cái trong số chúng. Thuộc tính nhiều giá trị mà chúng ta thường gặp nhất là class, những thuộc tính khác bao gồm rel, rev, accept-charset, headers và accesskey. BeatifulSoup sẽ chuyển những giá trị của các thuộc tính này vào một list, và ta có thể xử lý chúng như một list thông thường:

css_soup = BeautifulSoup('

') css_soup.p['class'] # ["body"] css_soup = BeautifulSoup('

') css_soup.p['class'] # ["body", "strikeout"]

Nếu một thuộc tính trông giống như là một thuộc tính nhiều giá trị. Nhưng nếu thuộc tính đó không được định nghĩa trong bất cứ phiên bản HTML tiêu chuẩn nào thì BeautifulSoup sẽ để giá trị thuộc tính đó một mình mà không tách ra thành một list như ở bên trên:

id_soup = BeautifulSoup('

') id_soup.p['id'] # 'my id'

Khi bạn biến một tag trở về thành một chuỗi, thì các giá trị của thuộc tính sẽ được hợp lại:

rel_soup = BeautifulSoup('

Back to the homepage

') rel_soup.a['rel'] # ['index'] rel_soup.a['rel'] = ['index', 'contents'] print(rel_soup.p) #

Back to the homepage

Bạn có thể vô hiệu hóa điều này bằng cách truyền vào multi_valued_attributes=None như một keyword argument trong BeautifulSoup constructor:

no_list_soup = BeautifulSoup('

', 'html', multi_valued_attributes=None) no_list_soup.p['class'] # u'body strikeout'

Bạn có thể dùng get_attribute_list để nhận giá trị luôn là một list, bất kể nó có là thuộc tính đa giá trị hay không.

id_soup.p.get_attribute_list('id') # ["my id"]

Nếu bạn băm tài liệu như một XML, nó sẽ không còn là thuộc tính đa giá trị nữa:

xml_soup = BeautifulSoup('

', 'xml') xml_soup.p['class'] # u'body strikeout'

Một lần nữa, bạn có thể cấu hình điều này bằng cách sử dụng đối số multi_valued_attributes:

class_is_multi= { '*' : 'class'}
xml_soup = BeautifulSoup('

', 'xml', multi_valued_attributes=class_is_multi) xml_soup.p['class'] # [u'body', u'strikeout']

Có thể bạn sẽ không cần phải làm điều này, nhưng nếu bạn làm thế, sử dụng giá trị mặc định như hướng dẫn. Chúng thực hiện các quy tắc được mô tả trong đặc tả HTML:

from bs4.builder import builder_registry
builder_registry.lookup('html').DEFAULT_CDATA_LIST_ATTRIBUTES

Một chuỗi tương ứng với một bit văn bản chứa trong một tag. Beautiful Soup sử dụng class NavigableString để chứa những bit văn bản này:

tag.string
# u'Extremely bold'
type(tag.string)
# 

Một NavigableString giống như một chuỗi Unicode trong Python, ngoài ra nó còn được hỗ trợ thêm một vài tính năng sẽ được nói đến trong phần Điều hướng trong cây và Tìm kiếm trong cây. Bạn có thể chuyển đổi một NavigableString thành một chuỗi Unicode bằng hàm str():

unicode_string = str(tag.string)
unicode_string
# u'Extremely bold'
type(unicode_string)
# 

Bạn không thể chỉnh sửa một chuỗi tại chỗ. Nhưng bạn có thể thay thế một chuỗi bằng một chuỗi khác bằng cách sử dụng phương thức replace_with:

tag.string.replace_with("No longer bold")
tag
# 
No longer bold

NavigableString hỗ trợ hầu hết các tính năng được mô tả trong Điều hướng trong cây và Tìm kiếm trong cây, nhưng không phải là tất cả. Cụ thể, vì một chuỗi dạng này không thể chứa bất cứ thứ gì (một thẻ chỉ có thể chứa một chuỗi hoặc một thẻ khác), các chuỗi dạng này không hỗ trợ thuộc tính .content hay .string, hay phương thức find().

Nếu bạn muốn dùng NavigableString bên ngoài BeautifulSoup, bạn nên chuyển nó thành chuỗi Unicode thông thường bằng hàm unicode(). Nếu không, chuỗi của bạn sẽ mang theo một tham chiếu đến toàn bộ parse tree của BeautifulSoup, ngay cả khi bạn đã sử dụng xong BeautifulSoup. Điều này rất gây lãng phí bộ nhớ.

BeautifulSoup

Bản thân BeautifulSoup object đại diện cho toàn bộ tài liệu. Đối với hầu hết các mục đích, bạn có thể coi nó như một Tag object. Điều này có nghĩa là nó hỗ trợ hầu hết các phương thức được mô tả trong Điều hướng trong cây và Tìm kiếm trong cây.

Vì đối tượng BeautifulSoup không phải là một tag HTML hay XML thực tế nào, nên nó không có name và attribute. Nhưng đôi khi thật hữu ích khi ta có được .name của nó, nó sẽ trả về một .name đặc biệt đó là “[document]”:

soup.name
# u'[document]'

Comment và vài chuỗi đặc biệt khác

Tag, NavigableString, BeautifulSoup là những thứ bạn sẽ thường bắt gặp trong tệp HTML hoặc XML, nhưng còn sót lại một thứ. Đó là Comment:

markup = ""
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# 

Comment object là một dạng đặc biệt của NavigableString:

comment
# u'Hey, buddy. Want to buy a used parser'

Nhưng khi nó xuất hiện như một phần trong tài liệu HTML, Comment được hiển thị dưới một định dạng đặc biệt:

print(soup.b.prettify())
# 
# 
# 

Beautiful Soup định nghĩa các class cho bất cứ thứ gì khác có thể xuất hiện trong một tài liệu XML: Cdata, ProcessingInstruction, Declaration, và Doctype. Như Comment, tất cả các class này đều là class con của NavigableString, nó sẽ thêm một cái gì đó vào chuỗi. Tại ví dụ này ta sẽ thay thế một Comment bằng một CDATA block:

from bs4 import CData
cdata = CData("A CDATA block")
comment.replace_with(cdata)

print(soup.b.prettify())
# 
# 
# 

Điều hướng trong cây

Đây, một lần nữa lại là tài liệu HTML “Three Sisters”:

html_doc = """
The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.

...

""" from bs4 import BeautifulSoup soup = BeautifulSoup(html_doc, 'html.parser')

Ta sẽ sử dụng cái này làm ví dụ xuyên suốt trong phần này.

Đi xuống

Các Tag có thể chứa string hoặc một tag khác. Những phần tử như này gọi là tag con (tag's children). BeautifulSoup cung cấp rất nhiều thuộc tính khác nhau để điều hướng và lặp qua các tag con này.

Lưu ý nữa là các Beautiful Soup string không hỗ trợ các thuộc tính này vì một string không thể có tag con.

Điều hướng bằng tên thẻ

Cách đơn giản nhất để điều hướng trong một parse tree là sử dụng tên của tag mà bạn muốn duyệt. Nếu bạn muốn duyệt tag , hãy dùng soup.head:

soup.head
# The Dormouse's story

soup.title
# The Dormouse's story

Bạn có thể sử dụng cách này nhiều lần để đi xuống một phần nhất định nằm sâu trong parse tree. Với code này, bạn sẽ lấy ra được thẻ bên dưới thẻ :

soup.body.b
# The Dormouse's story

Sử dụng tên thẻ như một thuộc tính ta sẽ chỉ lấy được thẻ đầu tiên bằng tên đó:

soup.a
# Elsie

Nếu bạn cần lấy hết tất cả các thẻ , hay bất kỳ thứ gì phức tạp hơn thẻ đầu tiên với một tên nhất định, bạn sẽ cần một phương thức được mô tả trong Tìm kiếm trong cây đó là file_all():

soup.find_all('a')
# [Elsie,
# Lacie,
# Tillie]

.content và .children

Một tag con được chứa trong một list được gọi là .contents:

head_tag = soup.head
head_tag
# The Dormouse's story

head_tag.contents
# [The Dormouse's story]

title_tag = head_tag.contents[0]
title_tag
# The Dormouse's story
title_tag.contents
# [u'The Dormouse's story']

Bản thân BeautifulSoup object cũng có một thẻ con. Trong trường hợp này, thẻ là thẻ con của BeautifulSoup object:

len(soup.contents)
# 1
soup.contents[0].name
# u'html'

Một string thì không có .content, bởi vì nó có chứa cái thứ gì đâu:

text = title_tag.contents[0]
text.contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

Thay vì để chúng trong một list, ta có thể lặp qua một thẻ con bằng cách sử dụng .children generator:

for child in title_tag.children:
    print(child)
# The Dormouse's story

.descendants

Hai thuộc tính .contents và .children chỉ duyệt qua một phần tử con trực tiếp của thẻ. Chẳng hạn, thẻ có một thẻ con trực tiếp là thẻ .</p><pre><code>head_tag.contents # [<title>The Dormouse's story]

Nhưng chính thẻ cũng có một phần tử con, là chuỗi “The Dormouse’s story”. Điều này cũng có nghĩa là chuỗi này cũng là một phần tử con của thẻ <head>. Thuộc tính .descendants cho bạn lặp qua các thẻ con, bằng cách đệ quy: Phần tử con trực tiếp, phần tử con của phần tử con trực tiếp và vân vân:</p><pre><code>for child in head_tag.descendants:     print(child) # <title>The Dormouse's story # The Dormouse's story

Thẻ chỉ có một phần tử con, nhưng nó có hai phần tử descendant (con cháu): Đó là thẻ và phần tử con của thẻ <title>. BeautifulSoup object chỉ có một phần tử con trực tiếp (thẻ <html>) nhưng nó có rất nhiều descendants:</p><pre><code>len(list(soup.children)) # 1 len(list(soup.descendants)) # 25</code></pre><h3>.string</h3><p>Nếu một thẻ chỉ có một phần tử con, và nó là một NavigableString, thì phần tử con đó sẽ được gọi ra bằng .string:</p><pre><code>title_tag.string # u'The Dormouse's story'</code></pre><p>Nếu phần tử con của một thẻ là một thẻ khác và thẻ đó có một .string, thì thẻ cha được coi là có cùng .string với thẻ con của nó.</p><pre><code>head_tag.contents # [<title>The Dormouse's story] head_tag.string # u'The Dormouse's story'

Nếu một thẻ chứa nhiều hơn một phần tử con, thì nó sẽ không hiểu .string nói đến cái gì. Vì vậy .string được định nghĩa là None:

print(soup.html.string)
# None

.strings and .stripped_strings

Nếu có nhiều hơn một phần tử con có trong một thẻ. Bạn cũng có thể lấy ra những string có trong những thẻ đó. Bằng cách sử dụng bộ tạo .strings:

for string in soup.strings:
    print(repr(string))
# u"The Dormouse's story"
# u'\n\n'
# u"The Dormouse's story"
# u'\n\n'
# u'Once upon a time there were three little sisters; and their names were\n'
# u'Elsie'
# u',\n'
# u'Lacie'
# u' and\n'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# u'...'
# u'\n'

Những chuỗi này có xu hướng có thêm những khoảng trắng bổ xung, bạn có thể loại bỏ chúng bằng cách sử dụng bộ tạo .stripped_strings thay thế:

for string in soup.stripped_strings:
    print(repr(string))
# u"The Dormouse's story"
# u"The Dormouse's story"
# u'Once upon a time there were three little sisters; and their names were'
# u'Elsie'
# u','
# u'Lacie'
# u'and'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'...'

Ở đây các chuỗi chứa toàn khoảng trắng sẽ bị bỏ qua, và khoảng trắng ở phần bắt đầu và kết thúc của một chuỗi cũng sẽ bị loại bỏ.

Đi lên

Tiếp tục với “family tree”, mỗi tag hay mỗi string đều có phần tử cha chứa nó.

.parent

Bạn có thể truy cập phần tử cha bằng thuộc tính .parent. Trong tài liệu ví dụ “Three Sisters”, thẻ là thẻ cha của thẻ :</p><pre><code>title_tag = soup.title title_tag # <title>The Dormouse's story title_tag.parent # The Dormouse's story

String trong thẻ title cũng có một phần tử cha chứa nó, đó là thẻ :</p><pre><code>title_tag.string.parent # <title>The Dormouse's story

Phần tử cha chứa thẻ là một BeautifulSoup object:

html_tag = soup.html
type(html_tag.parent)
# 

Và .parent của BeautifulSoup object được định nghĩa là None:

print(soup.parent)
# None

.parents

Bạn có thể lặp qua tất cả các phần tử cha bằng .parents. Ví dụ này sử dụng .parents để đi từ một thẻ nằm sâu trong tài liệu, đi lên nơi cao nhất của tài liệu:

link = soup.a
link
# Elsie
for parent in link.parents:
    if parent is None:
        print(parent)
    else:
        print(parent.name)
# p
# body
# html
# [document]
# None

Đi ngang

Ta sẽ sử dụng ví dụ bên dưới:

sibling_soup = BeautifulSoup("text1text2")
print(sibling_soup.prettify())
# 
#  
#   
#    
#     text1
#    
#    
#     text2
#    
#   
#  
# 

Thẻ và thẻ là hai thẻ cùng level. Chúng là hai phần tử con trực tiếp của cùng một thẻ. Chúng ta gọi chúng là siblings (anh em). Khi một tài liệu được pretty-printed, siblings được in trên cùng một độ thụt dòng. Bạn cũng có thể sử dụng mối quan hệ này trong code bạn viết.

.next_sibling và .previous_sibling

Bạn có thể sử dụng .next_sibling và .previous_sibling để điều hướng giữa những phần tử trong trang có cùng level trong parse tree:

sibling_soup.b.next_sibling
# text2

sibling_soup.c.previous_sibling
# text1

Thẻ có một .next_sibling (sau), nhưng không có .previous_sibling (trước), bởi vì không thẻ nào ở trước thẻ trong cùng level trong cây. Giống như ví dụ trên, thẻ có .previous_sibling nhưng không có .next_sibling:

print(sibling_soup.b.previous_sibling)
# None
print(sibling_soup.c.next_sibling)
# None

Hai chuỗi “text1” và “text2” không phải là siblings (anh em) bởi vì chúng không có cùng một phần tử cha:

sibling_soup.b.string
# u'text1'

print(sibling_soup.b.string.next_sibling)
# None

Trong thực tế, .next_sibling hay previous_sibling của một thẻ thường là một chuỗi chứa khoảng trắng. Ta cùng quay lại với tài liệu “three sisters”:

Elsie
Lacie
Tillie

Bạn có thể nghĩ rằng .next_sibling của thẻ đầu tiên sẽ là thẻ thứ hai. Nhưng thực ra, nó là một chuỗi gồm: dấu phẩy và newline để tách thẻ đầu tiên với thẻ thứ hai:

link = soup.a
link
# Elsie

link.next_sibling
# u',\n'

Thẻ thứ hai thực ra là .next_sibling của dấu phẩy:

link.next_sibling.next_sibling
# Lacie

.next_siblings và .previous_siblings

Bạn có thể lặp qua tất cả các thẻ siblings (anh em) bằng .next_siblings và .previous_siblings:

for sibling in soup.a.next_siblings:
    print(repr(sibling))
# u',\n'
# Lacie
# u' and\n'
# Tillie
# u'; and they lived at the bottom of a well.'
# None

for sibling in soup.find(id="link3").previous_siblings:
    print(repr(sibling))
# ' and\n'
# Lacie
# u',\n'
# Elsie
# u'Once upon a time there were three little sisters; and their names were\n'
# None

Đi tới đi lùi

Hãy nhìn vào phần đầu của tài liệu “three sisters”:

The Dormouse's story

The Dormouse's story

HTML parser lấy chuỗi ký tự này và biến nó thành một chuỗi các sự kiện: "open an tag", "open a tag", "open a tag", "add a string", "close the <title> tag", "open a <p> tag", và vân vân. Beautiful Soup cung cấp các công cụ để xây dựng lại initial parse của tài liệu.</p><h3 id="next-element-va-previous-element">.next_element và .previous_element</h3><p>Thuộc tính .next_element của một chuỗi hoặc một tag trỏ tới bất cứ thứ gì đã được parse ngay sau đó. Nó có thể giống như .next_sibling, nhưng nó thường rất khác nhau.</p><p>Ở đây là thẻ <a> cuối cùng của tài liệu "three sister". .next_sibling của nó là một chuỗi. Câu kết thúc bị ngắt khi bắt đầu thẻ <a>:</p><pre><code>last_a_tag = soup.find("a", id="link3") last_a_tag # <a class="sister" href="/redirect?Id=f%2fKgPq4IDV0SyEq0zfYr0LirHL60gbBHH3VIishi9CqgtHAKmbGoKNvFheNkumnh" id="link3">Tillie</a> last_a_tag.next_sibling # '; and they lived at the bottom of a well.</code></pre><p>Nhưng .next_element của thẻ <a>, đó là thứ mà được parse ngay sau khi parse thẻ <a>, nó không phải là phần còn lại của câu, mà nó là từ “Tillie”:</p><pre><code>last_a_tag.next_element # u'Tillie'</code></pre><p>Đó là vì original markup, từ "Tillie" được phát hiện trước dấu chấm phẩy. Parse đã gặp một thẻ <a>, sau đó là từ Tillie, sau đó là đóng thẻ </a>, sau đó là dấu chấm phẩy và phần còn lại của câu. Dấu chấm phẩy có cùng cấp với thẻ <a>, nhưng nó gặp được từ "Tillie" đầu tiên.</p><p>Thuộc tính .previous_element hoàn toàn ngược lại với .next_element. Nó trỏ tới phần tử mà được parse ngay trước phần tử này.</p><p><pre><code>last_a_tag.previous_element # u' and\n' last_a_tag.previous_element.next_element # <a class="sister" href="/redirect?Id=f%2fKgPq4IDV0SyEq0zfYr0LirHL60gbBHH3VIishi9CqgtHAKmbGoKNvFheNkumnh" id="link3">Tillie</a></code></pre></p><h3 id="next-elements-va-previous-elements">.next_elements và previous_elements</h3><p>Bằng cách này bạn có thể duyệt tiến tới hoặc duyệt lùi trong tài liệu được phân tích:</p> <pre><code>for element in last_a_tag.next_elements:     print(repr(element)) # u'Tillie' # u';\nand they lived at the bottom of a well.' # u'\n\n' # <p class="story">...</p> # u'...' # u'\n' # None</code></pre><h2 id="tim-kiem-trong-cay">Tìm kiếm trong cây</h2><p>Beautiful Soup định nghĩa rất nhiều phương thức dùng để tìm kiếm trong parse tree, nhưng chúng rất giống nhau. Mình sẽ dành nhiều thời gian để giải thích về hai phương thức phổ biến nhất: find() và find_all(). Các phương thức khác có các đối số gần như giống nhau vì thế mình sẽ chỉ trình bày ngắn gọn về chúng.</p><p>Một lần nữa, ta sẽ sử dụng tài liệu “three sisters” làm ví dụ:</p><pre><code>html_doc = """ <html><head><title>The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.

...

""" from bs4 import BeautifulSoup soup = BeautifulSoup(html_doc, 'html.parser')

Bằng cách truyền vào một hàm như find_all() một bộ lọc với đối số, ta có thể đi tới một phần trong tài liệu mà bạn quan tâm.

Các loại bộ lọc

Trước khi nói chi tiết về phương thức find_all() và các phương thức tương tự. Mình muốn show cho các bạn các ví dụ về các bộ lọc khác nhau mà bạn có thể truyền vào các phương thức này. Các bộ lọc này xuất hiện nhiều lần trong suốt search API. Bạn có thể sử dụng chúng để lọc dựa trên tên của một tag, thuộc tính của nó, văn bản trong một chuỗi, hoặc kết hợp lại các thứ trên.

Chuỗi

Đây là bộ lọc đơn giản nhất. Truyền một chuỗi vào một phương thức tìm kiếm và BeautifulSoup sẽ thực hiện so khớp chính xác với chuỗi đó. Đoạn code này tìm tất cả thẻ có trong tài liệu:

soup.find_all('b')
# [The Dormouse's story]

Nếu bạn truyền vào một byte string, Beautiful Soup sẽ cho rằng chuỗi được mã hóa UTF-8. Thay vào đó bạn có thể tránh điều này bằng cách truyền vào một Unicode string thay thế.

Regular Expression

Nếu bạn truyền vào một regular expression object, Beautiful Soup sẽ lọc theo regular expression đó bằng cách sử dụng phương thức search() của nó. Đoạn code này sẽ tìm tất cả các thẻ có tên bắt đầu bằng chữ “b”; trong trường hợp này là thẻ và thẻ :

import re
for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
# body
# b

Đoạn code này sẽ tìm tất cả các thẻ mà trong tên có chữ “t”:

for tag in soup.find_all(re.compile("t")):
    print(tag.name)
# html
# title

List

Nếu bạn truyền vào một list, Beautiful Soup sẽ cho phép một chuỗi match bất kì phần tử nào có trong list đó. Code này sẽ tìm tất cả thẻ và thẻ :

soup.find_all(["a", "b"])
# [The Dormouse's story,
# Elsie,
# Lacie,
# Tillie]

True

Giá trị True sẽ so khớp tất cả. Đoạn code này sẽ tìm tất cả thẻ có trong tài liệu, nhưng không có text string nào:

for tag in soup.find_all(True):
    print(tag.name)
# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

Function

Nếu không có matcher nào hợp với ý bạn, hãy định nghĩa một function có 1 param là tag (param này mặc định sẽ được truyền vào một element). Hàm này trả về True nếu element đó match, và ngược lại trả về False.

Đây là một hàm, nó sẽ trả về True nếu trong một tag có định nghĩa thuộc tính class mà không định nghĩa thuộc tính id:

def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')

Truyền hàm này vào trong find_all() và bạn sẽ chọn tất cả các thẻ

:

soup.find_all(has_class_but_no_id)
# [

The Dormouse's story

, #

Once upon a time there were...

, #

...

]

Hàm này chỉ chọn những thẻ

. Nó không chọn những thẻ , vì những thẻ đó định nghĩa cả hai thuộc tính là “class” và “id”. Nó không chọn những thẻ như và , vì những thẻ đó không định nghĩa thuộc tính “class”.</p><p>Nếu bạn truyền vào một hàm lọc một thuộc tính cụ thể như href, đối số truyền vào hàm sẽ là giá trị của thuộc tính, không phải toàn bộ tag. Hàm này tìm tất cả các thẻ a có thuộc tính href không khớp với biểu thức chính quy:</p><p><pre><code>def not_lacie(href):     return href and not re.compile("lacie").search(href) soup.find_all(href="/redirect?Id=AKny2RJ6C%2b9D35jCqyMr%2bw%3d%3d" # [<a class="sister" href="/redirect?Id=f%2fKgPq4IDV0SyEq0zfYr0L1x0DM4mpSt97%2ftYgbxlC2B7n4pvJNhhvRwo8bxiO4B" id="link1">Elsie</a>, # <a class="sister" href="/redirect?Id=f%2fKgPq4IDV0SyEq0zfYr0LirHL60gbBHH3VIishi9CqgtHAKmbGoKNvFheNkumnh" id="link3">Tillie</a>]</code></pre></p><p>Bạn có thể làm nó phức tạp hơn tùy theo mục đích của bạn. Đây là một hàm sẽ trả về True nếu một thẻ được bao quanh bởi những string object.</p><p><pre><code>from bs4 import NavigableString def surrounded_by_strings(tag):     return (isinstance(tag.next_element, NavigableString)         and isinstance(tag.previous_element, NavigableString)) for tag in soup.find_all(surrounded_by_strings): print tag.name # p # a # a # a # p</code></pre></p><p>Sau khi đã đọc hết các phần trên thì bạn đã sẵn sàng đến phần tiếp theo, chi tiết về các phương thức tìm kiếm.</p><h2>find_all()</h2><pre><code>find_all(name, attrs, recursive, string, limit, **kwargs)</code></pre><p>Phương thức find_all() sẽ duyệt qua tất cả các thẻ con và lấy tất cả các thẻ mà phù hợp với bộ lọc của bạn. Mình sẽ đưa ra một số ví dụ về các loại bộ lọc, như ví dụ dưới đây:</p><pre><code>soup.find_all("title") # [<title>The Dormouse's story] soup.find_all("p", "title") # [

The Dormouse's story

] soup.find_all("a") # [Elsie, # Lacie, # Tillie] soup.find_all(id="link2") # [Lacie] import re soup.find(string=re.compile("sisters")) # u'Once upon a time there were three little sisters; and their names were\n'

Bạn đã khá quen thuộc với một số ví dụ có trong ví dụ trên, nhưng có một số khá mới. Các giá trị truyền vào như string hay id chúng có ý nghĩa gì? Tại sao hàm find_all(“p”, “title”) sẽ tìm thẻ

cùng với CSS class “title”? Nào, ta cùng tìm hiểu về đối số của hàm find_all().

Name argument

Truyền một giá trị vào name và bạn sẽ nói cho Beautiful Soup biết là chỉ xem xét những thẻ có tên là giá trị được truyền vào name. Các text string sẽ bị bỏ qua cũng như các thẻ có tên không khớp.

Đây là cách sử dụng đơn giản nhất:

soup.find_all("title")
# [The Dormouse's story]

Xem lại phần Các loại bộ lọc, giá trị truyền vào name có thể là một chuỗi, biểu thức chính quy, list, function, hoặc True.

Keyword arguments

Bất kỳ đối số nào không có trong hàm find_all() đều sẽ được chuyển thành một dạng bộ lọc trong thuộc tính của thẻ. Nếu bạn truyền vào một giá trị cho một đối số được gọi là id, Beautiful Soup sẽ lọc các thẻ có thuộc tính id với giá trị được chỉ định:

soup.find_all(id='link2')
# [Lacie]

Nếu bạn truyền một giá trị vào href, Beautiful Soup sẽ lọc dựa theo thuộc tính href:

soup.find_all(href="/redirect?Id=VV4DCjRYHnaSds2LlPoP7D5C1bH%2bvGxljiVtPSdQHhws6gq91nZVQ8U74LNWms1V"
# [Elsie]

Bạn cũng có thể lọc các thuộc tính dựa theo một chuỗi, biểu thức chính quy, list, function, hay bằng giá trị True:

Đoạn code này sẽ tìm tất cả các thẻ mà trong nó có thuộc tính id, bất kể giá trị trong nó là gì:

soup.find_all(id=True)
# [Elsie,
# Lacie,
# Tillie]

Bạn có thể lọc nhiều thuộc tính cùng một lúc bằng cách truyền vào nhiều hơn một keyword argument:

soup.find_all(href="/redirect?Id=VV4DCjRYHnaSds2LlPoP7IYWyJrlG8f8MIWKFhNe73RQef%2b%2fpwIqwlc%2bcVx%2bmwJJ" id='link1')
# [three]


Một vài thuộc tính, như data-* trong HTML 5, có tên không thể được sử dụng như tên của keyword arguments:

data_soup = BeautifulSoup('
foo!
') data_soup.find_all(data-foo="value") # SyntaxError: keyword can't be an expression

Bạn có thể sử dụng những thuộc tính này để lọc bằng cách đưa chúng vào một dict, dict này sẽ được truyền vào đối số attrs trong hàm find_all():

data_soup.find_all(attrs={"data-foo": "value"})
# [
foo!
]

Bạn không thể sử dụng một keyword argument để tìm phần tử name trong HTML, vì Beautiful Soup sử dụng đối số name để chứa tên của các thẻ. Thay vào đó, bạn hãy đưa nó vào một dict thông qua đối số attrs:

name_soup = BeautifulSoup('')
name_soup.find_all(name="email")
# []
name_soup.find_all(attrs={"name": "email"})
# []

Tìm kiếm bằng class CSS

Thật hữu ích khi có thể tìm kiếm một tag bằng một class CSS cụ thể. Nhưng tên của thuộc tính CSS, “class”, nó trùng với từ khóa class trong Python. Sử dụng class như một keyword argument sẽ bắn ra syntax error. Từ Beautiful Soup 4.1.2, bạn có thể tìm kiếm bằng CSS class bằng cách sử dụng keyword argument class_:

soup.find_all("a", class_="sister")
# [Elsie,
# Lacie,
# Tillie]

Giống như bất kỳ keyword argument nào, bạn có thể truyền vào class_ một chuỗi, một biểu thức chính quy, hàm hoặc True:

soup.find_all(class_=re.compile("itl"))
# [

The Dormouse's story

] def has_sicharacters(css_class):     return css_class is not None and len(css_class) == 6 soup.find_all(class_=has_sicharacters) # [Elsie, # Lacie, # Tillie]

Hãy nhớ rằng, một thẻ đơn có thể có nhiều giá trị trong thuộc tính "class" của nó. Khi bạn tìm kiếm một thẻ match với CSS class chỉ định, bạn có thể match bất kỳ CSS class nào của nó.

css_soup = BeautifulSoup('

') css_soup.find_all("p", class_="strikeout") # [

] css_soup.find_all("p", class_="body") # [

]

Bạn cũng có thể tìm kiếm cách đưa y nguyên giá trị của thuộc tính class vào:

css_soup.find_all("p", class_="body strikeout")
# [

]

Nhưng tìm ngược lại thì lại không hoạt động, ở đây chuỗi strikeout body bị đảo ngược so với body strikeout, cho nên nó không thể tìm thấy:

css_soup.find_all("p", class_="strikeout body")
# []

Nếu bạn muốn tìm kiếm thẻ khớp hai hay nhiều hơn class CSS, bạn có thể dùng CSS selector:

css_soup.select("p.strikeout.body")
# [

]

Trong các phiên bản cũ hơn của Beautiful Soup, thì không có shortcut class_, bạn có thể sử dụng mẹo attrs được đề cập bên trên. Tạo một dict chứa key là "class" và value là một chuỗi (hoặc một regular expression, hay bất cứ thứ gì) bạn muốn tìm:

soup.find_all("a", attrs={"class": "sister"})
# [Elsie,
# Lacie,
# Tillie]

String argument

Với string bạn có thể tìm kiếm chuỗi thay cho thẻ. Như name và keyword arguments, bạn có thể truyền vào một chuỗi, một biểu thức chính quy, một list, một hàm, hoặc giá trị True. Dưới đây là một ví dụ:

soup.find_all(string="Elsie")
# [u'Elsie']

soup.find_all(string=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(string=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):
    """Return True if this string is the only child of its parent tag."""
    return (s == s.parent.string)

soup.find_all(string=is_the_only_string_within_a_tag)
# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

Mặc dù string là để tìm chuỗi, bạn có thể kết hợp nó với các đối số tìm kiếm khác: Beautiful Soup sẽ tìm tất cả các thẻ có .string khớp với giá trị string. Đoạn code này sẽ tìm những thẻ có .string là "Elsie":

soup.find_all("a", string="Elsie")
# [Elsie]

string là một đối số mới trong Beautiful Soup 4.4.0. Trong các phiên bản trước đó nó được gọi là text:

soup.find_all("a", text="Elsie")
# [Elsie]

Limit argument

find_all() trả về tất cả những thẻ và chuỗi khớp với bộ lọc của bạn. Điều này có thể khá tốn thời gian nếu tài liệu của bạn lớn. Nếu bạn không cần lấy hết kết quả, bạn có thể truyền thêm một số cho đối số limit. Nó hoạt động giống như keyword LIMIT trong SQL. Nó nói cho Beautiful Soup biết rằng ngừng thu thập kết quả sau khi lấy được một số lượng nhất định.

Có ba link trong tài liệu "three sisters", nhưng đoạn code này chỉ tìm thấy hai:

soup.find_all("a", limit=2)
# [Elsie,
# Lacie]

Recursive argument

Nếu bạn gọi mytag.find_all(), Beautiful Soup sẽ kiểm tra tất cả các descendant (con cháu) của mytag, con của con của nó, vân vân. Nếu bạn muốn Beautiful Soup duyệt trực tiếp phần tử con, bạn có thể truyền recursive=False, không đệ quy tiếp vào các phần tử con của phần tử con. Hãy xem sự khác biệt dưới đây:

soup.html.find_all("title")
# [The Dormouse's story]

soup.html.find_all("title", recursive=False)
# []

Đây là phần đó trong tài liệu:


  
    The Dormouse's story
  
...

Thẻ nằm bên dưới thẻ <html>, nhưng nó không trực tiếp nằm dưới <html> mà nó phải đi qua thẻ <head>. Beautiful Soup tìm thấy thẻ <title> khi nó được phép xem tất cả các descendant của thẻ <html> nhưng khi recursive=False nó sẽ giới hạn Beautiful Soup ở phần tử con trực tiếp của thẻ <html> vì thế nó sẽ không tìm thấy gì.</p><p>Beautiful Soup cung cấp nhiều phương thức tree-searching (sẽ được nói dưới dây), và hầu hết chúng đều có các đối số giống như find_all(): name, attrs, string, limit, và keyword argument. Nhưng recursive thì khác: Nó chỉ có ở hai phương thức find_all() và find(). Truyền recursive=False vào một phương thức như find_parents() sẽ không hữu ích.</p><h2 id="goi-mot-the-nhu-goi-find-all">Gọi một thẻ như gọi find_all()</h2><p>Vì find_all() là phương thức phổ biến nhất trong Beautiful Soup search API, bạn có thể sử dụng nó một cách gọn hơn bằng cách lược bỏ nó. Nếu bạn coi BeautifulSoup object hoặc Tag object như thể nó là một hàm, thì nó cũng giống như gọi find_all() trên object đó. Hai dòng code này là tương tự nhau:</p><pre><code>soup.find_all("a") soup("a")</code></pre><p>Hai dòng này cũng vậy:</p><pre><code>soup.title.find_all(string=True) soup.title(string=True)</code></pre><h2>find()</h2><pre><code>find(name, attrs, recursive, string, **kwargs)</code></pre><p>Phương thức find_all() quét toàn bộ tài liệu để tìm kiếm kết quả. Nhưng đôi khi bạn chỉ muốn tìm một kết quả. Nếu bạn biết một tài liệu chỉ có một thẻ <body>, thì thật lãng phí thời gian để quét toàn bộ tài liệu để tìm kiếm thêm. Thay vì thêm limit=1 mỗi khi gọi find_all, bạn có thể sử dụng phương thức find(). Hai dòng code này gần như giống nhau:</p><pre><code>soup.find_all('title', limit=1) # [<title>The Dormouse's story] soup.find('title') # The Dormouse's story

Chỉ khác đó là find_all() trả về một list chứa kết quả, và find() thì trả về kết quả.

Nếu find_all() không thể tìm thấy gì, nó sẽ trả về một list rỗng. Còn với find(), không tìm thấy gì thì nó trả về None:

print(soup.find("nosuchtag"))
# None

Bạn có nhớ mẹo soup.head.title từ Duyệt bằng cách sử dụng tag name? Mẹo đó hoạt động bằng cách gọi find() liên tục:

soup.head.title
# The Dormouse's story

soup.find("head").find("title")
# The Dormouse's story

find_parents() và find_parent()

find_parents(name, attrs, string, limit, **kwargs)
find_parent(name, attrs, string, **kwargs)

Mình đã dành nhiều thời gian để nói về find_all() và find(). Beautiful Soup API định nghĩa tới mười phương thức khác nhau để tìm kiếm trong cây. Nhưng đừng sợ. Năm trong số những phương thức đó cơ bản thì giống như find_all(), và năm phương thức còn lại thì tương tự find(). Chỉ khác là những phần trong cây mà chúng tìm kiếm.

Đầu tiên ta sẽ bắt đầu với find_parents() và find_parent(). Nhớ rằng cách mà find_all() và find() hoạt động là đi xuống trong một cây. Nhìn vào những các descendant (thẻ con cháu) của thẻ. Hai phương thức này thì làm ngược lại: cách mà chúng hoạt động là đi lên trong một cây, nhìn vào các phần tử cha của một thẻ hoặc một chuỗi. Ta sẽ thử xem, bắt đầu là một chuỗi nằm sâu trong tài liệu “three sisters”:

a_string = soup.find(string="Lacie")
a_string
# u'Lacie'

a_string.find_parents("a")
# [Lacie]

a_string.find_parent("p")
# 

Once upon a time there were three little sisters; and their names were # Elsie, # Lacie and # Tillie; # and they lived at the bottom of a well.

a_string.find_parents("p", class="title") # []

Một trong ba thẻ là phần tử cha trực tiếp của chuỗi được đề cập. Vì vậy ta có thể tìm thấy nó. Hoặc một trong ba thẻ

là phần tử cha gián tiếp của chuỗi này. Cho nên ta cũng tìm thấy nó luôn. Vẫn còn một thẻ

có CSS class là “title” nằm ở đâu đó trong tài liệu, nhưng nó không là phần tử cha của chuỗi này, cho nên ta không thể tìm thấy nó bằng find_parents().

Bạn có thể tạo ra kết nối giữa find_parent() và find_parents() và hai thuộc tính .parent và .parents đã được đề cập bên trên. Sự kết nối này rất mạnh. Các phương thức tìm kiếm này thực ra là sử dụng .parents để lặp qua tất cả phần tử cha, và kiểm tra từng cái một trong bộ lọc được cung cấp để xem chúng có khớp hay không.

find_next_siblings() và find_next_sibling()

find_next_siblings(name, atts, string, limit, **kwargs)
find_next_sibling(name, atts, string, **kwargs)

Phương thức này sử dụng .next_siblings để lặp qua những siblings còn lại của một phần tử trong một cây. Phương thức find_next_siblings() trả về tất cả những siblings phù hợp, và find_next_sibling() chỉ trả về một kết quả:

first_link = soup.a
first_link
# Elsie

first_link.find_next_siblings("a")
# [Lacie,
# Tillie]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_next_sibling("p")
# 

...

find_previous_siblings() và find_previous_sibling()

find_previous_siblings(name, atts, string, limit, **kwargs)
find_previous_sibling(name, atts, string, **kwargs)

Hai phương thức này sử dụng .previous_siblings để lặp qua các siblings của phần tử đó ở trước nó trong cây. Phương thức find_previous_siblings() trả về một list chứa tất cả các siblings phù hợp, và find_previous_sibling() chỉ trả về một kết quả:

last_link = soup.find("a", id="link3")
last_link
# Tillie

last_link.find_previous_siblings("a")
# [Lacie,
# Elsie]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# 

The Dormouse's story

find_all_next() và find_next()

find_all_next(name, attrs, string, limit, **kwargs)
find_next(name, attrs, string, **kwargs)

Hai phương thức này sử dụng .next_element để lặp qua bất cứ thẻ hay chuỗi nào ở phía sau nó trong tài liệu. Phương thức find_all_next() trả về một list chứa tất cả các kết quả phù hợp, còn find_next() chỉ trả về một kết quả:

first_link = soup.a
first_link
# Elsie

first_link.find_all_next(string=True)
# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")
# 

...

Trong ví dụ đầu tiên, chuỗi “Elsie” xuất hiện, mặc dù ta bắt đầu từ thẻ có chứa nó. Trong ví dụ thứ hai, thẻ

cuối cùng trong tài liệu xuất hiện, mặc dù nó không nằm trong cùng một phần trong cây với thẻ nơi mà ta bắt đầu lặp. Điều ta cần quan tâm khi sử dụng những phương thức này là phần tử khớp với bộ lọc và hiển thị sau so với phần tử bắt đầu trong tài liệu.

find_all_previous() và find_previous()

find_all_previous(name, attrs, string, limit, **kwargs)
find_previous(name, attrs, string, **kwargs)

Hai phương thức này sử dụng .previous_elements để lặp qua nhưng thẻ và chuỗi đứng trước nó trong tài liệu. Phương thức find_all_previous() trả về một list chứa tất cả các kết quả trùng khớp, còn find_previous() thì trả về một kết quả:

first_link = soup.a
first_link
# Elsie

first_link.find_all_previous("p")
# [

Once upon a time there were three little sisters; ...

, #

The Dormouse's story

] first_link.find_previous("title") # The Dormouse's story

Việc gọi find_all_previous("p") tìm thấy đoạn đầu tiên trong tài liệu (đoạn có class=”title”), nhưng nó tìm thấy tới hai đoạn, thẻ

có chứa thẻ nơi mà chúng ta bắt đầu. Điều này không quá bất ngờ. Ta xem xét tất cả thẻ xuất hiện sớm hơn trong tài liệu so với vị trí mà ta bắt đầu. Thẻ

chứa thẻ phải xuất hiện trước thẻ mà nó chứa.

CSS selectors

Trong phiên bản 4.7.0, Beautiful Soup hỗ trợ hầu hết các CSS4 selectors thông qua dự án SoupSieve. Nếu bạn đã cài đặt Beautiful Soup thông qua pip, thì SoupSieve đã được cài đặt cùng lúc đó, vì thế bạn không cần phải làm gì thêm.

BeautifulSoup có phương thức .select() sử dụng SoupSieve để chạy một CSS selector lên tài liệu đã được parse và trả về tất cả các phần tử match. Tag có một phương thức tương tự chạy một CSS selector lên contents của một thẻ.

Tài liệu SoupSieve liệt kê tất cả các CSS selectors hiện tại được hỗ trợ, nhưng ở đây ta chỉ đề cập tới những CSS selectors cơ bản:

Bạn có thể tìm kiếm các tag:

soup.select("title")
# [The Dormouse's story]

soup.select("p:nth-of-type(3)")
# [

...

]

Tìm các thẻ bên dưới các thẻ khác:

soup.select("body a")
# [Elsie,
# Lacie,
# Tillie]

soup.select("html head title")
# [The Dormouse's story]

Tìm những tag con trực tiếp của tag khác:

soup.select("head > title")
# [The Dormouse's story]

soup.select("p > a")
# [Elsie,
# Lacie,
# Tillie]

soup.select("p > a:nth-of-type(2)")
# [Lacie]

soup.select("p > #link1")
# [Elsie]

soup.select("body > a")
# []

Tìm các siblings của tag:

soup.select("#link1 ~ .sister")
# [Lacie,
# Tillie]

soup.select("#link1 + .sister")
# [Lacie]

Tìm thẻ bằng CSS class:

soup.select(".sister")
# [Elsie,
# Lacie,
# Tillie]

soup.select("[class~=sister]")
# [Elsie,
# Lacie,
# Tillie]

Tìm thẻ bằng CSS ID:

soup.select("#link1")
# [Elsie]

soup.select("a#link2")
# [Lacie]

Tìm thẻ match với bất kỳ selector nào nằm trong danh sách các selectors:

soup.select("#link1,#link2")
# [Elsie,
# Lacie]

Kiểm tra sự tồn tại của một thuộc tính:

soup.select('a[href]')
# [Elsie,
# Lacie,
# Tillie]

Tìm thẻ bằng giá trị của thuộc tính:

soup.select('a[href="/redirect?Id=f%2fKgPq4IDV0SyEq0zfYr0L1x0DM4mpSt97%2ftYgbxlC3lW0cWzhMK4ll9WwwLq4Ce"
# [Elsie]

soup.select('a[href^="http://example.com/"]')
# [Elsie,
# Lacie,
# Tillie]

soup.select('a[href$="tillie"]')
# [Tillie]

soup.select('a[href*=".com/el"]')
# [Elsie]

Ngoài ra cũng có phương thức select_one() lấy ra thẻ đầu tiên match với selector:

soup.select_one(".sister")
# Elsie

Nếu bạn parse XML định nghĩa namespaces, bạn có thể sử dụng chúng trong CSS selectors:

from bs4 import BeautifulSoup
xml = """
    I'm in namespace 1
    I'm in namespace 2
 """
soup = BeautifulSoup(xml, "xml")

soup.select("child")
# [I'm in namespace 1, I'm in namespace 2]

soup.select("ns1|child", namespaces=namespaces)
# [I'm in namespace 1]

Khi xử lý CSS selector sử dụng namespaces, Beautiful Soup sử dụng các từ viết tắt namespace nó đã tìm thấy khi parse tài liệu. Bạn có thể ghi đè chúng bằng cách truyền vào một dict chứa các từ viết tắt của riêng bạn:

namespaces = dict(first="http://namespace1/", second="http://namespace2/")
soup.select("second|child", namespaces=namespaces)
# [I'm in namespace 2]

Công cụ CSS selector là một công cụ hữu ích cho những ai đã biết về cú pháp của CSS selector. Bạn có thể làm tất cả điều này với Beautiful Soup API. Và nếu CSS selector là tất cả những gì bạn cần, bạn nên parse tài liệu bằng lxml: Nó nhanh hơn rất nhiều. Nó cho phép bạn kết hợp CSS selectors với Beautiful Soup API.

Chỉnh sửa cây

Sức mạnh chính của Beautiful Soup là tìm kiếm trong parse tree, nhưng ngoài ra bạn cũng có thể chỉnh sửa tree và ghi lại những thay đổi của bạn thành một tài liệu HTML/XML mới.

Thay đổi tên thẻ và thuộc tính

Mình đã đề cập cái này trước đó, trong phần Attributes, nhưng ta sẽ ôn lại một chút, thay đổi giá trị của các thuộc tính, thêm thuộc tính và xóa thuộc tính:

soup = BeautifulSoup('Extremely bold')
tag = soup.b

tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# 
Extremely bold
del tag['class'] del tag['id'] tag #
Extremely bold

Chỉnh sửa .string

Nếu bạn đặt giá trị cho thuộc tính .string của thẻ, nội dung của thẻ đó sẽ được thay thế bằng giá trị bạn truyền vào:

markup = 'example.com'
soup = BeautifulSoup(markup)

tag = soup.a
tag.string = "New link text."
tag
# 

Cẩn thận: Nếu một thẻ chứa một thẻ khác, chúng và tất cả những nội dung bên trong sẽ bị hủy.

.append()

Bạn có thể thêm nội dung cho một thẻ bằng Tag.append(). Nó hoạt động giống như .append() với list trong Python:

soup = BeautifulSoup("Foo")
soup.a.append("Bar")

soup
# FooBar
soup.a.contents
# [u'Foo', u'Bar']

.extend()

Bắt đầu từ Beautiful Soup 4.7.0, Tag có thêm một phương thức được gọi là .extend(), nó hoạt động giống như .extend() với list trong Python:

soup = BeautifulSoup("Soup")
soup.a.extend(["'s", " ", "on"])

soup
# Soup's on
soup.a.contents
# [u'Soup', u''s', u' ', u'on']

Nếu bạn cần thêm một chuỗi vào tài liệu, không thành vấn đề - bạn chỉ cần truyền một chuỗi vào trong append(), hoặc bạn có thể gọi NavigableString constructor:

soup = BeautifulSoup("")
tag = soup.b
tag.append("Hello")
new_string = NavigableString(" there")
tag.append(new_string)
tag
# Hello there.
tag.contents
# [u'Hello', u' there']

Nếu bạn muốn tạo một comment hoặc vài lớp con khác của NavigableString, chỉ cần gọi constructor:

from bs4 import Comment
new_comment = Comment("Nice to see you.")
tag.append(new_comment)
tag
# Hello there
tag.contents
# [u'Hello', u' there', u'Nice to see you.']

(Đây là một chức năng mới trong Beautiful Soup 4.4.0)

Còn nếu bạn muốn tạo một thẻ mới thì sao? Cách tốt nhất là gọi BeautifulSoup.new_tag():

soup = BeautifulSoup("")
original_tag = soup.b

new_tag = soup.new_tag("a", href="/redirect?Id=6UNFqWfJ6w%2fJl4oWIm1IFaq5Qh%2buJOMoNmtk93G2mLtWA3qLMNPV%2fQLdOE8kXVGC"
original_tag.append(new_tag)
original_tag
# 

Chỉ đối số đầu tiên, tên thẻ yêu cầu phải có.

insert()

Tag.insert cũng giống như Tag.append(), chỉ khác là phần tử mới không nhất thiết phải ở cuối của .contents. Nó sẽ được chèn vào bất cứ vị trí nào mà bạn gọi. Nó giống như .insert trong list Python:

markup = 'example.com'
soup = BeautifulSoup(markup)
tag = soup.a

tag.insert(1, "but did not endorse ")
tag
# example.com
tag.contents
# [u'I linked to ', u'but did not endorse', example.com]

insert_before() and insert_after()

Phương thức insert_before() chèn thẻ hoặc chuỗi ngay trước bất cứ thứ gì khác trong parse tree:

soup = BeautifulSoup("stop")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# Don'tstop


Phương thức insert_after() chèn thẻ hoặc chuỗi ngay sau bất cứ thứ gì khác trong parse tree:

div = soup.new_tag('div')
div.string = 'ever'
soup.b.i.insert_after(" you ", div)
soup.b
# Don't you 
ever
stop
soup.b.contents # [Don't, u' you',
ever
, u'stop']

clear()

Tag.clear() loại bỏ nội dung của một thẻ:

markup = 'example.com'
soup = BeautifulSoup(markup)
tag = soup.a

tag.clear()
tag
# 

extract()

PageElement.extract() loại bỏ một thẻ hay một chuỗi ra khỏi tree. Nó trả về thẻ hay chuỗi đã bị loại bỏ:

markup = 'example.com'
soup = BeautifulSoup(markup)
a_tag = soup.a

i_tag = soup.i.extract()

a_tag
# 

i_tag
# example.com

print(i_tag.parent)
None

Tại lúc này bạn có hẳn cho mình hai parse tree, một là từ BeautifulSoup bạn parse từ markup và còn lại là thẻ mà bạn đã extract ra trước đó. Bạn có thể gọi extract lên phần tử con mà bạn đã extract trước đó:

my_string = i_tag.string.extract()
my_string
# u'example.com'

print(my_string.parent)
# None
i_tag
# 

decompose()

Tag.decompose() loại bỏ một thẻ khỏi cây, sau đó phá hủy hoàn toàn thẻ đó và nội dung của nó:

markup = 'example.com'
soup = BeautifulSoup(markup)
a_tag = soup.a

soup.i.decompose()

a_tag
# 

replace_with()

PageElement.replace_with() loại bỏ một thẻ hoặc chuỗi khỏi cây, và thay thế nó bằng thẻ hay chuỗi khác:

markup = 'example.com'
soup = BeautifulSoup(markup)
a_tag = soup.a

new_tag = soup.new_tag("b")
new_tag.string = "example.net"
a_tag.i.replace_with(new_tag)

a_tag
# example.net

replace_with() trả về thẻ hoặc chuỗi được thay thế, nhằm mục đích là để bạn có thể kiểm tra nó và thêm nó trở lại vào một phần khác của cây.

wrap()

PageElement.wrap() sẽ bao bọc một phần tử trong một thẻ mà bạn chỉ định. Nó trả về thẻ bao bọc đó:

soup = BeautifulSoup("

I wish I was bold.

") soup.p.string.wrap(soup.new_tag("b")) # I wish I was bold. soup.p.wrap(soup.new_tag("div") #

I wish I was bold.

Phương thức này xuất hiện trong phiên bản 4.0.5

unwrap()

Tag.unwrap() thì ngược lại với wrap(). Nó thay thế một thẻ bằng bất cứ thứ gì có trong thẻ đó. Hữu ích trong việc loại bỏ markup:

markup = 'example.com'
soup = BeautifulSoup(markup)
a_tag = soup.a

a_tag.i.unwrap()
a_tag
# 

Giống như replace_with(), unwrap() trả về thẻ bị thay thế.

Output

Xuất định dạng đúng chuẩn

Phương thức pretty() sẽ trả về một Beautiful Soup parse tree trong một chuỗi Unicode được format đúng chuẩn tài liệu HTML/XML, mỗi thẻ hay chuỗi HTML/XML nằm trên một dòng và được tab vào trong:

markup = 'example.com'
soup = BeautifulSoup(markup)
soup.prettify()
# '\n \n \n \n  
#  
#  
#  
#   
#     example.com
#    
#   
#  
# 

Bạn có thể gọi pretty() trên top-level BeautifulSoup object, hoặc trên bất kì Tag object của nó:

print(soup.a.prettify())
# 
#     example.com
#   
# 

Không cần đúng chuẩn

Nếu bạn chỉ muốn một chuỗi không cần định dạng đúng chuẩn, bạn có thể gọi unicode() hoặc str() trên một BeautifulSoup object, hoặc một Tag bên trong nó:

str(soup)
# 'example.com'

unicode(soup.a)
# u'example.com'

Hàm str() trả về một chuỗi được mã hóa UTF-8.

Bạn cũng có thẻ gọi encode() để nhận được một bytestring và decode() để nhận lại Unicode.

Định dạng đầu ra

Nếu bạn gọi BeautifulSoup() trên một tài liệu có chứa các HTML entity giống như này: “&lquot;”, chúng sẽ được chuyển đổi sang ký tự Unicode:

soup = BeautifulSoup("“Dammit!” he said.")
unicode(soup)
# u'\u201cDammit!\u201d he said.'

Nếu sau đó bạn muốn chuyển đổi tài liệu đó thành một chuỗi, các ký tự Unicode sẽ được mã hóa thành UTF-8. Bạn sẽ không thể nhận lại những ký tự HTML đó nữa:

str(soup)
# '\xe2\x80\x9cDammit!\xe2\x80\x9d he said.'

Mặc định, những kí tự duy nhất được escape (nghiên cứu escaped sequence sẽ hiểu cái này) đó là ký tự & và cặp kí tự < và >. Chúng sẽ được chuyển thành “&”, “<”, và “>”, thì thế Beatiful Soup không vô tình tạo ra HTML hay XML không hợp lệ:

soup = BeautifulSoup("

The law firm of Dewey, Cheatem, & Howe

") soup.p #

The law firm of Dewey, Cheatem, & Howe

soup = BeautifulSoup('') soup.a #

Bạn có thể thay đổi hành vi này bằng cách cung cấp giá trị cho đối số formatter trong các hàm prettify(), encode(), hoặc decode(). Beautiful Soup cung cấp cho đối số formatter 6 giá trị. Mặc định là formatter=”minimal”. Các chuỗi sẽ được xử lý đủ để đảm bảo rằng Beautiful Soup tạo ra HTML/XML hợp lệ:

french = "

Il a dit <<Sacré bleu!>>

" soup = BeautifulSoup(french) print(soup.prettify(formatter="minimal")) # #   #    

#     Il a dit <<Sacré bleu!>> #    

#   #

Nếu bạn truyền vào formatter="html" , Beautiful Soup sẽ chuyển chuỗi Unicode thành các ký tự HTML bất cứ khi nào có thể:

print(soup.prettify(formatter="html"))
# 
#   
#     

#     Il a dit <<Sacré bleu!>> #    

#   #


Nếu bạn truyền vào formatter="html5", nó giống như formatter=”html”, như Beautiful Soup sẽ bỏ qua dấu gạch chéo trong các void tag như
, :

soup = BeautifulSoup("
") print(soup.encode(formatter="html")) #
print(soup.encode(formatter="html5")) #

Nếu bạn truyền vào formatter=None, Beautiful Soup sẽ không sửa đổi chuỗi nào khi output. Đây là tùy chọn nhanh nhất, nhưng nó có thể khiến Beautiful Soup tạo HTML/XML không hợp lệ, như ví dụ sau:

print(soup.prettify(formatter=None))
# 
#   
#     

#     Il a dit <> #    

#   # link_soup = BeautifulSoup('
') print(link_soup.a.encode(formatter=None)) #


Nếu bạn muốn kiểm soát sâu hơn output của bạn, bạn có thể sử dụng class Formatter của Beautiful Soup. Ở đây Formatter chuyển chuyển thành in hoa cho dù chúng xuất hiện trong một nút văn bản hoặc trong một giá trị thuộc tính:

Cuối cùng, nếu bạn truyền một hàm vào forrmater, Beautiful Soup sẽ gọi hàm đó một lần cho mỗi chuỗi và giá trị của thuộc tính trong tài liệu. Bạn có thể làm bất cứ thứ gì bạn muốn trong hàm này. Ở ví dụ bên dưới, hàm không làm gì khác ngoài chuyển chuỗi thành chữ in hoa:

from bs4.formatter import HTMLFormatter
def uppercase(str):
    return str.upper()
formatter = HTMLFormatter(uppercase)

print(soup.prettify(formatter=formatter))
# 
#  
#   

#    IL A DIT <> #  

#  # print(link_soup.a.prettify(formatter=formatter)) #


Subclassing HTMLFormatter hoặc XMLFormatter sẽ cung cấp cho bạn quyền kiểm soát sâu hơn đầu ra. Ví dụ, mặc định Beautiful Soup sắp xếp các thuộc tính trong mỗi thẻ:

attr_soup = BeautifulSoup(b'

') print(attr_soup.p.encode()) #

Để tắt tính năng này, bạn có thể định nghĩa lại phương thức Formatter.attribute(), kiểm soát thuộc tính nào là đầu ra và theo thứ tự nào. Việc này cũng lọc ra thuộc tính m bất cứ khi nào nó xuất hiện:

class UnsortedAttributes(HTMLFormatter):
    def attributes(self, tag):
        for k, v in tag.attrs.items():
            if k == 'm':
                continue
            yield k, v
print(attr_soup.p.encode(formatter=UnsortedAttributes()))
# 

Cuối cùng: Nếu bạn tạo một Cdata object, text bên trong Object đó luôn trình bày chính xác như nó xuất hiện, không có định dạng. Beautiful Soup sẽ gọi formatter method, chỉ trong trường hợp bạn viết một phương thức đếm tất cả các string có trong tài liệu hay một cái gì đó. Nhưng nó sẽ bỏ qua giá trị trả về:

from bs4.element import CData
soup = BeautifulSoup("")
soup.a.string = CData("one < three")
print(soup.a.prettify(formatter="xml"))
# 
# 
# 

get_text()

Nếu bạn chỉ muốn lấy phần text có trong tài liệu hoặc trong một thẻ, bạn có thể sử dụng get_text() method. Nó trả về một chuỗi Unicode là tất cả text trong một tài liệu hoặc bên dưới một thẻ:

markup = 'example.com\n'
soup = BeautifulSoup(markup)

soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'


Bạn có thể chỉ định chuỗi để nối các bit văn bản lại với nhau:

soup.get_text("|")
# u'\nI linked to |example.com|\n'

Bạn có thể nói cho Beautiful Soup cắt khoảng trắng ở nơi bắt đầu và kết thúc của mỗi bit văn bản:

soup.get_text("|", strip=True)
# u'I linked to|example.com'

Nhưng lúc đó bạn có thể sử dụng .stripped_strings generator thay thế, và sau đó bạn có thể tự xử lý:

[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']

Chỉ định parser

Nếu bạn chỉ cần parse một vài HTML, bạn có thể truyền nó vào hàm BeautifulSoup(), vậy là được rồi. Beautiful Soup sẽ chọn một parser cho bạn và parse data. Nhưng có một vài optional argument bạn có thể truyền vào trong constructor này để thay đổi parser mà bạn muốn sử dụng.

Argument đầu tiên truyền vào BeautifulSoup() là một chuỗi hoặc một filehandle đang được mở - là markup mà bạn muốn parse. Argument thứ hai là parser mà bạn muốn dùng để parse markup.

Nếu bạn không chỉ định parser nào. Nó sẽ dùng parser HTML tốt nhất được cài đặt trên máy của bạn. Beautiful Soup xếp hạng parser lxml là parser tốt nhất, sau đó là html5lib, sau đó là parse có sẵn của Python. Bạn có thể ghi đè bằng cách chỉ định một trong những parser dưới đây:

  • Markup mà bạn muốn parse. Hiện tại hỗ trợ 'html", "xml" và "html5".

  • Tên parser mà bạn muốn sử dụng. Hiện tại hỗ trợ "lxml", "html5lib", và "html.parser" (parser HTML có sẵn của Python).
     

Bạn có thể xem lại phần cài đặt parser.

Nếu bạn chưa cài đặt parser thích hợp, Beautiful Soup sẽ bỏ qua yêu cầu của bạn và chọn một parser khác. Hiện tại, parser XML được hỗ trợ duy nhất là lxml cho nên nếu bạn muốn parse XML thì phải cài lxml, không thì nó sẽ không làm hoạt động.

Sự khác nhau giữa các parser

Beautiful Soup có cùng một interface cho từng parser khác nhau, sự khác nhau là nằm ở parser. Sư khác nhau này sẽ tạo ra sự khác nhau giữa các parse tree được tạo ra bởi parser với cùng một tài liệu. Khác nhau nhất là giữa HTML parsers và XML parsers. Ở đây ta có một tài liệu HTML ngắn đã được parse:

BeautifulSoup("")
# 

Ta thấy một thẻ là một thẻ HTML không hợp lệ, nên parser biến nó thành cặp thẻ .

Cùng là một tài liệu, nhưng ta sẽ xem nó như XML và parse nó bằng lxml (yêu cầu cài đặt lxml). Lưu ý rằng thẻ này được để đứng một mình và tài liệu được cung cấp thêm một khai báo XML thay vì đưa vào trong thẻ :

BeautifulSoup("", "xml")
# 
# 

Cũng có sự khác biệt giữa các HTML parser với nhau. Nếu bạn đưa vào Beautiful Soup một tài liệu HTML hoàn hảo, không có lỗi cú pháp. Những khác biệt này sẽ không là vấn đề. Chúng sẽ vẫn trả về một parse tree giống hệt tài liệu gốc.

Nhưng sẽ ra sao nếu tài liệu của bạn như ví dụ trên, các parse khác nhau sẽ cho ra những kết quả khác nhau. Dưới đây là một tài liệu ngắn, không hợp lệ được parse bằng lxml. Lưu ý rằng thẻ

đứng một mình sẽ bị bỏ qua:

BeautifulSoup("

", "lxml") #

Với cùng một tài liệu ta sẽ parse nó bằng html5lib:

BeautifulSoup("

", "html5lib") #

Thay vì bỏ qua thẻ

, html5lib sẽ cho nó đứng cùng với một thẻ

mở nữa. Parser này thêm một cặp thẻ trống vào tài liệu.

Cũng là tài liệu trên, ta phân tích nó bằng parser có sẵn của Python:

BeautifulSoup("

", "html.parser") #

Như lxml, parser này bỏ qua thẻ đóng

. Không như html5lib, parser này không cố gắng tạo thêm thẻ để tạo thành một tài liệu HTML hoàn chỉnh. Không giống như lxml, thậm chí nó không cần thêm thẻ .

Encoding

Bất kì tài liệu HTML hay XML nào được viết dưới một bộ mã hóa chỉ định như ASCII hoặc UTF-8. Nhưng khi bạn cho tài liệu đó vào Beautiful Soup, nó sẽ được chuyển sang Unicode:

markup = "

Sacr\xc3\xa9 bleu!

" soup = BeautifulSoup(markup) soup.h2 #

Sacré bleu!

soup.h2.string # u'Sacr\xe9 bleu!'


Đó không phải là magic (nếu có thì ngầu vãi lồn). Beautiful Soup sử dụng một thư viện con có tên "Unicode, Dammit" để đoán bộ mã của tài liệu và chuyển đổi nó thành Unicode. Bộ mã của tài liệu được chứa trong thuộc tính .original_encoding của BeautifulSoup object:

soup.original_encoding
'utf-8'

"Unicode, Dammit" đoán đúng hầu hết các trường hợp, nhưng đôi lúc nó cũng có sai sót. Đôi khi nó đoán chính xác nhưng chỉ khi nó tìm kiếm theo từng byte của tài liệu, điều đó sẽ rất tốn thời gian. Nếu bạn biết được tài liệu nằm trong bộ mã nào trước lúc đó, bạn có thể tránh sai sót và tốn thời gian bằng cách truyền nó vào đối số from_encoding trong BeautifulSoup constructor.

Đây là một tài liệu được mã hóa ISO-8895-8. Tài liệu này quá ngắn, "Unicode, Dammit" không thể khóa nó và xác định ngầm là nó được mã hóa ISO-8895-7:

markup = b"

\xed\xe5\xec\xf9

" soup = BeautifulSoup(markup) soup.h2

νεμω

soup.original_encoding 'ISO-8859-7'


Ta có thể sửa nó bằng cách truyền giá trị đúng vào from_encoding:

soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h2

םולש

soup.original_encoding 'iso8859-8'

Nếu bạn không biết chính xác bảng mã của nó, nhưng bạn biết “Unicode, Dammit” đoán sai, bạn có thể truyền dự đoán sai vào exclude_encodings:

soup = BeautifulSoup(markup, exclude_encodings=["ISO-8859-7"])
soup.h2

םולש

soup.original_encoding 'WINDOWS-1255'


Windows-1255 không đúng 100%, nhưng mã hóa này là một compatible superset của ISO-8859-8, nó đủ gần với ISO-8859-8. (exclude_encodings là một tính năng mới của Beautiful Soup 4.4.0)

Trong một vài trường hợp hi hữu (thường là khi một tài liệu mã hóa UTF-8 chứa text được viết trong một bộ mã hoàn toàn khác), cách duy nhất để có được Unicode là thay thế một số kí tự bằng ký tự Unicode đặc biệt “REPLACEMENT CHARACTER” (U+FFFD, �). Nếu Unicode, Dammit cần làm điều này, nó sẽ đặt thuộc tính .contains_replacement_characters bằng True trên UnicodeDammit hoặc BeautifulSoup Object. Điều này cho bạn biết rằng bản trình bày Unicode không phải là bản gốc mà là bản gốc đã bị mất một vài dữ liệu. Nếu một tài liệu chứa �, nhưng .contains_replacement_characters bằng False, bạn sẽ tự hiểu rằng kí tự � là một dữ liệu gốc (như trong đoạn này) và không có nghĩa là thiếu dữ liệu.

Output encoding

Khi bạn in ra một tài liệu từ Beautiful Soup, bạn nhận được một tài liệu được mã hóa UTF-8, ngay cả khi tài liệu đó không bắt đầu UTF-8. Tài liệu dưới đây được viết bằng bộ mã Latin-1:

markup = b'''

  
  
  
  
  

Sacr\xe9 bleu!

  ''' soup = BeautifulSoup(markup) print(soup.prettify()) # #  #   #  #  #  

#    Sacré bleu! #  

#  #

Chú ý: Thẻ được viết lại để nói rằng tài liệu đang ở UTF-8.

Nếu bạn không muốn UTF-8, bạn có thể truyền kiểu mã hóa vào prettify():

print(soup.prettify("latin-1"))
# 
#   
#     
# ...


Bạn cũng có thể gọi encode() lên BeautifulSoup object, hoặc bất cứ phần tử nào trong soup, giống như một string thông thường:

soup.p.encode("latin-1")
# '

Sacr\xe9 bleu!

' soup.p.encode("utf-8") # '

Sacr\xc3\xa9 bleu!

'

Bất kỳ kí tự nào không thể được hiển thị dưới dạng mã hóa bạn chọn sẽ được chuyển đổi thành dạng tham chiếu thực thể số XML (numberic XML entrity). Tài liệu dưới đây bao gồm ký tự Unicode SNOWMAN:

markup = u"\N{SNOWMAN}"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b

Kí tự SNOWMAN có thể là một phần của một tài liệu UTF-8 (nó trông giống như này ☃), nhưng không có đại diện nào cho ký tự đó ở ISO-Latin-1 hay ASCII, vì thế nó sẽ được chuyển đổi thành ☃ cho các bảng mã đó:

print(tag.encode("utf-8"))
# 

print tag.encode("latin-1")
# 

print tag.encode("ascii")
# 

Unicode, Dammit

Bạn có thể sử dụng Unicode, Dammit mà không cần sử dụng Beautiful Soup. Nó hữu ích bất cứ khi nào bạn có dữ liệu không rõ dạng mã hóa và bạn chỉ muốn nó thành Unicode:

from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'

Dự đoán của Unicode, Dammit sẽ chính xác hơn rất nhiều nếu bạn cài đặt thư viện chardet hoặc cchardet. Bạn cung cấp càng nhiều dữ liệu thì Unicode, Dammit sẽ đoán chính xác hơn. Nếu bạn nghi ngờ về dạng mã hóa, bạn có thể truyền chúng vào dưới dạng một list:

dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'

Unicode, Dammit có hai phương thức đặc biệt mà BeautifulSoup không có.

Smart quotes

Bạn có thể sử dụng Unicode, Dammit để chuyển đổi Microsoft smart quotes (“”) thành HTML hoặc XML entity:

markup = b"

I just \x93love\x94 Microsoft Word\x92s smart quotes

" UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup # u'

I just “love” Microsoft Word’s smart quotes

' UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup # u'

I just “love” Microsoft Word’s smart quotes

'

Bạn cũng có thể chuyển đổi Microsoft smart quotes thành ASCII quotes:

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'

I just "love" Microsoft Word\'s smart quotes

'

Hy vọng bạn sẽ thấy tính năng này hữu ích, nhưng Beautiful Soup không sử dụng nó. Beautiful Soup ưu tiên sử dụng các phương thức mặc định để chuyển từ Microsoft smart quotes thành Unicode cùng với những thứ khác:

UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'

I just \u201clove\u201d Microsoft Word\u2019s smart quotes

'

Xung đột encodings

Đôi lúc một tài liệu hầu hết là mã hóa UTF-8, nhưng lại chứa những kí tự được mã hóa Windows-1252, một lần nữa là Microsoft smart quotes. Điều này xảy ra khi một website thu thập dữ liệu từ nhiều nguồn. Bạn có thể sử dụng UnicodeDammit.detwingle() để biến một tài liệu như vậy thành UTF-8 thuần túy. Đây là một ví dụ đơn giản:

snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")

Tài liệu này là một mớ hỗn độn. Snowmen là mã hóa UTF-8 còn quotes là mã hóa Windows-1252. Bạn có thể hiển thị snowmen hoặc quotes, nhưng không thể cùng lúc cả hai:

print(doc)
# ☃☃☃'I like snowmen!'

print(doc.decode("windows-1252"))
# ⠃⠃⠃"I like snowmen!"

Decode tài liệu thành mã hóa UTF-8 thì sẽ nâng lên lỗi UnicodeDecodeError, còn decode nó sang Windows-1252 thì lại cho bạn một câu vô nghĩa. May mắn thay, UnicodeDammit.detwingle() sẽ chuyển đổi chuỗi thành UTF-8 thuần túy, cho phép bạn decode nó thành Unicode và hiển thị đồng thời snowmen và dấu ngoặc kép:

new_doc = UnicodeDammit.detwingle(doc)

print(new_doc.decode("utf8"))

# ☃☃☃“I like snowmen!”

UnicodeDammit.detwingle() chỉ biết cách xử lý Windows-1252 được nhúng trong UTF-8 (hoặc ngược lại, mình cho là vậy), nhưng đây là trường hợp phổ biến nhất.

Lưu ý rằng bạn phải gọi UnicodeDammit.detwingle() trên dữ liệu của bạn trước khi truyền nó vào BeautifulSoup hay UnicodeDammit. BeautifulSoup giả định rằng một tài liệu có một mã hóa duy nhất, bất kể nó là gì. Nếu bạn truyền cho nó một tài liệu có chứa cả UTF-8 và Windows-1252, có thể bạn sẽ nghĩ toàn bộ tài liệu là Windows-1252 và tài liệu sẽ xuất hiện như thế ☃☃☃“I like snowmen!”.

UnicodeDammit.detwingle() có từ Beautiful Soup 4.1.0

So sánh bằng giữa hai object

Beautiful Soup gọi hai đối tượng NavigableString và Tag là hai đối tượng bằng nhau khi chúng đại diện cho cùng một HTML hoặc XML markup. Trong ví dụ này, hai thẻ được coi là ngang bằng, mặc dù chúng ở những phần khác nhau trong một cây, vì cả hai đều là “pizza”:

markup = "

I want pizza and more pizza!

" soup = BeautifulSoup(markup, 'html.parser') first_b, second_b = soup.find_all('b') print first_b == second_b # True print first_b.previous_element == second_b.previous_element # False

Nếu bạn muốn xem liệu hai biến tham chiếu có chính xác cùng một đối tượng hay không, sử dụng toán tử is:

print first_b is second_b
# False

Sao chép các BeauifulsSoup object

Bạn có thể sử dụng copy.copy() để tạo một copy của bất cứ Tag hay NavigableString nào:

import copy
p_copy = copy.copy(soup.p)
print p_copy
# 

I want pizza and more pizza!

Bản copy giống với bản gốc, vì nó đại diện cho cùng một markup như bản gốc, nhưng nó không cùng tham chiếu tới một đối tượng:

print soup.p == p_copy
# True

print soup.p is p_copy
# False

Sự khác biệt duy nhất là bản copy được tách hoàn toàn ra khỏi Beautiful Soup object tree ban đầu, cũng như ta gọi extract() lên nó vậy:

print p_copy.parent
# None

Điều này là vì hai Tag object khác nhau không thể chiếm cùng một vùng nhớ trong một lúc.

Chỉ parse một phần của tài liệu

Giả sử bạn muốn tìm tất cả thẻ có trong tài liệu bằng cách sử dụng Beautiful Soup. Nó sẽ rất tốn thời gian và lãng phí bộ nhớ để phân tích toàn bộ tài liệu sau đó tiếp tục việc tìm kiếm các thẻ . Sẽ nhanh hơn nếu ta bỏ qua mọi thứ không phải là thẻ trong lần parse đầu tiên. Lớp SoupStrainer cho phép bạn chọn phần nào trong tài liệu để có thể parse. Bạn chỉ cần tạo một SoupStrainer và truyền vào trong đối số parse_only một BeautifulSoup constructor.

(Chú ý rằng tính năng này không hoạt động nếu bạn sử dụng html5lib parser. Nếu bạn sử dụng html5lib parser, thì toàn bộ tài liệu của bạn sẽ được parse bất kể thứ gì. Điều này là do html5lib liên tục sắp xếp lại parse tree khi nó hoạt động và nếu một số phần trong tài liệu không thực sự biến nó thành parse tree, nó sẽ sập. Để tránh nhầm lẫn, trong các ví dụ dưới đây, mình sẽ chỉ sử dụng parser tích hợp sẵn của Python.)

SoupStrainer

Lớp SoupStrainer có những params giống như các method ở phần Tìm kiếm trong cây: name, attrs, string, và **kwargs. Dưới đây là ba SoupStrainer object:

from bs4 import SoupStrainer

only_a_tags = SoupStrainer("a")
only_tags_with_id_link2 = SoupStrainer(id="link2")

def is_short_string(string):
    return len(string) < 10

only_short_strings = SoupStrainer(string=is_short_string)

Ta sẽ sử dụng lại tài liệu “three sisters” một lần nữa, và chúng ta sẽ thấy tài liệu nó trông ra sao khi được parse bằng ba SoupStrainer object:

html_doc = """
The Dormouse's story

The Dormouse's story

Once upon a time there were three little sisters; and their names were Elsie, Lacie and Tillie; and they lived at the bottom of a well.

...

""" print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify()) # # Elsie # # # Lacie # # # Tillie # print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify()) # # Lacie # print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify()) # Elsie # , # Lacie # and # Tillie # ... #

Bạn có thể truyền một SoupStrainer vào trong bất kì phương thức nào được giới thiệu trong phần Tìm kiếm trong cây.

soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u'\n\n', u'...', u'\n']

Giải quyết lỗi

diagnose()

Nếu bạn gặp khó khăn trong việc hiểu Beautiful Soup đã làm gì với tài liệu, hãy truyền tài liệu đó vào hàm diagnose(). (Mới có trong Beautiful Soup 4.2.0) Beautiful Soup sẽ in ra một báo cáo cho bạn thấy cách các parser khác nhau xử lý tài liệu như nào và cho bạn biết nếu bạn thiếu một parse mà Beautiful Soup có thể đang sử dụng:

from bs4.diagnose import diagnose
with open("bad.html") as fp:
    data = fp.read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug 1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...

Chỉ cần nhìn vào output của diagnose() nó có thể cho bạn cách giải quyết các vấn đề. Thậm chí nếu không giải quyết được, bạn có thể copy y nguyên cái mà diagnose() xuất ra và yêu cầu trợ giúp trên HowKteam hay Stackoverflow.

Lỗi khi phân tích một tài liệu

Có hai loại lỗi phân tích khác nhau. Đầu tiên đó là những sự cố, trong đó bạn truyền tài liệu vào BeautifulSoup và nó gây ra một Exception, thường là HTMLParser.HTMLParseError. Thứ hai là hành vi không đoán trước, khi mà parse tree do Beautiful Soup phân tích ra khác hoàn toàn so với tài liệu được sử dụng để tạo ra nó.

Hầu hết các vấn đề kiểu này không xảy ra với Beautiful Soup. Không phải là vì Beautiful Soup là một phần mềm được thiết kế tốt một cách đáng kinh ngạc. Đó là vì Beautiful Soup không chứa bất kỳ đoạn code nào thực hiện công việc parse. Thay vào đó, nó dựa vào các parser bên ngoài. Nếu một parser không hoạt động trên một tài liệu nhất định, giải pháp tốt nhất là thử một parser khác. Xem phần Cài đặt Parser để biết chi tiết và so sánh các parser.

Các lỗi parse phổ biến là HTMLParser.HTMLParseError: malformed start tag và HTMLParser.HTMLParseError: bad end tag. Cả hai lỗi này tạo ra bởi HTML parser được tích hợp sẵn của Python, và giải pháp là cài đặt lxml hoặc html5lib.

Một vấn đề nữa khá phổ biến là bạn không thể tìm thấy thẻ mà bạn biết là nó có trong tài liệu. Bạn thấy nó có trong tài liệu nhưng find_all() trả về [] hoặc find() trả về None. Đây là một vấn đề phổ biến với HTML parser được tích hợp sẵn trong Python, đôi khi nó sẽ bỏ qua các thẻ mà nó không hiểu. Một lần nữa, giải pháp là cài lxml hoặc html5lib.

Vấn đề phiên bản không phù hợp

SyntaxError: Invalid syntax (on the line ROOT_TAG_NAME = u'[document]'): Nguyên nhân là do chạy code Beautiful Soup được code trong Python2 ở trong Python 3, mà không chuyển đổi code.

ImportError: No module named HTMLParser – Nguyên nhân là do chạy code Beautiful Soup được code trong Python 2 ở trong Python 3.

ImportError: No module named html.parser – Nguyên nhân là do chạy code Beautiful Soup được code trong Python 3 ở trong Python 2.

ImportError: No module named BeautifulSoup – Nguyên nhân là do chạy Beautiful Soup 3 trên hệ thống chưa được cài đặt BS3. Hoặc, có thể là do viết code Beautiful Soup 4 mà không đổi tên package thành bs4.

ImportError: No module named bs4 – Nguyên nhân là do chạy code Beautiful Soup 4 trên hệ thống chưa cài BS4.

Parsing XML

Mặc định, Beautiful Soup parse tài liệu HTML. Để parse một tài liệu XML thì bạn cần phải truyền thêm ‘xml’ vào đối số thứ hai của BeautifulSoup constructor:

soup = BeautifulSoup(markup, "xml")

Và bạn sẽ cần cài lxml.

Những vấn đề về parser khác

  • Nếu script của bạn hoạt động trên một máy mà sang một máy khác nó không hoạt động, hoặc trong một virtual environment này nhưng một virtual environment thì không, hoặc bên ngoài virtual environment thì hoạt động như khi đưa vào trong thì không, nguyên nhân là vì thư viện parser hiện có của hai môi trường đó khác nhau. Ví dụ, bạn có thể phát triển một đoạn script hoạt động trên một máy tính có cài lxml, nhưng sau đó bạn thử chạy nó trong một máy tính mà chỉ cài html5lib. Xem lại phần Sự khác nhau giữa các parser để hiểu lý do tại sao, và để khắc phục vấn đề này chỉ cần bạn chỉ định parser sử dụng trong BeautifulSoup constructor.

  • Vì các thẻ và thuộc tính trong HTML không phân biệt in hoa hay in thường, cả ba HTML parser chuyển đổi tên thẻ và thuộc tính thành in thường. Ví dụ, cặp thẻ được chuyển đổi thành . Nếu bạn muốn giữ nguyên định dạng của chúng, bạn sẽ cần parse tài liệu của bạn dưới dạng XML.
     

  • UnicodeEncodeError: 'charmap' codec can't encode character u'\xfoo' in position bar (hay bất cứ UnicodeEncodeError nào khác) – Vấn đề này không thuộc về Beautiful Soup. Vấn đề này xuất hiện trong hai trường hợp sau. Đầu tiên, khi bạn cố gắng in một ký tự Unicode mà màn hình console không biết hiển thị như thế nào. (Xem trang này trên Python wiki để nhận sự giúp đỡ) Thứ hai, khi bạn viết một file và bạn truyền vào trong nó một ký tự Unicode mà bộ mã mặc định của bạn không hỗ trợ. Trong trường hợp này, giải pháp đơn giản nhất là mã hóa chuỗi Unicode thành UTF-8 bằng u.encode("utf8").

  • KeyError: [attr] – Nguyên nhân là do truy cập tag['attr'] khi trong thẻ không có định nghĩa thuộc tính attr. Các lỗi phổ biến nhất là KeyError: 'href' và KeyError: 'class'. Sử dụng tag.get('attr') nếu bạn không chắc trong tag có định nghĩa attr, như cách mà bạn dùng nó với một dict trong Python.

  • AttributeError: 'ResultSet' object has no attribute 'foo' – Lỗi này thường xảy ra vì bạn mong đợi find_all() trả về một thẻ hay một chuỗi. Nhưng find_all() trả về một list chứa các thẻ hoặc chuỗi – một ResultSet object. Bạn cần lặp qua list và lấy ra .foo của mỗi phần tử. Hoặc, nếu bạn chỉ muốn một kết quả, bạn cần sử dụng find() thay vì find_all().

  • AttributeError: 'NoneType' object has no attribute 'foo' – Lỗi này thường xảy ra khi bạn gọi find() và sau đó cố gắng truy cập thuộc tính .foo của kết quả trả về. Nhưng trong trường hợp của bạn, find() không tìm thấy gì, và nó trả về None thay vì trả về một thẻ hay một chuỗi. Bạn cần tìm hiểu tại sao hàm find() không trả về thứ gì.
     

Cải thiện hiệu suất

Beautiful Soup sẽ không bao giờ nhanh bằng các parser mà nó sử dụng. Nếu bạn quan trọng tốc độ, nếu bạn phải trả tiền cho việc ngồi máy tính hàng giờ, hay nếu có bất cứ lý do nào khác khiến tốc độ có giá trị cao hơn thời gian mà lập trình viên giành ra để code. Thì bạn nên quên Beautiful Soup đi và làm việc trực tiếp trên lxml.

Điều đó nói rằng, có một vài thứ có thể làm tăng tốc Beautiful Soup. Nếu bạn không sử dụng lxml làm parser cơ bản, thì mình khuyên bạn nên bắt đầu. Beautiful Soup parse tài liệu khi sử dụng lxml nhanh hơn đáng kể khi sử dụng html.parser hay html5lib.

Bạn có thể tăng tốc độ phát hiện mã hóa nhanh hơn bằng thư viện cchardet.

Chỉ parse một phần của tài liệu sẽ không giúp bạn giảm thời gian parse, nhưng bạn có thể tiết kiệm nhiều bộ nhớ, và giúp bạn tìm kiếm trong tài liệu nhanh hơn.

Đây là bài dịch đầu tiên của mình quăng lên web, mong sự ủng hộ, đón nhận, chia sẻ và quan tâm đến từ các bạn. Mọi góp ý, thắc mắc các bạn cứ comment bên dưới để mình cải thiện chất lượng của bài dịch, cũng như giải đáp các thắc mắc của các bạn nhé. Mong muốn mang đến cho Kteam những bài dịch chất lượng.