实战:爬取简书之搭建程序框架

上一篇一共提到了四个模块,这一篇我们来实现它们

  1. 请求模块
  2. uid 解析模块
  3. 数据爬取模块
  4. 数据保存模块

一、请求模块

分析:

  1. 随机选择 user-agent:可以预先设置一个保存了许多 user-agent的数组,然后用 random库从数组中随机选取一个 user-agent
  2. 设置代理:使用 **kwargs参数直接传递给 request模块
  3. 预处理:抛弃预处理,直接返回一个 xpath对象

随机选择 ua,将下面这段代码单独放到一个文件中(user-agent太多了╯︿╰):

#file random_user_agent.py
#-*- coding: utf-8 -*
import random

def randomUserAgent():
    USER_AGENTS = [
        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
        "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
        "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
        ... ...
    ]

    return random.choice(USER_AGENTS)

请求模块主体:

接受的参数:url, **kwargs

先检查 kwargs里是否有 headers,没有的话使用默认的 headers

第二步,用 url和 kwargs和 headers(如果有的话)发起 request请求(默认 get)

代理和一些其他的设置直接通过 kwargs传递给 requests请求

第三步,用 etree.HTML() 处理 requests的响应

第四步,返回处理后的结果

代码如下(前面讲得很详细了,我就没打注释了):

#-*- coding: utf-8 -*
import requests
from lxml import etree
from random_user_agent import randomUserAgent


def getResponse(url, **kwargs):
    if 'headers' not in kwargs:
        kwargs['headers'] = {
            'User-Agent': randomUserAgent(),
        }

    r = requests.get(url, **kwargs)
    dom = etree.HTML(r.text)

    return dom

二、uid解析模块

分析:

  1. 自动去重:从 uid模块移除,uid模块只负责返回 uid,去重的工作交给爬取模块
  2. uid生成器:使用 yield
  3. 无限爬取:通过递归的方式将第一次爬取的信息作为参数再传递给 uid解析模块

接受参数示例:

start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
  • uid:用户 uid
  • follow_num:用户关注数量
  • fans_num:用户粉丝数量
  • article_num:用户文章数量

刚开始爬取时的种子用户,为了方便只挑了一个用户,实际爬取时应该是一个由多个用户组成的数组。

yield返回示例,与接受的参数一致:

{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}

代码如下:

def getUserUids(start_users):
    #保存本次爬取的用户
    next_users = []

    #爬取 start_users里每个用户的所有关注对象的 uid
    for user in start_users:
        uid = user['uid']
        follow_num = user['follow_num']
        #如果 follow_num可以整除每次请求返回的 uid数量,max_page为 int(follow_num / 9),否则为 int(follow_num / 9)+1, 这里 PER_NUM为 9
        max_page = int(follow_num / PER_NUM) if (follow_num % PER_NUM) == 0 else int(follow_num / PER_NUM)+1

        following_urls = ['https://www.jianshu.com/users/{}/following?page={}'.format(uid, i)
                          for i in range(1, max_page+1)]

        for following_url in following_urls:
            dom = getResponse(following_url)
            items = dom.xpath('//ul/li//div[@class="info"]')

            for item in items:
                user = {}
                try:
                    user['uid'] = item.xpath('./a/@href')[0].split('/')[2]
                    user['follow_num'] = int(item.xpath('./div/span[1]/text()')[0].replace('关注','').strip())
                    user['fans_num'] = int(item.xpath('./div/span[2]/text()')[0].replace('粉丝', '').strip())
                    user['article_num'] = int(item.xpath('./div/span[3]/text()')[0].replace('文章','').strip())
                    next_users.append(user)
                    yield user
                except ValueError:
                    pass

    #递归 将本次的爬取结果作为参数再传递给 getUserUids()
    next_user_uids = getUserUids(next_users)
    #实现无限爬取
    for user in next_user_uids:
        yield user

这样当我们调用 getUserUids()时,就得到了一个可以无限生成 uid的生成器,使用方法如下:

start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]

uids = getUserUids(start_users)
for uid in uids:
    print(uid)

理论上来说,上面这段代码会一直在你的控制台上打印 uid,直到打印完绝大部分简书用户或者你选择停止运行

三、数据爬取模块

数据爬取模块可以直接复用之前的代码

分析:

  1. 去重:用一个 seen数组保存已经爬取过的 uid,每次爬取之前先判断 uid是否在 seen数组内

将之前的代码整合为一个模块:

def getArticleInfo(user):
    uid = user['uid']
    article_num = user['article_num']
    #这里 PER_NUM为 9
    max_page = int(article_num / PER_NUM) if (article_num % PER_NUM) == 0 else int(article_num / PER_NUM)+1
    article_urls = ['https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, i) for i in
                    range(1, max_page+1)]

    details = []
    for article_url in article_urls:
        dom = getResponse(article_url)
        items = dom.xpath('//ul[@class="note-list"]/li')

        for item in items:
            # 对每个 li标签再提取
            details_xpath = {
                'link': './div/a/@href',
                'title': './div/a/text()',
                'read_num': './/div[@class="meta"]/a[1]/text()',
                'comment_num': './/div[@class="meta"]/a[2]/text()',
                'heart_num': './/div[@class="meta"]/span[1]/text()',
            }

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

            #将数字转换为整数
            for key in ['read_num', 'comment_num', 'heart_num']:
                detail[key] = int(detail[key])

            details.append(detail)

    #返回爬取结果
    return details

语句:

int(article_num / PER_NUM) if (article_num % PER_NUM) == 0 else int(article_num / PER_NUM)+1

使用了 python三目表达式 if else

使用方法:

seen = []

start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
users = getUserUids(start_users)

for user in users:
    if user['uid'] not in seen:
        seen.append(user['uid'])
        info = getArticleInfo(user)

四、数据保存模块

分析:

  1. 接受一个字典列表:使用 csv库的 DictWriter.writerows()方法
  2. 自动判断文件是否已存在,选择合适的模块打开文件:用 os库的 os.path.isfile(filepath) 来判断
  3. 将数据保存模块定义为一个类,这样方便对文件的管理

代码如下:

class simplifiedCsv:
    def __init__(self, filepath):
        self.file, self.writer = self.openFile(filepath)

    def __del__(self):
        self.file.close()

    def openFile(self, filepath):
        if os.path.isfile(filepath):
            file = open(filepath, 'a', encoding='utf-8', newline='')
            writer = csv.DictWriter(file, ['link', 'title', 'read_num', 'comment_num', 'heart_num'])
            return file, writer
        else:
            file = open(filepath, 'w', encoding='utf-8', newline='')
            writer = csv.DictWriter(file, ['link', 'title', 'read_num', 'comment_num', 'heart_num'])
            writer.writeheader()
            return file, writer

    def writerows(self, data_list):
        self.writer.writerows(data_list)

使用方法:

seen = []

start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
users = getUserUids(start_users)
writer = simplifiedCsv('data.csv')

for user in users:
    if user['uid'] not in seen:
        seen.append(user['uid'])
        info = getArticleInfo(user)
        writer.writerows(info)

以上就是我们上一篇讲过的所有模块的实现,至于断点续爬我们下一篇单独讲

这一次的代码版本为 v1.0

代码在 GitHub上的链接:version_1_simple_struct_all.py

下载后可以直接用 python运行(前提是安装好了所需的库)

程序停止后会在当前目录下生成一个 data.csv的文件

我试运行了十分钟左右,爬取了大概 1万 4千条数据,大家也可以下载源码自己测试一下,也算是完成了第一个小小目标,结果截图:

最后,觉得不错的话,记得关注、点赞、评论哦(❤ ω ❤)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏深度学习与计算机视觉

如何打开ipynb文件

最近在学习《TensorFlow 实战GoogLe深度学习框架》这本书,发现里面提供的源码后缀是.ipynb,而不是.py,那么我们应该怎么打开.ipynb后缀...

1.4K60
来自专栏码农阿宇

ASP.Net Core的Code Fist代码先行操作方法

Asp.Net  core的Code Fist(代码先行)主要有以下几步: 1.创建实体类 2.创建数据库上下文 3.填加连接字符串 4.依赖注入 5.添加基架...

376170
来自专栏林德熙的博客

Latex 论文elsevier,手把手如何用Latex写论文

这几天在开始写论文,准备发的是elsevier,这个网站的instruction有问题,下载的东西基本上好多的错误,所以我就写博客记录。

1.4K20
来自专栏Spark学习技巧

Kafka源码系列之Consumer高级API性能分析

一,kafka的Consumer的高级API内部模型介绍 1,一个主线程,入口,也是数据迭代的出口 用户主线程使用KafkaStream(ConsumerIte...

48080
来自专栏向治洪

android混淆那些坑

ProGuard简介 在最新的Android Studio 2.2.2版本创建的Android工程中,module中的build.gradle有如下一段配置。这...

28560
来自专栏linux驱动个人学习

Linux内存描述之概述--Linux内存管理(一)

传统的多核运算是使用SMP(Symmetric Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个...

22530
来自专栏hbbliyong

VisualStudio 怎么使用Visual Leak Detector

那么在Windows下有什么好的内存泄漏检测工具呢?微软提供Visual Studio开发工具本身没有什么太好的内存泄漏检测功能,我们可以使用第三方工具Visu...

57770
来自专栏中国Android研究院

如何优雅的从网络加载点九图?

其实点九图和我们用的其他格式的图没有什么大的不同,只不过是在图片的四周各增加了1px的纯黑(#FF000000)的线进行标记。例如:

19220
来自专栏noteless

批处理启动vm虚拟机服务 vm12启动无界面启动vm虚拟机系统 windows上如何操作服务 sc net启动关闭服务

"D:\ApplicationFiles\System\VMware Workstation\vmrun.exe" start "F:\virtualMachi...

50630
来自专栏知识分享

关于STM32 IAP

转眼间天亮了...... 然后就想起了一个朋友QQ的个性签名:年轻人总是要为一些自己认为有意义的事情而废寝忘食,通宵达旦,直至白发方休........ 对了这篇...

62940

扫码关注云+社区

领取腾讯云代金券