基于Python的爬虫之一

什么是爬虫

爬虫就是通过编写程序模拟浏览器上网,然后让其去互联网上抓取数据的过程。

哪些语言可以实现爬虫

php:可以实现爬虫,但是php在实现爬虫中支持多线程和多进程方面做的不好。

java:可以实现爬虫。java可以非常好的处理和实现爬虫,是唯一可以与python并驾齐驱且是python的头号劲敌。但是java实现爬虫代码较为臃肿,重构成本较大。

c/c++:可以实现爬虫。但是使用这种方式实现爬虫纯粹是是某些人(大佬们)能力的体现,却不是明智和合理的选择。

python:可以实现爬虫。python实现和处理爬虫语法简单,代码优美,支持的模块繁多,学习成本低,具有非常强大的框架(scrapy等)!

爬虫的分类

通用爬虫

通用爬虫是搜索引擎(Baidu、Google、Yahoo等)“抓取系统”的重要组成部分。主要目的是将互联网上的网页下载到本地,形成一个互联网内容的镜像备份。 简单来讲就是尽可能的;把互联网上的所有的网页下载下来,放到本地服务器里形成备分,在对这些网页做相关处理(提取关键字、去掉广告),最后提供一个用户检索接口。

聚焦爬虫

聚焦爬虫是根据指定的需求抓取网络上指定的数据。例如:获取豆瓣上电影的名称和影评,而不是获取整张页面中所有的数据值。

robots.txt协议

如果自己的门户网站中的指定页面中的数据不想让爬虫程序爬取到的话,那么则可以通过编写一个robots.txt的协议文件来约束爬虫程序的数据爬取。robots协议的编写格式可以观察淘宝网的robots(访问www.taobao.com/robots.txt即可)。但是需要注意的是,该协议只是相当于口头的协议,并没有使用相关技术进行强制管制(防君子不防小人).

反爬虫

门户网站通过相应的策略和技术手段,防止爬虫程序进行网站数据的爬取。

反反爬虫

爬虫程序通过相应的策略和技术手段,破解门户网站的反爬虫手段,从而爬取到相应的数据。

requests模块

requests模块是python中原生的基于网络请求的模块,其主要作用是用来模拟浏览器发起请求。功能强大,用法简洁高效。在爬虫领域中占据着重要的地位。

请求载体身份标识的伪装:

  • User-Agent:请求载体身份标识,通过浏览器发起的请求,请求载体为浏览器,则该请求的User-Agent为浏览器的身份标识,使用爬虫程序发起的请求,则该请求的载体为爬虫程序,则该请求的User-Agent为爬虫程序的身份标识。可以通过判断该值来获知该请求的载体究竟是基于哪款浏览器还是基于爬虫程序。
  • 反爬机制:某些门户网站会对访问该网站的请求中的User-Agent进行捕获和判断,如果该请求的UA为爬虫程序,则拒绝向该请求提供数据。
  • 反反爬策略:将爬虫程序的UA伪装成某一款浏览器的身份标识。

使用

1
2
3
4
5
6
requests.get(url, params,data, headers, proxies)  # url地址/get携带的参数/post的参数/请求头信息/代理
requests.get() # get请求获取
requests.post() # post请求获取
requests.get().text # 返回原网页内容
requests.get().content # 返回二进制
requests.get().json() # 返回json数据

正解解析

常用正则表达式

常用元字符

. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

常用限定符

* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

常用反义词

\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

特殊(包括python的re模块)

(ab) 分组
.* 贪婪匹配
.*? 惰性匹配
re.I 忽略大小写
re.M 多行匹配
re.S 单行匹配
re.sub 正则表达式, 替换内容, 字符串

Xpath解析

常用xpath表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 属性定位: 
'''找到class属性值为song的div标签'''
//div[@class="song"]
# 层级&索引定位:
'''找到class属性值为tang的div的直系子标签ul下的第二个子标签li下的直系子标签a'''
//div[@class="tang"]/ul/li[2]/a
# 逻辑运算:
'''找到href属性值为空且class属性值为du的a标签'''
//a[@href="" and @class="du"]
# 模糊匹配:
//div[contains(@class, "ng")]
//div[starts-with(@class, "ta")]
# 取文本:
'''/表示获取某个标签下的文本内容
//表示获取某个标签下的文本内容和所有子标签下的文本内容'''
//div[@class="song"]/p[1]/text()
//div[@class="tang"]//text()
# 取属性:
//div[@class="tang"]//li[2]/a/@href

etree

1
2
3
4
5
6
7
8
from lxml import etree
# 将html文档或者xml文档转换成一个etree对象,然后调用对象中的方法查找指定的节点
# 本地文件:
tree = etree.parse(文件名)
tree.xpath("xpath表达式")
# 网络数据
tree = etree.HTML(网页内容字符串)
tree.xpath("xpath表达式")

使用

1
2
3
4
5
# 使用xpath对url_conten进行解析
# 使用xpath解析从网络上获取的数据
tree=etree.HTML(url_content)
# 解析获取当页所有的标题
title_list=tree.xpath('xpath表达式')

BeautifulSoup解析

1
from bs4 import BeautifulSoup

使用

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
soup = BeautifulSoup('字符串类型或者字节类型', 'lxml')
1)根据标签名查找
- soup.a 只能找到第一个符合要求的标签
2)获取属性
- soup.a.attrs 获取a所有的属性和属性值,返回一个字典
- soup.a.attrs['href'] 获取href属性
- soup.a['href'] 也可简写为这种形式
3)获取内容
- soup.a.string
- soup.a.text
- soup.a.get_text()
【注意】如果标签还有标签,那么string获取到的结果为None,而其它两个,可以获取文本内容
4)find:找到第一个符合要求的标签
- soup.find('a') 找到第一个符合要求的
- soup.find('a', title="xxx")
- soup.find('a', alt="xxx")
- soup.find('a', class_="xxx")
- soup.find('a', id="xxx")
5)find_all:找到所有符合要求的标签
- soup.find_all('a')
- soup.find_all(['a','b']) 找到所有的a和b标签
- soup.find_all('a', limit=2) 限制前两个
6)根据选择器选择指定的内容
select:soup.select('#feng')
- 常见的选择器:标签选择器(a)、类选择器(.)、id选择器(#)、层级选择器
- 层级选择器:
div .dudu #lala .meme .xixi 下面好多级
div > p > a > .lala 只能是下面一级
【注意】select选择器返回永远是列表,需要通过下标提取指定的对象

图片懒加载

图片懒加载是一种网页优化技术。图片作为一种网络资源,在被请求时也与普通静态资源一样,将占用网络资源,而一次性将整个页面的所有图片加载完,将大大增加页面的首屏加载时间。为了解决这种问题,通过前后端配合,使图片仅在浏览器当前视窗内出现时才加载该图片,达到减少首屏图片请求数的技术就被称为“图片懒加载”。

实现

在网页源码中,在img标签中首先会使用一个“伪属性”(通常使用src2,original……)去存放真正的图片链接而并非是直接存放在src属性中。当图片出现到页面的可视化区域中,会动态将伪属性替换成src属性,完成图片的加载。

图片验证码

云打码/打码兔

处理验证码的实现流程

  • 对携带验证码的页面数据进行抓取

  • 可以将页面数据中验证码进行解析,验证码图片下载到本地

  • 可以将验证码图片提交给三方平台进行识别,返回验证码图片上的数据值

    云打码平台:

    1. 在官网中进行注册(普通用户和开发者用户)
    2. 登录开发者用户:
      • 实例代码的下载+开发文档
      • 创建一个软件
      • 使用示例代码中的源码文件中的代码进行修改,让其识别验证码图片中的数据值
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
#该函数就调用了打码平台的相关的接口对指定的验证码图片进行识别,返回图片上的数据值
def getCode(codeImg):
# 云打码平台普通用户的用户名
username = ''

# 云打码平台普通用户的密码
password = ''

# 软件ID,开发者分成必要参数。登录开发者后台【我的软件】获得!
appid =

# 软件密钥,开发者分成必要参数。登录开发者后台【我的软件】获得!
appkey = ''

# 验证码图片文件
filename = codeImg

# 验证码类型,# 例:1004表示4位字母数字,不同类型收费不同。请准确填写,否则影响识别率。在此查询所有类型 http://www.yundama.com/price.html
codetype = 3000

# 超时时间,秒
timeout = 20

# 检查
if (username == 'username'):
print('请设置好相关参数再测试')
else:
# 初始化
yundama = YDMHttp(username, password, appid, appkey)

# 登陆云打码
uid = yundama.login();
print('uid: %s' % uid)

# 查询余额
balance = yundama.balance();
print('balance: %s' % balance)

# 开始识别,图片路径,验证码类型ID,超时时间(秒),识别结果
cid, result = yundama.decode(filename, codetype, timeout);
print('cid: %s, result: %s' % (cid, result))

return result

用云打码+session爬取人人网个人信息页

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
import urllib
import requests
from lxml import etree

session = requests.Session()
url = "http://www.renren.com/SysHome.do"
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
}
# 获取验证码
code_obj = requests.get(url=url, headers=header).text
tree = etree.HTML(code_obj)
code_url = tree.xpath('//*[@id="verifyPic_login"]/@src')[0]
# 保存验证码
urllib.request.urlretrieve(code_url,"code.jpg")
# 识别验证码(code_content:用上面文档所封装的函数)
code_content = identity_code("code.jpg", 2004)
data = {
"captcha_type":"web_login",
"domain":"renren.com",
"email":"13183355361",
"f":"http%3A%2F%2Fwww.renren.com%2F969892425",
"icode":code_content,
"key_id":"1",
"origURL":"http://www.renren.com/home",
"password":"05ef6d3441bc78ba004cb8a7bb3dd60165ec17bc270aaf1e3e37448fb6e9f1c1",
"rkey":"948db2d8639bcd2664994c49454256d1"
}
login_url = "http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=2019141515944"
# 该次请求产生的cookie会被自动存储到session对象中
session.post(url=login_url, data=data, headers=header).text
profile_url = "http://www.renren.com/969892425/profile"
# 爬取目标页的信息
page_text = session.get(url=profile_url, headers=header).text
with open("renren.html","w",encoding="utf-8") as f:
f.write(page_text)

selenium

selenium是Python的一个第三方库,对外提供的接口可以操作浏览器,然后让浏览器完成自动化的操作。 

而我们就可以通过浏览器的自动化操作应付懒加载类问题.

浏览器的驱动程序(以谷歌浏览器为例)

谷歌浏览器驱动下载地址:http://chromedriver.storage.googleapis.com/index.html

版本映射表: http://blog.csdn.net/huilan_same/article/details/51896672

使用

find_element_by_id 根据id找节点
find_elements_by_name 根据name找
find_elements_by_xpath 根据xpath查找
find_elements_by_tag_name 根据标签名找
find_elements_by_class_name 根据class名字查找

driver.find_element_by_link_text() 定位文字链接

switch_to.frame(‘login_frame’) 定位到一个具体的iframe

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
from selenium import webdriver
from time import sleep

# 浏览器驱动位置,记得前面加r'','r'是防止字符转义的
chr_obj = webdriver.Chrome(r'驱动程序路径')
# 用get打开百度页面
chr_obj.get("http://www.baidu.com")
# 查找页面的“设置”选项,并进行点击
chr_obj.find_elements_by_link_text('设置')[0].click()
sleep(2)
# # 打开设置后找到“搜索设置”选项,设置为每页显示50条
chr_obj.find_elements_by_link_text('搜索设置')[0].click()
sleep(2)

# 选中每页显示50条
m = chr_obj.find_element_by_id('nr')
sleep(2)
m.find_element_by_xpath('//*[@id="nr"]/option[3]').click()
m.find_element_by_xpath('.//option[3]').click()
sleep(2)

# 点击保存设置
chr_obj.find_elements_by_class_name("prefpanelgo")[0].click()
sleep(2)

# 处理弹出的警告页面 确定accept() 和 取消dismiss()
chr_obj.switch_to_alert().accept()
sleep(2)
# 找到百度的输入框,并输入 女神
chr_obj.find_element_by_id('kw').send_keys('女神')
sleep(2)
# 点击搜索按钮
chr_obj.find_element_by_id('su').click()
sleep(2)
# 在打开的页面中找到,并打开这个页面
chr_obj.find_elements_by_link_text('女神_百度图片')[0].click()
sleep(3)

# 关闭浏览器
chr_obj.quit()
1
2
3
4
5
6
7
8
9
# 打开窗口
browser.get("https://www.baidu.com/")
# 打开新窗口
newwindow = 'window.open("https://www.baidu.com");'
browser.execute_script(newwindow)

# 切换到新的窗口
handles = browser.window_handles
browser.switch_to_window(handles[-1])

phantomJs

PhantomJS是一款无界面的浏览器,其自动化操作流程和上述操作谷歌浏览器是一致的。由于是无界面的,为了能够展示自动化操作流程,PhantomJS为用户提供了一个截屏的功能,使用save_screenshot函数实现。(因为我们不可能让用户去看着浏览器自动操作,所以要用无界面)

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
from selenium import webdriver
from time import sleep
import time

if __name__ == '__main__':
url = 'https://movie.douban.com/typerank?type_name=%E6%81%90%E6%80%96&type=20&interval_id=100:90&action='
# 发起请求前,可以让url表示的页面动态加载出更多的数据
path = r'C:\Users\Administrator\Desktop\ziliao\phantomjs-2.1.1-windows\bin\phantomjs.exe'
# 创建无界面的浏览器对象
bro_obj = webdriver.PhantomJS(path)
# 发起url请求
bro_obj.get(url)
time.sleep(3)
# 截图
bro_obj.save_screenshot('1.png')

# 执行js代码(让滚动条向下偏移n个像素(作用:动态加载了更多的电影信息))
js = 'window.scrollTo(0,document.body.scrollHeight)'
bro_obj.execute_script(js) # 该函数可以执行一组字符串形式的js代码
time.sleep(2)

bro_obj.execute_script(js) # 该函数可以执行一组字符串形式的js代码
time.sleep(2)
bro_obj.save_screenshot('2.png')
time.sleep(2)
# 使用爬虫程序爬去当前url中的内容
html_source = bro_obj.page_source # 该属性可以获取当前浏览器的当前页的源码(html)
with open('./source.html', 'w', encoding='utf-8') as f:
f.write(html_source)
bro_obj.quit()

谷歌无头浏览器

由于PhantomJs最近已经停止了更新和维护,所以推荐大家可以使用谷歌的无头浏览器,是一款无界面的谷歌浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

# 创建一个参数对象,用来控制chrome以无界面模式打开
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
# 驱动路径
path = r'C:\Users\ZBLi\Desktop\ziliao\chromedriver.exe'

# 创建浏览器对象
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)

# 上网
url = 'http://www.baidu.com/'
browser.get(url)
time.sleep(3)

browser.save_screenshot('baidu.png')

browser.quit()

session处理cookie

cookie概念:当用户通过浏览器首次访问一个域名时,访问的web服务器会给客户端发送数据,以保持web服务器与客户端之间的状态保持,这些数据就是cookie。

cookie作用:我们在浏览器中,经常涉及到数据的交换,比如你登录邮箱,登录一个页面。我们经常会在此时设置30天内记住我,或者自动登录选项。那么它们是怎么记录信息的呢,答案就是今天的主角cookie了,Cookie是由HTTP服务器设置的,保存在浏览器中,但HTTP协议是一种无状态协议,在数据交换完毕后,服务器端和客户端的链接就会关闭,每次交换数据都需要建立新的链接。就像我们去超市买东西,没有积分卡的情况下,我们买完东西之后,超市没有我们的任何消费信息,但我们办了积分卡之后,超市就有了我们的消费信息。cookie就像是积分卡,可以保存积分,商品就是我们的信息,超市的系统就像服务器后台,http协议就是交易的过程。

所以有时爬取到文件中的数据,不是个人页面的数据,而是登陆的首页面

使用

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
import requests
if __name__ == "__main__":

#登录请求的url(通过抓包工具获取)
post_url = 'http://www.renren.com/ajaxLogin/login?1=1&uniqueTimestamp=201873958471'
#创建一个session对象,该对象会自动将请求中的cookie进行存储和携带
session = requests.Session()
#伪装UA
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
}
formdata = {
'email': '1316231847',
'icode': '',
'origURL': 'http://www.renren.com/home',
'domain': 'renren.com',
'key_id': '1',
'captcha_type': 'web_login',
'password': '7b456e6c3eb6615b2e122a2942ef3845da1f91e3de075179079a3b84952508e4',
'rkey': '44fd96c219c593f3c9612360c80310a3',
'f': 'https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3Dm7m_NSUp5Ri_ZrK5eNIpn_dMs48UAcvT-N_kmysWgYW%26wd%3D%26eqid%3Dba95daf5000065ce000000035b120219',
}
#使用session发送请求,目的是为了将session保存该次请求中的cookie
session.post(url=post_url,data=formdata,headers=headers)

get_url = 'http://www.renren.com/960481378/profile'
#再次使用session进行请求的发送,该次请求中已经携带了cookie
response = session.get(url=get_url,headers=headers)
#设置响应内容的编码格式
response.encoding = 'utf-8'
#将响应内容写入文件
with open('./renren.html','w') as fp:
fp.write(response.text)

代理

一些网站会有相应的反爬虫措施,例如很多网站会检测某一段时间某个IP的访问次数,如果访问频率太快以至于看起来不像正常访客,它可能就会会禁止这个IP的访问。所以我们需要设置一些代理IP,每隔一段时间换一个代理IP,就算IP被禁止,依然可以换个IP继续爬取。

代理的分类:

  • 正向代理:代理客户端获取数据。正向代理是为了保护客户端防止被追究责任。
  • 反向代理:代理服务器提供数据。反向代理是为了保护服务器或负责负载均衡。

免费代理ip提供网站

使用

1
2
3
4
5
6
7
8
9
10
11
#不同的代理IP
proxy_list = [
{"http": "112.115.57.20:3128"},
{'http': '121.41.171.223:3128'}
]
#随机获取UA和代理IP
header = random.choice(header_list)
proxy = random.choice(proxy_list)
url = 'http://www.baidu.com/s?ie=UTF-8&wd=ip'
#参数3:设置代理
response = requests.get(url=url,headers=header,proxies=proxy)

线程池

基于线程池的爬取

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
import requests
import random
from lxml import etree
import re
from fake_useragent import UserAgent
#安装fake-useragent库:pip install fake-useragent
#导入线程池模块
from multiprocessing.dummy import Pool
#实例化线程池对象
pool = Pool()
url = 'http://www.pearvideo.com/category_1'
#随机产生UA
ua = UserAgent().random
headers = {
'User-Agent':ua
}
#获取首页页面数据
page_text = requests.get(url=url,headers=headers).text
#对获取的首页页面数据中的相关视频详情链接进行解析
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@id="listvideoList"]/ul/li')

detail_urls = []#存储二级页面的url
for li in li_list:
detail_url = 'http://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
title = li.xpath('.//div[@class="vervideo-title"]/text()')[0]
detail_urls.append(detail_url)

vedio_urls = []#存储视频的url
for url in detail_urls:
page_text = requests.get(url=url,headers=headers).text
vedio_url = re.findall('srcUrl="(.*?)"',page_text,re.S)[0]
vedio_urls.append(vedio_url)
#使用线程池进行视频数据下载
func_request = lambda link:requests.get(url=link,headers=headers).content
video_data_list = pool.map(func_request,vedio_urls)
#使用线程池进行视频数据保存
func_saveData = lambda data:save(data)
pool.map(func_saveData,video_data_list)
def save(data):
fileName = str(random.randint(1,10000))+'.mp4'
with open(fileName,'wb') as fp:
fp.write(data)
print(fileName+'已存储')

pool.close()
pool.join()
-------------The End-------------