1. 程式人生 > >爬取簡書網30日熱門得到詞雲

爬取簡書網30日熱門得到詞雲

這幾天在看《從零開始學python網路爬蟲》,裡面有一章是爬取簡書網7天熱門,不過我在開啟簡述網七天熱門的時候發現壓根就只有一頁(可能連一頁都不到。。。),之後感覺不夠難度就改而選擇爬取30天熱門。

1.連結分析

首先,簡書網30天熱門的第一個連結是:https://www.jianshu.com/trending/monthly?utm_medium=index-banner-s&utm_source=desktop

這個連結確實是第一頁的連結,簡單地加上User-Agent確實能夠獲得第一頁的連結,不過對於之後的連結就相形見絀了。其實簡書網是非同步載入網頁的(一般情況下,頁面元素增加且頁面不會重新整理時表示該頁面採用了非同步載入)。

簡單地說,瀏覽器請求伺服器時,伺服器先發送一個模板給瀏覽器,瀏覽器之後等待伺服器傳送資料,在客戶端接收到資料後再進行元素填充。

那麼接下來就要開始分析簡書網的請求了(推薦使用Chrome)

滑鼠右鍵點選“檢查”,再轉而到XHR,之後按下Ctrl+R重新載入頁面:

之後下拉滾動條,可以看到XHR中又有新的請求,

 那麼接下來就是分析下面的這幾個連結了,先分析第二個連結:

https://www.jianshu.com/trending/monthly?seen_snote_ids%5B%5D=37748501&seen_snote_ids%5B%5D=37776875&seen_snote_ids%5B%5D=37794473&seen_snote_ids%5B%5D=37197951&seen_snote_ids%5B%5D=36807878&seen_snote_ids%5B%5D=36463859&seen_snote_ids%5B%5D=36765004&seen_snote_ids%5B%5D=36341650&seen_snote_ids%5B%5D=36798485&seen_snote_ids%5B%5D=36883342&seen_snote_ids%5B%5D=36152995&seen_snote_ids%5B%5D=36404763&seen_snote_ids%5B%5D=36786520&seen_snote_ids%5B%5D=36560243&seen_snote_ids%5B%5D=36808340&seen_snote_ids%5B%5D=36891380&seen_snote_ids%5B%5D=35443912&seen_snote_ids%5B%5D=36673167&seen_snote_ids%5B%5D=36553581&seen_snote_ids%5B%5D=36647346&page=2

 可以發現該連結非常地,額。。。長。為便於分析,先做一個簡單的切割。程式碼大致如下:

#! /usr/bin/python3.6
# -*-coding:utf-8 -*-

from urllib.parse import urlsplit, parse_qs
import pprint

url = 'https://www.jianshu.com/trending/monthly?seen_snote_ids%5B%5D=37748501&seen_snote_ids%5B%5D=37776875&seen_snote_ids%5B%5D=37794473&seen_snote_ids%5B%5D=37197951&seen_snote_ids%5B%5D=36807878&seen_snote_ids%5B%5D=36463859&seen_snote_ids%5B%5D=36765004&seen_snote_ids%5B%5D=36341650&seen_snote_ids%5B%5D=36798485&seen_snote_ids%5B%5D=36883342&seen_snote_ids%5B%5D=36152995&seen_snote_ids%5B%5D=36404763&seen_snote_ids%5B%5D=36786520&seen_snote_ids%5B%5D=36560243&seen_snote_ids%5B%5D=36808340&seen_snote_ids%5B%5D=36891380&seen_snote_ids%5B%5D=35443912&seen_snote_ids%5B%5D=36673167&seen_snote_ids%5B%5D=36553581&seen_snote_ids%5B%5D=36647346&page=2'

result = urlsplit(url)
#得到引數
queries = result.query
#解析引數
params = parse_qs(queries)
pprint.pprint(params)

pprint庫是主要是進行格式化輸出,可用print代替或pip3 install pprint。

輸出如下:

 page屬性還好說,那麼seen_snote_ids[]中的數字是哪來的呢?(注:該引數的名稱為seen_snote_ids[],別弄錯了)好吧,先分析引數名吧,seen 看過的;snote 嗯~,note可以認為是文章; id ID。那麼可以大膽猜測這個陣列應該表示已經看到過的文章的ID。俗話說,大膽猜測,仔細驗證,接著看第三頁的連結:分析得到輸出如下:

 因為比較佔空間,所以改為使用print了,上述兩個分析之後,發現前若干個id是一樣的。那麼目前基本可以認為我們的分析是正確的了,好吧,接著下拉吧:

 拉到底出現了一個"閱讀更多"。。。。這是我見過的最沒骨氣的非同步載入。

手動點選後,出現了新的XHR請求:

該連結引數和前幾頁大致相同,只不過多了幾個固定的引數,一個是utm_medium,另一個則是utm_source。

2.抓取網頁連結

接下來總結一下簡書網請求的特點:

首先,第一頁因為沒看過任何文章,所以seen_snote_ids[]陣列可以留空;第二頁和第三頁只是增加了前面頁的文章的id;從第四頁開始又增加了兩個新的欄位。

嗯~,看起來沒什麼問題,不過,我倒是發現一個問題,就是get請求的長度是有限制的,瀏覽器和伺服器雙方都會存在限制,所以此30天熱門註定沒幾頁(-_-!好總結)。

接下來就編碼獲取頁面的所有文章的詳細連結、文章ID、和文章標題,並存入csv檔案中。

page_month.py

#! /usr/bin/python3.6
# -*-coding:utf-8 -*-

import requests
import time
from lxml import etree
import pprint
from urllib.parse import urljoin
import csv

import config

headers = {
        'User-Agent' : config.get_random_user_agent(),
        }

載入了一些常用的庫,另外,config為配置檔案,它內部主要有一個方法get_random_user_agen()用來隨機獲取User-Agent,偽裝爬蟲(具體見最下方github連結)。

def get_monthly_info(url, params = None):
    ''' 
    獲取對應頁面的詳細頁面url
    @param url 主要連結
    '''
    response = requests.get(url, headers = headers, params = params)

    #print(response.url)
    #解析
    selector = etree.HTML(response.text)

    infos = selector.xpath('//ul[@class="note-list"]/li')
    for info in infos:
        #文章id
        note_id = info.xpath('./@data-note-id')[0]
        #文章名
        title = info.xpath('.//*[@class="title"]/text()')[0]
        #連結
        href = info.xpath('.//*[@class="title"]/@href')[0]

        yield {
            'note_id' : note_id,
            'title' : title,
            'url' : urljoin(base_url, href)
        }

get_monthly_info為生成器,它的作用就是獲取頁面下的所有文章的id、標題和連結,並yield。

def main():
    start_url = 'https://www.jianshu.com/trending/monthly?'

    seen_snote_ids = []
    #儲存
    fp = open('month_urls.csv', 'w', encoding = 'utf-8')
    fieldnames = ['note_id', 'title', 'url']

    writer = csv.DictWriter(fp, fieldnames = fieldnames)
    #讀取前幾頁
    for page in range(1, 7):
        params = {'seen_snote_ids[]' : seen_snote_ids, 'page' : page}

        if page > 3:
            params['utm_medium'] = 'index-banner-s'
            params['utm_source'] = 'desktop'

        print('正在爬取第%d頁' % page)

        for data in get_monthly_info(start_url, params):
            writer.writerow(data)
            print('儲存:', data['title'])
            seen_snote_ids.append(data['note_id'])
    #close
    fp.close()

main方法負責連結的構造,比如main方法中就維護了一個seen_snote_ids列表來表示已瀏覽過的文章,之後根據剛才的分析加上欄位,然後發給get_monthly_info,之後獲取到資料並寫入到csv檔案中。儲存的csv檔案大致如下:

37748501,夢之歷險記10,https://www.jianshu.com/p/b354d22f5b61
37776875,我只剩手機,https://www.jianshu.com/p/c5da7511dd13
37794473,讀林覺民《與妻書》感擬,https://www.jianshu.com/p/ad646b4a2c39
37197951,我於今夜猝死,年僅22歲,https://www.jianshu.com/p/d81d39e69277
36807878,古力娜扎親密視訊曝光:暴露女生隱私的都是渣男!,https://www.jianshu.com/p/e19a05d63737
36463859,不要在該舔狗的年紀,患上性冷淡,https://www.jianshu.com/p/70b2d1113c3e
36765004,你努力的樣子,真的好迷人,https://www.jianshu.com/p/1a89808c58a6
36341650,讀大學前的顏值 VS 讀大學後顏值,堪比換頭...,https://www.jianshu.com/p/0f049ef02105
...

文章具體連結爬取完成,那麼接下來就是爬取文章內容了。

3.文章內容抓取

本次的目標主要是分析近一個月簡書網的文章的詞頻,所以主要爬取的有文章標題、內容,並儲存到mongo資料庫(可根據需要自己更改為其他資料庫或文字)。

info_month.py

#! /usr/bin/python3.6
# -*-coding:utf-8 -*-

import requests
from lxml import etree
import csv 
import json
import pprint
import logging
import pymongo
from multiprocessing import Pool

import config

logging.basicConfig(level=logging.WARNING,#控制檯列印的日誌級別
                    filename='new.log',
                    filemode='a',##模式,有w和a,w就是寫模式,每次都會重新寫日誌
,覆蓋之前的日誌
                    #a是追加模式,預設如果不寫的話,就是追加模式
                    format=
                    '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                    #日誌格式
                    )

文章抓取採用的是多執行緒,因為抓取文章時可能會出錯,所以logging增加了一個配置增加了一個日誌。

#寫入檔案
client = pymongo.MongoClient('localhost', 27017)
mydb = client['mydb']
jianshu = mydb['jianshu']

這部分則是打開了本地的mongo資料庫,以便於存取抓取的文章(linux需要先開一個伺服器,之後才能訪問mongo)。

def get_info_of_note(li):
    '''
    @param li 文章id 文章標題 文章連結
    @return 
    '''
    note_id, title, url = li[0], li[1], li[2]

    print('開啟連結:', url, title)
    #開啟頁面
    headers = {'User-Agent': config.get_random_user_agent()}
    response = requests.get(url, headers = headers)

    #記錄一些出問題的連結
    if response.status_code != 200:
        logging.warning("%s開啟錯誤%d" % (url, response.status_code))

    selector = etree.HTML(response.text)
    #文章list
    texts = selector.xpath('//div[@class="show-content-free"]/p/text()')

    data = {
        'title': title,
        'text': '\n'.join(texts),
    }
    #為什麼不加鎖?
    jianshu.insert_one(data)

get_info_of_note的引數就是之前存入csv的一行資料。這裡面相對比較簡單,只是獲取了對應的文章而已。

這裡不太明白的是python中使用了多執行緒卻並沒有加鎖。

def get_comment_of_note(li):
    '''
    @param li 文章id 文章標題 文章連結
    @return 
    '''
    note_id, title, url = li[0], li[1], li[2]
    url = 'https://www.jianshu.com/notes/{}/comments?'.format(note_id)
    params = {
        'comment_id' : '',
        'author_only' : 'false',
        'since_id' : 0,
        'max_id' : '1586510606000',
        'order_by' : 'desc',
        'page': 1,
    }
    #開啟頁面
    headers = {'User-Agent': config.get_random_user_agent()}
    response = requests.get(url, headers = headers, params = params)

    pprint.pprint(response.text)

這個方法就是獲取文章下的所有評論,在本次抓取中並沒有用到。。。

if __name__ == '__main__':
    fp = open('month_urls.csv', 'r', encoding = 'utf-8')
    reader = csv.reader(fp)

    pool = Pool(processes = 4)
    pool.map(get_info_of_note, reader)

主動呼叫這個指令碼,就會開啟四個執行緒抓取month_urls.csv中所有的文章,並儲存到mongo資料庫中。

mongo資料庫的內容大致如下:

4.文章內容分析

在有了資料之後,就可以對文章進行分詞,之後統計,最後使用詞雲顯示出來。

簡單地使用了jieba進行分詞;統計則是使用的是collections.Counter;wordcloud用來繪製詞雲。

analysis.py

#! /usr/bin/python3.6
# -*-coding:utf-8 -*-

import jieba
import pymongo
from collections import Counter
import numpy as np
from matplotlib import pyplot as plt 
from wordcloud import WordCloud
from PIL import Image
import sys 

import config

一些必要的庫,若報錯可預先安裝一下。

def analysis(db_name, collection_name):
    '''
    分析資料
    @param db_name mongo資料庫名
    @param collection_name 集合名稱
    @return 返回collections.Counter
    '''
    client = pymongo.MongoClient('localhost', 27017)
    mydb = client[db_name]
    jianshu = mydb[collection_name]

    #獲取所有資料,返回的為一個迭代器
    results = jianshu.find()

    counter = Counter()

    for result in results:
        text = result['text']
        #分詞處理
        seg_list = jieba.cut(text, cut_all = False)

        for word in seg_list:
            #新增前先清洗
            if word not in config.bad_words:
                counter[word] += 1

    return counter

首先獲取剛才爬取的所有文章,之後遍歷每個文章的同時進行jieba分詞;最後只有該詞不再config.bad_words中,才會新增這個詞。簡單地說,就是資料清洗,清除掉一些無意義的詞彙,比如標點符號、主語、賓語等。

def write_frequencies(counter, number, filename):
    '''
    把前number個高頻詞寫入對應檔案
    @param counter Counter計量器
    @param number 前number個高頻詞
    @filename 要寫入的檔名
    '''
    fp = open(filename, 'w')
    index = 0

    for k,v in counter.most_common(number):
        if index > number:
            break
        fp.write('\'%s\', ' % k)
        #print(k, v)
        index += 1
    print('共%d高頻詞寫入%s成功' % (number, filename))
    fp.close()

該函式是把計數器的前若干個存入到檔案中,該檔案主要是為了填充config.bad_words,免得一些無意義的詞霸佔榜首。

def word_cloud(words):
    '''
    生成詞雲
    '''
    img = Image.open('./timg.jpeg')
    img_array = np.array(img)

    wc = WordCloud(
            background_color = 'white',
            width = 1500,
            height = 1500,
            mask = img_array,
            font_path = './微軟雅黑+Arial.ttf')

    #wc.generate_from_text(text)
    wc.generate_from_frequencies(words)
    plt.imshow(wc)
    plt.axis('off')
    plt.show()
    wc.to_file('./new.png')

word_cloud就是生成詞雲,繪製並儲存。

if __name__ == '__main__':
    '''
    引數:1.word 分析並生成詞雲
          2.freq 高頻詞寫入
    '''
    length = len(sys.argv)
    param = 'word'
    number = 100
    filename = 'frequent.txt'

    #獲取資料
    if length > 1:
        param = sys.argv[1]

    if length > 2:
        number = int(sys.argv[2])

    if length > 3:
        filename = sys.argv[3]

    if param == 'word':
        counter = analysis('mydb', 'jianshu')
        word_cloud(counter)
    elif param == 'freq':
        counter = analysis('mydb', 'jianshu')
        write_frequencies(counter, number, filename)
    else:
        print('請輸入正確的引數 word|(freq [number] [filename])')

之後的主函式則是有了兩個選項,可以生成詞雲;也可以寫入高頻詞。

接下來生成就完事了:

 連結:https://github.com/sky94520/jianshu_month