之前我用 pyppeteer 绕过了淘宝登录时对于 web driver 的检测,但是这并不意味着登录后就没有检测了,今天我就来以爬取搜索关键字得到的商品名称为例操作一下。


整个过程有 4 步:1.登录,2.输入关键字并点击搜索,3.滑到最底部并获取数据,4.点击下一页,然后重复步骤 3,4 直到没有下一页(实际上一个账号并不能每一页全部爬完,要想全部爬完可能要买或者借账号,下面的教程我只爬前几页的数据)。然后就是要搭好一个框架,为了让程序看起来简单,我就使用面向对象的设计方法来设计这个程序,大致代码如下:

class TaoBaoSpider:
async def login(self):
pass

async def search(self):
pass

async def crawl(self):
pass

因为 pyppeteer 是通过 async 和 await 异步机制来实现的,所以方法必须都是异步的,所以我在每个方法定义之前都加上了 async 关键字。

使用 pyppeteer 爬取淘宝商品_数据

登录

登录有两种方式,一种是手机扫码登录,另一种是输入账号密码登录,为了尽可能简单和尽可能的不被检测,我使用手机扫码登录。既然使用手机扫码登录,那么 login 方法的实现就非常简单,只要 sleep 一会就行了。但这里需要注意的是,不能使用 time 模块的 sleep 函数(这是同步阻塞接口的等待,不能在异步函数中出现),我们需要一个异步等待,这个异步等待就是 asyncio 模块的 sleep 函数,调用时需要用关键字 await 修饰,如下所示。

from asyncio import sleep


class TaoBaoSpider:
@staticmethod
async def login():
await sleep(10)

async def search(self):
pass

async def crawl(self):
pass

这里我设置等待时间为 10 秒,在这 10 秒钟完成手机扫码登录应该够了,如果觉得时间紧迫可以把 10 改成更大的数。

下面我们需要进行测试,在测试之前,我们要先写好这个类的构造方法,来初始化一些属性,因为在 search 和 crawl 方法中都需要使用浏览器对象,所以我们要初始化一个浏览器对象,如下所示。

self.browser = await launch(headless=False, args=['--disable-infobars', f'--window-size={self.width},{self.height}'])

我们都知道,await 关键字必须在 async 修饰的函数体内,有些人可能会想到按照下面这种写法:

async def __init__(self)

这样写是错的,如图所示。

使用 pyppeteer 爬取淘宝商品_ide_02

稍微翻译一下,说函数“__init__”不可以是异步的!那么我们就需要单独定义一个异步方法来完成异步的初始化。这个异步方法我就定义为 init 了。下面就主要编写 __init__ 和 init 两个方法。

from asyncio import sleep, get_event_loop
from pyppeteer import launch


class TaoBaoSpider:
def __init__(self):
self.width, self.height = 1500, 800
get_event_loop().run_until_complete(self.init())
get_event_loop().run_until_complete(self.login())
get_event_loop().run_until_complete(self.search())
get_event_loop().run_until_complete(self.crawl())

async def init(self):
# noinspection PyAttributeOutsideInit
self.browser = await launch(headless=False,
args=['--disable-infobars', f'--window-size={self.width},{self.height}'])
# noinspection PyAttributeOutsideInit
self.page = await self.browser.newPage()
await self.page.setViewport({'width': self.width, 'height': self.height})
await self.page.goto('https://login.taobao.com/member/login.jhtml?redirectURL=https://www.taobao.com/')
await self.page.evaluate('()=>{Object.defineProperties(navigator,{webdriver:{get:()=>false}})}')

@staticmethod
async def login():
await sleep(10)

async def search(self):
pass

async def crawl(self):
pass


if __name__ == '__main__':
TaoBaoSpider()

运行程序会弹出一个 Chromium 浏览器,浏览器显示的就是淘宝登录页面,手机扫码可以成功登录,没有被检测,登录部分测试完毕,下面我们主要编写一下 search 方法。

使用 pyppeteer 爬取淘宝商品_数据

搜索

在编写搜索方法之前,我们来想一个问题,刚登录就搜索是不是太快了?我们都知道,访问太快容易被 BAN 的,所以我们需要降低访问速度。这个可以通过异步等待来实现。

下面又来了一个关键性的问题,我具体应该等待多久?时间太长效率就太低,时间太短容易被检测,我们可以想一下我们可以想一下自己手动访问淘宝速度是怎样的,我感觉是 1 到 4 秒进行一次点击啥的,有些人就想当然认为这就太简单了,直接构造方法中初始化不就完了吗?如下所示:

self.sleep_time = 1+random()*3

其实这样写是错的,因为这个字段值在中途不会被重新初始化,也就是每两个事件之间都是相同的时间间隔,这样不被检测出来就有鬼。毕竟人操作不可能这么机械化,那么我们需要在每次调用它的时候都能修改它的值,既然这个值在不断的变化,我们可以把它定义成方法,可是定义成方法后面的每次调用都要一对括号,这简直是太繁琐了。我们可以使用 property 来装饰这个方法,在调用的时候不需要加括号了,下面我实现搜索的方法。

from asyncio import sleep, get_event_loop
from pyppeteer import launch
from random import random


class TaoBaoSpider:
def __init__(self):
self.width, self.height = 1500, 800
get_event_loop().run_until_complete(self.init())
get_event_loop().run_until_complete(self.login())
get_event_loop().run_until_complete(self.search())
get_event_loop().run_until_complete(self.crawl())

async def init(self):
# noinspection PyAttributeOutsideInit
self.browser = await launch(headless=False,
args=['--disable-infobars', f'--window-size={self.width},{self.height}'])
# noinspection PyAttributeOutsideInit
self.page = await self.browser.newPage()
await self.page.setViewport({'width': self.width, 'height': self.height})
await self.page.goto('https://login.taobao.com/member/login.jhtml?redirectURL=https://www.taobao.com/')
await self.page.evaluate('()=>{Object.defineProperties(navigator,{webdriver:{get:()=>false}})}')

@staticmethod
async def login():
await sleep(10)

@property
def sleep_time(self):
return 1+random()*3

async def search(self):
await self.page.click('#q')
await sleep(self.sleep_time)
await self.page.keyboard.type('机械革命')
await sleep(self.sleep_time)
await self.page.click('#J_TSearchForm > div.search-button > button')
await sleep(self.sleep_time)

async def crawl(self):
pass


if __name__ == '__main__':
TaoBaoSpider()

在这里我搜索的关键字直接赋了初值,大家可以自己修改成输入传参的形式,这很简单,我就不讲了,下面是最关键的步骤,爬取数据。

使用 pyppeteer 爬取淘宝商品_数据

爬取数据

爬取数据我们就爬一下搜索到的商品名称,下图中价格下面的两行字。

使用 pyppeteer 爬取淘宝商品_ide_05

通过审查元素我们可以找到每一段字的对应的 HTML 的源码形式,如图所示。

使用 pyppeteer 爬取淘宝商品_ide_06

既然 a 标签下还有标签,那么我们就需要做出二次过滤,第一次提取 a 标签下的内容,第二次把第一次提取的数据的标签和空白字符去掉。所以需要写两个正则,首先是提取 a 标签下的内容,经过不停地比对,可以得出最后的正则:pattern = compile(r'<a id=".*?" class="J_ClickStat".*?>(.*?)</a>', S)

接着是用来替换的正则:repl_pattern = compile(r'<.*?>|\s+')

接下来我们尝试获取前 5 页的数据。先到底部(不能一蹴而就,一定要模拟人的操作。这里使用匀加速来实现,加速度是一个随机值,不随机可能会被检测),然后获取页面源码进行数据筛选。最后跳到下一页,重复之前的操作。下面直接给出完整的源代码。

from asyncio import sleep, get_event_loop
from pyppeteer import launch
from random import random
from re import compile, S


class TaoBaoSpider:
def __init__(self):
self.width, self.height = 1500, 800
get_event_loop().run_until_complete(self.init())
get_event_loop().run_until_complete(self.login())
get_event_loop().run_until_complete(self.search())
get_event_loop().run_until_complete(self.crawl())

async def init(self):
# noinspection PyAttributeOutsideInit
self.browser = await launch(headless=False,
args=['--disable-infobars', f'--window-size={self.width},{self.height}'])
# noinspection PyAttributeOutsideInit
self.page = await self.browser.newPage()
await self.page.setViewport({'width': self.width, 'height': self.height})
await self.page.goto('https://login.taobao.com/member/login.jhtml?redirectURL=https://www.taobao.com/')
await self.page.evaluate('()=>{Object.defineProperties(navigator,{webdriver:{get:()=>false}})}')

@staticmethod
async def login():
await sleep(10)

@property
def sleep_time(self):
return 1+random()*3

async def search(self):
await self.page.click('#q')
await sleep(self.sleep_time)
await self.page.keyboard.type('机械革命')
await sleep(self.sleep_time)
await self.page.click('#J_TSearchForm > div.search-button > button')
await sleep(self.sleep_time)

async def crawl(self):
pattern = compile(r'<a id=".*?" class="J_ClickStat".*?>(.*?)</a>', S)
repl_pattern = compile(r'<.*?>|\s+')
for i in range(5):
# document.body.clientHeight
height = await self.page.evaluate('document.body.clientHeight')
scrolled_height = 0
a = 1+random()
t = 1
# window.scrollTo(width,height)
while scrolled_height < height:
scrolled_height = int(1/2*a*t**2) # x=v0*t+1/2*a*t**2,v0=0
await self.page.evaluate(f'window.scrollTo(0,{scrolled_height})')
t += 1
await sleep(self.sleep_time)
html = await self.page.content()
results = pattern.findall(html)
for result in results:
result = repl_pattern.sub('', result)
print(result)
print()
await sleep(self.sleep_time)
await self.page.click('#mainsrp-pager > div > div > div > ul > li.item.next > a')
await sleep(self.sleep_time)
await sleep(self.sleep_time)


if __name__ == '__main__':
TaoBaoSpider()

运行结果如图所示。

使用 pyppeteer 爬取淘宝商品_数据_07

可以发现显示在网页上的数据被爬了下来,下面来总结一些应付这种反爬特别严的网站的一些技巧:

  1. 模拟人的操作,每次请求都要随机等待一会。
  2. 如果需要登录,尽量手动登录,自动登录可能被检测。
  3. 一个账号(如果要登录)或者一个 IP 访问次数不要太多,如果要爬很多数据可以使用多个账号(如果要登录)或者多个 IP。

今天的文章有不懂的可以后台回复“加群”,备注:小陈学Python,不备注可是会被拒绝的哦~!

使用 pyppeteer 爬取淘宝商品_数据_08