What is Beautiful Soup?

Beautiful Soup là một thư viện Python phân tích cú pháp các tài liệu HTML hoặc XML thành một cấu trúc cây giúp dễ dàng tìm kiếm và trích xuất dữ liệu. Nó thường được sử dụng để thu thập dữ liệu từ các trang web.

Beautiful Soup có giao diện Python đơn giản và khả năng chuyển đổi mã hóa tự động giúp bạn dễ dàng làm việc với dữ liệu trang web.

Các trang web là các tài liệu có cấu trúc và Beautiful Soup cung cấp cho bạn các công cụ để đi qua cấu trúc phức tạp đó và trích xuất các bit thông tin đó. Trong hướng dẫn này, bạn sẽ viết một tập lệnh Python để thu thập giá xe máy trên Craigslist. Tập lệnh sẽ được thiết lập để chạy theo các khoảng thời gian đều đặn bằng cách sử dụng một công việc cron và dữ liệu kết quả sẽ được xuất sang bảng tính Excel để phân tích xu hướng. Bạn có thể dễ dàng điều chỉnh các bước này cho các trang web hoặc truy vấn tìm kiếm khác bằng cách thay thế các URL khác nhau và điều chỉnh tập lệnh cho phù hợp.

Cài đặt Beautiful Soup

Cài đặt Python

1.Tải xuống và cài đặt Miniconda:

curl -OL https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh

2.Bạn sẽ được nhắc nhiều lần trong quá trình cài đặt. Xem lại các điều khoản và điều kiện và chọn “có” cho mỗi lời nhắc.

3.Khởi động lại phiên shell để những thay đổi trong PATH có hiệu lực.

4.Kiểm tra phiên bản Python của bạn:

python --version

Cài đặt Beautiful Soup và Dependencies

1.Cập nhật hệ thống của bạn:

 sudo apt update && sudo apt upgrade

2.Cài đặt phiên bản mới nhất của Beautiful Soup bằng pip:

pip install beautifulsoup4

3.Cài đặt các phụ thuộc:

pip install tinydb urllib3 xlsxwriter lxml

Xây dựng một Web Scraper

Các mô-đun bắt buộc

Lớp BeautifulSoupfrom bs4sẽ xử lý việc phân tích cú pháp các trang web. datetimeModule cung cấp khả năng xử lý ngày tháng. Tinydbcung cấp API cho cơ sở dữ liệu NoSQL và urllib3module được sử dụng để thực hiện các yêu cầu http. Cuối cùng, xlsxwriterAPI được sử dụng để tạo bảng tính excel.

Mở craigslist.pytrong trình soạn thảo văn bản và thêm các câu lệnh nhập cần thiết:

from bs4 import BeautifulSoup
import datetime
from tinydb import TinyDB, Query
import urllib3
import xlsxwriter

Thêm Biến Toàn Cầu

Sau các câu lệnh import, hãy thêm các biến toàn cục và tùy chọn cấu hình:

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

url = 'https://elpaso.craigslist.org/search/mcy?sort=date'
total_added = 0

urllưu trữ URL của trang web cần thu thập và total_addedsẽ được sử dụng để theo dõi tổng số kết quả được thêm vào cơ sở dữ liệu. urllib3.disable_warnings()Chức năng này bỏ qua mọi cảnh báo về chứng chỉ SSL.

Lấy lại trang web

Hàm này make_soupthực hiện yêu cầu GET tới url mục tiêu và chuyển đổi HTML kết quả thành đối tượng BeautifulSoup:

def make_soup(url):
    http = urllib3.PoolManager()
    r = http.request("GET", url)
    return BeautifulSoup(r.data,'lxml')

Thư urllib3viện có khả năng xử lý ngoại lệ tuyệt vời; nếu make_soupcó bất kỳ lỗi nào, hãy kiểm tra tài liệu urllib3 để biết thông tin chi tiết.

Beautiful Soup có nhiều trình phân tích cú pháp khác nhau có sẵn, ít nhiều nghiêm ngặt về cách cấu trúc trang web. Trình phân tích cú pháp lxml đủ cho tập lệnh ví dụ trong hướng dẫn này, nhưng tùy thuộc vào nhu cầu của bạn, bạn có thể cần kiểm tra các tùy chọn khác được mô tả trong tài liệu chính thức .

Xử lý đối tượng súp

Một đối tượng của lớp BeautifulSoupđược tổ chức theo cấu trúc cây. Để truy cập dữ liệu bạn quan tâm, bạn sẽ phải quen thuộc với cách dữ liệu được tổ chức trong tài liệu HTML gốc. Truy cập trang web ban đầu trong trình duyệt, nhấp chuột phải và chọn Xem nguồn trang (hoặc Kiểm tra , tùy thuộc vào trình duyệt của bạn) để xem lại cấu trúc dữ liệu mà bạn muốn trích xuất:

<li class="result-row" data-pid="6370204467">
  <a href="https://elpaso.craigslist.org/mcy/d/ducati-diavel-dark/6370204467.html" class="result-image gallery" data-ids="1:01010_8u6vKIPXEsM,1:00y0y_4pg3Rxry2Lj,1:00F0F_2mAXBoBiuTS">
    <span class="result-price">$12791</span>
  </a>
  <p class="result-info">
    <span class="icon icon-star" role="button">
    <span class="screen-reader-text">favorite this post</span>
    </span>
    <time class="result-date" datetime="2017-11-01 19:38" title="Wed 01 Nov 07:38:13 PM">Nov  1</time>
    <a href="https://elpaso.craigslist.org/mcy/d/ducati-diavel-dark/6370204467.html" data-id="6370204467" class="result-title hdrlnk">Ducati Diavel | Dark</a>
    <span class="result-meta">
            <span class="result-price">$12791</span>
            <span class="result-tags">
            pic
            <span class="maptag" data-pid="6370204467">map</span>
            </span>
            <span class="banish icon icon-trash" role="button">
            <span class="screen-reader-text">hide this posting</span>
            </span>
    <span class="unbanish icon icon-trash red" role="button" aria-hidden="true"></span>
    <a href="#" class="restore-link">
            <span class="restore-narrow-text">restore</span>
            <span class="restore-wide-text">restore this posting</span>
    </a>
    </span>
  </p>
</li>

3. Chọn các đoạn trích trang web bằng cách chỉ chọn các thẻ li html và thu hẹp hơn nữa các lựa chọn bằng cách chỉ chọn những thẻ li có lớp result-row . Biến results chứa tất cả các đoạn trích trang web phù hợp với tiêu chí này:

'pid': result['data-pid']

4.Các thuộc tính dữ liệu khác có thể được lồng sâu hơn trong cấu trúc HTML và có thể được truy cập bằng cách kết hợp ký hiệu chấm và mảng. Ví dụ, ngày kết quả được đăng được lưu trữ trong datetime, là thuộc tính dữ liệu của phần timetử, là phần tử con của pthẻ là phần tử con của result. Để truy cập giá trị này, hãy sử dụng định dạng sau:

'date': result.p.time['datetime']

5.Đôi khi thông tin cần thiết là nội dung thẻ (nằm giữa thẻ bắt đầu và thẻ kết thúc). Để truy cập nội dung thẻ, BeautifulSoup cung cấp phương stringpháp:

<span class="result-price">$12791</span> 

có thể truy cập bằng:

'cost': clean_money(result.a.span.string.strip()) 

Giá trị ở đây được xử lý thêm bằng cách sử dụng strip()hàm Python, cũng như một hàm tùy chỉnh clean_moneyloại bỏ dấu đô la.

6.Hầu hết các mặt hàng được bán trên Craigslist đều có hình ảnh của mặt hàng. Chức năng tùy chỉnh clean_picđược sử dụng để gán URL của hình ảnh đầu tiên cho pic :

'pic': clean_pic(result.a['data-ids'])

7.Siêu dữ liệu có thể được thêm vào bản ghi. Ví dụ, bạn có thể thêm một trường để theo dõi thời điểm một bản ghi cụ thể được tạo:

'createdt': datetime.datetime.now().isoformat()

8.Sử dụng đối tượng Query để kiểm tra xem bản ghi đã tồn tại trong cơ sở dữ liệu hay chưa trước khi chèn nó. Điều này tránh tạo ra các bản ghi trùng lặp.

Result = Query()
s1 = db.search(Result.pid == rec["pid"])

if not s1:
    total_added += 1
    print ("Adding ... ", total_added)
    db.insert(rec)

Xử lý lỗi

Có hai loại lỗi quan trọng cần xử lý. Đây không phải là lỗi trong tập lệnh mà là lỗi trong cấu trúc đoạn mã khiến API của Beautiful Soup đưa ra lỗi.

An AttributeErrorsẽ được ném ra khi ký hiệu dấu chấm không tìm thấy thẻ anh chị em với thẻ HTML hiện tại. Ví dụ, nếu một đoạn mã cụ thể không có thẻ neo, thì khóa cost sẽ ném ra lỗi, vì nó chuyển ngang và do đó yêu cầu thẻ neo.

Lỗi còn lại là KeyError. Lỗi này sẽ xảy ra nếu thiếu thuộc tính thẻ HTML bắt buộc. Ví dụ, nếu không có thuộc tính data-pid trong đoạn mã, khóa pid sẽ gây ra lỗi.

Nếu một trong hai lỗi này xảy ra khi phân tích kết quả, kết quả đó sẽ bị bỏ qua để đảm bảo đoạn mã không đúng định dạng không được chèn vào cơ sở dữ liệu:

except (AttributeError, KeyError) as ex:
    pass

Chức năng làm sạch

Đây là hai hàm tùy chỉnh ngắn để dọn dẹp dữ liệu đoạn trích. clean_moneyHàm này xóa mọi dấu đô la khỏi đầu vào của nó:

def clean_money(amt):
    return int(amt.replace("$",""))

Chức năng này clean_pictạo ra một URL để truy cập hình ảnh đầu tiên trong mỗi kết quả tìm kiếm:

def clean_pic(ids):
    idlist = ids.split(",")
    first = idlist[0]
    code = first.replace("1:","")
    return "https://images.craigslist.org/%s_300x300.jpg" % code

Hàm này trích xuất và xóa id của hình ảnh đầu tiên, sau đó thêm nó vào URL cơ sở.

Ghi dữ liệu vào bảng tính Excel

Hàm này make_excellấy dữ liệu trong cơ sở dữ liệu và ghi vào bảng tính Excel.

1.Thêm biến bảng tính:

Headlines = ["Pid", "Date", "Cost", "Webpage", "Pic", "Desc", "Created Date"]
row = 0

Biến Headlines là danh sách tiêu đề cho các cột trong bảng tính. Biến row theo dõi hàng bảng tính hiện tại.

2.Sử dụng xlsxwriterđể mở một bảng tính và thêm một trang tính để nhận dữ liệu.

workbook = xlsxwriter.Workbook('motorcycle.xlsx')
worksheet = workbook.add_worksheet()

3.Chuẩn bị phiếu bài tập:

worksheet.set_column(0,0, 15) # pid
worksheet.set_column(1,1, 20) # date
worksheet.set_column(2,2, 7)  # cost
worksheet.set_column(3,3, 10)  # webpage
worksheet.set_column(4,4, 7)  # picture
worksheet.set_column(5,5, 60)  # Description
worksheet.set_column(6,6, 30)  # created date

2 mục đầu tiên luôn giống nhau trong set_columnphương pháp. Đó là vì nó đang thiết lập các thuộc tính của một phần các cột từ cột đầu tiên được chỉ định đến cột tiếp theo. Giá trị cuối cùng là chiều rộng của cột tính bằng ký tự.

4.Viết tiêu đề cột vào bảng tính:

for col, title in enumerate(Headlines):
    worksheet.write(row, col, title)

5.Ghi các bản ghi vào cơ sở dữ liệu:

for item in db.all():
    row += 1
    worksheet.write(row, 0, item['pid'] )
    worksheet.write(row, 1, item['date'] )
    worksheet.write(row, 2, item['cost'] )
    worksheet.write_url(row, 3, item['webpage'], string='Web Page')
    worksheet.write_url(row, 4, item['pic'], string="Picture" )
    worksheet.write(row, 5, item['descr'] )
    worksheet.write(row, 6, item['createdt'] )

Hầu hết các trường trong mỗi hàng có thể được viết bằng worksheet.writeworksheet.write_urlđược sử dụng cho URL danh sách và hình ảnh. Điều này làm cho các liên kết kết quả có thể nhấp vào được trong bảng tính cuối cùng.

6.Đóng bảng tính Excel:

    workbook.close()

Thói quen chính

Thói quen chính sẽ lặp lại qua từng trang kết quả tìm kiếm và chạy hàm soup_process trên từng trang. Nó cũng theo dõi tổng số mục nhập cơ sở dữ liệu được thêm vào trong biến toàn cục total_added , được cập nhật trong hàm soup_process và hiển thị sau khi quá trình thu thập dữ liệu hoàn tất. Cuối cùng, nó tạo một cơ sở dữ liệu TinyDB db.jsonvà lưu trữ dữ liệu đã phân tích; khi quá trình thu thập dữ liệu hoàn tất, cơ sở dữ liệu được chuyển đến hàm make_excel để ghi vào bảng tính.

def main(url):
    total_added = 0
    db = TinyDB("db.json")

    while url:
        print ("Web Page: ", url)
        soup = soup_process(url, db)
        nextlink = soup.find("link", rel="next")

        url = False
        if (nextlink):
            url = nextlink['href']

    print ("Added ",total_added)

    make_excel(db)

Một lần chạy mẫu có thể trông giống như sau. Lưu ý rằng mỗi trang có chỉ mục được nhúng trong URL. Đây là cách Craigslist biết trang dữ liệu tiếp theo bắt đầu từ đâu:

$ python3 craigslist.py
Web Page:  https://elpaso.craigslist.org/search/mcy?sort=date
Adding ...  1
Adding ...  2
Adding ...  3
Web Page:  https://elpaso.craigslist.org/search/mcy?s=120&sort=date
Web Page:  https://elpaso.craigslist.org/search/mcy?s=240&sort=date
Web Page:  https://elpaso.craigslist.org/search/mcy?s=360&sort=date
Web Page:  https://elpaso.craigslist.org/search/mcy?s=480&sort=date
Web Page:  https://elpaso.craigslist.org/search/mcy?s=600&sort=date
Added  3

Thiết lập Cron để tự động thu thập

Phần này sẽ thiết lập một tác vụ cron để chạy tập lệnh thu thập dữ liệu tự động theo các khoảng thời gian đều đặn. Dữ liệu

1.Đăng nhập vào máy của bạn như một người dùng bình thường:

 ssh normaluser@<Linode Public IP>

2.Đảm bảo craigslist.pytập lệnh hoàn chỉnh nằm trong thư mục gốc:

from bs4 import BeautifulSoup
import datetime
from tinydb import TinyDB, Query
import urllib3
import xlsxwriter

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

url = 'https://elpaso.craigslist.org/search/mcy?sort=date'
total_added = 0

def make_soup(url):
    http = urllib3.PoolManager()
    r = http.request("GET", url)
    return BeautifulSoup(r.data,'lxml')

def main(url):
    global total_added
    db = TinyDB("db.json")

    while url:
        print ("Web Page: ", url)
        soup = soup_process(url, db)
        nextlink = soup.find("link", rel="next")

        url = False
        if (nextlink):
            url = nextlink['href']

    print ("Added ",total_added)

    make_excel(db)

def soup_process(url, db):
    global total_added

    soup = make_soup(url)
    results = soup.find_all("li", class_="result-row")

    for result in results:
        try:
            rec = {
                'pid': result['data-pid'],
                'date': result.p.time['datetime'],
                'cost': clean_money(result.a.span.string.strip()),
                'webpage': result.a['href'],
                'pic': clean_pic(result.a['data-ids']),
                'descr': result.p.a.string.strip(),
                'createdt': datetime.datetime.now().isoformat()
            }

            Result = Query()
            s1 = db.search(Result.pid == rec["pid"])

            if not s1:
                total_added += 1
                print ("Adding ... ", total_added)
                db.insert(rec)

        except (AttributeError, KeyError) as ex:
            pass

    return soup

def clean_money(amt):
    return int(amt.replace("$",""))

def clean_pic(ids):
    idlist = ids.split(",")
    first = idlist[0]
    code = first.replace("1:","")
    return "https://images.craigslist.org/%s_300x300.jpg" % code

def make_excel(db):
    Headlines = ["Pid", "Date", "Cost", "Webpage", "Pic", "Desc", "Created Date"]
    row = 0

    workbook = xlsxwriter.Workbook('motorcycle.xlsx')
    worksheet = workbook.add_worksheet()

    worksheet.set_column(0,0, 15) # pid
    worksheet.set_column(1,1, 20) # date
    worksheet.set_column(2,2, 7)  # cost
    worksheet.set_column(3,3, 10)  # webpage
    worksheet.set_column(4,4, 7)  # picture
    worksheet.set_column(5,5, 60)  # Description
    worksheet.set_column(6,6, 30)  # created date

    for col, title in enumerate(Headlines):
        worksheet.write(row, col, title)

    for item in db.all():
        row += 1
        worksheet.write(row, 0, item['pid'] )
        worksheet.write(row, 1, item['date'] )
        worksheet.write(row, 2, item['cost'] )
        worksheet.write_url(row, 3, item['webpage'], string='Web Page')
        worksheet.write_url(row, 4, item['pic'], string="Picture" )
        worksheet.write(row, 5, item['descr'] )
        worksheet.write(row, 6, item['createdt'] )

    workbook.close()

main(url)

Mục nhập mẫu này sẽ chạy chương trình python vào lúc 6:30 sáng hàng ngày.

30 6 * * * /usr/bin/python3 /home/normaluser/craigslist.py

Chương trình Python sẽ viết motorcycle.xlsxbảng tính ở định dạng /home/normaluser/.

Lấy lại báo cáo Excel

Trên Linux

Sử dụng scp để sao chép motorcycle.xlsxtừ máy từ xa đang chạy chương trình python của bạn sang máy này:

scp normaluser@<Linode Public IP>:/home/normaluser/motorcycle.xlsx .

Trên Windows

Sử dụng chức năng sftp tích hợp của Firefox. Nhập URL sau vào thanh địa chỉ và nó sẽ yêu cầu mật khẩu. Chọn bảng tính từ danh sách thư mục xuất hiện.

sftp://normaluser@<Linode Public IP>/home/normaluser