0

PyMOTM: Beautiful Soup 4 (Part III)

Beautiful Soup 4

Mục đích: Parse HTML, XML và Website scraping

Chúng ta tiếp tục sang phần III của series PyMOTM: Beautiful Soup 4 nhé. Như Phần II mình đã giới thiệu về Navigating the tree, sang phần này mình sẽ giới thiệu về phần Searching the tree, để bạn có cái nhìn chi tiết hơn về khái niệm website scraping với Beautiful Soup 4.

Searching the tree

Beautiful Soup 4 định nghĩa rất nhiều method để giúp chúng ta thực hiện việc searching the tree, nhưng về mặt cơ bản thì chúng khá giống nhau. Nên chúng ta sẽ đi tìm hiểu chi tiết về 2 method mà có lẽ chúng ta sẽ dùng nhiều nhất cho việc website scraping là .find().find_all() nhé 😄. Và chúng ta vẫn sẽ sử dụng nguyên liệu ở phần I để sử dụng trong phần này cho đồng bộ nhé!

Các loại filter

Trước khi đi vào 2 method .find(), .find_all() và các method khác, chúng ta tìm hiểu qua các filter (bộ lọc) mà bạn có thể sử dụng trong các method này nhé. Bạn có thể sử dụng các filter này để lọc các tag name, attributes, text, string, ...

String

Filter đơn giản nhất là string. Khi bạn truyền 1 string vào method .find_all(), nó sẽ tìm tất cả các tag có tên giống string mà bạn truyền vào. Ví dụ, mình sẽ tìm tất cả tag a trong bộ nguyên liệu đã chuẩn bị trước nhé:

(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>]
Regular Expression (Biểu thức quy tắc)

Nếu bạn truyền vào giá trị là một RegExp (Regular Expression), thì Beautiful Soup sẽ sử dụng method .match() của thư viện re để tìm kiếm các tag theo đúng quy tắc mà bạn muốn. Ví dụ, mình muốn tìm các tag name có kết thúc là iv (tag div) (vì trong nguyên liệu của chúng ta không đủ nhiều tag để viết 1 RegExp phức tạp nên mình lấy ví dụ đơn giản để mọi người có cái nhìn tổng quan nhé:

import re
(Pdb) for tag in html_dom.find_all(re.compile('iv$')): print tag.name
div
div
div
div
div
div

Hoặc bạn có thể tìm tất cả các tag có chứa 1 ký tự nào đó, ví dụ như chữ i:

(Pdb) for tag in html_dom.find_all(re.compile('i')): print tag.name
title
link
div
div
div
div
div
div
A list

Bạn cũng có thể truyền vào 1 danh sách các tag cần tìm, mình sẽ tìm các tất cả các tag ap thử nhé:

(Pdb) for tag in html_dom.find_all(["a", "p"]): print tag
<p class="title">Item 001</p>
<p class="price">Price: 01$</p>
<p><a href="#">Buy</a></p>
<a href="#">Buy</a>
<p class="title">Item 002</p>
<p class="price">Price: 02$</p>
<p><a href="#">Buy</a></p>
<a href="#">Buy</a>
<p class="title">Item 003</p>
<p class="price">Price: 03$</p>
<p><a href="#">Buy</a></p>
<a href="#">Buy</a>
<p class="title">Item 004</p>
<p class="price">Price: 04$</p>
<p><a href="#">Buy</a></p>
<a href="#">Buy</a>
<p class="title">Item 005</p>
<p class="price">Price: 05$</p>
<p><a href="#">Buy</a></p>
<a href="#">Buy</a>
True

Nếu bạn truyền vào một giá trị Boolean (True hoặc False), Beautiful Soup sẽ tìm kiếm tất cả các tag, ngoại trừ các giá trị là text và string (hoặc giá trị False sẽ ra kết quả không bao gồm comment <!-- -->). Cái này mình sẽ làm ví dụ mà không có kết quả nhé. Để trải nghiệm TrueFalse, bạn hãy tự kiểm tra nhé 😄

for tag in html_dom.find_all(True): print tag
A function

Nếu các filter đã giới thiệu ở trên vẫn chưa đủ để bạn sử dụng hay mục đích của bận cần phải tìm kiếm nhiều hơn, bạn có thể truyền vào 1 function để tự định nghĩa filter riêng cho mình. Function này nên trả về 1 giá trị Boolean, True cho việc các giá trị phù hợp, còn False cho những kết quả khác. Để thực hiện ví dụ này, mình sẽ thêm 1 chút vào file code Python nhé.

#!/usr/bin/env python

from bs4 import BeautifulSoup
import pdb

html_dom = BeautifulSoup(open('/tmp/bs4.html'), 'html5lib')

def paragraph_without_class(tag):
    return tag.name == "p" and not tag.has_attr("class")

# Dừng lại để chúng ta debug
pdb.set_trace();

OK, giờ chúng ta thử xem sao:

(Pdb) html_dom.find_all(paragraph_without_class)
[<p><a href="#">Buy</a></p>, <p><a href="#">Buy</a></p>, <p><a href="#">Buy</a></p>, <p><a href="#">Buy</a></p>, <p><a href="#">Buy</a></p>]

Ngoài việc bạn tìm theo tag name, bạn cũng có thể tìm theo các attribute của tag bằng 1 function. Mình sẽ ví dụ đơn giản (thực chất là làm lại việc của Beautiful Soup, nhưng mình vẫn giới thiệu để mọi người hiểu rõ hơn) là tìm tất cả thẻ p có class là title nhé. Đầu tiên chúng ta thêm 1 function nữa để làm việc này vào file code Python nhé:

def paragraph_with_class_title(class_name):
    return class_name == "title"
(Pdb) html_dom.find_all("p", class_=paragraph_with_class_title)
[<p class="title">Item 001</p>, <p class="title">Item 002</p>, <p class="title">Item 003</p>, <p class="title">Item 004</p>, <p class="title">Item 005</p>]

Vậy là bạn đã có cái nhìn khá rõ về việc sử dụng 1 function để tự tạo những filter cho riêng mình ngoài việc sử dụng những filter mặc định của Beautiful Soup rồi nhỉ 😄. Bây giờ chúng ta sẽ đi sâu hơn và method .find_all() nhé.

.find_all()

Arguments: .find_all(name, attrs, recursive, string, limit, **kwargs)

Method .find_all() sẽ duyệt tất cả các tag và trả về những tag nào phù hợp với filter mà bạn đưa vào. Chúng ta đã được biết qua những filter mà mình vừa giới thiệu ở trên, nhưng nó vẫn chưa thực sự đầy đủ. Và chúng ta sẽ đi tìm hiểu nhiều hơn trong phần này nhé.

Tham số name

Là khi bạn truyền tên của tag. Beautiful Soup sẽ duyệt và tìm tất cả các tag tương ứng với tên mà bạn đưa vào (không bao gồm text và string).

(Pdb) html_dom.find_all('title')
[<title>Document</title>]

Quay lại phần Các loại filter, thì tham số này bạn có thể truyền vào 1 string, biểu thức quy tắc, danh sách các tên tag, function hay 1 giá trị boolean.

Tham số keyword

Khi tham số bạn truyền vào mà Beautiful Soup không hiểu, thì nó tự động chuyển tham số đó thành việc tìm kiếm theo attribute của tag. Ví dụ nếu bạn truyền type="text/css" thì Beautiful Soup sẽ tìm các tag có attribute là type có chứa giá trị là text/css.

(Pdb) html_dom.find_all(type="text/css")
[<link href="css.css" rel="stylesheet" type="text/css"/>]

Bạn cũng có thể sử dụng chuỗi, biểu thức quy tắc, danh sách, function hay 1 giá trị boolean. Ví dụ về function thì bạn có thể xem lại phần A function của Các loại filter! Còn ở đây, mình sẽ ví dụ tìm tất cả các tag p có attribute class bằng 1 giá trị boolean nhé:

(Pdb) html_dom.find_all("p", class_=True)
[<p class="title">Item 001</p>, <p class="price">Price: 01$</p>, <p class="title">Item 002</p>, <p class="price">Price: 02$</p>, <p class="title">Item 003</p>, <p class="price">Price: 03$</p>, <p class="title">Item 004</p>, <p class="price">Price: 04$</p>, <p class="title">Item 005</p>, <p class="price">Price: 05$</p>]

Bạn cũng có thể truyền vào nhiều hơn 1 attribute bằng cách sử dụng nhiều tham số hoặc sử dụng 1 dictionary thông qua tham số attrs. Với những HTML5 attribute thì bạn không thể sử dụng bằng cách truyền tham số trực tiếp, mà chỉ có thể sử dụng thông qua tham số attrs. Ví dụ:

html_dom.find_all(class_="className", type="input")
html_dom.find_all(attrs{"class": "className", "type": "input"})

# Incorrect
(Pdb) html_dom.find_all(data-id="1");
*** SyntaxError: keyword can't be an expression (<stdin>, line 1)

# Correct
html_dom.find_all(attrs={"data-id": "1"})

Tiếp theo, chúng ta sẽ sang phần tìm kiếm bằng CSS class và selector nhé.

Searching by CSS class

Bạn có thắc mắc tại sao các attribute khác (như id, type, ...) thì được dùng như bình thường, còn attribute class thì lại phải thêm _ vào không 😄? Vì keyword class đã được sử dụng (reserved) bởi Python mất rồi 😃)! Cũng giống như các keyword khác, bạn có thể truyền giá trị cho tham số class_ là 1 chuỗi, biểu thức quy tắc, function, hay một giá trị boolean (như mình đã từng ví dụ ở trên). Nhưng có 1 điều cần lưu ý rằng, attribue class sẽ có thể có nhiều hơn 1 giá trị, thay vì việc chỉ tìm 1 class duy nhất, bạn cũng có thể tìm chính xác số lượng class trong thuộc tính class. Nhưng bạn phải viết đúng thứ tự của các class. Nếu bạn thử đảo ngược hoặc viết sai thứ tự, thì kết quả sẽ là con số 0 tròn trĩnh nhé 😄

(Pdb) len(html_dom.find_all(class_="item pull-right"))
5
(Pdb) len(html_dom.find_all(class_="pull-right item"))
0

Tiếp theo, chúng ta đi tìm hiểu tiếp các tham số còn lại của method .find_all() nhé.

Tham số string

Tham số này (trước phiên bản 4.4.0 thì nó có tên là text) được sử dụng khi bạn muốn tìm kiếm chuỗi ký tự thay vì tìm kiếm theo tên tag. Cũng giống như tham số namekeyword, bạn cũng có thể truyền giá trị cho tham số này là chuỗi, biểu thức quy tắc, function hay 1 giá trị boolean.

(Pdb) import re
(Pdb) html_dom.find_all(string="Item 002")
[u'Item 002']
(Pdb) html_dom.find_all(string=re.compile("02"))
[u'Item 002', u'Price: 02$']
Tham số limit

Tham số này được sử dụng khi bạn muốn giới hạn kết quả tìm kiếm được.

(Pdb) html_dom.find_all(string=re.compile("[0-9]+\$$"))
[u'Price: 01$', u'Price: 02$', u'Price: 03$', u'Price: 04$', u'Price: 05$']
(Pdb) html_dom.find_all(string=re.compile("[0-9]+\$$"), limit=3)
[u'Price: 01$', u'Price: 02$', u'Price: 03$']
Tham số recursive

Tham số được sử dụng để thông báo cho Beautiful Soup có tìm kiếm theo dạng đệ quy hay không. Giá trị mặc định của nó là True. Khi bạn không muốn nó duyệt đệ quy thì có thể truyền giá trị False cho nó 😄!

(Pdb) len(html_dom.find_all(string=True, recursive=True))
54
(Pdb) len(html_dom.find_all(string=True, recursive=False))
1

CSS Selector

Beautiful Soup cũng hỗ trợ bạn tìm kiếm bằng các sử dụng các CSS Selector thông thường. Để sử có thể sử dụng được, bạn cần sử dụng method .select(). Thử xem qua các ví dụ sau nhé:

(Pdb) html_dom.select("p:nth-of-type(2)")
[<p class="price">Price: 01$</p>]

(Pdb) html_dom.select(".item > p:nth-of-type(2)")
[<p class="price">Price: 01$</p>, <p class="price">Price: 02$</p>, <p class="price">Price: 03$</p>, <p class="price">Price: 04$</p>, <p class="price">Price: 05$</p>]

(Pdb) html_dom.select(".items-list p > a")
[<a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>, <a href="#">Buy</a>]

(Pdb) html_dom.select("[class$=e]")
[<p class="title">Item 001</p>, <p class="price">Price: 01$</p>, <p class="title">Item 002</p>, <p class="price">Price: 02$</p>, <p class="title">Item 003</p>, <p class="price">Price: 03$</p>, <p class="title">Item 004</p>, <p class="price">Price: 04$</p>, <p class="title">Item 005</p>, <p class="price">Price: 05$</p>]

Hoặc nếu bạn muốn tìm 1 tag đầu tiên, bạn sử dụng method .select_one():

(Pdb) html_dom.select_one("[class$=e]")
<p class="title">Item 001</p>

Lời kết

Vâng, bài viết đến phần này cũng đã đủ để bạn có thể hiểu được căn bản module Beautiful Soup của Python rồi. Documentation của nó còn khá là nhiều thứ. Bạn có thể xem thêm chi tiết tại trang chủ của nó nhé 😄! Đến đây, mình xin tạm dừng việc giới thiệu về module Beautiful Soup nhé. Hẹn gặp lại mọi người trong những module hay cho Python ở các bài viết tiếp theo (hy vọng nó không quá dài và bị chia thành nhiều phần như module này 😄).

Nhân đây, mình cũng có viết 1 chương trình nho nhỏ là tải ảnh hàng loại từ một topic của trang web vOzForums với sự kết hợp của các module mà mình đã giới thiệu trong series PyMOTM là: Requests, ArgParseBeautiful Soup. Nếu bạn thích thú, có thể xem source code tại Github: https://github.com/namnv609/voz-imgs-downloader!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.