Tôi đang thực hiện một sản phẩm tìm kiếm và muốn hiểu thêm về công nghệ tìm kiếm. Thỉnh thoảng tôi cũng phân tích dữ liệu và văn bản, vì vậy năm nay tôi đặt mục tiêu học Python cho bản thân, tập trung vào lập trình thu thập dữ liệu (web scraping) và phân tích dữ liệu.
Trong thời gian đại học, tôi đã học qua một ít ngôn ngữ C, mặc dù chưa bao giờ viết được một chương trình hoàn chỉnh, nhưng tôi nắm rõ các khái niệm cơ bản của lập trình như biến, hàm, kiểu dữ liệu và đối tượng. Năm ngoái tôi định học Python, đã xem qua cú pháp cơ bản nhưng mỗi khi cố gắng viết một chương trình hoàn chỉnh thì lại thấy khó khăn và từ bỏ. Vì vậy lần này tôi quyết định không học hệ thống lý thuyết mà bắt đầu ngay với các dự án nhỏ, gặp vấn đề gì tin tức bóng đá thì Google. Học lập trình đúng cách là phải giải quyết vấn đề cụ thể, vừa viết mã vừa debug.
Sau mỗi giai đoạn tôi sẽ tổng kết và viết thành bài viết chia sẻ. Đây là bài viết đầu tiên của tôi.
Mục tiêu của tôi là: thu thập toàn bộ các câu hỏi, câu trả lời và người dùng trên Zhihu, lưu trữ vào cơ sở dữ liệu MySQL và tiến hành phân tích dữ liệu đó. Đây là một dự án lớn, có lẽ đủ để tôi học trong cả năm.
Bước đầu tiên là làm thế nào để thu thập tất cả các câu hỏi trong một chủ đề cụ thể trên Zhihu.
Nguyên lý cơ bản của web scraping
Đầu tiên hãy xem đoạn mã sau đây, sử dụng Python để thực hiện chức năng cơ bản của web scraping: thu thập nội dung trang web.
import urllib2
url = '
req = urllib2.Request(url) #tạo đối tượng yêu cầu
res = urllib2.urlopen(req) #nhận phản hồi từ máy chủ
html = res.read() #đọc nội dung HTML từ phản hồi
print html #in ra nội dung HTML
Mã chỉ vỏn vẹn 6 dòng. Chạy trong IPython, kết quả in ra là mã nguồn HTML của trang blog này.
Quy trình thu thập trang web có thể tóm tắt thành hai bước:
- Gửi yêu cầu đến máy chủ
- Nhận Đăng Nhập New888 phản hồi từ máy chủ
Quy trình này giống hệt khi chúng ta truy cập trực tiếp một địa chỉ URL thông qua trình duyệt. Trong đoạn mã trên, req
và res
chính là hai dòng mã tương ứng với hai bước này. Trước tiên sử dụng urllib2.Request
để đóng gói URL thành đối tượng yêu cầu, sau đó dùng urllib2.urlopen
để nhận nội dung phản hồi từ máy chủ.
Thu thập các câu hỏi trong chủ đề Python trên Zhihu
Chủ đề Python Thay đổi URL trong đoạn mã trước thành liên kết của chủ đề Python trên Zhihu, chúng ta có thể thu thập toàn bộ trang web. Tuy nhiên, bây giờ cần phải trích xuất ID câu hỏi (số trong URL của trang câu hỏi) và tiêu đề câu hỏi. Do đó, chúng ta cần sử dụng biểu thức chính quy để khớp với các phần tử này (có thể tham khảo thêm về biểu thức chính quy tại “Lập trình Python cơ bản - Biểu thức chính quy”).
Sử dụng công cụ phát triển của Chrome, chúng ta có thể thấy rằng mỗi liên kết câu hỏi và tiêu đề đều nằm trong đoạn mã sau:
<a target="_blank" class="question_link" href="/question/39423081">theano bên trong biến trung gian self.p_y_given_x làm thế nào để theo dõi?</a>
Biểu thức chính quy phù hợp để khớp với ID câu hỏi và tiêu đề là href="/question/(.*?)">(.*?)</a>
Vậy đoạn mã hoàn chỉnh để thu thập ID câu hỏi và tiêu đề câu hỏi là như sau:
# Thu thập câu hỏi Zhihu
import urllib2
import re
url = '
req = urllib2.Request(url)
res = urllib2.urlopen(req)
html = res.read()
#khớp ID câu hỏi và tiêu đề câu hỏi
pattern = re.compile(r'href="/question/(.*?)">(.*?)</a>',re.S)
items = re.findall(pattern,html)
#in ra kết quả
for item in items:
print 'ID câu hỏi:',item[0],
print 'Tiêu đề câu hỏi:', item[1]
Kết quả chạy một phần:
ID câu hỏi: 39423081 Tiêu đề câu hỏi: theano bên trong biến trung gian self.p_y_given_x làm thế nào để theo dõi?
ID câu hỏi: 39422374 Tiêu đề câu hỏi: Làm thế nào để sử dụng BeautifulSoup để phân tích nội dung trong script?
ID câu hỏi: 39421438 Tiêu đề câu hỏi: Trong gói pandas của Python, lệnh read_sql, câu lệnh truy vấn (select) chứa một chuỗi cụ thể?
ID câu hỏi: 39419394 Tiêu đề câu hỏi: Python và ngôn ngữ Yi có điểm gì tương đồng?
Hoàn thiện mã nguồn
Có vài vấn đề cần cải thiện…
Sau khi hoàn thiện, mã nguồn đầy đủ như sau:
# Thu thập tất cả các câu hỏi trong một chủ đề trên Zhihu
import urllib2
import re
class ZhiHu:
#Phương thức khởi tạo, định nghĩa biến
def __init__(self):
self.pageIndex = 1
self.user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
#Khởi tạo headers
self.headers = {'User-Agent': self.user_agent}
#Danh sách lưu trữ các câu hỏi, mỗi phần tử là một trang câu hỏi (20 câu hỏi)
self.questions = []
#Trạng thái hoạt động của chương trình scraping
self.enable = False
#Nhận mã nguồn trang web
def getPage(self,pageIndex):
try:
url = ' + str(pageIndex)
#Tạo request
request = urllib2.Request(url)
#Nhận mã nguồn trang web
response = urllib2.urlopen(request)
#Chuyển mã nguồn sang UTF-8
pageCode = response.read().decode('utf-8')
return pageCode
except urllib2.Error, e:
if hasattr(e, 'reason'):
print u'Kết nối thất bại, lý do:',e.reason
return None
#Nhận danh sách câu hỏi theo số trang
def getPageItem(self,pageIndex):
pageCode = self.getPage(pageIndex)
#Danh sách lưu trữ các câu hỏi trên một trang, mỗi phần tử gồm ID và tiêu đề câu hỏi
pageQuestions = []
#Kiểm tra trang có được tải về thành công hay không
if not pageCode:
print u'Tải trang thất bại...'
return None
#Biểu thức chính quy khớp ID và tiêu đề câu hỏi
pattern = re.compile(r'href="/question/(.*?)">(.*?)</a>',re.S)
items = re.findall(pattern,pageCode)
#Duyệt qua kết quả khớp và lưu vào pageQuestions
for item in items:
#item[0] là ID câu hỏi, item[1] là tiêu đề câu hỏi
pageQuestions.append([item[0],item[1]])
return pageQuestions
#Tải và trích xuất nội dung trang web vào danh sách
def loadPage(self):
if self.enable == True:
#Nếu số trang chưa đọc dưới 2 trang thì tải thêm trang mới
if len(self.questions) < 2:
#Lấy trang mới
pageQuestions = self.getPageItem(self.pageIndex)
if pageQuestions:
#Lưu trang câu hỏi vào danh sách toàn cục
self.questions.append(pageQuestions)
#Tăng số trang
self.pageIndex += 1
#In ra một trang câu hỏi
def getQuestion(self,pageQuestions,page):
for question in pageQuestions:
print u'Trang thứ %s, Câu hỏi %s: %s' %(page,question[0],question[1])
#Phương thức bắt đầu
def start(self):
print u'Đang đọc các câu hỏi từ Zhihu, nhấn Enter để xem, nhập Q để thoát'
self.enable = True
nowPage = 0
while self.enable:
#Nhập lệnh
input = raw_input()
self.loadPage()
#Nhập 'Q' để dừng chương trình
if input == 'Q':
self.enable = False
break
elif len(self.questions)>0:
pageQuestions = self.questions[0]
nowPage += 1
del self.questions[0]
self.getQuestion(pageQuestions,nowPage)
print input
#Tạo đối tượng ZhiHu
spider = ZhiHu()
#Chạy chương trình
spider.start()
Kết luận
Đây là lần đầu tiên tôi viết được một chương trình hoàn chỉnh sau nhiều lần xem qua cú pháp cơ bản. Cảm giác thật tuyệt vời! Tôi đã gặp một số khó khăn:
…
Sửa đổi lần cuối vào 2025-02-03