首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

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

一、分析爬取逻辑

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

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

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

我们的 xpath如下:

代码语言:javascript
复制
#获取所有 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篇文章,所以在上次的例子中一个请求就获取到了我全部的文章。

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

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

代码语言:javascript
复制
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页,那我们的代码可以这样写:

代码语言:javascript
复制
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的时候,文章的标签是激活的,而当我们在动态的栏目下时,动态的标签是激活的(动态两个字下有一个横杠,表示处于激活状态)。

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

但是... ....

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

二、代码实现

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

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

代码语言:javascript
复制
#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,接受用户文章列表的链接,返回文章列表的对象数组:

代码语言:javascript
复制
#获取文章的 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对象,以字典格式返回文章的相关信息:

代码语言:javascript
复制
#获取文章的相关信息
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

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

代码语言:javascript
复制
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

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

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

代码语言:javascript
复制
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做一些改动:

代码语言:javascript
复制
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. 在进行数据读取的时候一定要注意编码,出错往往是编码的问题

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

下一篇
举报
领券