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 đó childparent 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ủ đề.