Chào các bạn đã quay lại với Series mang tên: Selenium không khó của Lập trình không khó. Ở bài viết trước mình đã hướng dẫn các bạn cách để tự động đăng nhập facebook rồi. Trong bài hôm nay, mình sẽ hướng dẫn các bạn xây dựng ứng dụng đầu tiên: tự động đăng nhập facebook với Selenium. Trong bài thứ 3 này mình sẽ chia sẻ tới các bạn ý tưởng và cách để lấy tất cả url video của một playlist trên youutube nhé.
Một số lưu ý trước khi thực hiện
- Youtube là một mạng xã hội video lớn và chủ sở hữu của nó là Google. Do đó, việc chúng ta thu thập dữ liệu trên trang này sẽ gặp đôi chút khó khăn hơn so với các website thông thường khác.
- Hiện tại, theo mình kiểm tra thì youtube không biết bằng cách nào họ chặn việc thực thi các lệnh javascript của chúng ta trên trang của họ. Vấn đề này ảnh hưởng ra sao lát nữa mình sẽ phân tích cho các bạn.
- Nếu chúng ta muốn code một lần và dùng lâu dài, hãy nghĩ đến trường hợp website của họ thay đổi cấu trúc html. Khi đó, code của chúng ta sẽ ít nhiều bị ảnh hưởng, khả năng làm lại từ đầu là rất cao.
Thế nào là một playlist trên Youtube?
Một playlist trên Youtube là một danh sách video do người dùng tạo ra, các video trong playlist này thường là các video của một series hoặc có cùng nội dung. Mục đích của playlist là để người dùng tiện theo dõi các video liên quan của một channel.
Dưới đây là một hình ảnh một playlist video:
Khi bạn vào trang playlist của Youtube, bạn sẽ nhìn thấy địa chỉ URL của trình duyệt đang hiển thị có dạng sau:
https://www.youtube.com/playlist?list=<play_list_id> # Ví dụ: https://www.youtube.com/playlist?list=PLh91SaQgRYnrFE9CMxpaN2x6zDc5vgb5E
Ở đây, bạn chú ý cho mình <play_list_id> nhé, vì chúng ta sẽ sử dụng nó làm input của bài toán lấy tất cả url video của một playlist trên youutube.
Khó khăn khi giải quyết bài toán này
Trong quá trình mình làm, mình gặp phải những khó khăn sau đây:
- Trang playlist không hiển thị toàn bộ video trong playlist đó sau khi tải trang, phải cuộn xuống thì nó mới hiển thị tiếp.
- Trang playlist của Youtube cũng như hầu hết các website khác, họ đều sử dụng JavaScript và dùng Infinity loading để tải và hiển thị dữ liệu. Cơ chế của Infinity loading là chỉ load khi người dùng cuộn tới cuối trang, tức là nếu không cuộn thì không load ra.
- Trang này chặn excute javascript, nếu họ không chặn thì có lẽ có nhiều cách làm hơn(cái này mới họ mới thêm vào gần đây thôi)
Ý tưởng lấy url video playlist trên Youtube
Ý tưởng thực hiện
Ở đây mình có biết có 2 cách khác nhau. Nhưng mình sẽ hướng dẫn sử dụng Selenium để lấy. Vì nó liên quan tới series này, và bản thân mình không thích dùng API của Google cho lắm 🙁
Sử dụng Youtube Data API
Các bạn tự trải nghiệm nha. Chỉ cần search youtube data api và đọc hướng dẫn của họ để làm hoặc tìm các sample mà mọi người hướng dẫn.
Sử dụng Selenium + ý tưởng táo bạo
Chúng ta là sẽ cuộn đến cuối trang của playlist cho tới khi nào không thể cuộn được nữa. Khi đó, chúng ta sẽ có toàn bộ source code của trang playlist với danh sách đầy đủ tất cả các video trong playlist này.
Sau khi có source code thì bạn chỉ cần tìm cách lấy hết các url video ra là được. Ở đây mình dùng regex cho nhanh. Sau đó bạn có thể lọc trùng lặp nếu cần(sử dụng cấu trúc dữ liệu set)
Làm sao để Selenium cuộn trang?
Cho tới hiện tại mình biết và đã sử dụng hai cách sau đây. Cách đầu tiên là cách dùng Javascript để cuộn trang. Mặc dù nó đã bị chặn bởi Youtube, nhưng các website khác thì không hề nha.
Sử dụng Javascript để cuộn trang
Nếu bạn nào có kinh nghiệm sử dụng Javascript ắt hẳn sẽ biết lệnh này dùng làm gì?
window.scrollTo(0, document.body.scrollHeight);
Bạn hãy thử vào Facebook, mở devtools(Ctrl Shift I) và dán lệnh này vào tab Console xem nhé. Hoặc vào bất cứ trang nào mà chúng ta có thể cuộn trang ấy.
Full source code cho phương án này như sau:
- Cuộn tới cuộn trang sử dụng lệnh Javascript phía trên
- Chờ vài giây cho nó tải dữ liệu
- Kiểm tra xem body.scrollHeight hiện tại với body.scrollHeight trước đó có bằng nhau không. Nếu có nghĩa là cuối trang rồi, hoàn thành. Nếu không thì lại quay lại bước 1.
SCROLL_PAUSE_TIME = 0.5 # Get scroll height last_height = driver.execute_script("return document.body.scrollHeight") while True: # Scroll down to bottom driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # Wait to load page time.sleep(SCROLL_PAUSE_TIME) # Calculate new scroll height and compare with last scroll height new_height = driver.execute_script("return document.body.scrollHeight") if new_height == last_height: break last_height = new_height
Sử dụng bàn phím để cuộn trang
Như mình đã nói, cách trên rất hay nhưng đã bị Youtube chặn, bạn thử sẽ thấy trang nó không hề cuộn. Nhưng Youtube không chặn bàn phím được. Và chúng ta có 2 phím để cuộn trang là PAGE_DOWN và PAGE_UP
Như vậy, ta sẽ cho Selenium thực thi giả lập nhấn phím PAGE_DOWN thay cho lệnh Javascript phía trên. Còn về quy trình và các bước thì vẫn giống y hệt.
Còn nữa, do chúng ta không execute script được nên không lấy được scrollHeight. Do đó, mình sẽ thay bằng việc đếm số video hiện đang hiện thị trên trình duyệt trước và sau khi cuộn trang để kiểm tra xem đã load hết video chưa.
Làm sao để lấy video url từ html code?
Sau khi cuộn tới cuối trang, chúng ta hoàn toàn có thể lấy source code của trang ở thời điểm đã hiện thị toàn bộ playlist lên trình duyệt. Việc tiếp theo là lấy các video url!
Nhận thấy các video url có video_id là 11 ký tự. Và các đường dẫn ở thời điểm hiện tại có thể match theo regex sau:
RE_VIDEO_ID = re.compile(""/watch?v=(.{11})")
Ở đây, mình sẽ chỉ cần lấy ID của video thôi, sau đó sẽ lắp ghép sau theo cú pháp này:
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v={}" # {} chính là chỗ điền video_id
Có thể còn nhiều cách khác nhau, bao gồm cả việc bóc tách html. Các bạn có thể thử thêm để học được nhiều hơn nhé.
Kết luận
Với ý tưởng này thì dù cho Youtube có thay đổi cấu trúc html thì chúng ta vẫn lấy được các url video như thường. Nhưng với điều kiện là bạn bóc tách các url ở bước tiếp theo sử dụng regex như mình có nói ở trên. Chứ nếu mà bóc tách html như bài hướng dẫn số 2 thì code sẽ phải sửa lại khi html của trang bị đổi.
Code lấy tất cả url video của playlist trên Youtube
Các bạn lưu ý code này chỉ hỗ trợ cho Python 3+. Mình chưa thử trên phiên bản Python 2 nên có thể sẽ phát sinh lỗi nếu bạn dùng Python phiên bản này.
Trong bài này mình giả sử các bạn đã đọc qua hết 2 bài hướng dẫn đầu tiên và đã làm thành công nhé. Nếu các bạn gặp lỗi gì thì hãy để lại comment mình sẽ giải đáp cho các bạn.
Cách chạy:
############################################### # Cách chạy: python3 GetAllVideoIDFromPlaylist.py <playlist_id> <output_file_path> # Cách tìm PLAYLIST_ID:[wpcc-iframe loading="lazy" title="Đừng Coi Thường Người Khác Qua Vẻ Bề Ngoài" width="720" height="405" src="https://www.youtube.com/embed/videoseries?list=PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen]=> Playlist ID: PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli # Ví dụ: python3 GetAllVideoIDFromPlaylist.py PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli output.txt ###############################################
Source code:
import re import sys import time from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.webdriver.firefox.options import Options YOUTUBE_PLAYLIST_URL = "https://www.youtube.com/playlist?list={}" YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v={}" RE_VIDEO_ID = re.compile(""/watch?v=(.{11})") class GetAllVideoID: def __init__(self): options = Options() # options.add_argument('--headless') self.driver = webdriver.Firefox(options=options) def get_all_video(self, channel_id): self.driver.get(YOUTUBE_PLAYLIST_URL.format(channel_id)) num_video = 0 body = self.driver.find_element_by_css_selector('body') while True: # Scroll down to the bottom. for i in range(5): body.send_keys(Keys.PAGE_DOWN) body.send_keys(Keys.PAGE_DOWN) body.send_keys(Keys.PAGE_DOWN) time.sleep(1) new_num_video = len(self.driver.find_elements_by_css_selector("#contents > ytd-playlist-video-renderer")) # print(str(num_video), str(new_num_video)) if num_video == new_num_video: break num_video = new_num_video ids = set(re.findall(RE_VIDEO_ID, self.driver.page_source)) return list(ids) if __name__ == '__main__': USAGE = """ ############################################### # Cách chạy: python3 GetAllVideoIDFromPlaylist.py <playlist_id> <output_file_path> # Cách tìm PLAYLIST_ID:[wpcc-iframe loading="lazy" title="Đừng Coi Thường Người Khác Qua Vẻ Bề Ngoài" width="720" height="405" src="https://www.youtube.com/embed/videoseries?list=PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen]=> Playlist ID: PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli # Ví dụ: python3 GetAllVideoIDFromPlaylist.py PLtti_OfloMJKtvnkuHwpAoulSg_cNcMli output.txt ############################################### """ channel_id = '' output_file = '' getAllVideo = GetAllVideoID() if len(sys.argv) < 3: print(USAGE) sys.exit(1) try: channel_id = sys.argv[1] output_file = sys.argv[2] ids = getAllVideo.get_all_video(channel_id) with open(output_file, 'w', encoding='utf8') as fp: for id in ids: fp.write(YOUTUBE_VIDEO_URL.format(id) + "n") except: sys.exit(2) finally: getAllVideo.driver.close()
Lưu ý: Để ẩn trình duyệt khi chạy code Selenium thì bạn bỏ comment dòng code này nhé:
# options.add_argument('--headless')
File kết quả sau khi chạy trông như thế này đây.
Giờ bạn có thể dùng youtube-dl để download chúng về đơn giản rồi.
Để lại một bình luận