json和csv库的使用

Posted 123Wheels

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了json和csv库的使用相关的知识,希望对你有一定的参考价值。

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

一、分析爬取逻辑

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

  • 文章的标题

  • 文章链接

  • 访问量

  • 评论数

  • 点赞数

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

我们的 xpath如下:

 
   
   
 
  1. #获取所有 li标签

  2. xpath_items = '//ul[@class="note-list"]/li'

  3. #对每个 li标签再提取

  4. xpath_link = './div/a/@href'

  5. xpath_title = './div/a/text()'

  6. xpath_comment_num = './/div[@class="meta"]/a[2]/text()'

  7. xpath_heart_num = './/div[@class="meta"]/span/text()'

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

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

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

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

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

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

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

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

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

 
   
   
 
  1. url = 'https://www.jianshu.com/u/3313b20a4e25?order_by=shared_at&page=1'

  2. #其中order_by是排序方式,这个不用管

  3. #page是当前页数

  4. #3313b20a4e25是一个类似用户 id的字符串,每个账号都不同

  5. #可以从主页链接中提取出来 如 https://www.jianshu.com/u/9bc194fde100

  6. https://www.jianshu.com/u/3313b20a4e25?order_by=shared_at&page=

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

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

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

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

 
   
   
 
  1. base_url = 'https://www.jianshu.com/u/9bc194fde100?order_by=shared_at&page='

  2. for i in range(12):

  3.    url = base_url + str(i + 1)

  4.    ... ...

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

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

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

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

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

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

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

但是... ....

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

二、代码实现

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

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

 
   
   
 
  1. #url生成器

  2. def urlsGenerater(uid):

  3.    # 设置请求头

  4.    headers = {

  5.        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'

  6.    }

  7.    r = requests.get('https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, 1), headers=headers)

  8.    dom = etree.HTML(r.text)

  9.    #获取文章数量和最大页数

  10.    article_num = int(dom.xpath('//div[@class="info"]//li[3]//p/text()')[0].strip())

  11.    print(article_num)

  12.    max_page_num = article_num / 9

  13.    i = 1

  14.    while True:

  15.        yield 'https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, i)

  16.        if i >= max_page_num:

  17.            break

  18.        i+=1

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

 
   
   
 
  1. #获取文章的 xpath数组

  2. def getArticleItems(url):

  3.    #设置请求头

  4.    headers = {

  5.        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'

  6.    }

  7.    # 获取所有 li标签

  8.    xpath_items = '//ul[@class="note-list"]/li'

  9.    r = requests.get(url, headers=headers)

  10.    dom = etree.HTML(r.text)

  11.    return dom.xpath(xpath_items)

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

 
   
   
 
  1. #获取文章的相关信息

  2. def getDetails(article_item):

  3.    # 对每个 li标签再提取

  4.    details_xpath = {

  5.        'link': './div/a/@href',

  6.        'title': './div/a/text()',

  7.        'comment_num': './/div[@class="meta"]/a[2]/text()',

  8.        'heart_num': './/div[@class="meta"]/span/text()',

  9.    }

  10.    items = details_xpath.items()

  11.    detail = {}

  12.    for key, path in items:

  13.        detail[key] = ''.join(article_item.xpath(path)).strip()

  14.    return detail

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

 
   
   
 
  1. uid = '9bc194fde100'

  2. urls = urlsGenerater(uid)

  3. for url in urls:

  4.    article_items = getArticleItems(url)

  5.    for article_item in article_items:

  6.        print(getDetails(article_item))

打印结果:

json和csv库的使用

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

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

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

 
   
   
 
  1. #通过 csv来保存数据 这里 csvobj要求是 csv.DictWriter

  2. def csvSaveMethod(csvobj, data):

  3.    csvobj.writerow(data)

  4. #通过 json来保存数据 这里的 data必须是所有结果组成的一个列表

  5. def jsonSaveMethod(fileobj, data):

  6.    json.dump(data, fileobj)

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

 
   
   
 
  1. uid = '9bc194fde100'

  2. urls = urlsGenerater(uid)

  3. #保存 json结果的容器

  4. results = []

  5. #用 csvSaceMethod

  6. with open('data.csv', 'w', newline='', encoding='utf-8') as csvfile:

  7.    fieldnames = ['link', 'title', 'comment_num', 'heart_num']

  8.    csvobj = csv.DictWriter(csvfile, fieldnames=fieldnames)

  9.    csvobj.writeheader()

  10.    for url in urls:

  11.        article_items = getArticleItems(url)

  12.        for article_item in article_items:

  13.            details = getDetails(article_item)

  14.            #将结果添加到 results中,等下用 json写入

  15.            results.append(details)

  16.            csvSaveMethod(csvobj, details)

  17. #用 jsonSaveMethod

  18. with open('data.json', 'w', encoding='utf-8') as fp:

  19.    jsonSaveMethod(results, fp)

结果截图:

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

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

 
   
   
 
  1. def jsonSaveMethod(data, fileobj):

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

这样就好多了:

完整的代码请访问 github:https://github.com/geebos/pythoncrawler/blob/master/projectjsonandcsv/crawljanshuarticles_info.py

三、总结

  1. 在敲代码之前要仔细分析

  2. 尽量写出模块化的代码,这样便于修改,代码的逻辑和结构页更加清晰

  3. json库不能实时写入数据,只能在最后一起写入,对内存要求较大

  4. csv库可以逐行写入也可以逐行读取,但是在操作时一定要注意数据的结构,任何一行出现缺漏都会造成很大影响

  5. 在进行数据读取的时候一定要注意编码,出错往往是编码的问题


以上是关于json和csv库的使用的主要内容,如果未能解决你的问题,请参考以下文章

如何在android库的片段中添加按钮和textView

错误代码:错误域 = NSCocoaErrorDomain 代码 = 3840“JSON 文本没有以数组或对象和允许未设置片段的选项开头。”

Pandas库的学习

Pandas库的学习

不使用支持库的 Android 4.0、4.1 (<4.2) 中嵌套片段的最佳实践

使用 json rereiver php mysql 在片段中填充列表视图