Hướng dẫn python crawler tutorial

Chào các bạn! Lại là mình đây [Thật ra đây là lần đầu tiên mình viết bài]

Hôm nay mình sẽ cùng các bạn tìm hiểu về một Module khá phổ biến của Python đó là Request và cùng sử dụng nó để làm một tool crawl dữ liệu nhé. Đầu tiên xem thử sau bài này chúng ta sẽ làm được gì nào.

Sau bài viết này chúng ta sẽ crawl được tất cả những tin tức mới từ một trang báo điện tử và sử dụng chúng để tạo ra những mẫu tin tức nhanh như trên ảnh với 4 bước chính:

  1. Cài đặt module
  2. Lấy danh sách tin tức mới nhất
  3. Lấy dữ liệu tin tức chi tiết từng bài
  4. Tạo những mẫu tin tức nhanh từ dữ liệu ở trên

Cùng bắt đầu thôi!!

1. Cài đặt Module [Hướng dẫn trên cmd Window]

  • Cài đặt Requests: 
    pip install requests [hoặc python –m pip install requests]​
  • Cài đặt Pilow: 
    pip install Pillow [hoặc python –m pip install Pillow]​

*Note: Nếu xài PIP cũ thì mọi người update lên pip mới trước khi cài Pillow nhé:

  • Update PIP: 
    pip install -–upgrade pip [hoặc python –m pip install -–upgrade pip]

Trong quá trình cài đặt nếu có lỗi gì thì mọi người post lên để cùng tìm cách fix nhé!

2. Cào dữ liệu danh sách tin tức mới

2.1. Lấy dữ liệu

Hiểu nôm na thì module Request dùng để gửi HTTP request, giống như thao tác bạn thường làm khi lướt mạng : Vào trình duyệt gõ codelearn.io và enter, bạn sẽ nhận được giao diện của trang web hoặc một dạng dữ liệu khác. Để lấy được dữ liệu trả về thì ta phải sử dụng một module hỗ trợ và Request sẽ giúp chúng ta làm điều đó. Cùng nhau tìm hiểu các dùng nhé !

requests.method[url, params, data, json, headers, cookies, files, auth, timeout, allow_redirects, proxies, verify, stream, cert]

What ?? Cần nhiều tham số thế cơ á ? Không, chúng ta chỉ cần lấy dữ liệu từ một trang tin tức thôi, không cần gửi đi dữ liệu gì cả. Hãy thử thế này nhé.

import requests

response = requests.get["//tuoitre.vn/tin-moi-nhat.htm"]
print[response]

Và đây là kết quả :

Thật là No hope , chúng ra đang cần một trang web cơ mà. Thử gọi vài thuộc tính ra thử xem nào :

print[response.content]

Kết quả:

\r\n    \r\n    Tin m\xe1\xbb\x9bi nh\xe1\xba\xa5t - Tu\xe1\xbb\x95i tr\xe1\xba\xbb Online
…[còn nữa>…

Chúng ra đã lấy được dữ liệu của một trang web, vấn đề bây giờ là tách dữ liệu.

2.2. Tách dữ liệu

Có nhiều cách để bóc tách dữ liệu từ một văn bản dài, sử dụng regex [biểu thức chính quy] cũng là một cách nhưng thực tế thì python đã hỗ trợ mạnh hơn. Cùng tìm hiểu về module beautifulSoup4 nhé.

Cách cài đặt:

pip install beautifulsoup4 [hoặc python –m pip install beautifulsoup4

Beautiful Soup sẽ giúp chúng ta phân tích dữ liệu HTML hay XML thành dữ liệu cây, từ đó giúp chúng ta truy xuất dữ liệu dễ dàng hơn. Cùng test thử nhé

import requests
from bs4 import BeautifulSoup

response = requests.get["//tuoitre.vn/tin-moi-nhat.htm"]
soup = BeautifulSoup[response.content, "html.parser"]
print[soup]

Thành quả là bạn sẽ được in ra một trang html rất gọn gàng như thế này:

2.3. Phân tích dữ liệu

Bước tiếp theo là chúng ta cần phân tích xem dữ liệu cần lấy ở đâu. Rõ ràng để lấy dữ liệu chi tiết một bài báo ta cần liên kết đến bài đó nhỉ. Bật f12 lên và phân tích chút:

Sau thời gian mò mẫm, chúng ta có thể tìm thấy link bài báo ở trong thẻ và thẻ này nằm trong thẻ h3 có class “title-news”. Vậy công việc của chúng ra là lọc tất cả thẻ h3 có class “title-news” và lấy thẻ a trong nó, cùng code tiếp nào:

titles = soup.findAll['h3', class_='title-news']
print[titles]

Sau khi thêm đoạn này chúng ta sẽ được một mảng các thẻ h3 là tiêu đề bài báo:

Tiếp tục công việc tiếp theo là lấy link của tất cả các bài viết đó:

links = [link.find['a'].attrs["href"] for link in titles]
print[links]

Kết quả:

['/moi-cac-truong-tham-gia-cam-nang-tuyen-sinh-dh-cd-hau-covid-19-tang-thi-sinh-20200605093106804.htm', '/truong-tre-vao-top-10-cong-bo-quoc-te-va-nghi-van-mua-bai-bao-khoa-hoc-2020060509193892.htm', '/nghi-si-9-nuoc-lap-lien-minh-dua-ra-lap-truong-cung-ran-hon-voi-trung-quoc-20200605084031973.htm', '/mo-tinh-xuat-huyet-nao-20200605092647937.htm',…]

Một mảng các liên kết, khá hợp lí. Giải thích một chút nhé: Ở trên để tìm tất cả các thẻ h3 nên ta sử dụng hàm findAll[] thõa mãn class truyền vào, nhưng ở dưới chỉ cần tìm duy nhất 1 thẻ a trong h3 nên ta chỉ cần dùng lệnh find[“a”] và dùng attrs[“thuoc_tinh”] để lấy thuộc tính của thẻ a.

3. Lấy dữ liệu chi tiết từng bài

OK coi như chúng ta đã lấy được tất cả bài viết. Công việc tiếp theo là truy cập từng bài viết, lấy một ảnh làm đại diện và một đoạn trích ngắn. Do phần này tương tự trên nên mình lướt nhanh một chút:

for link in links:
    news = requests.get["//tuoitre.vn" + link]
    soup = BeautifulSoup[news.content, "html.parser"]
    title = soup.find["h2", class_="article-title"].text
    abstract = soup.find["h2", class_="sapo"].text
    body = soup.find["div", id="main-detail-body"]
    content = body.findChildren["p", recursive=False][0].text +      body.findChildren["p", recursive=False][1].text
    image = body.find["img"].attrs["src"]
    print["Tiêu đề: " + title]
    print["Mô tả: " + abstract]
    print["Nội dung: " + content]
    print["Ảnh minh họa: " + image]
    print["_________________________________________________________________________"]

Vậy là xong phần crawl dữ liệu rồi. Cũng đơn giản phải không? Mình sẽ giải thích chút xíu

Đầu tiên chúng ta dùng một vòng for-loop để duyệt qua tất cả các link và truy cập các link đó, các bạn chú ý do href của thẻ a sẽ không có link gốc [dạng “/router-ne”] nên chúng ta cần chèn thêm BASE URL vào nhé :

requests.get["//tuoitre.vn" + link]

Ở bước lấy title, tóm tắt và ảnh. Bạn bật f12 lên tìm hiểu một tí là ra. Còn phần content mình cần tìm 2 thẻ p con chỉ dưới 

một cấp nên ta sẽ có tham số recursive như sau:

body.findChildren["p", recursive=False]

Thành quả

Kết quả đã có, bây giờ chúng ta sẽ xây dựng một hàm crawNewsData[] để tiện cho việc tái sử dụng nhé, hàm này sẽ nhận vào url gốc, url đến nơi lấy bài và trả về một list các bài viết gồm tiêu đề, tóm tắt, nội dung và một bức ảnh đại diện.

def crawNewsData[baseUrl, url]:
    response = requests.get[url]
    soup = BeautifulSoup[response.content, "html.parser"]

    titles = soup.findAll['h3', class_='title-news']
    links = [link.find['a'].attrs["href"] for link in titles]
    data = []
    for link in links:
        news = requests.get[baseUrl + link]
        soup = BeautifulSoup[news.content, "html.parser"]
        title = soup.find["h2", class_="article-title"].text
        abstract = soup.find["h2", class_="sapo"].text
        body = soup.find["div", id="main-detail-body"]
        content = ""
        try:
            content = body.findChildren["p", recursive=False][0].text + body.findChildren["p", recursive=False][1].text
        except:
            content = ""
        image = body.find["img"].attrs["src"]
        data.append[{
            "title": title,
            "abstract": abstract,
            "content": content,
            "image": image,
        }]
    return data

4. Trình bày tin tức trên một bức ảnh

Để trình bày nội dung lên một bức ảnh, chúng ta sẽ dử dụng module Pillow. Pillow là module hỗ trợ xử lí file ảnh khá thân thiện và dễ sử dụng. Bắt tay vào làm luôn nhé:

Test thử vài chức năng mà chúng ta sẽ sử dụng nào:

from PIL import Image, ImageDraw, ImageFont

img = Image.new['RGB', [650, 625], color="white"]
font = ImageFont.load_default[]

d = ImageDraw.Draw[img]
d.text[[10, 10], "Hello World", font=font, fill="black"]

img.show[]

Ok vậy là chúng ra đã tạo được một bức ảnh và viết chữ lên nó. Tiếp theo là chèn một bức ảnh vào bức ảnh khác và viết chữ Tiếng Việt lên:

img = Image.new['RGB', [650, 625], color="white"]
font = ImageFont.truetype["font/arial.ttf", 12]

d = ImageDraw.Draw[img]

addImg = Image.open["anh.jpg"]

img.paste[addImg, [10, 10]]

d.text[[300, 10], "Cái text nè ahihi", font=font, fill="black"]

img.show[]

Chúng ta sử dụng Image.open[] để mở thêm một ảnh và phương thức paste để chèn một ảnh vào ảnh khác với tham số truyền vào là ảnh được open ở trên và tuple chỉ tọa độ sẽ chèn.

Lưu ý: là để draw được Tiếng Việt thì bạn cần phải load font có hỗ trợ Tiếng Việt vào như đoạn trên, cái path tới font ấy bạn có thể dẫn tới font đang lưu trong hệ thống hoặc tạo một folder font cùng cấp với file code để lưu font như mình nhé.

Bước cuối cùng:

Mình sẽ tạo bố cục bài báo như thế này, mọi người có thể tùy sức sáng tạo theo ý mình nhé, code thôi nào.

Đầu tiên các bạn sẽ thấy công việc chèn chữ lên ảnh sẽ được lặp đi lặp lại nên mình sẽ viết riêng một hàm để chèn chữ nhé:

def writeToImage[image, text, position, font, color, maxLine]:
    charPerLine = 650 // font.getsize['x'][0]
    pen = ImageDraw.Draw[image]
    yStart = position[1]
    xStart = position[0]
    point = 0
    prePoint = 0
    while point < len[text]:
        prePoint = point
        point += charPerLine
        while point < len[text] and text[point] != " ":
            point -= 1
        pen.text[[xStart, yStart], text[prePoint:point], font=font, fill=color]
        yStart += font.getsize['hg'][1]
        maxLine -= 1
        if [maxLine == 0]:
            if [point < len[text]]:
                pen.text[[xStart, yStart], "...", font=font, fill="black"]
            break

Ơ… sao phức tạp thế nhỉ?? Hàm để ghi chữ lúc nãy ngắn lắm cơ mà. Thực ra chúng ta còn một vấn đề khi chèn chữ đó là khi bạn ghi một chuỗi dài, nó không biết tự động xuống dòng khi vượt ra ngoài bức ảnh. Chính vì vậy chúng ta sẽ xử lí theo hướng sau:

  • Biến charPerLine sẽ lưu số kí tự có thể chứa trên 1 dòng bằng cách lấy độ dài trang chia cho độ dài 1 kí tự, từ đó ta sẽ cắt chuỗi với mỗi chuỗi có charPerLine kí tự hoặc bé hơn
  • 2 biến prePoint và point sẽ lưu điểm đầu và điểm cuối của dòng được cắt
  • Sau khi in một dòng, chúng ta sẽ cộng tọa độ y thêm 1 đoạn đúng bằng độ cao 1 dòng chữ.
  • Trường hợp một nội dung quá dài, ta cũng chỉ in maxLine dòng rồi in thêm dấu …

Hợp lý hơn rồi đó. Đến phần cuối thôi:

def makeFastNews[data]:
    for index, item in enumerate[data]:
        # make new image and tool to draw
        image = Image.new['RGB', [650, 750], color="white"]
        pen = ImageDraw.Draw[image]
        # load image from internet => resize => paste to main image
        pen.rectangle[[[0, 0], [650, 300]], fill="grey"]
        newsImage = Image.open[requests.get[item["image"], stream=True].raw]
        newsImage.thumbnail[[650, 300], Image.ANTIALIAS]
        image.paste[newsImage, [650 // 2 - newsImage.width // 2, 300 // 2 - newsImage.height//2]]
        ## write title
        titleFont = ImageFont.truetype["font/arial.ttf", 22]
        writeToImage[image, item["title"], [10, 310], titleFont, "black", 3]
        abstractFont = ImageFont.truetype["font/arial.ttf", 15]
        writeToImage[image, item["abstract"], [10, 390], abstractFont, "gray", 4]
        contentFont = ImageFont.truetype["font/arial.ttf", 20]
        writeToImage[image, item["content"], [10, 460], contentFont, "black", 11]
        name = "news-" + str[index] + ".png"
        image.save["news/" + name]
        print["saved to " + "news/" + name]

Đây là đoạn code cuối cùng, mình xin giải thích chút nhé:

  • Hàm này sẽ nhận dữ liệu mình vừa crawl về lúc nãy.
  • Chúng ta sẽ dùng một vòng for để duyệt hết giá trị trong data, nhưng dùng thêm hàm enumerate[] để có thể lấy được chỉ số, để lát nữa mình tạo tên file khỏi bị trùng nhé.
# load image from internet => resize => paste to main image
        pen.rectangle[[[0, 0], [650, 300]], fill="grey"]
        newsImage = Image.open[requests.get[item["image"], stream=True].raw]
        newsImage.thumbnail[[650, 300], Image.ANTIALIAS]
        image.paste[newsImage, [650 // 2 - newsImage.width // 2, 300 // 2 - newsImage.height//2]]

Đoạn này sẽ xử lí phần chèn ảnh bài báo, chúng ra dùng pen.rectangle[] để vẽ một hình chữ nhật làm nền, chúng ta sẽ truyền vào tọa độ góc trên, chiều dài, rộng và màu.

Tiếp theo vì ảnh trên mạng sẽ có kích thước tùy ý nên chúng ta sẽ chuyển nó về ảnh thumbnail để dễ dàng chèn vào.

Mình vẫn dùng request.get[] để lấy dử liệu là một bức ảnh và thuộc tính raw để lấy file raw truyền vào  Image.open[]

Tiếp theo chúng ta sẽ dùng lệnh này để resize bước ảnh theo ý muốn vẫn giữ nguyên tỉ lệ

newsImage.thumbnail[[650, 300], Image.ANTIALIAS]

Và phần còn lại chỉ là gọi hàm: writeToImage[] để ghi tiêu đề, tóm tắt và nội dùng

Cuối cùng chúng ra dùng phương thức Image.save[fileName] để lưu ảnh

Bạn có thể tạo một folder để lưu những file ảnh tạo ra như mình và tạo tên ảnh bằng những index lúc nãy để tránh trùng tên: 

fileName = "folder_ban_tao/" + "news-" + str[index] + ".png"

Thành quả

Cuối cùng cũng đã xong, và đây là thành quả. 

Trong quá trình thực hiện nếu có lỗi gì thì mọi người cùng comment lên để thảo luận nhé. Thank for reading

Chủ Đề