【微博爬虫教程&实例】基于requests、mysql爬取大数据量博主关键字下博文及评论

【关键词:手把手教程、反爬、数据库、python爬虫、微博关键词爬虫、较大数据量、数据简单过滤】

本教程适合微博相关爬虫需求者阅读,完整实例源码将放置在文末github链接中。

该实例针对微博的反爬措施进行优化,可实现较大数据量的数据爬取需求(十万量级)。

项目准备

        Python & Pycharm

                   主要使用到的库有:

                        requests:requests库是Python中用来模拟浏览器发送网络请求与得到响应数据包的库,可以说是实现爬虫原理的核心

                        json:用于解析(如格式化为方便python处理的字典格式)得到的响应数据包,以便于后续对得到的数据进行操作处理

                        pymysql:是python下用于对mysql数据库进行相关操作的一个库,本项目中用于将爬取数据加入到本地构建好的数据库中,以及其他的相关操作(增删改查)

        Mysql数据库 & Datagrip

                        为了适用本项目较大规模数据的存放处理以及后续的使用,因此使用了数据库。

                        如果数据量不大或没有较高的数据分析需要,你也可以考虑将数据以.csv或.xlsx的格式保存(该方式直接在python源码中即可完成,可以看看其他教程)

                        本项目使用本地sql数据库存放爬取数据,为方便数据库的相关使用,选用Datagrip作为IDE对数据库进行一些操作。

                Datagrip界面如下:

需求分析

        本实例希望实现对账号特定时间段内按关键字筛选出的所有博文的爬取,并获得博文评论评论者个人信息

数据库结构设计

        爬取的数据分博主数据博文数据博文评论数据(包含评论者的信息)三种类型。因此我们需要在数据库中建立blogger_tableblog_tablecomment_table三个数据表分别存储。

        各数据表间通过索引相关联(如comment_table中一条评论数据可由索引对应到blog_table中其所属的博文数据行),以此实现数据间的联系。

       

         对每一个数据表,其内容是这样的:

         生成该表结构的mysql语句如下:

create table blog_base
(
    id_blog_base int(11) NOT NULL AUTO_INCREMENT,
    blog_date char(30) NOT NULL DEFAULT 'null' COMMENT '博文发布时间',
    blog_text varchar(6000) NOT NULL DEFAULT 'null' COMMENT '博文内容',
    blog_id varchar(20) NOT NULL DEFAULT 'null' COMMENT '博文id',
    blog_comments int NOT NULL DEFAULT -100 COMMENT '博文评论量',
    blog_likes int NOT NULL DEFAULT -100 COMMENT '博文点赞',
    blog_keywords varchar(100) NOT NULL DEFAULT 'null' COMMENT '博文关键词',
    retweeted_blogger_name varchar(50) NOT NULL DEFAULT 'null' COMMENT '转推原博主名称',
    retweeted_verified_type int NOT NULL DEFAULT -100 COMMENT '转推原博主认证',
    retweeted_text varchar(3000) NOT NULL DEFAULT 'null' COMMENT '转推原文',
    id_blogger varchar(20) NOT NULL DEFAULT 'null' COMMENT '所属博主id',
    PRIMARY KEY (id_blog_base)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

数据定位

           阅读本内容请先搜索requests库的基本使用,了解get请求与响应的基本概念

        定位博主个人信息

        1、浏览器打开微博网站(pc版,请务必登录),点开任一博主的个人界面,f12打开开发者工具。

        2、选中XHR

        3、选中该文件

        在右侧栏中找到响应,就可以得到博主的个人信息了(如screen_name、location、gender)。

        稍后我们在编写过程中使用requests包模拟浏览器得到该JSON响应文件的过程,然后获得如下图所示的JSON数据,再使用json包将该数据格式化为字典,方便获得想要的数据项。

         定位博主的博文信息

         微博博文的响应数据包是按照一页多条blog来发送的,该响应数据包对应的文件名为:

        uid这里指博主的用户id,page取不同数是不同页的博文数据包。

        下方的若干Object 每一项均为一条博文

                 点开一条博文的评论然后进入该博文的详情页,可以得到文件名为:

                鼠标滚轮往下翻评论的过程中会刷新出现若干新评论的响应数据包。但值得注意的是,与上一个博文定位不同,此时各数据包的区分并不是用page的增加来实现的

         该博文的评论区,除了最前面的若干评论(对应文件列表里未刷新时的唯一一个评论数据包)以外,其余评论数据包的GET请求参数都多了一项参数"max_id" (注意下列两项的GET参数区分,其中一个含有max_id参数)。

         博文的定位中,我们修改page的值然后模拟发送即可获得所有的博文内容,而此处我们无法采用同样的操作——很显然,max_id的数值不是按照page那样自增的规律。

        该如何获得该博文的所有的评论呢?我们需要解决如何获得max_id值的问题,否则我们无法正确模拟发送获得后续评论的请求。

        解决方法:

        1、你会发现前一个评论数据包的响应JSON包含了一个max_id(如图),这个max_id其实是下一个评论数据包GET请求中所需的max_id值

        2、因此,只需要获取首个评论数据包的响应JSON中的max_id值,我们就可以获得第二个评论数据包,第二个评论数据包的响应JSON中又包含了一个max_id值,对应下一个评论数据包的GET请求参数,以此类推。最后一个评论数据包的响应JSON不含max_id,可以作为python脚本中该部分过程结束的判断条件。

        3、这样我们就可以获得全部的评论信息了

评论用户个人信息定位

        该部分的采集伴随评论的采集(评论数据包会同时包含评论者的大部分个人信息)。

代码实现

           以下是基本功能的实现过程,下一步会对各函数进行修改以完善。

        从用户信息爬起

                关于requests库的基本使用在此不做叙述,请查阅其他文章。

                1、需要传入的headers项中,我设置了如下四项以确保获得响应JSON。

                        项目中的cookie我定义为了全局变量,以方便cookie的替换,Connection项设置为close避免过多请求带来的可能异常(或许这一项并不需要?)

                2、requests.get()后记得进行utf8编码

                3、用json.loads()解析res,得到字典格式的json_data

                4、参照之前开发者工具中显示的层级关系,在json_data中获得需要的数据

                5、认证信息存在为空但用户是认证用户的情况需要特判(verified_type是200、220这些)

import requests, json

def user_info(user_id):
    params = {
        'custom': user_id,
    }
    headers = {
        'Referer': 'https://weibo.com/' + str(user_id),
        'Host': 'weibo.com',
        'Cookie': cookie,
        'Connection': 'close'
    }

    res = requests.get('https://weibo.com/ajax/profile/info', headers=headers, params=params).content.decode("utf-8")
    json_data = json.loads(res)

    print('博主id:', json_data['data']['user']['id'])  # 博主id
    print('ip归属地:', json_data['data']['user']['location'])  # ip归属地
    print('博主名:', json_data['data']['user']['screen_name'])  # 博主名
    print('粉丝数:', json_data['data']['user']['followers_count'])  # 粉丝数
    print('关注数:', json_data['data']['user']['friends_count'])  # 关注数
    print('性别:', json_data['data']['user']['gender'])  # 性别(f & m)
    print('总微博数量:', json_data['data']['user']['statuses_count'])  # 总微博数量
    print('认证类型:', json_data['data']['user']['verified_type'])  # 认证类型
    if json_data['data']['user']['verified_type'] != -1:
        print('认证信息:', json_data['data']['user']['verified_reason'])  # 认证信息(注意缺省)
    else:
        print("Not verified")

verified_type的字段:

-1普通用户;

0名人,

1政府,

2企业,

3媒体,

4校园,

5网站,

6应用,

7团体(机构),

8待审企业,

200初级达人,

220中高级达人,

400已故V用户。

-1为普通用户,200和220为达人用户,0为黄V用户,其它即为蓝V用户

        爬取博主博文

从前述内容可以看出,博文是按照一个个json文件由服务器发给浏览器的。我们按照月份先打包所有该博主某月的所有博文(对应的一系列JSON文件),然后再根据关键字对其中需要的博文进行筛选以及进行评论的爬取。

注意,为了加快效率,我们在程序中设置了多线程。因而有task_queue.put(json_data)入队列的过程。(该处理原理是多线程任务的生产者-消费者模型

并且在爬取过程中有一定概率会请求服务器失败,我设置了重新请求的处理(retried_times)

# 请求单页博文页的函数
def post_req(user_id, page, curmonth, retried_times):
    headers = {
        'Host': 'weibo.com',
        'Referer': 'https://weibo.com/' + user_id + '?refer_flag=1001030103_',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
        'Cookie': cookie,
    }
    params = {
        'uid': user_id,
        'page': page,
        'feature': 0,
        'displayYear': 2023,
        'curMonth': curmonth,
        'stat_date': '2023' + str(curmonth).rjust(2, '0'),
    }
    if retried_times < 5:
        try:
            res = requests.get('https://weibo.com/ajax/statuses/mymblog', headers=headers,
                               params=params).content.decode('UTF-8')
        except:
            traceback.print_exc()
            random_sleep()
            retried_times += 1
            post_req(user_id, page, curmonth, retried_times)
        else:
            json_data = json.loads(res)  # 加载json文件
            if json_data['data']['list']:
                return json_data
            else:
                return None
    else:
        return -1


# 打包所有当月博文json
def post_page_list_producer(task_queue, user_id, curmonth):
    post_page = 1
    while True:
        # random_sleep()
        json_data = post_req(user_id, post_page, curmonth, retried_times=0)
        if json_data is None:
            print('全部博文包请求成功,完成打包')
            break
        elif json_data == -1:
            return -1
        else:
            task_queue.put(json_data)
            print('博文包请求成功', 'month:', curmonth, 'page:', post_page)
            post_page += 1
            random_sleep()

打包完成后,我们使用遍历对所有博文据关键字筛选,然后对博文信息进行写入数据库操作。

def post_json_data_insert(user_id, object, in_db, in_cursor, inserted_list):
    # 处理关键字并匹配
    pattern = re.compile('|'.join(key_words))
    text = object['text_raw']
    result_findall = pattern.findall(text)

    if result_findall and object['comments_count'] >= 10:
        if object['id'] not in inserted_list:
            date_formatted = date_format(object)
            if '<span class="expand">展开</span>' in object['text']:
                mblogid = object['mblogid']
                req = long_text_req(user_id, mblogid)
                print(object['text_raw'])
                object['text_raw'] = req['data']['longTextContent']
                print(object['text_raw'])
            if 'retweeted_status' in object:
                print("#################################################")
                print(object['retweeted_status']['user']['verified_type'])
                print(object['retweeted_status']['text_raw'])
                print("#################################################")
                in_cursor.execute(
                    "INSERT INTO blog_base("
                    "blog_date,"
                    "blog_text,"
                    "blog_id,"
                    "blog_comments,"
                    "blog_likes,"
                    "blog_keywords,"
                    "id_blogger,"
                    "retweeted_blogger_name,"
                    "retweeted_verified_type,"
                    "retweeted_text"
                    ") VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s); ",
                    (date_formatted,
                     object['text_raw'],
                     object['id'],
                     object['comments_count'],
                     object['attitudes_count'],
                     str(set(result_findall)).strip('{}'),
                     user_id,
                     object['retweeted_status']['user']['screen_name'],
                     object['retweeted_status']['user']['verified_type'],
                     object['retweeted_status']['text_raw']
                     )
                )
            else:
                in_cursor.execute(
                    "INSERT INTO blog_base("
                    "blog_date,"
                    "blog_text,"
                    "blog_id,"
                    "blog_comments,"
                    "blog_likes,"
                    "blog_keywords,"
                    "id_blogger"
                    ") VALUES(%s,%s,%s,%s,%s,%s,%s); ",
                    (date_formatted,
                     object['text_raw'],
                     object['id'],
                     object['comments_count'],
                     object['attitudes_count'],
                     str(set(result_findall)).strip('{}'),
                     user_id
                     )
                )
            in_db.commit()
            print('相关博文', object['text_raw'], '\n')
            counts = 0
            # 调用评论抓取函数
            if post_comment_data(user_id, object['id'], in_db, in_cursor, counts) is None:
                print('id%s 评论获得失败,删除该条博文数据、程序终止' % object['id'])
                in_cursor.execute("delete from comment_base where id_blog=%s" % object['id'])
                in_cursor.execute("delete from blog_base where blog_id=%s" % object['id'])
                in_cursor.commit()
                return -1
            inserted_list.append(object['id'])  # 记录该博文id防止重复
            return 1
        else:
            print('id%s 已存博文,跳过该条' % object['id'])
            return 1
    else:
        print('无关博文(comments_count:%s)' % object['comments_count'])
        return 1


# 消化队列中的博文json
def post_data_get_consumer(task_queue, user_id, inserted_list):  # 多线程处理page的json_data
    global thread_status
    in_db = pymysql.connect(host='localhost', port=3306, user='root', passwd='root', db='dataday0224',
                            charset='utf8mb4')
    in_cursor = in_db.cursor()
    while task_queue.empty() is not True:
        json_data = task_queue.get()
        for object in json_data['data']['list']:
            date_formatted = date_format(object)
            if is_that_day(date_formatted):
                if post_json_data_insert(user_id, object, in_db, in_cursor, inserted_list) == -1:
                    print('由于本条博文数据插入失败,该线程结束')
                    thread_status = -1
                    return -1
            else:
                print(date_formatted+'is not wanted date')
        task_queue.task_done()

在post_json_data_insert()的尾部我们调用了post_comment_data函数进行博文下评论的爬取。需要注意的是,由于评论信息存在一个需要特别关注的max_id参数需要处理,post_comment_data采集完第一次评论数据后,后续的评论数据会调用post_comment_data_rest()函数来处理max_id问题:

def post_comment_data_rest(user_id, post_id, max_id, in_db, in_cursor, counts):
    headers = {
        'Host': 'weibo.com',
        'Referer': 'https://weibo.com/' + str(user_id) + '?refer_flag=1001030103_',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
        'Cookie': cookie,
        # 'Connection': 'close'

    }
    params = {
        'flow': 1,
        'id': post_id,
        'is_show_bulletin': 2,
        'count': 20,
        'max_id': max_id,
    }
    try:
        res = requests.get('https://weibo.com/ajax/statuses/buildComments', headers=headers,
                           params=params).content.decode('UTF-8')

        json_data = json.loads(res)  # 加载json文件
    except:
        print('【' + str(post_id) + '】''post_comment_data_rest() request error, trying again...')
        traceback.print_exc()
        try:
            post_comment_data_rest(user_id, post_id, max_id, in_db, in_cursor, counts)
        except:
            print("retried failed")
            return None
    else:
        for object in json_data['data']:
            if counts >= 1000:
                break

            date_formatted = date_format(object)
            random_sleep()
            print('【', str(date_formatted), '[' + str(object['floor_number']) + ']】', '正在存储该条评论, postID:',
                  post_id)

            user_json = object['user']

            if user_json['verified_type'] != -1:
                try:
                    veri_info = user_json['verified_reason']  # 认证信息(注意缺省)
                except:
                    veri_info = ''
            else:
                veri_info = "Not verified"

            try:
                in_cursor.execute(
                    "INSERT INTO comment_base("
                    "comment_date,"
                    "comment_text,"
                    "commenter_name,"
                    "commenter_region,"
                    "commenter_id,"
                    "commenter_fans,"
                    "commenter_follow,"
                    "commenter_gender,"
                    "commenter_weibo,"
                    "commenter_veri_type,"
                    "commenter_veri_info,"
                    "id_blog"
                    ") VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);",
                    (date_formatted,
                     object['text'],
                     object['user']['screen_name'],
                     user_json['location'],
                     object['user']['id'],
                     user_json['followers_count'],
                     user_json['friends_count'],
                     user_json['gender'],
                     user_json['statuses_count'],
                     user_json['verified_type'],
                     veri_info,
                     post_id
                     )
                )
                in_db.commit()
                counts += 1
            except:
                print('An sql error on post_comment_data_rest')
                print(object['text'])
                traceback.print_exc()

        if json_data['max_id'] != 0:
            random_sleep()
            return post_comment_data_rest(user_id, post_id, json_data['max_id'], in_db, in_cursor, counts)
        else:
            return 1

该程序的主功能集成函数为:

def all_post_data(user_id):  # user_id的全体博文内容获取
    global thread_status
    thread_status = 1

    months = [2, ]

    restartPoint = None
    while True:  # 生产者队列获取页面json失败时,重复该月json获取
        for curmonth in months:
            if restartPoint:
                if curmonth != restartPoint:
                    continue
            print('正在打包博主%s月的全部博文页...' % curmonth)
            task_queue = Queue()
            if post_page_list_producer(task_queue, user_id, curmonth) == -1:  # 队列一次性获得全部的当月博文页
                restartPoint = curmonth
                break

            inserted_list = []
            thread_list = []
            for index in range(20):
                consumer_thread = Thread(target=post_data_get_consumer, args=(task_queue, user_id, inserted_list))
                thread_list.append(consumer_thread)
            for t in thread_list:
                t.start()
            for t in thread_list:
                t.join()
            if thread_status == -1:
                print('有线程未完成任务,程序终止:检查用户 %s 的 %s 月博文数据' % (user_id, curmonth))
                exit()
            else:
                print('用户 %s 的 %s 月数据采集完成' % (user_id, curmonth))

            restartPoint = None
        if restartPoint is None:
            break

自此,对代码的主要设计部分讲解结束。除此之外,脚本中还有一些边缘的函数(如日期格式化、处理文本内容采集不全的函数)。

完整代码请访问我的github项目:GitHub - otonashi-ayana/SpiderWeibo: Collect blog posts and comments, commentator information and build a database

一些问题的说明

国内的ip免费/付费代理商不接单访问微博的ip代理服务(政策原因),因而通过该方法解决微博反爬冻结ip不可行。本实例可在若干小时内采集到上十万规模的博文以及评论数据

本人代码水平堪忧,本代码中的多线程部分与异常处理非常混乱(),同时该项目前后经过了很长一段时间修改,其屎山程度有目共睹。不过本实例中的一些细节问题在网络上没有查阅到相关资料,博主希望能够帮助到遇到同样问题的各位。

效果展示

博文数据采集数据库的展示(549,186行数据):

其中每一行评论都会记录其所属的博文id,通过博文id可以找到博文数据表中对应的博文,实现了数据之间的关联:

 感谢你的阅读。

  • 12
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。
爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

narcissus1e7b97

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值