声明:本文内容和涉及到的代码仅限于我的学习,任何人不得做为商业用途。html
本文将介绍我最近在学习Python过程当中写的一个爬虫程序,将力争作到不须要有任何Python基础的程序员都能读懂。读者也能够先跳到文章末尾看最终收集的数据效果和完整代码。python
本次练习Python爬虫的目标需求为如下两点:mysql
首先经过浏览器查看www.huajiao.com网站上的各个页面,分析它的网站结构。获得以下信息:git
以“热门推荐”为例,以下图,每一个直播页面的url格式为http://www.huajiao.com/l/liveId, 这里的liveId惟一标识一个直播,好比http://www.huajiao.com/l/52860333
程序员
经过点击用户昵称能够进入主播的我的主页
github
更加完整的我的信息包括关注数,粉丝数,赞数,经验值等数据;也有主播的直播历史数据,以下图,每一个主播我的主页的url格式为http://www.huajiao.com/user/userId, 这里的userId惟一标识一个主播用户,好比http://www.huajiao.com/user/50647288
web
经过以上的分析,爬虫能够从直播列表页入手,获取到全部的直播url中的直播id,即上文提到的liveId;
拿到直播id后就能够进入直播页获取用户id,即前面提到的userId,
有了userId后就能够进入主播我的主页,在我的主页上有主播完整的我的信息和直播历史信息。
具体步骤以下:ajax
以上是根据观察网站页面,直观上得出的一个爬虫逻辑,但实际在开发过程当中,还要考虑更多,好比:sql
如上逻辑步骤分析清楚后,就是编码了,利用Python来实现以上的逻辑步骤。数据库
其中Tbl_Huajiao_User用于存储主播的我的数据,Tbl_Huajiao_Live用于存储主播的历史直播数据,其中字段FScrapedTime是每次记录更新的时间,依靠此字段能够实现简单的更新策略。
# filter out live ids from a url def filterLiveIds(url): html = urlopen(url) liveIds = set() bsObj = BeautifulSoup(html, "html.parser") for link in bsObj.findAll("a", href=re.compile("^(/l/)")): if 'href' in link.attrs: newPage = link.attrs['href'] liveId = re.findall("[0-9]+", newPage) liveIds.add(liveId[0]) return liveIds
关于python中如何定义函数,直接看以上代码就能够了,使用”def”和冒号,没有大括号。其中urlopen(url)是python的库函数,须要作import, 以下:
from urllib.request import urlopen
其中BeautifulSoup是一个第三方Python库,经过它就能够方便的解析html代码了,经过它的findAll()方法找出全部的a标签,而且这个方法支持正则,因此在它的参数里我传入了一个正则re.compile(“^(/l/)”)来表示寻找一”/l/”开头的全部连接地址,bsObj.findAll(“a”, href=re.compile(“^(/l/)”))的结果是一个列表,故使用for循环来遍历列表内的元素,在遍历过程当中经过使用正则re.findall(“[0-9]+”, newPage)匹配出liveId, 并临时保存在liveIds中,并将liveIds返回给调用者。
# get user id from live page def getUserId(liveId): html = urlopen("http://www.huajiao.com/" + "l/" + str(liveId)) bsObj = BeautifulSoup(html, "html.parser") text = bsObj.title.get_text() res = re.findall("[0-9]+", text) return res[0]
这里仍是使用BeautifulSoup分析直播页的html结构,使用bsObj.title.get_text()获取到主播Id的文本信息后,经过正则获取到最终的userId
#get user data from user page def getUserData(userId): html = urlopen("http://www.huajiao.com/user/" + str(userId)) bsObj = BeautifulSoup(html, "html.parser") data = dict() try: userInfoObj = bsObj.find("div", {"id":"userInfo"}) data['FAvatar'] = userInfoObj.find("div", {"class": "avatar"}).img.attrs['src'] userId = userInfoObj.find("p", {"class":"user_id"}).get_text() data['FUserId'] = re.findall("[0-9]+", userId)[0] tmp = userInfoObj.h3.get_text('|', strip=True).split('|') #print(tmp[0].encode("utf-8")) data['FUserName'] = tmp[0] data['FLevel'] = tmp[1] tmp = userInfoObj.find("ul", {"class":"clearfix"}).get_text('|', strip=True).split('|') data['FFollow'] = tmp[0] data['FFollowed'] = tmp[2] data['FSupported'] = tmp[4] data['FExperience'] = tmp[6] return data except AttributeError: #traceback.print_exc() print(str(userId) + ":html parse error in getUserData()") return 0
以上使用了python的try-except的异常处理机制,由于在使用BeautifulSoup分析html数据时,有时候会由于没有某个对象而报错,对于这种报错须要处理,不然整个程序就会中止执行,这里咱们打印出了日志,在日志中记录了相应的userId。固然这里仍是主要用到了BeautifulSoup便捷的功能,好比其中的get_text()方法,可以将多个标签的文本抽取出来而且可以制定文本的分隔符,和对空格等字符进行过滤。
# update user data def replaceUserData(data): conn = getMysqlConn() cur = conn.cursor() try: cur.execute("USE wanghong") cur.execute("set names utf8mb4") cur.execute("REPLACE INTO Tbl_Huajiao_User(FUserId,FUserName, FLevel, FFollow,FFollowed,FSupported,FExperience,FAvatar,FScrapedTime) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", (int(data['FUserId']), data['FUserName'],int(data['FLevel']),int(data['FFollow']),int(data['FFollowed']), int(data['FSupported']), int(data['FExperience']), data['FAvatar'],getNowTime()) ) conn.commit() except pymysql.err.InternalError as e: print(e)
这里使用了Python第三方库pymysql进行mysql的读写操做,而指定编码utf8mb4,也就是为了不文章开始提到的一个问题,关于emoji表情符,若是数据库使用经常使用的编码”CHARSET=utf8 COLLATE=utf8_general_ci”则会写入报错,注意上面sql语句里也声明了utf8mb4字符集和编码。
这里没有使用mysql的“INSERT”,而是使用了“REPLACE”,是当包含一样的FUserId的一条记录被写入时将替换原来的记录,这样可以保证爬虫定时更新到最新的数据。
#get user history lives def getUserLives(userId): try: url = "http://webh.huajiao.com/User/getUserFeeds?fmt=json&uid=" + str(userId) html = urlopen(url).read().decode('utf-8') jsonData = json.loads(html) if jsonData['errno'] != 0: print(str(userId) + "error occured in getUserFeeds for: " + jsonData['msg']) return 0 return jsonData['data']['feeds'] except Exception as e: print(e) return 0
前面说到,获取直播历史数据是经过直接请求ajax接口地址的,代码中的url即为接口地址,这是经过浏览器的调试工具得到的。这里用到了json的解码。
这里和以上第5项相似,就不详述了,读者能够在文章末尾的github地址获取完整的代码
#spider user ids def spiderUserDatas(): for liveId in getLiveIdsFromRecommendPage(): userId = getUserId(liveId) userData = getUserData(userId) if userData: replaceUserData(userData) return 1 #spider user lives def spiderUserLives(): userIds = selectUserIds(100) for userId in userIds: liveDatas = getUserLives(userId[0]) for liveData in liveDatas: liveData['feed']['FUserId'] = userId[0] replaceUserLive(liveData['feed']) return 1
所谓的骨架函数,就是控制单个小的功能函数,实现循环逻辑,一页一页的去采集数据。
spiderUserDatas()的逻辑:拿到liveId列表后,循环遍历的去取每个liveId对应的userId,进而渠道userData并写入mysql;
spiderUserLives()的逻辑:从mysql中选出上次爬虫时间最晚的100个userId, 循环遍历地去取每个user的直播历史数据并写入mysql;
def main(argv): if len(argv) < 2: print("Usage: python3 huajiao.py [spiderUserDatas|spiderUserLives]") exit() if (argv[1] == 'spiderUserDatas'): spiderUserDatas() elif (argv[1] == 'spiderUserLives'): spiderUserLives() elif (argv[1] == 'getUserCount'): print(getUserCount()) elif (argv[1] == 'getLiveCount'): print(getLiveCount()) else: print("Usage: python3 huajiao.py [spiderUserDatas|spiderUserLives|getUserCount|getLiveCount]") if __name__ == '__main__': main(sys.argv)
首先,要命名python在命令行模式下如何接收参数,经过sys.argv;
再有__name__的含义,若是文件被执行,则__name__的值为”__main__”;
这样经过以上代码就能够实现命令行调用和参数处理了。
好比要爬取主播的我的信息,则执行:
python3 huajiao.py spiderUserDatas
好比查看爬取了多少条用户数据信息,则执行:
python3 huajiao.py getUserCount
*/1 * * * * python3 /root/PythonPractice/spiderWanghong/huajiao.py spiderUserDatas >> /tmp/huajiao.py_spiderUserDatas.log */1 * * * * python3 /root/PythonPractice/spiderWanghong/huajiao.py spiderUserLives >> /tmp/huajiao.py_spiderUserLives.log
主播数据
直播历史数据
https://github.com/octans/PythonPractice/tree/master/spiderWanghong