抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

这篇博客承接前面的HTTP基本原理,对requests、Xpath和selenium三个库/工具做个简单介绍,并且用三个爬虫实例由浅到深理解爬虫的构思和实现过程,最后是用selenium+chromedriver模拟浏览器,实现对微信公众号文章的爬取。

1. requests

requests是python最常见的HTTP客户端库,可以调用requests模块的API伪装成浏览器对网站发起请求。

前面一篇爬虫博客介绍了requests的六种方法,这里不多赘述,主要回顾下发送请求和获得响应的过程。

requests库有两个重要的对象,RequestResponse,Request对象对应的是请求,向目标网址发送一个请求访问服务。而Response对象,是包含了爬虫返回的内容。

Request对象完整的发起get和post请求方式:

1
2
3
4
5
6
7
requests.get(url, params=None, **kwargs)
requests.post(url, data=None, **kwargs)

# url:想要获取的网页链接
# params:显示在url中的参数,字典形式
# data:不显示在url中,通过提交表单的方式提交参数,也是字典形式
# **kwargs:控制访问的参数,字典形式

当服务器正常响应时,返回状态码200,这个时候就可以用Response对象的属性来获取网页信息。

Response对象属性:

  • .status_code:HTTP响应状态码,200表示成功,其他状态码详见上篇博客
  • .text:HTTP响应体内容的字符串格式
  • .content:HTTP响应体内容的二进制格式
  • .encoding:从HTTP header中猜测的响应内容编码方式
  • .apparent_encoding:从内容中分析出的响应内容编码方式(备选编码方式)

这里需要注意,如果我们获得的响应内容是图片视频和音频的话,需要用二进制格式进行储存。

有了以上基础知识,就可以用request写一个爬虫项目了,我们现在目的是爬取豆瓣电影古装排行榜前20的电影图片。

进入豆瓣电影排行榜网页,按F12进入浏览器开发者工具,点击网页页面分类中的“古装”,对网页数据进行抓包。当鼠标滚轮往下滚动的时候我们可以发现,每次滚动更新,有一个名字很长的数据包会不断更新,还伴随着一大堆jpg图片的出现,很明显这个数据包是我们要抓取的对象。

点击表头选项的响应头,我们看到返回的数据是json格式,编码方式是utf-8。请求url栏中问号之前的部分是我们要的url,参数可以设置一个字典传入。

在负载部分,多次抓包以后可以看到前三个参数是一直不变的,猜测这几个参数可能是和电影类型和页面布局相关,这个可以不用管。翻页刷新后总是固定显示20个电影,因此limit和数据包内抓取的电影数相关,start和这个数据库中起始的电影编号相关。

再看看响应的json数据中,有一个“cover_url”的键对应值是电影的图片地址,至此思路就很明确了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests

url = 'https://movie.douban.com/j/chart/top_list'
# 传入的url参数设置
param = {
'type': '30', # 影片类型代号
'interval_id': '100:90',
'action':'',
'start': '0', # 从第一个影片开始
'limit': '10', # 需要抓取的影片数
}
# 伪装浏览器
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.46'
}
# 抓取上面定义范围内所有影片信息
response = requests.get(url = url, params = param, headers = headers)
# json字符串反序列化为list类型
ls = response.json()
# 解析电影图片地址,并抓取和保存图片
for p in ls:
with open('./' + str(p['title']) + '.jpg','wb') as img:
response1 = requests.get(url = p["cover_url"], headers = headers)
img.write(response1.content)

print('over!!!')

.json()这个requests库自带的函数还是挺有意思的,在这个例子中是将返回的字符串JSON数据反序列化为list数据,list中嵌套了字典数据,每个电影的信息都储存在字典中。因此这里也可以用.json()['cover_url']直接对图片网址进行抓取,注意下这里第二次抓取的是图片,返回的是二进制数据,所以用content而不是text。

2. Xpath

前面例子抓包返回的是JSON字符串,所以可以直接提取我们要的信息。如果返回的是HTML源代码,就可以用正则或者Xpath来解析我们想要的数据。

Xpath是一种解析XML文档信息的工具,我们可以通过lxml库(XML和HTML的解析库)中导入etree模块,实例化etree对象,使用xpath函数结合xpath表达式进行标签定位和指定数据的获取。

Xpath常用规则:

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取所有子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

Xpath常用表达式:

1
2
3
4
5
6
7
8
9
10
11
12
属性定位
//div[@class="slist"] # 找到所有class属性值为slist的div标签
层级定位
//div[@class="slist"]/../li[2] # 找到class属性值为slist的div的父标签下的第二个直系子标签li
多属性解析
//div[@class="slist" and @name="Id"] # 找到class属性为slist以及name属性为Id的div标签
模糊匹配
//div[contains(@class, "slist")] # 找到class属性中包含slist值得div标签
文本获取
//li[@class="item"]//text() # 取出class属性值为item的所有li标签下的所有标签文本(包括子标签)
获取属性
//li/a/@href # 取出所有li标签下a标签下的href属性值

etree对象实例化:

1
2
3
4
5
6
7
本地文件(解析保存在本地的HTML文件):
tree = etree.parse(文件名)
tree.xpath("xpath表达式")

网络数据(实例化一个html类):
tree = etree.HTML(网页内容字符串)
tree.xpath("xpath表达式")

注意下xpath解析出来的数据是以列表形式存储的,接下来示范一下requests结合Xpath写一个爬虫程序,目的是抓取彼岸图网的4k动漫封面图。

进入页面以后同样F12审查元素,点击不同页抓取返回的数据包,发现翻到第x页能抓到index_x.html,但是第一页没有下划线和其他页稍有不同,为了方便起见就从第二页开始抓。

仔细观察可以发现,所有封面图都在属性值为slist的div标签下的ul标签下的li标签中(这么说着好绕= =),前面http原理的博客说过,这样的标签可以看出是是无序列表,我们要找的封面图片地址可以通过img标签的src属性获得,图片名称可以通过alt属性获得,因此可以写如下的爬虫代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
from lxml import etree
import os
# 举个例子,这里抓取的是第二页数据
for i in range(2,3):
url = 'http://pic.netbian.com/4kdongman/index_'+str(i)+'.html'
headers = {
'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.3'
}
response = requests.get(url = url, headers = headers)
# 获取网页内容字符串
page_text = response.text

# 实例化etree对象,获取所有图片li标签信息(列表格式)
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@class="slist"]/ul/li')

# 创建文件夹
if not os.path.exists('./pic'):
os.mkdir('./pic')

# 解析li标签下孙子标签img的src属性和alt属性
for li in li_list:
img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src')[0]
img_name = li.xpath('./a/img/@alt')[0] + '.jpg'
# 这里需要处理中文乱码问题,重新编码和解码,gbk是中文最常用的
img_name = img_name.encode('iso-8859-1').decode('gbk')
img_data = requests.get(url = img_src, headers = headers).content
img_path = 'pic/' + img_name
with open(img_path,'wb') as fp:
fp.write(img_data)
print(img_name,'下载成功!!!')

当然,上面爬虫抓取的只是封面图片,并不是高清图片,因为高清图片是需要登录账号花钱下载的….我们只能合法地从我们能在浏览器中看到的图片爬取信息。而且如果你在短时间内发起大量请求的话,ip是很有可能被封的,以后再讲一些预防反爬的措施。

3. selenium

selenium是一个自动化测试工具,本质是通过驱动浏览器,完全模拟浏览器中的操作,比如拖动、点击下拉等等。为什么要模仿浏览器中的操作呢?因为很多网站是动态加载的,requests这一类的模块无法直接执行JavaScript代码,这个时候就可以通过测试工具selenium模仿人在浏览器中的操作,从而获得网页渲染之后的结果。

selenium官方网站:Selenium

(很暖心地有中文文档)以最新的selenium指南为基础,简单介绍一下其用法。

强调两点:

简单地以淘宝首页作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from time import sleep
# 启动前先将驱动程序放在当前页面
bro = Service(executable_path = './chromedriver.exe')
# 启动谷歌浏览器
driver = webdriver.Chrome(service = bro)
# 进入淘宝网页页面
driver.get('https://www.taobao.com/')
# 标签定位,输入栏id属性值为'q',id属性是整个html中唯一的,不会重复
search_input = driver.find_element(By.ID, 'q')
# 节点交互,向输入栏中输入数据'Iphone'
search_input.send_keys('Iphone')
# 标签定位,找到搜索按钮(可以用css选择器、id值或者Xpath等定位,这里用css选择器)
btn = driver.find_element(By.CSS_SELECTOR, '.btn-search')
# 节点交互,点击搜索按钮
btn.click()
driver.get('https://www.baidu.com')
sleep(2)
# 回退
driver.back()
sleep(2)
# 前进
driver.forward()
sleep(2)
# 退出浏览器
driver.quit()

简单来说,流程可以分为

  1. 创建驱动实例开启会话 driver = webdriver.Chrome()
  2. 在浏览器中执行操作 driver.get("https://www.baidu.com")
  3. 请求浏览器信息 title = driver.title
  4. 建立等待策略(隐式或显示) driver.implicitly_wait(0.5)或者用sleep(1)
  5. 定位标签 text_box = driver.find_element(by=By.NAME, value="my-text")
  6. 节点交互 text_box.send_keys("Selenium")
  7. 获取信息 value = message.text
  8. 结束会话,关闭浏览器 driver.quit()

浏览器创建

selenium支持的浏览器有Chrome、Firefox、Edge、Internet Explorer和Safari。参考支持的浏览器列表 | Selenium

元素定位

webdriver提供了8种内置的定位方法:

1
2
3
4
5
6
7
8
9
10
from selenium.webdriver.common.by import By

find_element(By.ID, 'value')
find_element(By.NAME, 'value')
find_element(By.CLASS_NAME, 'value')
find_element(By.TAG_NAME, 'value')
find_element(By.LINK_TEXT, 'value')
find_element(By.PARTIAL_LINK_TEXT, 'value')
find_element(By,XPATH, 'value')
find_element(By.CSS_SELECTOR, 'value')

节点交互

常见的节点交互有以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
driver.get("https://www.baidu.com")		# 打开网站
driver.back() # 浏览器后退
driver.forward() # 浏览器前进
driver.refresh() # 浏览器刷新
driver.add_cookie({"name": "key", "value": "value"}) # 当前浏览器添加cookie
driver.find_element(By.LINK_TEXT, "new window").click() # 新窗口中打开链接
driver.find_element(By.ID, 'value').send_keys('value') # 定位并发送内容
driver.switch_to.new_window('tab') # 打开新标签页并切换到新标签页
driver.switch_to.new_window('window') # 打开新窗口并切换到新窗口
driver.switch_to.window(original_window) # 切回之前的标签页或窗口
driver.close() # 关闭标签页或窗口
driver.quit() # 关闭浏览器

动作链

上面说的交互是对页面中存在的标签或者说是元素进行交互,而对于没有特定对象的,比对鼠标的双击、拖拽,鼠标滚轮的操作,键盘的输入等,这些操作就需要动作链来执行。该部分内容官网有详细介绍,这里就举个例子了解一下怎么用的就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time
from selenium.webdriver.common.by import By
from selenium import webdriver
from time import sleep
from selenium.webdriver import ActionChains

bro = webdriver.Chrome(executable_path = './chromedriver')
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

# 如果定位的标签是存在于iframe标签之中的则必须通过如下操作在进行标签定位
bro.switch_to.frame('iframeResult') # 切换浏览器标签定位的作用域
div = bro.find_element(By.ID, 'draggable')

# 动作链
action = ActionChains(bro)
# 点击长按指定的标签
action.click_and_hold(div)

for i in range(10):
action.move_by_offset(5,0) # move_by_offset(x,y):x水平方向 y竖直方向
action.perform() # 执行动作链操作
time.sleep(3)
#释放动作链
action.release()
bro.quit()

微信公众号爬虫范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import random
import re
from pandas import DataFrame
import os
import requests
import time
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from time import sleep
from selenium.webdriver import ChromeOptions
from lxml import etree
# moder可以为author或者article,前者为按公众号搜索,后者为按文章关键字搜索
modern='author'
# 搜索的关键字,如果modern = author,输入公众号名字,否则输入文章关键字
keyword = '冷兔'
# 爬取多少页,建议先手动搜索最大页码数,page大于最大页码将会报错
page = 2
headers = {
'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'
}
# 实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
bro = webdriver.Chrome(executable_path = './chromedriver', options=option)
bro.get("https://www.sogou.com/index.php")
wait = WebDriverWait(bro, 2)
search_input = bro.find_element(By.ID, 'query')
search_input.send_keys(keyword)
# 点击搜索按钮
btn = bro.find_element(By.ID, 'stb')
btn.click()
# 点击微信登陆
time.sleep(2)
btn = bro.find_element(By.XPATH, '//*[@id="loginBtn"]')
btn.click()
# 用微信扫码,只有十秒
time.sleep(2)
btn = bro.find_element(By.XPATH, '/html/body/div[3]/div[3]/div[2]/div[4]/div/a[2]')
btn.click()
time.sleep(10)
button = bro.find_element(By.ID, 'sogou_weixin')
button.click()
article_button = bro.find_element(By.XPATH, '//*[@id="scroll-header"]/form/div/input[1]')
article_button.click()
# 最后需要爬取的文章url都在这里
url_list=[]
for i in range(page):
page_text = bro.page_source

# 解析
tree = etree.HTML(page_text)
author=tree.xpath('/html/body/div[2]/div[1]/div[3]/ul/li/div[2]/div/a/text()')
print(author)
url_page_list=tree.xpath('/html/body/div[2]/div[1]/div[3]/ul/li/div[2]/h3/a/@href')
# 下面这个循环判断按author爬取还是按照aiticle爬取
for j in range(len(author)):
if author[j]==keyword:
url_list.append(url_page_list[j])
elif modern=='article':
url_list.append(url_page_list[j])
else:
continue
if i != page - 1:
next_button = bro.find_element(By.ID, 'sogou_next')
next_button.click()
time.sleep(2)
# 拼接地址
url_list=['https://weixin.sogou.com/'+i for i in url_list]
title_list=[]
content_list=[]
time_list=[]
author_list=[]
for url in url_list:
response = bro.get(url=url)
time.sleep(random.randint(1, 3)) # 爬一个,休息1-3秒,怕被抓
page_text = bro.page_source
tree_content = etree.HTML(page_text) # 获取爬到的动态页面源码
title = tree_content.xpath('/html/body/div[1]/div[2]/div[1]/div/div[1]/h1/text()')
title = re.sub(r'\s', '', ''.join(title)) # 获取到的文章题目
content = tree_content.xpath('/html/body/div[1]/div[2]/div[1]/div/div[1]/div[3]//text()')
content = re.sub(r'\s*', '', ''.join(content)) # 获取到的文章内容
time1=tree_content.xpath('//*[@id="publish_time"]/text()')[0]
author=tree_content.xpath('//*[@id="js_name"]/text()')[0]
author=re.sub(r'\s', '', ''.join(author))
title_list.append(title)
content_list.append(content)
time_list.append(time1)
author_list.append(author)

# 写入xlsx文件中
data = {'title': title_list, 'time':time_list,'author':author_list,'content': content_list}
df = DataFrame(data)
df.to_excel('./微信公众号_'+keyword+'.xlsx')

这里规避检测识别是设置Chromedriver的启动参数,在启动Chromedriver之前,为Chrome开启实验性功能参数excludeSwitches,它的值为[‘enable-automation’]。这个参数有什么作用呢?

我们正常运行selenium时,最上方是有提示”Chrome正受到自动测试软件的控制“的,这个参数设置就是禁用浏览器的提示。如果我们用selenium模拟chrome浏览器访问网站,网站可以通过检查window.navigator.webdriver返回值判断我们是用正常的浏览器访问还是用selenium模拟浏览器发起的访问。

如果返回值为undefined,说明是正常浏览器;如果返回true说明用selenium模拟的浏览器

为Chrome开启实验性功能参数excludeSwitches后,和正常浏览器一样返回的是undefined。

当然,反爬手段不仅仅是这一个,这个以后的有空再细说。上面的爬虫代码参考了刘丹老师,不是最终版本,还可以对输出内容和样式继续做优化。简单看一下实现的结果:

欢迎小伙伴们留言评论~