前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python爬虫系列之数据存储实战:爬取简书用户文章列表并保存

python爬虫系列之数据存储实战:爬取简书用户文章列表并保存

作者头像
渔父歌
发布2018-09-28 15:58:57
1.8K0
发布2018-09-28 15:58:57
举报

前面讲了 json和 csv两个存储数据的库,在数据量比较少的时候,用这两个库很方便。

一、分析爬取逻辑

这一篇我们来爬取简书用户的文章列表,和之前爬取我的文章列表一样,我们要爬取的信息有:

  • 文章的标题
  • 文章链接
  • 访问量
  • 评论数
  • 点赞数

网页分析请看:python爬虫系列之 html页面解析:如何写 xpath路径

我们的 xpath如下:

#获取所有 li标签
xpath_items = '//ul[@class="note-list"]/li'
#对每个 li标签再提取
xpath_link = './div/a/@href'
xpath_title = './div/a/text()'
xpath_comment_num = './/div[@class="meta"]/a[2]/text()'
xpath_heart_num = './/div[@class="meta"]/span/text()'

我们的爬取目标是列表里的一位文章数较多的:Python测试开发人工智能

他写了111篇文章,累计24万余字。

我们今天的目标就是爬取他所有文章的标题、链接、访问量、评论数和点赞数。

分析完成了,就到了爬虫时间。

大家刚一看可能会觉得很简单,但是当开始爬时就会发现问题并不简单。

在前面爬我的文章列表的例子里,一次请求就可以获得我的全部文章了,但那是因为我的文章还比较少,所以一次请求就全部获取到。

实际上简书在这里使用了懒加载,当你向下滚动页面时会自动加载下一页,每次加载9篇文章,所以在上次的例子中一个请求就获取到了我全部的文章。

那怎么办呢?别担心,经过一番抓包,终于找到了懒加载的链接,大家可以直接拿去用。

至于抓包是什么,怎么抓包就留到以后讲。链接如下:

url = 'https://www.jianshu.com/u/3313b20a4e25?order_by=shared_at&page=1'
#其中order_by是排序方式,这个不用管
#page是当前页数
#3313b20a4e25是一个类似用户 id的字符串,每个账号都不同
#可以从主页链接中提取出来 如 https://www.jianshu.com/u/9bc194fde100
https://www.jianshu.com/u/3313b20a4e25?order_by=shared_at&page=

链接返回的是一个 html代码片段,和页面上的文章列表那一段相同,我们可以直接应用 xpath。

另外,一个爬虫应该是自动化的,也就是说至少得要能够在爬取完毕后自动停止,所以我们的第一个问题就是:

question-1:如何判断数据爬取完毕了

这里我们仔细一想,这个账号下有111篇文章,那么最多只有111 / 9 + 1 = 13页,那我们的代码可以这样写:

base_url = 'https://www.jianshu.com/u/9bc194fde100?order_by=shared_at&page='
for i in range(12):
    url = base_url + str(i + 1)
    ... ...

这样写很不好,虽然爬虫可以自动停止了,但是过几个月再来爬说不定就有150篇了,这时候我们就得改代码。

而且不可能每个人的文章都刚好是13页,换个人我们页得改代码,所以说这是假的自动化。

那怎么办呢?我们知道当爬到13页时应该没有文章了,那让我们看一下访问第14页会怎么样

可以看到第 14页是动态页面,这里不得不吐槽一下简书,竟然多个接口混用,不应该是 404 not found吗。这样平白给我们的爬取增添了一些麻烦。

不过还好已经知道问题是什么了,这样就只要想出解决办法就好。

观察一下发现当我们在文章栏目下,也就是页数小于 14的时候,文章的标签是激活的,而当我们在动态的栏目下时,动态的标签是激活的(动态两个字下有一个横杠,表示处于激活状态)。

显然在这两个之间同时只能有一个处于激活状态,所以我们可以通过查看文章标签的状态来判断是否爬取完成。

但是... ....

我们又发现在用户的名字下面就有用户的文章数,我们可以获取用户的文章数再计算出总页面数啊!!!(简直被自己蠢哭(;´д`)ゞ)

二、代码实现

分析结束,下面看代码部分:

我们先定义一个生成器,接受简书用户的唯一标识符,先获取用户当前的文章数,然后通过文章数计算出页面数,再根据页面数来生成对应用户的文章列表的链接:

#url生成器
def urlsGenerater(uid):
    # 设置请求头
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
    }
    r = requests.get('https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, 1), headers=headers)
    dom = etree.HTML(r.text)

    #获取文章数量和最大页数
    article_num = int(dom.xpath('//div[@class="info"]//li[3]//p/text()')[0].strip())
    print(article_num)
    max_page_num = article_num / 9

    i = 1
    while True:
        yield 'https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, i)
        if i >= max_page_num:
            break
        i+=1

定义一个函数 getArticleItems,接受用户文章列表的链接,返回文章列表的对象数组:

#获取文章的 xpath数组
def getArticleItems(url):
    #设置请求头
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
    }
    # 获取所有 li标签
    xpath_items = '//ul[@class="note-list"]/li'
    
    r = requests.get(url, headers=headers)
    dom = etree.HTML(r.text)
    return dom.xpath(xpath_items)

定义一个函数 getDetails,接受一个文章的 xpath对象,以字典格式返回文章的相关信息:

#获取文章的相关信息
def getDetails(article_item):
    # 对每个 li标签再提取
    details_xpath = {
        'link': './div/a/@href',
        'title': './div/a/text()',
        'comment_num': './/div[@class="meta"]/a[2]/text()',
        'heart_num': './/div[@class="meta"]/span/text()',
    }

    items = details_xpath.items()
    detail = {}
    for key, path in items:
        detail[key] = ''.join(article_item.xpath(path)).strip()
    return detail

将上面的几个模块组合起来,先把获取到的数据打印出来看是否符合要求:

uid = '9bc194fde100'
urls = urlsGenerater(uid)

for url in urls:
    article_items = getArticleItems(url)
    for article_item in article_items:
        print(getDetails(article_item))

打印结果:

可以看到,爬取的信息已经基本符合我们的要求了,下面就剩如何把信息保存下来了。

我们用 json和 csv两个库来保存数据。

根据模块化的编程思想,我们先写两个函数 csvSaveMethod和 jsonSaveMethod

#通过 csv来保存数据 这里 csvobj要求是 csv.DictWriter
def csvSaveMethod(csvobj, data):
    csvobj.writerow(data)
    
#通过 json来保存数据 这里的 data必须是所有结果组成的一个列表
def jsonSaveMethod(fileobj, data):
    json.dump(data, fileobj)

下面是使用 csvSaveMethod和 jsonSaveMethod的代码:

uid = '9bc194fde100'
urls = urlsGenerater(uid)

#保存 json结果的容器
results = []


#用 csvSaceMethod
with open('data.csv', 'w', newline='', encoding='utf-8') as csvfile:
    fieldnames = ['link', 'title', 'comment_num', 'heart_num']
    csvobj = csv.DictWriter(csvfile, fieldnames=fieldnames)
    csvobj.writeheader()

    for url in urls:
        article_items = getArticleItems(url)
        for article_item in article_items:
            details = getDetails(article_item)
            #将结果添加到 results中,等下用 json写入
            results.append(details)
            csvSaveMethod(csvobj, details)

#用 jsonSaveMethod
with open('data.json', 'w', encoding='utf-8') as fp:
    jsonSaveMethod(results, fp)

结果截图:

我们发现 jsonSaveMethod方法产生的 json文件里的内容没有排版,而且中文全部转化成 ascii编码了,这样不便于查阅。

为了解决这个问题,我们对 jsonSaveMethod做一些改动:

def jsonSaveMethod(data, fileobj):
    json.dump(data, fileobj, ensure_ascii=False, indent=2)

这样就好多了:

完整的代码请访问 github:https://github.com/geebos/python_crawler/blob/master/project_json_and_csv/crawl_janshu_articles_info.py

三、总结

  1. 在敲代码之前要仔细分析
  2. 尽量写出模块化的代码,这样便于修改,代码的逻辑和结构页更加清晰
  3. json库不能实时写入数据,只能在最后一起写入,对内存要求较大
  4. csv库可以逐行写入也可以逐行读取,但是在操作时一定要注意数据的结构,任何一行出现缺漏都会造成很大影响
  5. 在进行数据读取的时候一定要注意编码,出错往往是编码的问题

觉得不错就点个赞吧(ˇ∀ˇ)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.07.10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、分析爬取逻辑
  • 二、代码实现
  • 三、总结
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档