python脚本爬取王者荣耀吧
本篇博客介绍了这次由于项目需求,爬取百度贴吧--王者荣耀吧的帖子的过程。
一、安装第三方库
pip install requests
pip install bs4
pip install lxml
pip install html5lib
二、源码分析
1. 分析请求链接的规律
F12打开开发者工具,百度搜索王者荣耀吧,在Network选项卡找到对应请求,可以看到其请求链接基本遵循如下规律:
https://tieba.baidu.com/f?kw=%E7%8E%8B%E8%80%85%E8%8D%A3%E8%80%80&ie=utf-8&tab=corearea&pn=450
kw: 贴吧名字(王者荣耀)
ie: 编码方式
tab: 首页标签
pn: 页码
其中首页标签的选项卡如下,可以一个一个点一遍试试,看看对应URL的tab字段是什么值:
2. 分析Response
找到该请求的响应,可以看到每一条帖子的概要内容都在,没有用AJAX,故无需分析XHR。
但是需要注意的是,我们需要爬取的内容是每一条帖子的信息和内容,这一部分没有包含在<html>...</html>标签内,而是在之后另外用<code>......</code>包裹,并且,是注释内容<!-- .... -->。因此,在用beautifulsoup解析的时候,如果不做处理,是无法解析出我们想要的内容的。
3. 正式开始爬取
需求:爬取王者荣耀吧的帖子并保存到本地,可以选择页数,选择标签,选择指定日期之前的帖子,选择包含关键词的帖子。每一条帖子包含标题、链接、发表日期,详细内容,所有回帖。
(1)发送请求,获取响应
1 def get_html(post_name, tab, pn): 2 """ 3 获取html 4 :param post_name: 贴吧名 5 :param tab: 标签名 6 :param pn: 页码 7 :return: 8 """ 9 try: 10 url = 'https://tieba.baidu.com/f' 11 12 headers = { 13 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \ 14 Chrome/75.0.3770.100 Safari/537.36' 15 } 16 # tag: 17 # 核心区:corearea; 看帖:main 18 data = { 19 'kw': post_name, 20 'tab': tab, 21 'pn': pn, 22 } 23 response = requests.get(url, params=data, headers=headers, timeout=30) 24 # 必须修改HTML页面,把HTML结束标签改到最后,否则soup解析只到原来的HTML标签就结束了,后面的code标签里的内容被丢弃 25 html = response.text.replace('</body>', '') 26 html = html.replace('</html>', '') 27 response = html + '</body></html>' 28 # response.encoding = 'utf-8' 29 # print(response.text) 30 return response 31 except RuntimeError: 32 return 'ERROR'
注意代码中的注释,根据之前的分析,我们要的内容都不在<html><body>...</body></html>标签包裹之内,后面soup无法解析到,所以要修改获得的源码。
(2)解析响应
1 def get_post_info(html, m, pn): 2 """ 3 获取帖子的标题、链接信息,并从中筛选出有特定关键词的帖子 4 :param html: 处理后的HTML页面 5 :param m: month 6 :param pn: 页码 7 :return: 帖子信息 8 """ 9 url = 'https://tieba.baidu.com' 10 soup = BeautifulSoup(html, 'lxml') 11 # 找到目标code标签,返回tag列表 12 code = soup.find_all('code', attrs={'id': 'pagelet_html_frs-list/pagelet/thread_list'}) 13 # 提取code标签的内容(注释),返回列表 14 comment = code[0].contents 15 # print(type(comment[0])) 16 # comment = code[0].string 17 # print(type(comment)) 18 # 重新开始解析comment 19 soup = BeautifulSoup(comment[0], 'lxml') 20 # soup = BeautifulSoup(comment, 'lxml') 21 22 # 找到目标li标签 23 info = [] 24 25 # # 先找到置顶帖 26 # litags_top = soup.find_all('li', attrs={'class': 'j_thread_list thread_top j_thread_list clearfix'}) 27 # for li in litags_top: 28 # info_top = dict() 29 # try: 30 # info_top['title'] = li.find('a', attrs={'class': 'j_th_tit'}).text.strip() 31 # info_top['link'] = ''.join([url, li.find('a', attrs={'class': 'j_th_tit'})['href']]) 32 # info_top['time'] = li.find('span', attrs={'class': 'pull-right is_show_create_time'}).text.strip() 33 # info.append(info_top) 34 # except: 35 # print("错误:获取置顶帖标题失败!") 36 37 # 再找到常规帖,提取标题、链接、发表日期、摘要信息 38 litags = soup.find_all('li', attrs={'class': 'j_thread_list clearfix'}) 39 for li in litags: 40 try: 41 info_norm = dict() 42 info_norm['title'] = li.find('a', attrs={'class': 'j_th_tit'}).text.strip() 43 info_norm['link'] = ''.join([url, li.find('a', attrs={'class': 'j_th_tit'})['href']]) 44 info_norm['date'] = li.find('span', attrs={'class': 'pull-right is_show_create_time'}).text.strip() 45 info_norm['abstract'] = li.find('div', attrs={'class': 'threadlist_abs threadlist_abs_onlyline'}). \ 46 text.strip() 47 info.append(info_norm) 48 except AttributeError as e: 49 print("错误:%s,可能是因为没有找到相应的标签" % e.args) 50 except: 51 print("错误:获取常规帖标题及摘要失败!") 52 53 print('第 %s 页已经爬取成功, 开始处理...' % (pn/50+1)) 54 # 筛选发表日期在一个月以内,且标题和摘要里有关键词['发热','卡', '掉帧', '']的帖子 55 # 获取当日日期 56 today = time.strftime('%m-%d', time.localtime(time.time())) 57 month = int(today.split('-')[0]) 58 day = int(today.split('-')[1]) 59 60 if month - m >= 1: 61 last_month = month - m 62 else: 63 last_month = 12 + (month - m) 64 # if last_month == 2 and day >= 29: 65 # one_month_before = ''.join([str(last_month), '-', '28']) 66 # else: 67 # one_month_before = ''.join([str(last_month), '-', str(day)]) 68 69 # num = len(info) 70 info_new = [] 71 for post in info: 72 if ':' in post['date']: 73 info_new.append(post) 74 elif int(post['date'].split('-')[0]) == last_month and int(post['date'].split('-')[1]) >= day: 75 info_new.append(post) 76 elif int(post['date'].split('-')[0]) == month and int(post['date'].split('-')[1]) <= day: 77 info_new.append(post) 78 79 # # 关键词分开存放 80 # keywords = ['发热', '卡顿', '掉帧', '卡死'] 81 # num = len(keywords) 82 # info_has_kw = [[] for i in range(num)] 83 # for post in info_new: 84 # for i in range(num): 85 # if keywords[i] in post['abstract']: 86 # info_has_kw[i].append(post) 87 # break 88 89 print('第 %s 页已经处理完成,开始爬取下一页...' % (pn/50+1)) 90 # return info_has_kw 91 return info_new
(3)保存到本地
def save2file(info, savepath=os.path.dirname(os.path.realpath(__file__))+'\\post.txt'): """ 将爬取到的帖子内容写入到本地,保存到指定目录的txt文件中,保存目录默认为当前目录。 :param info: 帖子内容 :param savepath: 输出文件路径,默认为当前目录 :return: """ # num = len(info) # for i in range(num): # with open(savepath, 'a+') as f: # for post in info[i]: # f.write('标题:{} \t 链接:{} \t'.format(post['title'], post['link'])) with open(savepath, 'a+') as f: for post in info: f.write('标题:{} \t 链接:{} \t'.format(post['title'], post['link'])) print("当前页面已经保存到本地!")
(4)主程序
if __name__ == '__main__': post_name = '王者荣耀' tab = 'main' # 循环控制爬取的页数 for pn in range(10): html = get_html(post_name, tab, pn*50) info = get_post_info(html, 3, pn*50) # print(info) save2file(info) print('-------所有帖子下载完成-------')