簡書爬蟲項目

爬取內容:

  簡書中每一篇文章的具體數據,主要包括文章標題、鑽石數、發表日期、文章字數、文章閱讀量、文章的評論量和點贊量等,這裏爬取2000條左右保存至數據庫html

 

代碼實如今文末!python

 

分析思路:

  • 首先,谷歌瀏覽器抓包,獲取簡書首頁加載新文章的方式,當咱們點擊主頁的加載更多的按鈕的時候,後臺發送了一個異步的POST請求來加載新文章

請求有一個page參數,便是咱們須要的。並且能夠觀察到返回的並不是json數據,而是html數據mysql

 

 接下來,咱們作的就是從返回的html中找出跳到具體文章內容的連接,這裏過程就不加贅述,一會會在代碼裏看到sql

  • 因此咱們可有有以下的一個邏輯過程:
    • 經過向簡書發送POST請求,獲取新的文章列表的html,固然參數page要變,以控制頁碼
    • 從返回的html代碼中解析出真正文章的url
    • 向真正的文章頁面發送請求,獲取響應
    • 從響應的頁面代碼中提取出咱們想要的文章數據
    • 整理文章數據後保存至數據庫

相關庫與方法:

  • Python網絡請求requests庫
  • 解析方法xpath,這裏用到的是Python的lxml庫
  • 數據庫連接mysql的庫,pymysql
  • 多線程及隊列,Python的threading和queue模塊

代碼結構及實現:

 1 # 簡書爬蟲項目
 2 
 3 ### 目錄結構
 4 - lilei_spider
 5     - ____init____.py
 6     - jianshu_index.py
 7     - config.py
 8     - storage.py
 9     - utils
10         - db.py
11         - tools.py
12         
13 ## 文件說明:
14 #### jianshu_index.py
15 > 簡書爬蟲總體邏輯實現,也是運行項目的入口文件
16 
17 #### config.py
18 > 項目中用到的配置參數
19 
20 #### storage.py
21 > 實現數據的存儲
22 
23 #### db.py
24 > 數據庫鏈接相關
25 
26 #### tools.py
27 > 工具函數及工具類
28 
29 #### article.sql
30 > 導出的數據庫文件
31 
32 #### requirements.txt
33 > 項目依賴庫
34 
35 ### 技術說明
36 - python網絡請求庫:requests
37 - 數據解析方法:xpath(lxml庫)
38 - 鏈接數據庫:pymysql
39 - 多線程:threading,queue

代碼實現:

jianshu_index.py數據庫

  1 import requests
  2 import threading
  3 from queue import Queue
  4 from lxml import etree
  5 
  6 from config import INDEX_ADDRESS as index_address
  7 from config import ADDRESS as address
  8 from config import HEADERS as headers
  9 from storage import Storage
 10 from utils.tools import DecodingUtil
 11 
 12 
 13 class JianshuSpider(object):
 14     """爬取簡書首頁數據的爬蟲類"""
 15     def __init__(self):
 16         self.max_page = 300  # 爬取總文章數:300*7=2100
 17         self.params_queue = Queue()  # 存放地址發送post請求參數的隊列
 18         self.url_queue = Queue()  # 存放文章url的隊列
 19         self.index_queue = Queue()  # 存放首頁響應的新文章列表的隊列
 20         self.article_queue = Queue()  # 存放文章響應內容的隊列
 21         self.content_queue = Queue()  # 存放格式化後的文章數據內容的隊列
 22 
 23     def get_params_list(self):
 24         """構造post請求的page參數隊列"""
 25         for i in range(1, self.max_page+1):
 26             self.params_queue.put({'page': i})
 27 
 28     def pass_post(self):
 29         """發送POST請求,獲取新的文章列表,請求參數從隊列中取出"""
 30         while True:
 31             response = requests.post(index_address, data=self.params_queue.get(), headers=headers)
 32             self.index_queue.put(DecodingUtil.decode(response.content))  # 返回結果放入隊列
 33             self.params_queue.task_done()  # 計數減一
 34             print('pass_post', '@'*10)
 35 
 36     def parse_url(self):
 37         """根據首頁返回的新的文章列表,解析出文章對應的url"""
 38         while True:
 39             content = self.index_queue.get()  # 從隊列中取出一次POST請求的文章列表數據
 40             html = etree.HTML(content)
 41             a_list = html.xpath('//a[@class="title"]/@href')  # 每一個li標籤包裹着一篇文章
 42             for a in a_list:
 43                 url = a  # xpath解析出文章的相對路徑
 44                 article_url = address + url
 45                 self.url_queue.put(article_url)  # 放入隊列
 46             self.index_queue.task_done()
 47             print('parse_url', '@'*10)
 48 
 49     def pass_get(self):
 50         """發送GET請求,獲取文章內容頁"""
 51         while True:
 52             article_url = self.url_queue.get()  # 從隊列中獲取文章的url
 53             response = requests.get(article_url, headers=headers)
 54             self.article_queue.put(DecodingUtil.decode(response.content))  # 返回結果放入隊列
 55             self.url_queue.task_done()
 56             print('pass_get', '@'*10)
 57 
 58     def get_content(self):
 59         while True:
 60             article = dict()
 61             article_content = self.article_queue.get()
 62             html = etree.HTML(article_content)
 63             # 標題:title,鑽石:diamond,建立時間:create_time,字數:word_number
 64             # 閱讀量:read_number,評論數:comment_number,點贊數:like_number,文章內容:content
 65             article['title'] = html.xpath('//h1[@class="_2zeTMs"]/text()')[0].strip('\n').strip('\t')
 66             try:
 67                 article['diamond'] = html.xpath('//span[@class="_3tCVn5"]/span/text()')[0]
 68             except IndexError:
 69                 article['diamond'] = ''
 70             article['create_time'] = html.xpath('//div[@class="s-dsoj"]/time/text()')[0].replace('\n', '')
 71             article['word_number'] = html.xpath('//div[@class="s-dsoj"]/span[2]/text()')[0].split(' ')[-1]
 72             article['read_number'] = html.xpath('//div[@class="s-dsoj"]/span[last()]/text()')[0].split(' ')[-1]
 73             article['comment_number'] = html.xpath('//div[@class="_3nj4GN"][1]/span/text()[last()]')[0]
 74             article['like_number'] = html.xpath('//div[@class="_3nj4GN"][last()]/span/text()[last()]')[0]
 75             content = html.xpath('//article[@class="_2rhmJa"]')  # html富文本內容
 76             article['content'] = DecodingUtil.decode(etree.tostring(content[0], method='html', encoding='utf-8'))
 77             self.content_queue.put(article)  # 放入隊列
 78             self.article_queue.task_done()  # 上一隊列計數減一
 79             print('get_content', '@'*10)
 80 
 81     def save(self):
 82         """保存數據"""
 83         while True:
 84             article_info = self.content_queue.get()  # 隊列中獲取文章信息
 85             # print(article_info)
 86             Storage.save_to_mysql(article_info)  # 文章數據保存到mysql數據庫
 87             self.content_queue.task_done()
 88             print('save', '*'*20)
 89 
 90     def run(self):
 91         # 0.各個方法之間利用隊列來傳送數據
 92         # 1.簡書首頁加載新數據方式爲POST請求,url不變,參數page變化,因此首先構造一個params集
 93         # 2.遍歷params集發送POST請求,獲取響應
 94         # 3.根據每一次獲取的文章列表,再獲取對應的真正文章內容的頁面url
 95         # 4.向文章內容頁面發送請求,獲取響應
 96         # 5.提取對應的數據
 97         # 6.保存數據,一份存入數據庫,一份存入excel
 98         thread_list = list()  # 模擬線程池
 99         t_params = threading.Thread(target=self.get_params_list)
100         thread_list.append(t_params)
101         for i in range(2):  # 爲post請求開啓3個線程
102             t_pass_post = threading.Thread(target=self.pass_post)
103             thread_list.append(t_pass_post)
104         for j in range(2):  # 爲解析url開啓3個線程
105             t_parse_url = threading.Thread(target=self.parse_url)
106             thread_list.append(t_parse_url)
107         for k in range(5):  # 爲get請求開啓5個線程
108             t_pass_get = threading.Thread(target=self.pass_get)
109             thread_list.append(t_pass_get)
110         for m in range(5):  # 爲提取數據開啓5個線程
111             t_get_content = threading.Thread(target=self.get_content)
112             thread_list.append(t_get_content)
113         # for n in range(5):  # 爲保存數據開啓5個線程
114         t_save = threading.Thread(target=self.save)  # 保存數據一個線程
115         thread_list.append(t_save)
116         # =====================================================================================================
117         for t in thread_list:
118             t.setDaemon(True)  # 把子線程設置爲守護線程,主線程結束,子線程結束
119             t.start()
120         for q in [self.params_queue, self.url_queue, self.index_queue, self.article_queue, self.content_queue]:
121             q.join()  # 讓主線程等待阻塞,等待隊列的任務完成以後再結束
122         print('主線程結束......')
123 
124 
125 if __name__ == '__main__':
126     jianshu_spider = JianshuSpider()
127     jianshu_spider.run()

config.pyjson

 1 import os
 2 
 3 
 4 ADDRESS = 'https://www.jianshu.com'  # 簡書網地址
 5 
 6 INDEX_ADDRESS = 'https://www.jianshu.com/trending_notes'  # 首頁加載數據的地址
 7 
 8 HEADERS = {
 9     'x-pjax': 'true',
10     'referer': 'https://www.jianshu.com/',
11     'Content-Type': 'application/x-www-form-urlencoded',
12     'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
13                   'Chrome/77.0.3865.120 Safari/537.36',
14     'x-csrf-token': 'PRFNi9/FDZmm/bV4f8ZueVNFln0PpQ5kgsMcSERpwpNugy/bcOBgNEZvBo4/aTwrm28awdmuTfcMaHcogJ1mdA=='
15 }

storage.py 瀏覽器

 1 import pandas as pd
 2 from utils.db import conn
 3 from utils.tools import rep_invalid_char
 4 
 5 
 6 class Storage(object):
 7     """存儲數據"""
 8     cursor = conn.cursor()
 9     excel_writer = pd.ExcelWriter('article.xlsx')
10 
11     def __init__(self):
12         pass
13 
14     @classmethod
15     def save_to_mysql(cls, article:dict):
16         """字典類型的文章數據保存到數據庫"""
17         title = article.get('title', '')
18         diamond = article.get('diamond', '')
19         create_time = article.get('create_time', '')
20         word_number = article.get('word_number', '')
21         read_number = article.get('read_number', '')
22         comment_number = article.get('comment_number', '')
23         like_number = article.get('like_number', '')
24         content = rep_invalid_char(article.get('content', ''))
25         # content = article.get('content', '')
26 
27         sql = "INSERT INTO `article2` (`title`,`diamond`,`create_time`,`word_number`,`read_number`,`comment_number`," \
28               "`like_number`,`content`) VALUES ('{}','{}','{}','{}','{}','{}','{}','{}');".format(title, diamond,
29               create_time, word_number, read_number, comment_number, like_number, content)
30         try:
31             cls.cursor.execute(sql)
32             conn.commit()
33         except Exception as e:
34             print(e)
35             # raise RuntimeError('保存至數據庫過程Error!')
36             print('保存至數據庫過程Error!')

db.py網絡

 1 import pymysql
 2 
 3 
 4 class DB(object):
 5     """數據庫鏈接"""
 6     _host = '127.0.0.1'
 7     _user = 'root'
 8     _password = '******'
 9     _db = 'homework'
10 
11     @classmethod
12     def conn_mysql(cls):
13         return pymysql.connect(host=cls._host, user=cls._user, password=cls._password, db=cls._db, charset='utf8')
14 
15 
16 conn = DB.conn_mysql()

tools.py   多線程

 1 import re
 2 import chardet
 3 
 4 
 5 class DecodingUtil(object):
 6     """解碼工具類"""
 7     @staticmethod
 8     def decode(content):
 9         """
10         讀取字節數據判斷其編碼,並正確解碼
11         :param content: 傳入的字節數據
12         :return: 正確解碼後的數據
13         """
14         # print(chardet.detect(content))
15         the_encoding = chardet.detect(content)['encoding']
16         try:
17             return content.decode(the_encoding)
18         except UnicodeDecodeError:
19             print('解碼Error!')
20             try:
21                 return content.decode('utf-8')
22             except:
23                 return '未能成功解碼的文章內容!'
24 
25 
26 def rep_invalid_char(old:str):
27     """mysql插入操做時,有無效字符,替換"""
28     invalid_char_re = r"[/?\\[\]*:]"
29     return re.sub(invalid_char_re, "_", old)
相關文章
相關標籤/搜索