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

作者: 渔父歌 | 来源:发表于2018-07-13 22:51 被阅读61次

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

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

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

    上一篇:一个大胆的想法,爬取简书所有的文章信息

    相关文章

      网友评论

      • 向阳_0c4b:谢谢分享 有个开始有个信任的bug报错 HTTPSConnectionPool(host='www.jianshu.com', port=443): Max retries exceeded with url: /users/a3ea268aeb60/following?page=1 (Caused by SSLError(SSLError("bad handshake: Error([('SSL routines', 'ssl3_get_server_certificate', 'certificate verify failed')],)" 也找到了解决方案 谢谢大佬

      本文标题:实战:爬取简书之搭建程序框架

      本文链接:https://www.haomeiwen.com/subject/tmxepftx.html