Pyppeteer爬取移动端微博评论区简单案例

在简单学习了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的文件夹,文件夹下面出现三个文本文件,三个图片文件。

图片效果一览:

Pyppeteer爬取移动端微博评论区简单案例_第1张图片

comments.png

Pyppeteer爬取移动端微博评论区简单案例_第2张图片

ip.png

Pyppeteer爬取移动端微博评论区简单案例_第3张图片

user_names.png

写在最后:

爬取的时候有一点没有考虑,那就是每条评论的回复评论,这点我没有进行爬取。

由于本人确实水平有限,所以写的仍不够完善,写这篇博客只是为了记录我自己写的一个案例。当然如果对大家有帮助那是最好不过了。如果有大佬指正问题,随时欢迎喔。

你可能感兴趣的:(Python爬虫学习,python,学习,网络爬虫)