在简单学习了Pyppeteer之后,就想利用其来实现一个爬取实战来巩固知识,也是为了做点东西,让学的东西不那么空洞。
然后选取了微博评论区进行爬取。
但是在复制网页端的微博的节点的Selector并进行查找的时候,发现怎么都找不到。(这个问题我暂时也不知道什么情况)
于是,在这种情况下,我选择了移动端的微博,(好像确实要容易爬取一些)
以下是我在写程序时记录的一些问题及解决吧(应该算日志吧):
"""
思路:先进入wb网页,然后利用选择器点击每个动态下面的评论,接着获取查看全部评论的href,然后拼凑成新的url,进入动态的详情页,然后爬取里面的评论。
爬取评论包含:用户名,评论内容,ip地址
然后在分别把所有评论用逗号分隔,拼凑成一个文本,进行文本词频统计
生成词云,用AG图片作为词云形状
同理对用户名和ip地址进行操作 - 3.14
"""
"""
-3.16
问题:
solved-1.网页版的微博在爬取的时候,使用复制过来的Selector会报错,显示找不到节点,没有找到解决方案,于是转向爬取移动端的微博网页.
solved-2. click函数如何使用才会跳转。
unsolved-3. click在使用该函数之后并没有在显示的界面上跳转,还需要手动点击一下,函数才开始运行,暂未得到解决.
####################(还未解决) ------> solved : 换选择器,点微博正文也能跳转(3.20)
solved-4. 在跳转到动态的详情页之后,如何让网页往下滑成了新的问题.在大量搜索后仍然没有找到解决方案,然后寻求Pyppeteer的API,发现里面也没有对应的方法可以使用,最后想到了可以通过键盘的PageDown键来进行下滑操作.但这样做有缺点,首先就是不知道要获取全部评论要按多少次PageDown键,其次是这样做好像有一点耗时。
solved-5. 但是在下滑时,如果滑到新评论,需要进行登录,本来想要使用模拟登录来解决该问题,然后想起来pyppeter的launch里面有可以记录登录状态的参数,于是先模拟一次打开界面,然后停留一段时间进行登录,从而使得登录状态的数据被保存下来。在后面的下滑网页的操作中不再需要操作。
to-be-solved-6. 在获取评论信息并分类时,发现存在一些数组越界问题,于是采用“列表推导 if else”的结构来解决这种情况。同时对于没有获取到的评论,使用"暂无"来代替,这在后面进行词频统计的时候要剔除掉该词。########################(需要解决)
---------> solved: 通过查看网页结构发现,在评论出现表情或者图片的时候,评论内容会在标签h3下面在延伸.解决方法就是用户名和评论内容分别获取,一个用h4标签, 一个用h3标签。
Now:
1.现在已经可以爬取一个动态里面的部分评论,并进行分类存储在一个字典里面,仍需储存在一个文件里面。 -> solved(3.20)
2.现在还需要事先获取每条动态的评论数,从而得到每次下滑网页按键的次数。 -> solved(3.20)
3.还需要设置动态页的按键次数,从而获取更多的动态 ->solved(3.20)
4.还需要能够爬取多条动态。 ->solved(3.20)
5.现在使用BASIC_URL_2,SELECTOR_BASIC_2
函数(现有):
1.初始化
2.爬取URL
3.点击评论进入详情页
4.解析详情页评论信息
3.20:
5. 保存数据到文件里面
6. 获取更多动态
7. 获取文件内容并进行分词后的结果
8. 设置屏幕尺寸为全屏
更新思路:
路:先进入wb网页,然后利用选择器点击每个动态下面的评论按钮,进入动态的详情页,不断下滑获取更多评论,然后爬取里面的评论。
同时网页也需要下滑操作来获取更多的动态。
爬取评论包含:用户名,评论内容,ip地址
然后在分别把所有评论用逗号分隔,拼凑成一个文本,进行文本词频统计
生成词云,用AG图片作为词云形状
同理对用户名和ip地址进行操作 - 3.16
"""
"""
-3.20
日志更新库:
1. logging
2. 在经过初始化设置后,用logging.info输出的信息带有时间,可以把logging.info当成一个print来用
3. 初始化设置:logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s : %(message)s')
中文分词库:
1. jieba
词云设置:
# w=wordcloud.WordCloud(font_path='msyh.ttc',width=800,height=600,max_words=150,font_step=2,stopwords=stopwords,collocations=False)
1. font_path :表示字体路径,可以从C->Windows->Fonts 获取。
2. width: 输出画布的宽度
3. height: 输出画布的高度
4. max_words: 画布里面显示词数的最大数
5. font_path: 字体步长
6. stopwords: 停用词,表示需要屏蔽的词,在这里用了中文的停用词,防止一些特殊符号影响输出。
7. collocations: 将参数设置为False,防止关键词出现重复的现象。
词云常规函数:
1. w.generate(txt) #向 WordCloud 对象 w 中加载文本 txt
2. w.to_file(PNG_PATH.format(str=FIlE_NAME[i])) # 将词云输出为图像文件 .jpg或.png文件
"""
下面是源代码:
import logging
import tkinter
from pyppeteer.errors import TimeoutError
from wordcloud import WordCloud
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
# 词频统计
import jieba
import wordcloud
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s : %(message)s')
TIMEOUT = 10
DOWN_NUMBER = 260 # 动态页按下的次数
BEGIN_NUMBER = 4 # 开始动态的index
END_NUMBER = 50 # 结束动态的index
FILE_PATH = "./KPL-COMMENT/{str}.txt" # 保存文件路径
PNG_PATH = "./KPL-COMMENT/{str}.png"
FIlE_NAME = ["comments","ip","user_names"] # 储存三个文件名
# WINDOW_WIDTH, WINDOW_HEIGHT = 1366, 768
HEADLESS = False
BASIC_URL = "https://weibo.com/agcwh"
COMMENT_URL = "https://weibo.com{comment_url}"
SELECTOR_BASIC = "#scroller > div.vue-recycle-scroller__item-wrapper > div:nth-child(1) > div > article > footer > div > div:nth-child(2) > div > span"
BASIC_URL_2 = "https://m.weibo.cn/u/5878848794?uid=5878848794&t=0&luicode=10000011&lfid=100103type%3D1%26q%3Dag%E8%B6%85%E7%8E%A9%E4%BC%9A" # 实际用到的网页链接
SELECTOR_BASIC_2 = "#app > div:nth-child(1) > div:nth-child(1) > div:nth-child({index}) > div > div > div > footer > div:nth-child(3) > h4" # 实际用到的评论内容选择器
SELECTOR_BASIC_3 ="#app > div:nth-child(1) > div:nth-child(1) > div:nth-child({index}) > div > div > div > article > div.weibo-og > div.weibo-text" # 实际用到的点击用的选择器
#app > div:nth-child(1) > div:nth-child(1) > div:nth-child(4) > div > div > div > article > div.weibo-og > div.weibo-text
# index从4开始,这是点击评论的selector
# 设置中文停用词
stopwords = set()
content = [line.strip() for line in open('stop_words.txt','r',encoding='utf-8').readlines()]
stopwords.update(content)
browser, page = None, None
def screen_size():
# 设置屏幕尺寸为全屏
tk = tkinter.Tk()
width = tk.winfo_screenwidth()
height = tk.winfo_screenheight()
tk.quit()
return {'width': width, 'height': height}
async def init( ):
global browser, page
browser = await launch(headless=HEADLESS, userDataDir='./userdata', args=['--start-maximized','--disable-infobars'])#网页全屏
page = await browser.newPage()
await page.setViewport(screen_size())
await page.evaluateOnNewDocument('Object.defineProperty(navigator,"webdriver",{get:()=>undefined})')
# 隐藏WebDriver信息,防止被拦截
await page.setJavaScriptEnabled(enabled=True)
async def scrape(url, selector):
"""爬取网页的基本操作"""
logging.info("Scraping %s...", url)
try:
await page.goto(url)
await page.waitForSelector(selector, options={'timeout':TIMEOUT * 100})
except :
logging.error("Error occurred while scraping %s", url, exc_info=True)
async def click_comment(index):
"""点击微博正文从而进入相应的详情页"""
global page
selector = SELECTOR_BASIC_2.format(index=index)
# await scrape(BASIC_URL_2, selector=selector)
count = await get_count(selector)
selector = SELECTOR_BASIC_3.format(index=index)# 这是微博正文的Selector,可以点击,但是评论的数字点不了
try:
await asyncio.gather( # 先等待后跳转
page.waitForNavigation(), # 等待页面跳转函数
page.click(selector,options={'button':'left','delay':1})
)
except :
logging.info("No node found for selector.....")
await get_comment(count)
# 复制过来的微博评论用户名的Selector
#app > div:nth-child(1) > div:nth-child(1) > div:nth-child(6) > div > div > div > footer > div:nth-child(3) > h4
# 复制过来的微博正文的Selector
#app > div:nth-child(1) > div:nth-child(1) > div:nth-child(5) > div > div > div > article > div.weibo-og > div.weibo-text
async def get_count(selector):
"""获取每条动态的评论数"""
# await scrape(BASIC_URL_2, selector=selector)
try:
await page.waitForSelector(selector, options={'timeout':TIMEOUT * 100})
except TimeoutError:
logging.error("Error occurred while scraping ...", exc_info=True)
doc = pq(await page.content())
count = doc(selector).text() # 获取的count为str类型
print(count)
if count:
return int(count)
else :
return 260
async def get_comment(count):
"进入详情页之后,先利用键盘下滑,然后爬取评论数据"
for i in range(count):
# 把评论数乘以10作为按下ArrowDown键的次数,这样仍不能够实现完全获取全部评论的效果,但可以获取大部分
# 利用键盘上的ArrowDown键,或者叫做PageDown键,来实现网页不断往下滑,不断更新内容的功能
await page.keyboard.press('ArrowDown')
try: #有时会莫名其妙出一些找不到结点的问题(我也不知道为什么),用try except解决
await page.waitForSelector(".comment-content ", options={'timeout':TIMEOUT * 100})
except:
logging.info("TimeoutError")
doc = pq(await page.content())
# messages = [item.text().split('\n') for item in doc(".comment-content .m-text-box ").items()]
names = [item.text() for item in doc(".comment-content .m-text-box h4").items()]
comments = [item.text() for item in doc(".comment-content .m-text-box h3").items()]
# 由于网页结构关系,每条评论的用户名和评论内容的文本可以一块获取,所以messages数组储存每条评论的用户名和评论内容,后面再分开
ips = [item.text().split() for item in doc(".comment-content .time").items()]
# ip地址和发布时间的文本连在一块,这里把每条评论的发布时间和ip地址用spilt分开,以便后续储存ip地址
try:
comment_info={
'user_names' : names,
'comments':comments,
'ip':[ips[i][-1][2:] for i in range(len(ips))]
# 这里面利用切片[2:] 把ip地址里面的“来自”两个字删掉 ,同时考虑了数组下标越界的情况(其实是因为在运行时存在越界情况导致出错,所以需要进行改善)
}
except IndexError :
logging.info("ips:%s",ips)
logging.info("出现数组下标越界情况,后面评论暂不爬取")
# 用comment_info这样一个字典来存储获取的评论信息
for i in range(3):
await save_data(FIlE_NAME[i], comment_info[FIlE_NAME[i]])
# logging.info(comment_info)
# logging.info(len(messages))
async def get_more(): #等到某动作完成
"""获取更多的动态,其实质是先不断往下滑,让网页加载更多内容"""
await page.goto(BASIC_URL_2)
for i in range(DOWN_NUMBER):
# 把评论数乘以10作为按下ArrowDown键的次数,这样仍不能够实现完全获取全部评论的效果,但可以获取大部分
# 利用键盘上的ArrowDown键,或者叫做PageDown键,来实现网页不断往下滑,不断更新内容的功能
await page.keyboard.press('ArrowDown')
async def save_data(str, data):
"""保存数据"""
file_name = FILE_PATH.format(str=str)
with open(file_name, "a", encoding="utf-8") as f:
f.write(','.join(data))
async def get_text(filename):
"""获取文件中的内容,并用中文分词库进行分词后返回"""
f = open(filename, "r", encoding='utf-8')
txt = f.read()
f.close()
txt=' '.join(jieba.lcut(txt))
return txt
async def main():
await init()
await page.goto(BASIC_URL_2)
await get_more()
while True :
for i in range(BEGIN_NUMBER, END_NUMBER+1):
await click_comment(i)
logging.info("Scraping number:%d......",i-3)
# await click_comment(5)
for i in range(3):
txt = await get_text(FILE_PATH.format(str=FIlE_NAME[i]))
logging.info("txt:%s",txt)
print(type(txt))
w=wordcloud.WordCloud(font_path='msyh.ttc',width=800,height=600,max_words=150,font_step=2,stopwords=stopwords,collocations=False)
w.generate(txt)
w.to_file(PNG_PATH.format(str=FIlE_NAME[i]))
logging.info('make wordCloud successfully!')
break
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
运行结束就会在同级文件夹下一个KPL-COMMENT的文件夹,文件夹下面出现三个文本文件,三个图片文件。
图片效果一览:
爬取的时候有一点没有考虑,那就是每条评论的回复评论,这点我没有进行爬取。
由于本人确实水平有限,所以写的仍不够完善,写这篇博客只是为了记录我自己写的一个案例。当然如果对大家有帮助那是最好不过了。如果有大佬指正问题,随时欢迎喔。