上一篇博文 新浪微博数据爬取Part 1:用户个人信息 介绍了如何爬取用户个人资料,使用了BeautifulSoup以及正则表达式,最后得到了与用户有关的14个字段。在这篇文章里,将继续介绍如何爬取微博数据,爬取的内容包括用户的粉丝、用户的关注者、用户个人发表的微博三部分信息。博主知道,没有完整的项目源码支持,对于初学者来说,确实是一件不容易的事。所以,笔者打算先将每个部分的代码单独抽取出来,等到下一篇附上源码的同时,介绍如何运行整个爬虫项目。
本文继续使用第二种方法进行数据爬取,与登录有关的信息,此处就不重复介绍了,读者可参考前面的文章 模拟新浪微博登录:从原理分析到实现 AND 新浪微博数据爬取Part 1:用户个人信息 。
说明一下,以下所有方法都是写在SinaClient类中,关于SinaClient类所有方法,本文不会细说,不过会在后续文章中给出,还请见谅。
登录微博之后,可以使用浏览器观察某个用户关注者的url链接,最后你会发现,用户 1669282904
的关注者url为 http://weibo.cn/1669282904/follow?page=1
,后面的 page=1
为参数,所以我们需要做的就是获取所有page的源码,并将关注着的uid取出来,然后合并到一起。整个代码如下:
#爬取单个用户的follow,ulr = http://weibo.cn/%uid/follow?page=1 def getUserFollows(self, uid, params="page=1"): time.sleep(2) url = "http://weibo.cn/%s/follow?%s" %(uid, params) self.logger.info("page is: " + url) text = self.openURL(url) soup = BS(text, "html.parser") res = soup.find_all('table') reg_uid = r"uid=(/d+)&" #匹配uid follows = {"uid": uid, "follow_ids": list(set([y for x in [re.findall(reg_uid, str(elem)) for elem in res] for y in x ]))} next_url = re.findall('<div><a href="(.*?)">下页</a> ', text) #匹配"下页"内容 if len(next_url) != 0: url_params = next_url[0].split("?")[-1] follows['follow_ids'].extend(self.getUserFollows(uid, params=url_params)["follow_ids"]) #将结果集合并 return follows
温馨提示:在获取用户关注者时,有的用户因不明原因被sina确定为广告用户,所以不会显示出来,因此在得到的follows列表里,uid的总数会有所缩减。最后返回一个JSON字符串,包括uid、follow_ids两个字段,即:
uid:用户ID follow_ids:关注人ID列表
同理,可以使用浏览器观察某个用户粉丝的url链接,以用户 1669282904
为例,粉丝的url链接为 http://weibo.cn/1669282904/fans?page=1
,后面的 page=1
为参数,所以我们需要做的也是获取所有page的源码,并将用户粉丝的uid取出,最后合并到一起。整个代码如下:
#获取用户粉丝对象UID列表 ulr = http://weibo.cn/%uid/fans?page=1 def getUserFans(self, uid, params="page=1"): time.sleep(2) url = "http://weibo.cn/%s/fans?%s" %(uid, params) self.logger.info("page is: " + url) text = self.openURL(url) soup = BS(text, "html.parser") res = soup.find_all('table') reg_uid = r"uid=(/d+)&" #正则匹配uid fans = {"uid": uid, "fans_ids": list(set([y for x in [re.findall(reg_uid, str(elem)) for elem in res] for y in x ]))} next_url = re.findall('<div><a href="(.*?)">下页</a> ', text) #匹配"下页"内容 if len(next_url) != 0: url_params = next_url[0].split("?")[-1] fans['fans_ids'].extend(self.getUserFans(uid, params=url_params)["fans_ids"]) #将结果集合并 return fans
温馨提示:在获取用户粉丝时,有的用户同样因不明原因被sina确定为广告用户,所以不会显示出来,因此在得到的fans列表里,uid的总数会也有所缩减。最后返回一个JSON字符串,包括uid、fans_ids两个字段,即:
uid:用户ID fans_ids:粉丝列表
为了获取用户发表的微博,我们同样需要知道显示微博的url,在这里我们使用的网址是 http://weibo.cn/ ,该url内容相对而言易于爬取。值得一提的是,爬取时需要注意的细节比较多,尤其是用户发微博的时间,需要仔细斟酌。下面是获取用户tweets的源码,每个tweets结果包括用户ID、创建时间、评论数、转发数、内容、发表来源、转发理由、点赞量、tweets类型(转发、原创)等字段。完整的代码如下:
""" @func: 获取用户的发的微博信息 """ def getUserTweets(self, uid, tweets_all, params="page=1"): self.switchUserAccount(myconf.userlist) url = r"http://weibo.cn/%s/profile?%s" %(uid, params) self.logger.info("URL path is: " + url) text = self.openURL(url) soup = BS(text, "html.parser") res = soup.find_all("div", {"class":"c"}) #规则:如果div中子div数量为1,则为一个原厂文本说说;数量为2,则根据cmt判断是原创图文还是转发文本说说;数量为3,则为转发图文 tweets_list = [] for elem in res: tweets = {} unicode_text = unicode(elem) sub_divs = elem.find_all("div") today = time.strftime('%Y-%m-%d',time.localtime(time.time())) if len(sub_divs) in [1, 2, 3]: tweets["uid"] = uid tweets["reason"] = "null" #tweets["content"] = re.findall("<span class=/"ctt/">(.*?)</span>", str(elem.find("span", {"class": "ctt"})))[0] tweets["content"] = elem.find("span", {"class": "ctt"}).text soup_text = elem.find("span", {"class": "ct"}).text created_at = re.findall("/d/d/d/d-/d/d-/d/d /d/d:/d/d:/d/d", unicode(soup_text)) post_time = re.findall("/d/d:/d/d", unicode(soup_text)) split_text = unicode(soup_text).split(u"/u5206/u949f/u524d") if not created_at: created_at = re.findall(u"/d/d/u6708/d/d/u65e5 /d/d:/d/d", unicode(soup_text)) tweets["created_at"] = time.strftime("%Y-",time.localtime()) + unicode(created_at[0]).replace(u"/u6708", "-").replace(u"/u65e5", "") + ":00" tweets["source"] = soup_text.split(created_at[0])[-1].strip(u"/u00a0/u6765/u81ea") elif created_at: tweets["created_at"] = unicode(created_at[0]).replace(u"/u6708", "-").replace(u"/u65e5", "") tweets["source"] = soup_text.split(created_at[0])[-1].strip(u"/u00a0/u6765/u81ea") elif post_time: tweets["created_at"] = today + " " + post_time[0] + ":00" tweets["source"] = soup_text.split(post_time[0])[-1].strip(u"/u00a0/u6765/u81ea") elif len(split_text) == 2: tweets["created_at"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - int(split_text[0])*60)) tweets["source"] = split_text[-1].strip(u"/u00a0/u6765/u81ea") tweets["like_count"] = re.findall(u'/u8d5e/[(/d+)/]', unicode_text)[-1] tweets["repost_count"] = re.findall(u'/u8f6c/u53d1/[(/d+)/]', unicode_text)[-1] tweets["comment_count"] = re.findall( u'/u8bc4/u8bba/[(/d+)/]', unicode_text)[-1] if len(sub_divs) == 0: pass elif len(sub_divs) == 1: #self.logger.info("text") tweets["type"] = "original_text" elif len(sub_divs) == 2: #self.logger.info("image") tweets["type"] = "original_image" #根据cmt的存在判断是否为转发的文字和原创的图文说说 cmt = elem.find_all("span", {"class": "cmt"}) if cmt: tweets["type"] = "repost_text" tweets["reason"] = re.findall("</span>(.*?)<a", str(sub_divs[1]))[0] elif len(sub_divs) == 3: #self.logger.info("repost") tweets["type"] = "repost_image" tweets["reason"] = re.findall("</span>(.*?)<a", str(sub_divs[2]))[0] else: self.logger.error("parse error") pass if tweets: tweets_list.append(json.dumps(tweets)) self.output("/n".join(tweets_list), "output/" + uid + "/" + uid + "_tweets_" + params.replace("=", "") + ".json") next_url = re.findall('<div><a href="(.*?)">下页</a> ', text) #匹配"下页"内容 if len(next_url) != 0: url_params = next_url[0].split("?")[-1] tweets_all.extend(tweets_list) self.getUserTweets(uid, tweets_all, params=url_params) return tweets_list
getUserTweets 方法传入三个参数,外部调用时只需传入前两个参数即可,一个是uid,一个是类型为list的tweets_list。最后返回就是该list,每个元素都是一个JSON字符串,字段如下:
uid:用户ID content:微博内容 created_at:发表时间 source:发布工具/平台 comment_count:评论数 repost_count:转载数 type:微博类型(原创/转发) like_count:点赞量 reason:转发理由(原创博文无理由取值为空)
根据上面的代码运行可得到如下样例结果:
{"follow_ids": ["6049590367", "2239044693", "1974808274", "1674175865"], "uid": "1669282904"}
{"fans_ids": ["5883245860", "3104658267"], "uid": "1669282904"}
{"uid": "1669282904", "created_at": "2016-12-16 11:42:00", "comment_count": "0", "repost_count": "0", "content": "#/u5317/u4eac/u7a81/u53d1#/u3010/u96fe/u973e/u8fdb/u57ce/u4e86/u2026[/u6cea]/u3011@/u6c14/u8c61/u5317/u4eac /uff1a/u96fe/u973e/u5df2/u5230/u5317/u4eac/uff0c/u6b64/u523b/u80fd/u89c1/u5ea6/u5357/u5317/u5dee/u8ddd/u5f88/u5927/uff0c/u5357/u90e8/u6c61/u67d3/u7269/u6d53/u8f83/u9ad8/uff0c/u901a/u5dde/u3001/u95e8/u5934/u6c9f/u548c/u6d77/u6dc0/u8fd8/u80fd/u770b/u89c1/u84dd/u5929/uff0c/u4f46/u5927/u5174/u90e8/u5206/u5730/u533a/u80fd/u89c1/u5ea6/u5df2/u4e0d/u8db32/u516c/u91cc/u3002/u3002/u3002/u5927/u5bb6/u4e00/u5b9a/u8bb0/u5f97/u6234/u53e3/u7f69/uff0c/u8fd9/u51e0/u5929/u4e5f/u5c3d/u91cf/u4e0d/u8981/u5f00/u7a97/u901a/u98ce/u3002", "source": "iPhone 6s Plus", "reason": "/u6709/u79cd/u707e/u96be/u7247/u7684/u8d76/u811a[/u611f/u5192] ", "like_count": "0", "type": "repost_image"} {"uid": "1669282904", "created_at": "2016-12-01 21:03:00", "comment_count": "3", "repost_count": "0", "content": "/u679c/u7136/u6211/u5927/u6570/u636e/u4e07/u80fd/u7684/uff0c/u505a/u5b8cweb/u505aapp/uff0c/u505a/u5b8capp/u505a/u5e7f/u544a[/u4e8c/u54c8][/u4e8c/u54c8]", "source": "iPhone 6s Plus", "reason": "null", "like_count": "1", "type": "original_text"}
匆忙之际赶出本文,篇幅有些简洁,代码部分只给出了核心内容,如有任何问题,可在下面留言。对于微博数据爬取的全部内容,将在下一篇文章中介绍,敬请期待!