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

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

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

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

上一篇:一个大胆的想法,爬取简书所有的文章信息
下一篇:实战:爬取简书之多线程爬取(一)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容