Nhện Python Thu thập tất cả chủ đề từ Zhihu - 99win club
Tiếp nối bài trước "Nhện Python: Đăng nhập giả lập vào Zhihu".
Phân tích yêu cầu trang
LZ dự định thu thập tất cả các chủ đề và cấu trúc của chúng từ trang chủ đề của Zhihu. Trên giao diện người dùng, có hai loại yêu cầu AJAX để lấy dữ liệu: "Hiển thị các chủ đề con" và "Tải thêm". Cả hai đều trả về 10 chủ đề mỗi lần.
URL yêu cầu như sau:
1[URL_YÊU_CẦU]
Trong đó child
và parent
là ID của các chủ đề. Yêu cầu "Hiển thị các chủ đề con" chỉ cần tham số parent
, trong kèo trực tuyến khi "Tải thêm" cần cả hai tham số. Nếu không có bất kỳ tham số nào, nó đại diện cho "Chủ đề gốc".
Dữ liệu phản hồi của cả hai yêu cầu đều ở dạng JSON với cấu trúc tương tự, ngoại trừ phần cuối cùng của phần tử msg
.
Lô-gic thu thập
Cấu trúc chủ đề của Zhihu có phân cấp. Để dễ miêu tả, dưới đây chúng ta gọi các chủ đề con trực tiếp của "Chủ đề gốc" là cấp độ một, các chủ đề con của cấp độ một là cấp độ hai, v.v...
Để thu thập toàn bộ danh sách chủ đề, quy trình sẽ như sau: Bắt đầu từ "Chủ đề gốc", thu thập tất cả các chủ đề cấp độ một và lưu trữ chúng. Sau đó phân tích từng chủ đề cấp độ một, nếu có chủ đề con, tiến hành thu thập và lưu trữ tất cả các chủ đề con (cấp độ hai), rồi tiếp tục quá trình này lặp đi lặp lại.
Theo quy trình trên, cần triển khai ba chức năng chính:
- Đăng nhập giả lập.
- Thu thập dữ liệu theo cấu trúc phân cấp.
- Lưu trữ kết quả.
Lưu trữ chủ đề
Do LZ muốn giữ nguyên cấu trúc của các chủ đề và cần sử dụng ID của chủ đề cha trong quá trình "Tải thêm", nên mỗi chủ đề được biểu diễn dưới dạng danh sách bốn phần tử: tên chủ đề, ID chủ đề, ID chủ đề cha, và trạng thái có hay không có chủ đề con (kiểu bool). Ví dụ: ['Thực thể', '19778287', '19776749', 1]
. Khi kiểm tra trùng lặp, chỉ coi là trùng khi cả bốn phần tử đều giống nhau.
Thực hiện mã nguồn
Mã nguồn phía dưới bắt đầu bằng đoạn mã đăng nhập giả lập vào Zhihu. Sau khi nhập mã xác nhận, chương trình sẽ bắt đầu thu thập dữ liệu và lưu trữ vào tệp tin cục bộ sau khi hoàn thành. Vì thời gian chạy khá dài (khoảng một giờ), nên đã thêm một số thông báo trạng thái.
1# all_topics.py
2import requests
3from bs4 import BeautifulSoup
4
5# Biến session
6s = requests.Session()
7headers = {
8 "Accept": "*/*",
9 "Accept-Encoding": "gzip,deflate",
10 "Accept-Language": "en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4",
11 "Connection": "keep-alive",
12 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
13 "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
14 "Referer": "[REFERER]"
15}
16_xsrf = ''
17
18def set_xsrf():
19 global _xsrf
20 html = s.get('[URL]', headers=headers) # Yêu cầu GET, lấy phản hồi
21 soup = BeautifulSoup(html.text, 'lxml-xml') # Phân tích HTML bằng BeautifulSoup
22 _xsrf = soup.findAll(type='hidden')[0]['value']
23
24def get_captcha():
25 # Lấy hình ảnh mã xác nhận và lưu vào file cục bộ
26 captcha_url = '[CAPTCHA_URL]'
27 captcha = s.get(captcha_url, stream=True, headers=headers)
28 with open('captcha.gif', 'wb') as f:
29 for line in captcha.iter_content(10):
30 f.write(line)
31 # Nhập mã xác nhận và lưu vào biến captcha_str
32 print('Nhập mã xác nhận:', end='')
33 captcha_str = input()
34 return captcha_str.strip()
35
36# Yêu cầu đăng nhập
37def login():
38 global s
39 global _xsrf
40 set_xsrf()
41 captcha = get_captcha()
42 data = {
43 '_xsrf': _xsrf,
44 'email': '654408619@qq.com',
45 'password': 'mftx1029',
46 'remember_me': True,
47 'captcha': captcha
48 }
49 r = s.post(url='[LOGIN_URL]', data=data, headers=headers)
50 return r
51
52all_topics = [['Chủ đề gốc', '19776749', '', 1]] # Lưu trữ tất cả các chủ đề, ban đầu chỉ có "Chủ đề gốc"
53has_children = [['Chủ đề gốc', '19776749', '', 1]] # Các chủ đề có con nhưng chưa thu thập, ban đầu chỉ có "Chủ đề gốc"
54cnt_a = 0 # Đếm phần tử trong all_topics
55cnt_h = 0 # Đếm phần tử trong has_children
56
57# Lấy dữ liệu JSON từ một yêu cầu
58def get_json(child_id='', parent_id=''):
59 global s
60 global headers
61 url = '[JSON_URL]'
62 data = {'child': child_id, 'parent': parent_id, '_xsrf': _xsrf}
63 res = s.post(url, data=data, headers=headers)
64 json_dict = res.json()
65 return json_dict
66
67# Phân tích dữ liệu JSON, lấy thông tin chủ đề con và "Tải thêm"
68def json_process(json_dict):
69 single_dict = {} # Lưu trữ children và more
70 children = [] # Lưu trữ chủ đề con
71 more = [] # Lưu trữ thông tin "Tải thêm"
72 parent_id = json_dict['msg'][0][2]
73 if len(json_dict['msg'][1]) == 11:
74 more_child_id = json_dict['msg'][1][9][0][2]
75 more = [more_child_id, parent_id]
76 for i in json_dict['msg'][1][:10]:
77 child_name, child_id = i[0][1], i[0][2]
78 if i[1]:
79 children.append([child_name, child_id, parent_id, 1])
80 else:
81 children.append([child_name, child_id, parent_id, 0])
82 single_dict['children'] = children
83 single_dict['more'] = more
84 return single_dict
85
86# Lấy tất cả các chủ đề con của một chủ đề
87def get_children(parent_id='', parent_name=''):
88 global cnt_a
89 global cnt_h
90 children = []
91 more = ['', parent_id]
92 while more:
93 json_dict = get_json(child_id=more[0], parent_id=more[1])
94 single_dict = json_process(json_dict)
95 children += single_dict['children']
96 more = single_dict['more']
97 # Xóa thông báo cũ
98 print(' '*140, end='\r')
99 # Thông báo trạng thái thu thập
100 print('Đã lấy {} chủ đề. Còn {} chủ đề đang chờ xử lý, đang lấy «{}» - chủ đề con thứ {}...'.format(
101 cnt_a, cnt_h, str(parent_name).encode('utf-8', 'ignore').decode('utf-8'), len(children)), end='\r')
102 return children
103
104# Hàm làm việc chính, thu thập tất cả các chủ đề và lưu vào file
105def work():
106 global cnt_h
107 global cnt_a
108 login()
109 while has_children:
110 first_child = has_children.pop(0)
111 children = get_children(first_child[1], first_child[0])
112 for c in children:
113 # Kiểm tra trùng lặp và thêm vào all_topics và has_topics, sau đó kiểm tra xem có chủ đề con không
114 if c not in all_topics:
115 all_topics.append(c)
116 if c[-1] == 1:
117 has_children.append(c)
118 cnt_a = len(all_topics)
119 cnt_h = len(has_children)
120
121 # Lưu vào file
122 for item in all_topics:
123 with open('all_topics.txt', 'a', encoding='utf-8') as f:
124 string = str(item[0]) + '\t' + str(item[1]) + '\t' + str(item[2]) + '\t' + str(item[3]) + '\n'
125 f.write(string)
126
127if __name__ == "__main__":
128 work()
Trạng thái chạy chương trình:
1E:\@coding\python>python all_topics.py
2Nhập mã xác nhận: hbfu
3Đã lấy 53 chủ đề. Còn 46 chủ đề đang chờ xử lý, đang lấy ««Chưa phân loại»» - chủ đề con thứ 1600...
LZ đã chạy chương trình vào ngày hôm qua (2016-02-04) và thu thập được tổng cộng 46272 chủ đề.