使用Pyppeteer+Bs4完成一个爬虫爬取项目所需数据

1,818 阅读6分钟

关于工具集

1、 Python

3.6版本以上
建议使用virtualenv创建一个虚拟环境对当前项目进行一个隔离,
否则可能会出现比较痛苦的事情,pip与Python对不上。。。
如果使用pycharm这个IDE默认是可以使用virtualenv创建项目的。
(关于依赖环境的问题,这里多提一些,golang的go module在这方面非常先进,
通过一个go.mod文件定位各种依赖库及其相应版本,在Python中就得使用一些工具进行虚拟处理,实际就是一个隔离。
上次很惨痛的一个教训,就是用pip装完依赖库后import不了,磕了一天,最后也只是治标而没有治本)

2、Chromium浏览器

Chrome的开源版本,此次我们的web自动化控制的浏览器对象

3、asyncio

Python自3.4版本引入的标准库,是一个异步协程库,通过它我们实现多协程、异步操作。
(说实话,因为GIL的机制问题留下的阴影,到现在我也还不能确定这玩意是不是好使的异步,总觉得它又是假的。。。)

4、Pyppeteer

Python版本的Puppeteer,Puppeteer是Google基于Node.js开发的一个工具,主要是用来操纵Chrome浏览器的API。
简单来说,就是用Pyppeteer去控制浏览器,获取对应的浏览页。

5、bs4

解析网页HTML等的解析库,bs4功能强大,较为完全。

6、总结

总结一下上述工具集的关系,我们使用Python进行开发,使用pyppeteer控制Chromium浏览器模拟请求
获取网页内容,使用bs4以及正则表达式等对内容进行清洗和提炼得出关键性的数据,然后存入csv/json/数据库中去

具体步骤

1 、新建项目

# 命令行创建
python -m venv project # 创建项目环境
source project/bin/activate # 启动项目环境
deactivate # 退出项目环境

# Pycharm直接选择创建

2、查看网址robots.txt

虽然爬虫本身就是耍流氓,但是耍流氓之前我们要先看看规定,人家是否让我们耍流氓 (当然了,大部分是不让的,也可以偷偷耍... 这里就不得不提豆瓣了,写了那么多disallow,大部分入门教程还是拿它练手。。。 两年前它就是我的练手项目第一个) 举个例子,假设我们的url——www.httpbin.org/headers, 那么我们要先看看www.httpbin.org/headers/rob… 还好,此次项目的robots.txt如下

User-agent:*
Disallow:/zzbm/tjr/

这段话的意思是对于所有用户 /zzbm/tjr/ 下的所有东西不允许爬取,其它没说就是默认可以...

3、browser相关操作

辅助函数

from pyppeteer import launch

import tkinter
def screen_size():
    """使用tkinter获取屏幕大小"""
    tk = tkinter.Tk()
    width = tk.winfo_screenwidth()
    height = tk.winfo_screenheight()
    tk.quit()
    return width, height

browser启动参数及配置

这里重点提一下executablePath,指定好对应目录,否则启动pyppeteer自己会去下载,比较慢

start_parm = {
        # 启动chrome的路径
        "executablePath": "/Applications/Chromium.app/Contents/MacOS/Chromium",
        # 无头浏览器
        "headless": False,
        "args": [
            '--disable-infobars',  # 关闭自动化提示框
            # '--window-size=1920,1080',  # 窗口大小
            '--no-sandbox',  # 关闭沙盒模式
            '--start-maximized',  # 窗口最大化模式
        ],
    }
# 创建浏览器对象
browser = await launch(**start_parm)

# 新建标签页
page = await browser.newPage()
width, height = screen_size()
# 设置窗口视图大小
await page.setViewport(viewport={"width": width, "height": height})

# 根据对应url获取页面内容
await page.goto(url)
page_text = await page.content()

# 进行解析操作


# 关闭浏览器
await browser.close()

4、分析HTML结构,提取关键信息

刚才获取的page_text我们可以先存储起来,通过文件查看并分析

soup = BeautifulSoup(page_text, "lxml")
    # print(soup.prettify())
    with open(f"html/gaokao_{count}.html", 'w') as f:
        # print(type(soup.prettify()))
        f.write(soup.prettify())

在gaokao.html文件中找到关键性结构

<tr>
        <td class="js-yxk-yxmc">
         <a href="/sch/schoolInfo--schId-1.dhtml" target="_blank">
          北京大学
         </a>
        </td>
        <td>
         北京
        </td>
        <td>
         教育部
        </td>
        <td>
         综合
        </td>
        <td>
         本科
        </td>
        <td class="ch-table-center">
         <i class="iconfont ch-table-tick"></i>
        </td>
        <td class="ch-table-center">
        </td>
        <td class="ch-table-center">
         <i class="iconfont ch-table-tick"></i>
        </td>
        <!--todo 院校满意度-->
        <td class="ch-table-center ch-table-link">
         <a class="js-alert-myd" data-id="99617187" href="###">
          4.7
         </a>
        </td>
       </tr>

接下来我们就可以根据它的标签、class、id等用bs4进行定位(这一步需要一些前端基础)

from bs4 import BeautifulSoup
content = soup.select("html body div div div tr td")

# 初始化表头
headers = ["院校名称", "院校所在地", "教育行政主管", "院校类型", "学历层次", "一流大学建设高校", "一流学科建设高校", "研究生院", "满意度"]
write_to_csv(count, headers)

提取具体内容

    tmp_result = []
    result = []
    for index in range(len(content)):
        # print(type(c))
        # print(c.name, c.attrs, len(c.contents), type(c.contents))
        # print("c.contents: ", c.contents)
        # print("========================")
        c = content[index]
        # 中文匹配模式
        pattern = re.compile(r'[\u4e00-\u9fa5]+')
        # 评分匹配模式
        pattern_num = re.compile(r'\d\.\d')
        p = re.compile(r'<i class="iconfont ch-table-tick"></i>')
        flag = False
        for i in c.contents:
            # print(type(i))
            # print(i)
            res = re.search(pattern, str(i))
            if res != None:
                # print(type(res.group()))
                tmp_result.append(str(res.group()))
                # print(res.group())
                break
            else:
                num = re.search(pattern_num, str(i))
                if num != None:
                    tmp_result.append(str(num.group()))
                    # print(num.group())
                    break
                else:
                    rep = re.search(p, str(i))
                    if rep != None:
                        flag = True
                        break

总结——合并成bs4_parse函数

def bs4_parse(count, page_text):
    """使用bs4对文本进行解析,以及存储html源文件和写入csv文件"""
    soup = BeautifulSoup(page_text, "lxml")
    # print(soup.prettify())
    with open(f"html/gaokao_{count}.html", 'w') as f:
        # print(type(soup.prettify()))
        f.write(soup.prettify())
    content = soup.select("html body div div div tr td")
    # print(content)
    # print(type(content), len(content))

    # 初始化表头
    headers = ["院校名称", "院校所在地", "教育行政主管", "院校类型", "学历层次", "一流大学建设高校", "一流学科建设高校", "研究生院", "满意度"]
    write_to_csv(count, headers)

    tmp_result = []
    result = []
    for index in range(len(content)):
        # print(type(c))
        # print(c.name, c.attrs, len(c.contents), type(c.contents))
        # print("c.contents: ", c.contents)
        # print("========================")
        c = content[index]
        # 中文匹配模式
        pattern = re.compile(r'[\u4e00-\u9fa5]+')
        # 评分匹配模式
        pattern_num = re.compile(r'\d\.\d')
        p = re.compile(r'<i class="iconfont ch-table-tick"></i>')
        flag = False
        for i in c.contents:
            # print(type(i))
            # print(i)
            res = re.search(pattern, str(i))
            if res != None:
                # print(type(res.group()))
                tmp_result.append(str(res.group()))
                # print(res.group())
                break
            else:
                num = re.search(pattern_num, str(i))
                if num != None:
                    tmp_result.append(str(num.group()))
                    # print(num.group())
                    break
                else:
                    rep = re.search(p, str(i))
                    if rep != None:
                        flag = True
                        break
        if res == None and num == None:
            tmp_result.append(str(flag))
            # print("flag: ", flag)
        if (index+1) % 9 == 0:
            print(tmp_result)
            tmp_result = list(map(lambda x: [x], tmp_result))
            #write_to_csv(count, tmp_result)
            result.append(tmp_result)
            tmp_result = []
    write_to_csv(count, result, 2)

辅助函数——写入csv文件

def write_to_csv(count, lst, flag = 1):
    path = f'data/yuanxiaoku_{count}.csv'
    with open(path, 'a+', newline='') as f:
        csv_write = csv.writer(f)
        if flag == 1:
            csv_write.writerow(lst)
        elif flag == 2:
            csv_write.writerows(lst)

5、根据url特征,进行异步操作

此次url特征是20条内容为一页,也就是参数以20递增,总共141页,使用异步协程完成

if __name__ == '__main__':
    tasks = []
    for i in range(0, 2800, 20):
        url = f'https://xxx{i}.dhtml'
        coroutine = main(url, str(i))
        tasks.append(coroutine)

    asyncio.get_event_loop().run_until_complete(asyncio.gather(*tasks))

总结

这个项目因为网址的反爬手段措施基本没有,所以其实用requests进行一个get请求就能完成。
我这个仅仅是想锻炼一下pyppeteer以及asyncio的使用,已经大半年没用Python,略有些生疏,这里做一个个小小的记录。
如果涉及到天猫淘宝那之类种验证码、滑块登录会相对更复杂一些,甚至还要会写一点js脚本,更夸张有时候还有抠像素点

爬虫方面的工具集更新还是比较快的,去年这个时候还在做Python讲师,完了吭哧吭哧用phantomjs写了个小玩意
一顿撸完发现因为大牛之间的矛盾,phantomjs已经不更新。。。
希望哪天也能这么秀,说不干一群人人用的轮子就抛锚了

补充:这个项目还没有完全对需求得更改进行适应,后续完成再放出git地址。

闲话

应黄老板的需求,共同完成一个项目,我一个后台工程师去写前端。
数据库没数据叫我写爬虫去抓,今儿还问我能不能改PHP代码...这很不对头。
话说golang和C++我还没写完全写明白呢...
技术栈深度比广度更重要,有更多的时间可以多去看看各种开源组件的源码比如redis之类的,大牛的源码有很多精彩。
(我老师看到这应该想捶我了,道理都知道,就是爱瞎玩。。。)
全栈工程师本身就是个伪概念,无论哪方面的开发尽量往一个方向深挖,其它的看看猪跑就好,不要所有猪肉都去吃。

Keep It Simple, Stupid!