Hiện nay, chủ đề về Data Science vẫn liên tục được mọi người quan tâm. Để được kết quả tốt cho việc phân tích dữ liệu (Data Analyst), bước chuẩn bị dữ liệu rất quan trọng. Một bài viết liên quan đến Data Science trong series Tự học và phát triển ứng dụng thực tế AI, ML, DL, DS mình gửi tới các bạn đó là Crawl dữ liệu điểm thi THPT các năm 2018, 2019, 2020.
Mình lấy một đề tài cũng khá phổ biến để thực hiện, từ bài viết này các bạn cũng có thể áp dụng vào các đề tài khác.
Trước tiên, chúng ta cần tìm hiểu qua một số câu hỏi. Crawl là gì? Vì sao chúng ta phải crawl dữ liệu?
Crawl là gì?
Crawl là một thuật ngữ mô tả quá trình thu thập dữ liệu trên website. Crawler có nhiệm vụ chính là thu thập dữ liệu từ một trang web bất kỳ hoặc được chỉ định trước, sau đó phân tích mã nguồn HTML để đọc dữ liệu và trích xuất thông tin theo yêu cầu.
Chúng ta cũng thấy được rất nhiều ứng dụng của việc crawl data như việc thu thập tin tức, marketing, Search Engine bot…
Tầm quan trọng của dữ liệu (Data) trong Data Science là rất quan trọng, nếu bạn không có dữ liệu trong tay thì việc crawl dữ liệu sẽ giúp chúng ta rất nhiều.
Crawl dữ liệu điểm thi THPT các năm 2018, 2019, 2020
Chúng ta có rất nhiều cách để lấy dữ liệu, trong bài viết này mình sẽ hướng dẫn các bạn một cách đơn giản trong Python.
Đầu tiên, chúng ta xác định nguồn lấy dữ liệu ở đâu. Ở đây mình thấy báo Thanh Niên cung cấp dữ liệu điểm thi THPT qua nhiều năm, nhưng mình chỉ lấy các năm 2018, 2019, 2020.
Mình sẽ lấy thử điểm thi một năm của 1 thí sinh xem sao.
1 |
curl --location --request GET 'https://thanhnien.vn/ajax/diemthi.aspx?kythi=THPT&nam=2018&text=02000001' |
Response chúng ta nhận được như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<tr> <td class="">1</td> <td class="mobile-tab-content mobile-tab-1 visible"></td> <td class="mobile-tab-content mobile-tab-1 visible"></td> <td class="">02000001</td> <td class="mobile-tab-content mobile-tab-1 visible"></td> <td class="mobile-tab-content mobile-tab-1 visible"></td> <td class="mobile-tab-content mobile-tab-2">6.6</td> <td class="mobile-tab-content mobile-tab-2">6</td> <td class="mobile-tab-content mobile-tab-2">4.5</td> <td class="mobile-tab-content mobile-tab-2">3.5</td> <td class="mobile-tab-content mobile-tab-2">5.25</td> <td class="mobile-tab-content mobile-tab-2">4.42</td> <td class="mobile-tab-content mobile-tab-3"></td> <td class="mobile-tab-content mobile-tab-3"></td> <td class="mobile-tab-content mobile-tab-3"></td> <td class="mobile-tab-content mobile-tab-3"></td> <td class="mobile-tab-content mobile-tab-3">6</td> <td class="mobile-tab-content mobile-tab-3"></td> </tr> |
Mình dùng thư viện BeautifulSoup để lấy dữ liệu, kết quả ta được 1 mảng danh sách điểm thi các môn.
1 2 3 4 5 6 7 8 9 |
response = requests.get('https://thanhnien.vn/ajax/diemthi.aspx?kythi=THPT&nam=2018&text=02000001') soup = BeautifulSoup(response.content, "html.parser") row = soup.find('tr') my_list = [] if(row is not None): content = row.find_all('td') for values in content: my_list.append(values.get_text()) print(my_list) |
Rất đơn giản, chúng ta đã crawl được dữ liệu điểm thi THPT từ báo Thanh Niên rồi.
Nhưng chúng ta chưa dừng tại đây. Bây giờ, mình sẽ lưu lại dưới file .csv
1 2 3 4 5 |
header = ['STT','Cum thi','Ho ten','SBD','Ngay sinh','Gioi tinh','Toan','Ngu van','Vat li','Hoa hoc','Sinh hoc','KHTN','Lich su','Dia li','GDCD','KHXH','Ngoai ngu'] with open(file_output_path, 'w') as f: writer = csv.writer(f, delimiter=',', quoting=csv.QUOTE_ALL) writer.writerow(header) writer.writerow(my_list) |
Xong rồi đó, nhưng mục tiêu của chúng ta là lấy hết điểm thi của tất cả thí sinh các năm 2018, 2019, 2020. Vậy chúng ta chỉ cần viết thêm vài dòng nữa thôi, mình nghĩ các bạn làm được 🙂
Nhưng chúng ta sẽ gặp một khó khăn đó là báo Thanh Niên sẽ giới hạn số lượng request của chúng ta. Vậy bây giờ phải làm sao?
Giải pháp mình thực hiện là sẽ thay đổi proxy cho mỗi lần request.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
ua = UserAgent() proxies = [] def list_proxy(): proxies_req = Request('https://www.sslproxies.org/') proxies_req.add_header('User-Agent', ua.random) proxies_doc = urlopen(proxies_req).read().decode('utf8') soup = BeautifulSoup(proxies_doc, 'html.parser') proxies_table = soup.find(id='proxylisttable') for row in proxies_table.tbody.find_all('tr'): proxies.append({ 'ip': row.find_all('td')[0].string, 'port': row.find_all('td')[1].string }) def random_proxy(): return random.randint(0, len(proxies) - 1) list_proxy() proxy_index = random_proxy() proxy = proxies[proxy_index] |
Vấn đề tiếp theo xảy ra! Chúng ta cần lấy điểm của nhiều thí sinh, do đó lượng request khá lớn. Để chạy nhanh hơn mình sẽ sử dụng kỹ thuật Multiprocessing. Các bạn cũng có thể dùng Multithreading
Mình sẽ chia nhỏ số lượng thí sinh ra cho mỗi process xử lý.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def chunks(l, n): return [l[i:i+n] for i in range(0, len(l), n)] def do_job(year, job_id, data_slice): for item in data_slice: data = diem_thi(year, item) if len(data) > 0: write_file(data, file_path, True) def dispatch_jobs(year, data, job_number): total = len(data) chunk_size = int((total-1) / job_number + 1) slice = chunks(data, chunk_size) jobs = [] for i, s in enumerate(slice): j = Process(target=do_job, args=(year, i, s)) j.start() jobs.append(j) for j in jobs: j.join() |
Giải thích chút nhé. Mình tạo ra 3 hàm chunks(), do_job(), dispatch_jobs().
- dispatch_jobs(year, data, job_number): 1 lần chúng ta sẽ lấy tất cả điểm thi THPT của 1 năm, với số lượng process (job_number). Danh sách số báo danh cần lấy (data)
- do_job(year, job_id, data_slice): Một process ta sẽ lấy lượng thí sinh mà đã được chia nhỏ ra (data_slice) theo job_number. Sau đó ghi xuống file với hàm write_file()
Danh sách số báo danh mình sẽ tạo như sau:
1 2 3 4 5 |
min = 1000001 max = 9001000 ids = [] for i in range(min, max): ids.append(str(i).zfill(8)) |
Số báo danh gồm 8 ký tự, mình sẽ lấy từ SBD 02000001 đến 0900999.
Code đầy đủ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
from urllib.request import Request, urlopen from bs4 import BeautifulSoup import csv from multiprocessing import Process from fake_useragent import UserAgent import random ua = UserAgent() proxies = [] def list_proxy(): proxies_req = Request('https://www.sslproxies.org/') proxies_req.add_header('User-Agent', ua.random) proxies_doc = urlopen(proxies_req).read().decode('utf8') soup = BeautifulSoup(proxies_doc, 'html.parser') proxies_table = soup.find(id='proxylisttable') for row in proxies_table.tbody.find_all('tr'): proxies.append({ 'ip': row.find_all('td')[0].string, 'port': row.find_all('td')[1].string }) def random_proxy(): return random.randint(0, len(proxies) - 1) def write_file(data, file_output_path, is_append = False): print('Writing to file...') if len(data) <= 0: return mode = 'w' if (is_append): mode = 'a' with open(file_output_path, mode) as f: writer = csv.writer(f, delimiter=',', quoting=csv.QUOTE_ALL) writer.writerow(data) def crawl(url): proxy_index = random_proxy() proxy = proxies[proxy_index] request = Request(url) request.add_header('User-Agent', ua.chrome) request.set_proxy(proxy['ip'] + ':' + proxy['port'], 'http') request_doc = urlopen(request).read().decode('utf8') soup = BeautifulSoup(request_doc, "html.parser") row = soup.find('tr') my_list = [] if(row is not None): content = row.find_all('td') for values in content: my_list.append(values.get_text()) return my_list def diem_thi(year, id): print('Fetching data {',year, '} {',id, '}...') url = 'https://thanhnien.vn/ajax/diemthi.aspx?kythi=THPT&nam='+year+'&text='+id data = crawl(url) return data def list_id(min, max): print('Generating IDs...') ids = [] for i in range(min, max): ids.append(str(i).zfill(8)) return ids def chunks(l, n): return [l[i:i+n] for i in range(0, len(l), n)] def do_job(year, job_id, data_slice): for item in data_slice: data = diem_thi(year, item) if len(data) > 0: write_file(data, file_path, True) def dispatch_jobs(year, data, job_number): total = len(data) chunk_size = int((total-1) / job_number + 1) slice = chunks(data, chunk_size) jobs = [] for i, s in enumerate(slice): j = Process(target=do_job, args=(year, i, s)) j.start() jobs.append(j) for j in jobs: j.join() def main(year, ids, file_path): job_number = 5 dispatch_jobs(year, ids, job_number) if __name__ == '__main__': list_proxy() years = ["2018","2019","2020"] min = 2000001 max = 2000002 ids = list_id(min, max) header = ['STT','Cum thi','Ho ten','SBD','Ngay sinh','Gioi tinh','Toan','Ngu van','Vat li','Hoa hoc','Sinh hoc','KHTN','Lich su','Dia li','GDCD','KHXH','Ngoai ngu'] for year in years: file_path = 'diemthi'+year+'.csv' write_file(header, file_path) main(year, ids, file_path) print('Finish!') |
Chúc các bạn thành công!
Cho mình hỏi chút ạ, việc crawl này thì làm trên app nào ạ
Mình viết bằng Python bạn nhé
xin chào admin
làm như nào lấy được link api https://thanhnien.vn/ajax/diemthi.aspx?kythi=THPT&nam=2018&text=02000001
mình cũng đang làm việc về crawl dữ liệu, dùng python; hi vọng sẽ có dịp giao lưu