[Data Science][1] A tour on now.vn

pandas
beautiful-soup
re

#1

1. Lời giới thiệu

Source code: https://github.com/deepai-solutions/data-science/blob/master/DS1.A_tour_on_Now_vn.ipynb

Các bạn ở trong forum đều khá quen thuộc với các thuật ngữ như Machine Learning, DeepLearning hay tên các thuật toán, models. Tuy nhiên điều quan trọng nhất trong phân tích, xử lý, dự đoán,… đó là dữ liệu. Đặc biệt dữ liệu phải sạch, có nhãn,…

Trong thực tế, có được bộ dữ liệu như vậy là không hề đơn giản. Nếu như việc phân tích dữ liệu được chia thành 3 phần: Data Collection - Data Processing - Data Analysis thì hai phần đầu mất nhiều thời gian và công sức nhất. Trong series bài về Data Science này, tôi sẽ cùng các bạn đi từ đầu đến cuối, rất hi vọng sẽ mang lại cho các bạn ít nhất một cảm giác đó là “Phân tích dữ liệu thật là thú vị”.

2. Ngôn ngữ và thư viện

  • Ngôn ngữ: Python 3 - Ipython Notebook
  • Packages (update theo từng bài):
    • BeautifulSoup: download dữ liệu từ web
    • requests: gửi yêu cầu đến trang web
    • re: xử lý, làm sạch dữ liệu
    • pandas: tạo dữ liệu dạng bảng, dễ dàng cho qúa trình phân tích, model dữ liệu

Qua series bài này, các bạn sẽ hiểu thêm về sức mạnh của Python trong việc xử lý và phân tích dữ liệu.

3. Đặt vấn đề

Tôi là một người có niềm đam mê ăn uống, đang có cửa hàng (hoặc dự kiến mở cửa hàng) tôi muốn phân tích dữ liệu về các cửa hàng, các đối thủ để cho thể giúp tôi ra quyết định. Sau vài tìm kiếm đơn giản trên Google, tôi biết được rằng trang web: “https://www.now.vn” có rất nhiều dữ liệu mà tôi cần. Vậy tôi phải làm gì để có thể thu thập, phân tích dữ liệu đó?

Data Collection

Trước hết chúng ta sẽ thực hiện 1 lệnh tìm kiếm từ khóa “royaltea” trong ô tìm kiếm của “https://www.now.vn

Link tương ứng: “https://www.now.vn/ha-noi/danh-sach-dia-diem-giao-tan-noi?q=royaltea

Kết quả trả về là các quán trà sữa royaltea tại Hà Nội.

Giả sử chúng ta muốn tải thông tin của quán đầu tiên, ta sẽ thực hiện các bước sau:

  1. Tô đậm nơi muốn tải thông tin
  2. Click chuột phải, chọn Inspect

Tiếp theo chúng ta lựa chọn tag p và click:

Đến đây chúng ta có thể hình dung các thông tin hiển thị ở trên web sẽ được lưu trữ dưới các tags của html. Nếu chúng ta có 1 thư viện giúp load dữ liệu này về có cấu trúc tương tự thì có thể lấy được các thông tin này 1 cách tự động.

Đó chính là BeautifulSoup (bs)

from bs4 import BeautifulSoup 
import requests

## Load trang web, tách theo các thẻ tag như html
page = requests.get("https://www.now.vn/ha-noi/danh-sach-dia-diem-giao-tan-noi?q=royaltea")
soup = BeautifulSoup(page.content, 'html.parser')

## Save trang web dưới dạng text, để dễ dàng tìm kiếm các thẻ tag 
text_file = open("Output.txt", "w")
text_file.write(soup.prettify())
text_file.close()

Đến đây, nếu chung ta mở file Ouput.txt và tìm kiếm từ khóa “256” thì kết quả sẽ như sau:

Địa chỉ “256 Thái Hà,…” nằm trong tag ‘span’ (tag con);

tag cha của tag ‘span’ là tag ‘p’ có class =‘font14…’;

ngang hàng với tag ‘p’ này có tag ‘p’ khác đó là class =‘light-grey…’;

tag cha của tag ‘p’ là tag ‘div’ có class = “info-list-restaurant pull-left”

Điều đó có nghĩa là nếu chúng ta có thể truy cập đến tag cha , thì thêm 1 lần filter nữa, chúng ta có thể tách được thông tin của tag con

Để tìm kiếm tất cả các thẻ ‘div’ có class = “info-list-restaurant pull-left”, ta thực hiện câu lệnh như bên dưới

Lưu ý với BeautifulSoup(bs) :

- find_all: trả về tất cả giá trị có thể
- find: trả về giá trị đầu tiên
alls = soup.find_all('div',class_="info-list-restaurant pull-left")
print (alls[0])
<div class="info-list-restaurant pull-left">
<p class="light-grey uppercase mar0">Café/Dessert - Món Á</p>
<a class="name-restaurant bold capitalize" href="/ha-noi/royaltea-thai-ha"><h2>Royaltea - Thái Hà</h2></a>
<p class="font14 clearfix" style="margin:0 0 8px;">
<span>256 Thái Hà, Quận Đống Đa, Hà Nội</span>
</p>
<div class="clearfix" style="padding-bottom:10px;border-bottom:1px solid #e1e1e1;">
<p class="light-grey mar0 pull-left">
<i class="ico-deli ico-deli-time mar-right3"></i>
<span style="position:relative;top:-3px;font-size:15px;">08:30 - 22:30</span>
<span style="margin:0 3px;">|</span>
<i class="ico-deli ico-deli-money mar-right3"></i>
<span style="position: relative; top: -3px; font-size: 15px;">30,000 - 70,000</span>
</p>
</div>
<div class="info-delivery-restaurant clearfix">
<div class="column-info-deli pull-left">
<p class="mar0">
<span class="uppercase light-grey">tối thiểu</span>
<i class="ico-deli ico-deli-bill" style="margin-left:4px;position:relative;top:2px;"></i>
</p>
<span class="bold font15">-1 đ</span>
</div>
<div class="column-info-deli pull-left">
<p class="mar0">
<span class="uppercase light-grey">phí vận chuyển</span>
<i class="ico-deli ico-deli-money" style="margin-left:4px;position:relative;top:2px;"></i>
</p>
</div>
<div class="column-info-deli pull-left" style="margin-left:30px;">
<p class="mar0">
<span class="uppercase light-grey">Giao hàng</span>
<i class="ico-deli ico-deli-delivery" style="margin-left:4px;position:relative;top:2px;"></i>
</p>
<span class="font15"><span style="color: #D52F33;font-weight: bold;">Now.vn</span></span>
</div>
</div>
<a class="waves-effect btn-view-restaurant border-radius4" href="/ha-noi/royaltea-thai-ha">xem thực đơn</a>
</div>

Đến đây, nếu chúng ta muốn lấy thông tin địa chỉ quán, chỉ cần:

alls[0].find('span').get_text()

‘256 Thái Hà, Quận Đống Đa, Hà Nội’

Nếu muốn lấy thông tin tên quán thì chúng ta có thể dùng lệnh ‘find’ hoặc sử dụng câu lệnh ‘select’ của bs

  • select: ví dụ select(‘a h2’): lựa chọn tất cả các dòng có chứa đồng thời 2 thẻ a và h2
alls[0].select('a h2')[0].get_text()

‘Royaltea - Thái Hà’

Nếu muốn lấy thông tin hyperlink, chúng ta sẽ sử dụng câu lệnh, ‘find’ và thêm method ‘get’

alls[0].find('a').get('href')

Lưu ý là chúng ta đang sử dụng phần tử đầu tiên alls[0], nếu lặp lại toàn bộ với các phần tử khác, chúng ta sẽ lấy được toàn bộ thông tin của các quán khác.

Cuối cùng, để có thể lưu trữ, visualize, xử lý dữ liệu, chúng ta sử dụng dataframe của pandas

4. Final Code 1

from bs4 import BeautifulSoup 
import requests
import re
import pandas as pd
def dataframe_keyword(location,keyword):
    """
    input:
        vitri: ho-chi-minh, ha-noi,...
        keyword: tra+sua,royaltea,com+tam,...
        
    output:
        dataframe bao gom:
            ten_quan:
            dia_chi:
            hyper_link:
    """
    prefix = 'https://www.now.vn'
    ten_quan = []
    dia_chi = []
    link = []
    for i in range(1,100):
        url = 'https://www.now.vn/%s/danh-sach-dia-diem-giao-tan-noi-trang-%s?q=%s'%(location,i,keyword)
        page = requests.get(url)
        soup = BeautifulSoup(page.content, 'html.parser')
        alls = soup.find_all('div',class_="info-list-restaurant pull-left")
        if len(alls) == 0:
            break
        else:
            for a in alls:
                ten_quan.append(a.select('a h2')[0].get_text())
                dia_chi.append(a.find('span').get_text())
                link.append(prefix + a.find('a').get('href'))
    data_frame = pd.DataFrame({
        'ten_quan': ten_quan,
        'dia_chi': dia_chi,
        'link': link,})
    return data_frame

royaltea_ha_noi = dataframe_keyword(‘ha-noi’,‘royaltea’)

Dataframe

royaltea_ha_noi

Tuy nhiên Tiếng Việt xử lý khá khó, nên chúng ta sẽ chuyển toàn bộ về tiếng Việt không dấu với source code open trên Github

def no_accent_vietnamese(utf8_str):
    """
    ref: https://gist.github.com/thuandt/3421905#file-no_accent_vietnamese-py
    input: "Tôi yêu Việt Nam"
    output: "Toi yeu Viet Nam"
    """
    INTAB = "ạảãàáâậầấẩẫăắằặẳẵóòọõỏôộổỗồốơờớợởỡéèẻẹẽêếềệểễúùụủũưựữửừứíìịỉĩýỳỷỵỹđẠẢÃÀÁÂẬẦẤẨẪĂẮẰẶẲẴÓÒỌÕỎÔỘỔỖỒỐƠỜỚỢỞỠÉÈẺẸẼÊẾỀỆỂỄÚÙỤỦŨƯỰỮỬỪỨÍÌỊỈĨÝỲỶỴỸĐ"
    #INTAB = [ch.encode('utf8') for ch in unicode(INTAB, 'utf8')]
    OUTTAB = "a" * 17 + "o" * 17 + "e" * 11 + "u" * 11 + "i" * 5 + "y" * 5 + "d" + \
         "A" * 17 + "O" * 17 + "E" * 11 + "U" * 11 + "I" * 5 + "Y" * 5 + "D"
    r = re.compile("|".join(INTAB))
    replaces_dict = dict(zip(INTAB, OUTTAB))
    return r.sub(lambda m: replaces_dict[m.group(0)], utf8_str)
no_accent_vietnamese("Tôi yêu Việt Nam")

‘Toi yeu Viet Nam’

Chúng ta sẽ sử dụng method “apply” với ánh xạ “lambda” để thực hiện đồng loạt cho tất cả các hàng trong 1 cột của dataframe

royaltea_ha_noi['dia_chi_new'] = royaltea_ha_noi['dia_chi'].apply(lambda x:no_accent_vietnamese(x))
royaltea_ha_noi['ten_quan_new'] = royaltea_ha_noi['ten_quan'].apply(lambda x:no_accent_vietnamese(x))
royaltea_ha_noi

5. Thu thập dữ liệu mỗi quán

Nhà mình ở đường Trường Chinh, vì vậy mình quyết định tìm hiểu quán xem có quán royaltea nào ở Trường Chinh không.

Và nếu có thì giá thành, sản lượng bán ra sao.

Để làm điều đó, mình sẽ tìm xem ‘Truong Chinh’ có nằm trong địa chỉ cuả quán nào không bằng câu lệnh sau:

tc = royaltea_ha_noi[royaltea_ha_noi['dia_chi_new'].str.contains('Truong Chinh')]
tc

Vậy là có đến 2 quán royaltea ở gần nhà mình. Chúng ta thử tiếp tục phân tích 1 quán xem sao.

tc.loc[28]

Bằng cách tư duy và xử lý tương tự như trên. Chúng ta có thể collect được dữ liệu của quán 379 Trường Chinh bao gồm tên các món, giá bán mỗi món, số lần được đặt trên https://www.now.vn bằng hàm sau đây

def dataframe_res(url):
    """
    input: url quán
    ouput: tên món, số lần được đặt, giá bán
    """
    ten_mon = []
    so_lan_dat = []
    gia_ban = []
    page = requests.get(url)
    soup = BeautifulSoup(page_.content, 'html.parser')
    all_menu = soup.find_all('div',class_="name-food-detail pull-left")
    for t in all_menu:
        ten_mon.append(t.select('h3')[0].get_text().strip())
        so_lan_dat.append(int(re.findall(r'\d+',t.select('p')[0].get_text().strip())[0]))
    all_price = soup.find_all('div',class_="product-price")
    for p in all_price:
        gia_ban.append(float(re.findall(r'\d+',p.find_all('span',class_=True)[0].get_text())[0]))
    dataframe = pd.DataFrame({
    'ten_mon': ten_mon,
    'so_lan_dat': so_lan_dat,
    'gia_ban': gia_ban})
    return dataframe

Hàm trên sử dụng thêm 1 package “re” để có thể filter dữ liệu theo mong muốn của chúng ta

Ví dụ: re.findall(r’\d+’,…): Tìm tất cả số trong chuỗi string

royaltea_379tc = dataframe_res(tc.loc[28]['link'])
royaltea_379tc

Wow! Không ngờ có đến 90 món trà sữa khác nhau. Chúng ta thử sắp xếp dữ liệu 1 chút để xem món nào đắt nhất, món nào bán được nhiều nhất

Chúng ta sẽ chuyển toàn bộ tên các món về Tiếng Việt không dấu để dễ xử lý về sau

royaltea_379tc['ten_mon_new'] = royaltea_379tc['ten_mon'].apply(lambda x:no_accent_vietnamese(x))
royaltea_379tc[:5]

Món được đặt nhiều nhất

royaltea_379tc.sort_values(['so_lan_dat'],ascending=False)

Vâng. Trà Royal Xoài Kem Cheese có đến 542 lượt đặt. Món này nhất định rất ngon!

Đến đây chúng ta phát hiện ra 1 điều khá lạ lùng, tất các các món đều bị tính 2 lần. Chắc chắn có phần code chưa được tối ưu trong phần collection phía trên def dataframe_res(url)( xin được dành cho bạn đọc, rất mong các bạn có thể comment code tối ưu ở bên dưới). Tôi sẽ sử dụng method loại bỏ duplicates sẵn có của pandas

royaltea_379tc_drop = royaltea_379tc.drop_duplicates(subset=None, keep='first', inplace=False)
royaltea_379tc_drop

Món đắt nhất

royaltea_379tc_rop.sort_values(['gia_ban'],ascending=False)

Trong 3 món đắt nhất, Hoa quả đặc biệt là món có số lượng đặt không hề ít chút nào.


#2

Mình góp ý chút là title nên đặt khác chút cho gần với nội dung bài viết. Chẳng hạn “[…] Hướng dẫn crawl dữ liệu trên now.vn dùng xpath và BeutifulSoup”.


#3

Mình thấy topic này hay mà ít ai quan tâm nhỉ :open_mouth: Mình có chút đống góp: đối với những bạn bị lỗi Encode (khi chạy python thuần) thì có thể thay đoạn đầu của code bằng đoạn code sau

## Save trang web dưới dạng text, để dễ dàng tìm kiếm các thẻ tag 
text_file = open("Output.txt", "wb")
text_file.write(soup.prettify(encoding='utf-8'))
text_file.close()

#4

Cho em hỏi, ngoài việc phải đổi thành tiếng Việt không dấu, thì còn cách nào để xử lý không? Trong một số bài có tag VNLP lần trước có một số kỹ thuật thì có thể sử dụng vào xử lý được hay không ạ?


#5

Cái này tùy theo mục đích bài toán của mình là gì em à. Ví dụ nếu em tiếp tục phân tích comment của người dùng về mỗi quán trên now.vn để xem thái độ của họ ra sao thì em cần phải có phần xử lý tiếng việt như [VNLP CORE]. Nếu có thể em hãy giúp anh làm tiếp phần crawl comment, sau đó mình cùng phân tích tiếp.


#6

Em đang làm phần crawl nhưng về chủ đề tin tức. Em sẽ thử và đưa code của em lên để mọi người chỉ bảo thêm. Em đặt câu hỏi trên vì đề bài của em liên quan đến phân tích nội dung của bài báo để phân loại tin giả. Em thấy nếu mình chuyển qua tiếng Việt không dấu thì phù hợp các mô hình phân loại không dựa trên ngữ nghĩa hơn


#7

Mọi người cho e hỏi ạ , e chạy lại code nhưng tại sao chạy mà ko load được dữ liệu :’’