Chuyển đến nội dung chính

Trích xuất dữ liệu Reddit với Python và BeautifulSoup 4

Trong hướng dẫn này, bạn sẽ học cách lấy trang web bằng requests, phân tích trang trong trình duyệt và trích xuất thông tin từ HTML thô với BeautifulSoup.
Đã cập nhật 5 thg 10, 2026  · 13 phút đọc

Khám phá cùng AI

ChatGPTClaudePerplexity

Minh hoạ trích xuất dữ liệu reddit

Bạn có thể xem ví dụ hoàn chỉnh của script chúng ta sẽ viết tại đây.

Web Scraping là gì?

Vậy chính xác thì web scraping là gì? Đúng như tên gọi, đó là phương pháp "cào" hoặc trích xuất dữ liệu từ các trang web. Bất cứ thứ gì bạn thấy trên internet bằng trình duyệt, bao gồm cả hướng dẫn này, đều có thể được cào về ổ cứng cục bộ của bạn.

Có rất nhiều ứng dụng cho web scraping. Với mọi phân tích dữ liệu, bước đầu tiên là thu thập dữ liệu. Internet là một kho tàng khổng lồ chứa lịch sử và tri thức của nhân loại, và bạn có thể trích xuất bất cứ thứ gì bạn muốn rồi làm điều bạn cần với thông tin đó.

Trong hướng dẫn này, chúng ta sẽ dùng Python và gói BeautifulSoup 4 để lấy thông tin từ một subreddit. Chúng ta quan tâm đến subreddit datascience. Mục tiêu là lấy 1000 bài đăng đầu tiên trên subreddit và xuất ra tệp CSV. Chúng ta muốn biết ai là người đăng, cũng như số lượt thích và bình luận của bài.

Nội dung chúng ta sẽ đề cập trong hướng dẫn:

  • Lấy trang web bằng requests
  • Phân tích trang web trong trình duyệt để tìm thông tin
  • Trích xuất thông tin từ HTML thô bằng BeautifulSoup

Lưu ý: Chúng ta sẽ dùng phiên bản cũ của trang Reddit vì nó nhẹ hơn, do đó ít tốn tài nguyên máy hơn.

Điều kiện tiên quyết

Hướng dẫn này giả định bạn biết các điều sau:

  • Chạy script Python trên máy tính của bạn
  • Kiến thức cơ bản về cấu trúc HTML

Bạn có thể học các kỹ năng trên trong khóa học Python cho người mới bắt đầu của DataCamp. Tuy nhiên, các khái niệm dùng ở đây rất tối thiểu, và bạn có thể theo được ngay cả khi chỉ biết rất ít Python.

Giờ thì xong phần đó rồi, chúng ta có thể chuyển sang phần đầu tiên của việc tạo web scraper. Thực ra cũng là phần đầu tiên khi viết bất kỳ script Python nào: import.

Trong scraper này, chúng ta sẽ dùng các gói sau:

  • requests
  • beautifulsoup4

Bạn có thể cài các gói này bằng pip, như sau:

pip install package_name

Sau khi tải xong các gói, hãy import chúng vào mã của bạn.

import requests
import csv
import time
from bs4 import BeautifulSoup4

Chúng ta sẽ dùng mô-đun tích hợp csv của Python để ghi kết quả ra tệp CSV. Bạn có thể để ý một điểm lạ ở đoạn mã trên: chúng ta tải gói tên là beautifulsoup4, nhưng import từ mô-đun tên bs4. Điều này hợp lệ trong Python; dù thường không được khuyến khích, nhưng cũng không phải là phạm luật.

Ghi chú quan trọng về chính sách nền tảng

Trước khi sử dụng các kỹ thuật scraping trong các bước tiếp theo, hãy chắc chắn kiểm tra điều khoản dịch vụ hiện hành của Reddit hoặc bất kỳ website nào bạn dự định cào. Chính sách website có thể thay đổi theo thời gian. Thỏa thuận Người dùng Reddit từ tháng 9/2024 cấm scraping nếu không có sự đồng ý bằng văn bản trước (xem mục 7). Luôn đảm bảo bạn tuân thủ các hướng dẫn sử dụng mới nhất của trang trước khi tiếp tục.

Những bước đầu để thu thập Reddit

Chúng ta đã thiết lập môi trường sẵn sàng. Tiếp theo, cần có url của trang mà chúng ta muốn cào. Trong hướng dẫn này, chúng ta dùng subreddit 'datascience' của Reddit. Trước khi viết script, có chút "khảo sát thực địa" cần làm. Mở trình duyệt, và truy cập subreddit nói trên.

Lưu ý rằng chúng ta sẽ dùng phiên bản cũ của subreddit cho scraper. Phiên bản mới ẩn một số thông tin quan trọng sâu trong cấu trúc trang. Vẫn có thể trích xuất thông tin từ site mới, nhưng để đơn giản, chúng ta dùng phiên bản cũ, nơi mọi thứ đều được bày ra rõ ràng.

Khi mở liên kết, bạn sẽ đối mặt với lượng thông tin dồn dập. Chính xác thì chúng ta cần gì trong đống này? Khi kiểm tra các liên kết, bạn sẽ thấy bài đăng trên Reddit có hai loại:

  • Inbound
  • Outbound

Liên kết inbound là liên kết trỏ đến nội dung trên chính Reddit, còn outbound thì ngược lại. Điều này quan trọng vì chúng ta chỉ muốn liên kết inbound. Tức là các bài có nội dung do người dùng viết, không chỉ là liên kết đến website khác.

Vậy ta đã biết mình muốn gì, làm sao để trích xuất? Nếu bạn nhìn vào tiêu đề bài đăng, bạn sẽ thấy nó theo sau bởi một đoạn văn bản trong ngoặc. Các bài chúng ta quan tâm có phần theo sau là ‘(self.datascience)’. Hợp lý mà nói, ta có thể suy ra ‘self’ ám chỉ thư mục gốc của Reddit, và ‘.datascience’ ám chỉ subreddit.

Tốt, vậy ta có cách nhận diện bài nào inbound và bài nào outbound. Giờ cần xác định điều này trong cấu trúc DOM. Có cách nào tìm các định danh này trong DOM bằng cách dò theo thẻ, class, hoặc id không? Tất nhiên là có!

Trong trình duyệt, bấm chuột phải vào bất kỳ liên kết ‘self.datascience’ nào và chọn ‘inspect element’. Hầu hết các trình duyệt hiện đại có công cụ tuyệt vời cho phép nhà phát triển duyệt qua phần mã nguồn có ý nghĩa của trang một cách động. Nếu bạn dùng Safari, đảm bảo đã bật công cụ nhà phát triển trong phần cài đặt. Bạn sẽ thấy một bảng điều khiển cuộn đến phần mã nguồn chịu trách nhiệm cho định danh ‘(self.datascience)’ đó.

Trong bảng điều khiển, bạn có thể thấy định danh thực chất được tải bởi một thẻ anchor.

<a href="/r/datascience">self.datascience</a>

Nhưng nó được bao bởi một thẻ span.

<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>

Chính thẻ span này chứa đoạn văn bản mà ta thấy trên trang, tức ‘(self.datascience)’. Như bạn thấy, nó được gắn class ‘domain’. Bạn có thể kiểm tra các liên kết khác để xem chúng có theo cùng định dạng không, và đúng vậy; chúng có.

Lấy trang với BeautifulSoup

Chúng ta biết mình muốn gì trên trang, nhưng làm sao dùng Python để đọc nội dung trang? Cơ bản nó hoạt động tương tự như cách con người đọc nội dung trang trên trình duyệt.

Đầu tiên, chúng ta cần gửi yêu cầu lấy trang bằng thư viện ‘requests’.

url = "https://old.reddit.com/r/datascience"
# Headers to mimic a browser visit
headers = {'User-Agent': 'Mozilla/5.0'}

# Returns a requests.models.Response object
page = requests.get(url, headers=headers)

Bây giờ chúng ta có một đối tượng Response chứa văn bản thô của trang web. Hiện tại, ta chưa làm gì được với nó. Những gì ta có chỉ là một chuỗi lớn chứa toàn bộ mã nguồn HTML. Để hiểu được nó, chúng ta cần dùng BeautifulSoup 4.

Headers sẽ cho phép chúng ta giả lập truy cập từ trình duyệt. Vì phản hồi dành cho bot khác với phản hồi dành cho trình duyệt, và điểm tham chiếu của chúng ta là trình duyệt, nên tốt hơn là lấy phản hồi như trình duyệt.

BeautifulSoup cho phép chúng ta tìm các thẻ cụ thể bằng cách tìm theo tổ hợp class, id, hoặc tên thẻ. Nó tạo ra một cây cú pháp, nhưng chi tiết đó không liên quan đến mục tiêu của chúng ta (và nằm ngoài phạm vi hướng dẫn này).

Vậy hãy tạo cây cú pháp đó.

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

Biến soup là một đối tượng BeautifulSoup được tạo từ chuỗi mã nguồn thô. Lưu ý chúng ta cần chỉ định bộ phân tích html. Vì BeautifulSoup cũng có thể tạo soup từ XML.

Tìm các thẻ cần thiết

Chúng ta biết các thẻ mình muốn (thẻ span có class ‘domain’), và ta đã có soup. Bước tiếp theo là duyệt soup và tìm tất cả các trường hợp của các thẻ này. Bạn có thể bật cười vì việc này đơn giản thế nào với BeautifulSoup.

domains = soup.find_all("span", class_="domain")

Chúng ta vừa làm gì? Ta gọi phương thức ‘find_all’ trên đối tượng soup, nó sẽ tự tìm tất cả các thẻ anchor theo các tham số truyền vào làm đối số thứ hai. Chúng ta chỉ truyền một ràng buộc (class phải là ‘domain’), nhưng có thể kết hợp với nhiều thứ khác, hoặc chỉ dùng id.

Chúng ta dùng ‘class_=’ vì 'class' là từ khóa dành riêng của Python để định nghĩa lớp, v.v.

Nếu muốn truyền nhiều hơn một tham số, bạn chỉ cần biến tham số thứ hai thành một dictionary các đối số muốn đưa vào, như sau:

soup.find_all("span", {"class": "domain", "height", "100px"})

Chúng ta có tất cả thẻ span trong danh sách ‘domains’, nhưng điều chúng ta muốn là các domain ‘(self.datascience)’.

for domain in domains:
    if domain != "(self.datascience)":
        continue

    print(domain.text)

Bây giờ bạn sẽ thấy danh sách các loại bài trên trang, loại trừ những bài không phải ‘(self.datascience)’. Về cơ bản, chúng ta đã có mọi thứ cần. Chúng ta đã có tham chiếu đến tất cả các bài inbound.

Tìm thông tin của chúng ta

In ra vài dòng ‘(self.datascience)’ thì cũng hay, nhưng tiếp theo là gì? Hãy nhớ lại mục tiêu ban đầu. Lấy tiêu đề bài, tác giả, lượt thích và số bình luận.

Để làm vậy, chúng ta phải quay lại trình duyệt. Nếu bạn mở công cụ inspector tại định danh của chúng ta một lần nữa, bạn sẽ thấy thẻ span nằm trong một thẻ div… rồi lại nằm trong một thẻ div khác, v.v. Tiếp tục đi lên, bạn sẽ tới thẻ div cha của toàn bộ bài đăng.

<div class=" thing id-t3_8qccuv even  link self" ...>
    ...
    <div class="entry unvoted">
        <div class="top-matter">
            <p class="title">
                ...
                <span class="domain">
                ...
            </p>
        </div>
    </div>
</div>

Như bạn thấy, phần tử cha chung nằm ở mức 4 lớp phía trên trong cấu trúc DOM. Dấu ba chấm (…) đại diện cho những phần không cần thiết. Nhưng điều chúng ta cần bây giờ là tham chiếu đến thẻ div bài đăng cho mỗi bài trong danh sách domains. Chúng ta có thể tìm phần tử cha của bất kỳ phần tử nào trong soup bằng các phương thức của BeautifulSoup.

for domain in soup.find_all("span", class_="domain"):
    if domain != "(self.datascience)":
        continue

    parent_div = domain.parent.parent.parent.parent
    print(parent_div.text)

Chạy script sẽ in ra toàn bộ văn bản của tất cả bài inbound. Nhưng bạn có thể nghĩ rằng đoạn mã này xấu. Bạn đúng. Không bao giờ an toàn khi phụ thuộc hoàn toàn vào tính ổn định cấu trúc DOM. Đây là kiểu giải pháp chắp vá cần cập nhật liên tục và tương đương với một quả bom hẹn giờ trong mã.

Thay vào đó, hãy xem thẻ div cha của mỗi bài và xem liệu chúng ta có thể tách biệt liên kết inbound và outbound ngay từ cha không. Mỗi thẻ div cha có thuộc tính ‘data-domain’, với giá trị chính xác là những gì chúng ta muốn! Tất cả bài inbound đều có data-domain là ‘self.datascience’.

Như đã đề cập, chúng ta có thể tìm các thẻ theo tổ hợp thuộc tính bằng BeautifulSoup. May mắn là Reddit gán class ‘thing’ cho thẻ div cha của mỗi bài.

attrs = {'class': 'thing', 'data-domain': 'self.datascience'}

for post in soup.find_all('div', attrs=attrs):
    print(post.attrs['data-domain'])

Biến ‘attrs’ là một dictionary chứa các thuộc tính và giá trị cần tìm. Cách tiếp cận này an toàn hơn nhiều vì ít phần phụ thuộc hơn. Do chúng ta đã thêm ‘self.datascience’ vào điều kiện tìm kiếm, không cần dùng câu lệnh if để bỏ qua vòng lặp nào cả vì đảm bảo chỉ nhận các bài có thuộc tính data-domain là ‘self.datascience’.

Giờ khi đã có tất cả thông tin cần, phần còn lại chỉ là trích xuất thông tin từ các phần tử con. Và đúng như bạn nghĩ, việc này rất đơn giản.

Trích xuất thông tin từ Reddit

Với mỗi bài, chúng ta cần 4 mẩu thông tin.

  • Tiêu đề
  • Tác giả
  • Lượt thích
  • Bình luận

Nếu xem xét cấu trúc thẻ div của bài đăng, chúng ta có thể tìm được tất cả thông tin này.

Tiêu đề

Đây là phần đơn giản nhất. Trong mỗi thẻ div bài, có một thẻ đoạn văn nằm lồng dưới vài lớp div. Nó đặc biệt dễ tìm vì có class ‘title’ gắn vào.

title = post.find('p', class_="title").text

Đối tượng post nằm trong vòng lặp for từ trước. Nó cũng là một đối tượng BeautifulSoup, nhưng chỉ chứa cấu trúc DOM trong thẻ div của bài. Phương thức ‘find’ chỉ trả về một đối tượng, trái với 'find_all' trả về danh sách các đối tượng phù hợp. Sau khi tìm thẻ, chúng ta chỉ muốn chuỗi tiêu đề, nên dùng thuộc tính text để đọc. Chúng ta cũng có thể đọc các thuộc tính khác bằng ‘object.attrs[‘attribute’]’.

Tác giả

Phần này cũng tương đối đơn giản, mở inspector tại tên tác giả dưới tiêu đề bài. Bạn sẽ thấy tên tác giả nằm trong thẻ anchor có class ‘author’.

author = post.find('a', class_='author').text

Bình luận

Phần này cần thêm chút xử lý nhưng vẫn đơn giản. Chúng ta có thể tìm số bình luận tương tự như cách tìm tiêu đề và tác giả.

comments = post.find('a', class_='comments').text

Khi chạy, bạn sẽ nhận được dạng như “49 comments”. Ổn, nhưng sẽ tốt hơn nếu chỉ lấy số. Để làm vậy, ta cần dùng thêm một chút Python.

Nếu dùng hàm ‘str.split()’ của Python, nó sẽ trả về một mảng gồm các phần tử trong chuỗi được tách theo khoảng trắng. Trường hợp này, ta sẽ nhận được danh sách ‘[“49", “comments”]’. Tuyệt! Giờ ta chỉ cần lấy phần tử đầu tiên và lưu lại. Chỉ cần móc nối các hàm vào dòng lệnh.

comments = post.find('a', class_='comments').text.split()[0]

Nhưng vẫn chưa xong vì đôi khi ta không nhận được số, mà là ‘comment’. Điều này xảy ra khi bài chưa có bình luận nào. Vì biết điều đó, ta chỉ cần kiểm tra nếu nhận được ‘comments’ thì thay bằng ‘0’.

if comments == "comment":
    comments = 0

Lượt thích

Tìm số lượt thích rất dễ, theo cùng logic như trên.

likes = post.find("div", attrs={"class": "score likes"}).text

Tuy nhiên ở đây, bạn sẽ thấy chúng ta dùng kết hợp hai class. Đơn giản vì có nhiều thẻ div khác có class ‘score’ hoặc ‘likes’, nhưng chỉ có một thẻ có tổ hợp ‘score’ và ‘likes’.

Nếu số lượt thích là 0, chúng ta nhận về 0. Nhưng có điều kỳ lạ xảy ra nếu bài quá mới và chưa có lượt thích nào. Chúng ta nhận “•”. Hãy thay ký hiệu này bằng “None” để không gây nhầm lẫn trong kết quả cuối.

if likes == "•":
    likes = "None"

Ghi kết quả ra CSV

Đến giờ, chúng ta đã có vòng lặp trích xuất tiêu đề, tác giả, lượt thích và bình luận cho mỗi bài trên trang. Python có mô-đun tích hợp tuyệt vời để ghi và đọc tệp CSV tên là ‘csv’, đúng tinh thần Python: giữ mọi thứ đơn giản. Việc của chúng ta chỉ là thêm một dòng ở cuối khối vòng lặp để nối chi tiết bài vào tệp CSV.

counter = 1
for post in posts:
    ...
    post_line = [counter, title, author, likes, comments]
    with open('output.csv', 'a') as f:
        writer = csv.writer(f)
        writer.writerow(post_line)

    counter += 1

Khá đơn giản phải không? post_line được dùng để tạo một mảng lưu các phần tử cần được phân tách bằng dấu phẩy. Biến counter dùng để theo dõi số lượng bài chúng ta đã ghi nhận.

Chuyển sang trang tiếp theo

Vì chúng ta đang đếm số bài được lưu, tất cả những gì cần làm là bao toàn bộ logic vào một vòng lặp khác, vòng lặp sẽ yêu cầu các trang mới cho đến khi ghi nhận đủ số bài nhất định.

counter = 1

while (counter <= 100):
    for post in posts:
        # Getting the title, author, ...
        # Writing to CSV and incrementing counter...

    next_button = soup.find("span", class_="next-button")
    next_page_link = next_button.find("a").attrs['href']
    time.sleep(2)
    page = requests.get(next_page_link, headers=headers)
    soup = BeautifulSoup(page.text, 'html.parser')

Chúng ta vừa làm khá nhiều việc, hãy xem từng dòng. Trước hết, ta đưa biến counter lên một khối để vòng lặp while có thể dùng.

Tiếp theo, chúng ta biết vòng lặp for làm gì, còn các dòng khác thì sao. Biến ‘next_button' dùng để tìm và lưu nút 'next'. Sau đó, ta có thể tìm thẻ anchor bên trong và lấy thuộc tính 'href'; lưu vào 'next_page_link'.

Ta có thể dùng liên kết này để yêu cầu trang tiếp theo, lưu lại vào ‘page’ và tạo soup mới với BeautifulSoup.

Đoạn trên dừng ở 100, nhưng bạn có thể dừng ở bất kỳ số lượng bài nào bạn muốn.

Scraping có trách nhiệm

Dòng mà chúng ta chưa nói đến ở trên là ‘time.sleep(2)’. Bởi vì dòng đó xứng đáng có phần riêng. Bất kỳ máy chủ web nào cũng có tài nguyên hữu hạn, nên trách nhiệm của chúng ta là không dùng cạn chúng. Không chỉ khiến bạn bị cấm nếu gửi hàng trăm yêu cầu trong vài giây, mà còn là hành vi không hay.

Cần nhớ rằng việc website cho phép bạn cào dữ liệu đã là điều khá tốt, vì nếu muốn, họ có thể phát hiện bot chỉ sau 10 đến 20 yêu cầu, hoặc bắt được bạn dựa trên đối tượng request mà Python gửi. Họ cho phép bạn cào mà không cần xoay vòng IP, nên bạn hãy "đáp lễ" bằng cách làm chậm bot của mình.

Bạn có thể xem chính sách thu thập của website bằng cách xem tệp robots.txt. Thường nằm ở thư mục gốc của website (ví dụ: http://www.reddit.com/robots.txt).

Tiếp theo là gì?

Tuỳ bạn. Như chúng ta vừa chứng minh, bất cứ thứ gì bạn thấy trên web đều có thể được cào và lưu cục bộ. Bạn có thể dùng thông tin vừa thu thập cho nhiều mục đích. Chỉ với bốn mẩu thông tin chúng ta tìm được, bạn có thể rút ra nhiều kết luận.

Bằng cách phân tích sâu hơn, chúng ta có thể xác định loại bài Reddit nào nhận được nhiều lượt thích nhất, chúng chứa những từ gì, ai đăng. Hoặc ngược lại, bạn có thể tạo một "máy tính tranh cãi" bằng cách phân tích tỷ lệ giữa lượt thích và bình luận.

Hãy xét một bài có rất nhiều bình luận nhưng không có lượt thích. Nhiều khả năng bài đó chạm đến điều gì đó khiến mọi người phản hồi mạnh, nhưng không nhất thiết là thứ họ thích.

Có rất nhiều khả năng, và tuỳ bạn quyết định sẽ dùng thông tin như thế nào.

Nếu bạn thích hướng dẫn này, hãy xem thêm hướng dẫn về cào dữ liệu Amazon bằng python, cùng bộ sưu tập các khoá học python của chúng tôi.

Chủ đề
Python

Tìm hiểu thêm về Python

Khóa học

Nhập môn Python

4 giờ
7.1M
Nắm vững phân tích dữ liệu với Python chỉ trong 4 giờ. Khóa học online này giúp bạn làm quen với giao diện Python và các thư viện phổ biến.
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow
Liên quan

blog

Claude Opus 4.6: Tính năng, Điểm chuẩn, Bài kiểm tra thực hành và hơn thế nữa

Mô hình mới nhất của Anthropic dẫn đầu ở mã hóa tác tử và lập luận phức tạp. Thêm vào đó, nó có cửa sổ ngữ cảnh 1M.
Matt Crabtree's photo

Matt Crabtree

10 phút

Xem ThêmXem Thêm