爬取简书网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 嗯~,可以认为是文章; 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方法负责链接的构造,它内部维护了一个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数据库,以便于存取抓取的文章(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的参数li就是之前存入csv的一行数据。

get_info_of_note函数的实现相对简单,只是获取了对应的文章并保存(不太明白的是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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值