前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战:爬取简书之搭建程序框架

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

作者头像
渔父歌
发布2018-09-28 16:00:49
2960
发布2018-09-28 16:00:49
举报
文章被收录于专栏:数据结构笔记数据结构笔记

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

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

一、请求模块

分析:

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

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

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

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

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

代码语言:javascript
复制
#-*- 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解析模块

接受参数示例:

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

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

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

代码语言:javascript
复制
{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}

代码如下:

代码语言:javascript
复制
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的生成器,使用方法如下:

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

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

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

语句:

代码语言:javascript
复制
int(article_num / PER_NUM) if (article_num % PER_NUM) == 0 else int(article_num / PER_NUM)+1

使用了 python三目表达式 if else

使用方法:

代码语言:javascript
复制
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. 将数据保存模块定义为一个类,这样方便对文件的管理

代码如下:

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

使用方法:

代码语言:javascript
复制
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千条数据,大家也可以下载源码自己测试一下,也算是完成了第一个小小目标,结果截图:

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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、请求模块
  • 二、uid解析模块
  • 三、数据爬取模块
  • 四、数据保存模块
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档