三、 使用 Selenium 爬取淘宝商品
在分析 Ajax 抓取相关数据时,不是所有页面都可以通过分析 Ajax 来完成抓取。比如淘宝的整个页面数据确实是通过 Ajax 获取的,但这些 Ajax 接口参数复杂,并且包含有加密密钥等,如果要构造 Ajax 参数是很困难。像这种页面最方便的抓取方法是通过 Selenium 。接下就用 Selenium 模拟浏览器操作,抓取淘宝的商品信息,并用 pyquery 解析到商品的图片、名称、价格、购买人数、店铺名称和店铺所的地信息,并将结果保存到 MongoDB。
接下来需要用到的软件和库有:
Chrome浏览器软件
ChromeDriver驱动
Firefox浏览器并配置相应的驱动 GeckoDriver。
PhantomJS:一个无界面、可脚本编程的 webkit 浏览器引擎,支持多种 web 标准:DOM 操作, CSS 选择器,JSON、Canvas以及SVG。
Python 的 Selenium 库。
1、 接口分析
先分析淘宝的接口,看下它比一般的 Ajax 多了什么内容。进入淘宝首页,搜索商品,搜索 华为P20,进入开发者工具,未发现淘宝页面的 Ajax 请求。
2、 页面分析
这次爬取的是商品信息,包括商品图片、名称、价格、购买人数、店铺名称和店铺所在地等信息。抓取入口是淘宝的搜索页面,这个连接可以通过直接构造参数访问。例如搜索 华为P20,可直接访问 https://s.taobao.com/search?q=华为P20 ,展示的是第一页的搜索结果。在页面下方有分布导航,包括了前5页的连接,也包括下一页的连接,同时还有一个输入任意页面跳转的连接。如图3-1 所示。
图3-1 分页导航
可以看出搜索结果最大为 100 页,任意页面输入框的值默认是第2页。要获取每一页的内容,将页码从1 到 100 顺序遍历即可,页码数是确定的。可直接在页面跳转文本框中输入要跳转的页码,点击“确定”可跳转到页码对应的页面。
这里没有点击下一页来获取,是因为爬取过程中出现异常退出时,此时点击“下一页”就不能切换到对应的后续页面。因此在爬取过程中还要记录当前页码数,如果点击下一页后加载失败,还要做异常检测,检测当前页面是加载到第几页。整个流程相对复杂,所以直接用跳转方式爬取页面。
成功加载一页商品列表时,利用 Selenium 来获取页面源代码,再利用相应的解析库解析,这里使用 pyqeury 解析。
3、 获取商品列表
首先构造要抓取的URL:https://s.taobao.com/search?q=华为P20。参数 q 要是搜索的关键字。只要改变这个参数,就可获取不同商品的列表。可将商品的关键字定义成变量,接着构造出这样一个 URL。然后利用 Selenium 获取源代码,抓取列表页的方法如下:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from urllib.parse import quote
browser = webdriver.Chrome() # 初始化 Chrome浏览器对象
wait = WebDriverWait(browser, 10) # 设置等待时间10秒
KEYWORD = '华为P20' # 设置搜索关键字
def index_page(page):
"""
抓取索引面
:param page: 页码
"""
print('正在爬取第', page, '页')
try:
url = 'https://s.taobao.com/search?q=' + quote(KEYWORD) # 构造 URL
browser.get(url) # 连接 URL,首先访问搜索商品的连接,判断页码是否大于 1,
if page > 1:
input = wait.until(
# 使用 CSS 选择器获取下面的 input标签,也就是页码输入框
EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form > input'))
)
submit = wait.until(
# 使用 CSS 选择器获取到 span标签的“确定”按钮
EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form > span.btn.J_Submit'))
)
input.clear() # 清除输入框的默认值
input.send_keys(page) # 给输入框输入要获取的页码
submit.click() # 输入页码后点击提交
# page等于1或者大于1都执行下面的代码
#EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-pager li.item.active > span"), str(page))
obj = EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-pager li.item.active > span"), str(page))
print(obj)
wait.until(
# 使用 CSS 选择器获取当前 span 标签中的页码,并让页面与 page 的页码比较
EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-page li.item.active > span"), str(page))
)
# 等待商品信息加载出来,并使用选择器选择商品信息,调用 get_products() 方法提取商品信息,
# 如果不能正常加载就报 TimeoutException 异常
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))
get_products()
except TimeoutException:
#index_page(page)
print("请求超时")
上面这段代码首先使用 Chrome 浏览器构造一个 WebDriver 对象,接着指定一个关键词 华为P20,接着定义 index_page() 方法,用于抓取商品列表页。在 index_page() 方法中先访问搜索商品的链接,然后进行判断当前的页码,如果大于1就进行跳转页操作,否则等待页面加载完成。
等待加载时,使用了 WebDriverWait 对象,并指定等待条件,同时指定一个最长等待时间,这里是 10 秒。在这个时间内成功匹配等待条件,就说明页面元素成功加载出来,接着返回相应结果并继续向下执行,否则到了最大等待时间还没有加载出来,就直接抛出超时异常。
要等待商品信息加载出来,可指定 presence_of_element_located 这个条件,然后传入 .m-itemlist .items .item 这个CSS选择器,选择器对应的内容就是每个商品的信息块,可以网页源代码上查看。加载成功就执行后面的 get_products() 方法,提取商品信息。
对于翻页操作,先获取页码输入框,赋值为 input,接着获取“确定”按钮,赋值为 submit。接着调用 clear() 方法清空输入框,再调用 send_keys() 方法将页码填充到输入框中点击“确定”按钮即可。成功跳转后页码会高亮显示,只要对当前高亮的页码判断是当前的页码数即可,所以使用了另一外等待条件 text_to_be_present_in_element,它会等待指定的文本出现在某一个节点里面时即返回成功。将高亮的页码节点对应的 CSS 选择器和当前要跳转的页码通过参数传递给这个等待条件,这样它就会检测当前高亮的页面节点是不是本次传过来的,如果是就证明页面跳转到了这一页,页面就跳转成功。
这样在 index_page() 方法中可传入对应的页码,待加载出对应的页码商品列表后,再调用 get_products() 方法进行页面解析。
4、 解析商品列表
下面就使用 get_products() 方法对商品列表进行解析。直接获取页面源代码用 pyquery 进行解析。代码如下:
from pyquery import PyQuery as pq
def get_products():
"""
提取商品数据
"""
html = browser.page_source # 获取网页源代码
doc = pq(html) # 构造 pyquery 对象
# 使用 CSS 选择器获取商品信息列表,匹配的是整个页面的每个商品,匹配结果有多个
items = doc("#mainsrp-itemlist .items .item").items()
for item in items:
# 遍历每个商品,每一个 item 变量都是 pyquery 对象,调用它的 find() 方法,使用 CSS 选择器获取单个商品特定内容
product = {
'image': item.find('.pic .img').attr('data-src'),
'price': item.find('.price').text().replace("
",""),
'deal': item.find('.deal-cnt').text(), # 购买人数
'title': item.find('.title').text().strip(), # 商品描述信息
'shop': item.find('.shop').text(), # 商家信息
'location': item.find('.location').text() # 商家所在地区
}
print(product)
#save_to_mongo(product) # 将获取到的商品信息保存到 Mongodb
先来看一下商品信息的源码,如图3-2所示。
图3-2 商品信息源码
由图3-2 可知,它是一个 img 节点,包含id、class、data-src、alt和 src 等属性。src属性是图片的URL,此外还有 data-src 属性,也是图片的URL,不过它是完整的大图,src 是压缩后的小图,这里获取 data-src 属性来作为商品的图片。因此,先用 find()方法找到图片节点,接着调用 attr() 方法获取商品的 data-src 属性,这样就提取到商品图片连接。用同样的方法提取商品中的价格、成交量、名称、店铺所在地等信息。将提取结果赋值为 product 字典,接着调用 save_to_mongo() 方法将其保存到 MongoDB。
5、 保存到 MongoDB
将商品信息保存到 MongoDB的源代码如下:
import pymongo
MONGO_URL = 'localhost'
MONGO_DB = 'taobao'
MONGO_COLLECTION = 'products_hwP20'
client = pymongo.MongoClient(MONGO_URL) # 连接 MongoDB
db = client[MONGO_DB] # 指定数据库
def save_to_mongo(result):
"""
保存到 MongoDB
:param result: 结果
"""
try:
# 指定 Collection 名称,并将数据插入到 MongoDB
if db[MONGO_COLLECTION].insert(result):
print('存储到 MongoDB 成功')
except Exception:
print('存储到 MongoDB 失败')
6、 遍历每页和运行
前面定义的 get_index() 方法需要接收参数 page,使用 for 循环对页码遍历,传递 page 参数即可。代码如下所示:
MAX_PAGE = 100 # 定义最大访问的页面数
def main():
"""
遍历每一页
"""
for i in range(1, MAX_PAGE + 1):
index_page(i)
browser.close()
if __name__ == '__main__':
main()
成功运行代码的话,所有的信息都会保存到 MongoDB里面。
7、 Chrome Headless 模式
Chrome 最新的版本(从Chrome 59版本开始)支持 Headless 模式,即无界面模式,这样在爬取的时候不会弹出浏览器。使用无界面模式的方式如下:
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
browser = webdriver.Chrome(chrome_options=chrome_options)
创建ChromeOptions对象,接着添加 headless 参数,在初始化 Chrome 对象时通过 chrome_options传递这个 ChromeOptions对象,这样就启用 Chrome 的 Headless 模式。
要使用 Firefox 浏览器,只需要改这一处即可:
browser = webdriver.Firefox()
还可以使用 PhantomJS 的无界面浏览器抓取数据,同样不弹出窗口,只需要将 WebDriver 的声明修改如下即可:
browser = webdriver.PhantomJS()
PhantomJS 还支持命令行配置,例如,可以设置缓存和禁用图片加载功能来提高爬取效率:
SERVICE_ARGS = ['--load-images=false', '--disk-cache=true']
browser = webdriver.PhantomJS(service_args=SERVICE_ARGS)
最后,完整的代码如下所示:
import pymongo
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from urllib.parse import quote
from pyquery import PyQuery as pq
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
browser = webdriver.Chrome(chrome_options=chrome_options) # 初始化 Chrome浏览器对象
#browser = webdriver.Chrome() # 初始化 Chrome浏览器对象
#browser = webdriver.Firefox() # 初始化 Firefox浏览器对象
# SERVICE_ARGS = ['--load-images=false', '--disk-cache=true']
# browser = webdriver.PhantomJS(service_args=SERVICE_ARGS)
wait = WebDriverWait(browser, 10) # 设置等待时间10秒
KEYWORD = '华为P20' # 设置搜索关键字
def index_page(page):
"""
抓取索引面
:param page: 页码
"""
print('正在爬取第', page, '页')
try:
url = 'https://s.taobao.com/search?q=' + quote(KEYWORD) + '&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20190329&ie=utf8' # 构造 URL
#url = 'https://s.taobao.com/search?q=' + KEYWORD # 构造 URL
browser.get(url) # 连接 URL,首先访问搜索商品的连接,判断页码是否大于 1,
if page > 1:
input = wait.until(
# 使用 CSS 选择器获取下面的 input标签,也就是页码输入框
EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form > input'))
)
submit = wait.until(
# 使用 CSS 选择器获取到 span标签的“确定”按钮
EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form > span.btn.J_Submit'))
)
input.clear() # 清除输入框的默认值
input.send_keys(page) # 给输入框输入要获取的页码
submit.click() # 输入页码后点击提交
# page等于1或者大于1都执行下面的代码
#EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-pager li.item.active > span"), str(page))
#obj = EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-pager li.item.active > span"), str(page))
#print(obj)
wait.until(
# 使用 CSS 选择器获取当前 span 标签中的页码,并让页面与 page 的页码比较
EC.text_to_be_present_in_element((By.CSS_SELECTOR, "#mainsrp-pager li.item.active > span"), str(page))
)
# 等待商品信息加载出来,并使用选择器选择商品信息,调用 get_products() 方法提取商品信息,如果不能正常加载就报 TimeoutException 异常
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))
get_products()
except TimeoutException:
#index_page(page)
print("请求超时")
def get_products():
"""
提取商品数据
"""
html = browser.page_source # 获取网页源代码
doc = pq(html) # 转为 pyquery 对象
# 使用 CSS 选择器获取商品信息列表,匹配的是整个页面的每个商品,匹配结果有多个
items = doc("#mainsrp-itemlist .items .item").items()
for item in items:
# 遍历每个商品,每一个 item 变量都是 pyquery 对象,调用它的 find() 方法,使用 CSS 选择器获取单个商品特定内容
product = {
'image': item.find('.pic .img').attr('data-src'),
'price': item.find('.price').text().replace("
",""),
'deal': item.find('.deal-cnt').text(), # 购买人数
'title': item.find('.title').text().strip(), # 商品描述信息
'shop': item.find('.shop').text(), # 商家信息
'location': item.find('.location').text() # 商家所在地区
}
print(product)
save_to_mongo(product) # 将获取到的商品信息保存到 Mongodb
# 将商品信息保存到 MongoDB
MONGO_URL = 'localhost'
MONGO_DB = 'taobao'
MONGO_COLLECTION = 'products_hwP20'
client = pymongo.MongoClient(MONGO_URL) # 连接 MongoDB
db = client[MONGO_DB] # 指定数据库
def save_to_mongo(result):
"""
保存到 MongoDB
:param result: 结果
"""
try:
if db[MONGO_COLLECTION].insert(result):
print('存储到 MongoDB 成功')
except Exception:
print('存储到 MongoDB 失败')
MAX_PAGE = 100 # 定义最大访问的页面数
def main():
"""
遍历每一页
"""
for i in range(1, MAX_PAGE + 1):
index_page(i)
browser.close()
if __name__ == '__main__':
main()