首页 > 技术文章 > 爬虫日记

czlong 2018-08-02 23:45 原文

2018/07/04 d91 爬虫入门

一、爬虫
1.基本操作(自动投票、点赞)
- 登陆任意网站(伪造浏览器的任何行为)
2.性能相关(多线程、进程)
- 并发方案:
- 异步IO:gevent/Twisted/asyncio/aiohttp #现在都采取这种
- 自定义异步IO模块
- IO多路复用:select(回顾)
3.Scrapy框架
介绍: 异步IO:Twisted
- 基于scrapy源码自定义爬虫框架
- 使用scrapy
二、Tornado外部框架(异步非阻塞)
1.基本使用
- 小示例
- 自定义组件(form验证,session)
2.源码剖析

3.自定义异步非阻塞框架(window基于select实现)



1.爬虫基本操作
a.爬虫概念:搜索引擎出现前,黄页来记住一个个域名。搜索引擎爬取信息提供
- 定向:爬取指定网站
- 非定向:
b.
需求一:
下载页面:
http://www.autohome.com.cn/news/

筛选:
正则表达式

========== 开源模块 ==========

1. requests
安装:pip3 install requests

response = requests.get('http://www.autohome.com.cn/news/')
response.text #文本内容,字符串类型,需要先编码


总结:

response = requests.get('URL')
response.text
response.content #下载好是字节
response.encoding #以什么方式进行编码
response.aparent_encoding #下载爬取时字符串解码成字节的方式
response.status_code #状态码,301:永久重定向,访问这个网站就是访问另一个
response.cookies.get_dict() #拿到cookie字典


requests.get('http://www.autohome.com.cn/news/',cookie={'xx':'xxx'})

2. beautisoup模块
安装:pip3 install beautifulsoup4

from bs4 import BeautiSoup
soup = BeautiSoup(response.text,features='html.parser') #转换成对象,对象嵌套对象(标签)
#指定引擎或者方式,内置的,
另外有lxml,性能更好,安装即可引用
target = soup.find(id='auto-channel-lazyload-article')
print(target)

总结:
soup = beautifulsoup('<html>...</html>',features='html.parser') #也可以传html标签
v1 = soup.find('div') #找到第一个标签,对象类型
v1 = soup.find(id='i1')
v1 = soup.find('div',id='i1') #组合

v2 = soup.find_all('div') #找到所有的,列表[],不是对象了,不能find
v2 = soup.find_all(id='i1')
v2 = soup.find_all('div',id='i1')

obj = v1
obj = v2[0] #列表转为对象

obj.text #该标签的文本内容
obj.attrs #该标签的所有属性,字典类型,a.attrs.get('href')

保存图片
import uuid
img_response = requests.get(url='http:'+img_url)
file_name_new = str(uuid.uuid4())+'.jpg' #txt包括/,不能作为文件名
with open(file_name_new,'wb') as f:
f.write(img_response.content) #write是二进制写入,传content字节类型

需求二:
通过程序自动登录抽屉

post_dict = {
"phone": '111111111',
'password': 'xxx',
'oneMonth': 1
}
response = requests.post(
url="http://dig.chouti.com/login",
data = post_dict
)

print(response.text)
cookie_dict = response.cookies.get_dict() #登陆成功后服务器返回session对应的cookie
response.cookies是cookie对象,get_dict转为字典

一般情况下,带着登陆成功返回的cookie再登陆即可免data

但抽屉网在第一次登陆时返回一个cookie,登陆成功又返回一个cookie,并在服务器对第一个cookie进行授权,
下次登陆带着第一个cookie去才行,这是特殊案例

第一次请求网站,返回cookie
r1 = requests.get(
url='http://dig.chouti.com/'
)
第二次带着信息登陆
post_dict = {
"phone": '111111111',
'password': 'xxx',
'oneMonth': 1
}
r2 = requests.post(
url="http://dig.chouti.com/login",
data=post_dict,
cookies = r1.cookies.get_dict() #得带着过去,让服务器进行授权,这是抽屉的特殊案例
)
第三次爬取网页:
response = requests.get(
url="http://dig.chouti.com/profile",
cookies = {'gpsd':r1.cookies.get('gpsd')}
)
print(response.text) #这次带着第一次的cookie即可登陆成功

注:有些网站post还需要带着csrf_token,先访问网站get拿到csrf,以后带着发post请求即可
总结:web知识:cookie、session
http知识:csrf

c. 模块详细使用
requests

- 方法关系
requests.get(.....)
requests.post(.....)
requests.put(.....)
requests.delete(.....)
...

requests.request('POST'...) #本质调用这个
- 参数
request.request
- method: 提交方式
- url: 提交地址
- params: 在URL中传递的参数,GET方式
requests.request(
method='GET',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'}
)
# http://www.oldboyedu.com?k1=v1&k2=v2
- data: 在请求体里传递的数据,POST,字典、字节字符串或者文件对象

requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
data = {'use':'alex','pwd': '123','x':[11,2,3]} 本质上转为data="use=alex&pwd=123123"
)
发送时,以下格式:
请求头:
content-type: application/url-form-encod.....请求头是这个的话,把请求体的数据放到request.POST

请求体:
use=alex&pwd=123

- json 在请求体里传递的数据,会json.dumps一下,变成整体字符串
requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
json = {'use':'alex','pwd': '123'} #本质上序列化成一个整体字符串发送
)
注;假设往django发送请求,根据不同格式不同处理,但都到request.body里,而request.POST可能没有值
依据请求头来判断
请求头:
content-type: application/json

请求体:
"{'use':'alex','pwd': '123'}"

PS: 字典中嵌套字典时,只能这种方式,而data只发送字典的key

- headers 请求头

requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
json = {'use':'alex','pwd': '123'},
headers={
'Referer': 'http://dig.chouti.com/', #上一次登陆的网站,如果不同,可以拒绝登陆
'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
} #表示用的什么浏览器访问
)
- cookies Cookies,放到请求头发送,headers



- files 上传文件
requests.post(
url='xxx',
file={
'f1':open('1.py','rb'), #读取文件赋值给f1
'f2':('s1.py',open('1.py','rb')), #还能命名服务器的文件保存名字,文件对象
'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf") #还能自己写文件内容
}
)

- auth 基本认知(headers中加入加密的用户名和密码,发送过去)
ret = requests.get('https://api.github.com/user', auth=HTTPBasicAuth('wupeiqi', 'sdfasdfasdf'))

- timeout 请求和响应的超时时间
(connect timeout,read timeout)

- allow_redirects 是否允许重定向

- proxies 代理
response = requests.post(
url = '',
data = dic,
proxys={
'http':"http://1.14.452.5:5000" #发送请求时先往代理发,代理往url发
}
)

- verify 是否忽略证书

- cert 证书文件
requests.get(
url='https:', #SSL加密,而http本质是socket,发请求和响应没加密,但https服务器会先发一个证书,浏览器用证书对服务器发来的name加密
#然后发回服务器,解密,接着就可以发送数据了
#cert = 'fuck.pem', #可以自己制作,或者系统自带的购买后可以使用,本地用证书加密后,服务器购买证书后给厂商验证请求即可
verify = False, #忽略证书
cert = ('fuck.crt','xxx.key'), #第二种格式
)

- stream 流的方式
stream=True时一点点下载,False时立即下载

from contextlib import closing
with closing(requests.get('http://httpbin.org/get', stream=True)) as r:
for i in r.iter_content(): # 在此处理响应。
print(i)

- session: 用于保存客户端历史访问信息

session = requests.Session()

### 1、首先登陆任何页面,获取cookie
i1 = session.get(url="http://dig.chouti.com/help/service") #cookies等保存在session中

### 2、用户登陆,携带上一次的cookie,后台对cookie中的 gpsd 进行授权
i2 = session.post(
url="http://dig.chouti.com/login",
data={
'phone': "8615131255089",
'password': "xxxxxx",
'oneMonth': ""
}
)

i3 = session.post(
url="http://dig.chouti.com/link/vote?linksId=8589623", #直接访问
)
print(i3.text)

2018/07/04 day92

上节回顾:
- request
- requests.post()
- url
- data
- json
- params
- cookie
- headers #以上最重要
- files
- auth
- allow_redirects
- timeout
- verify
- cert
- stream
- proxies
- response
- session
- web知识 #实现爬取网页
-请求头、体:Referer,User-Agent
-正则表达式
-beautifulsoup
soup = BeautifulSoup(html_doc, features="lxml")
tag1 = soup.find(name='a')
tag2 = soup.find_all(name='a')
tag3 = soup.select('#link2') # 找到id=link2的标签,select使用css的选择器

1.soup对象的属性和方法

-name,标签名称
tag.name
tag.name = 'span' #设置
-attr,标签属性
tag.attrs #字典
tag.attrs={'':''} #赋值
del tag.attrs[''] #删除
-get,获取属性值
tag.get('id') #获取该标签的id属性值

-children, 所有子标签,找到标签Tag和纯文本(包括空以及换行)NavigableString
tag.children
-descendants,子子孙孙,先一条找到底
tag.descendants
-clear,将标签的所有子标签全部清空(保留标签名)
tag.clear()
-decompose,递归的删除所有的标签(不保留标签)
tag.decompose
-extract,递归的删除所有的标签,并获取删除的标签
body.extract()
- decode,转换为字符串(含当前标签);decode_contents(不含当前标签)
v = body.decode()
v = body.decode_contents()
- encode,转换为字节(含当前标签);encode_contents(不含当前标签)
v = body.encode()
v = body.encode_contents()

-find,获取匹配的第一个标签
tag = soup.find(name='a', attrs={'class': 'sister'}, recursive=True, text='Lacie')
tag = soup.find(name='a', class_='sister', recursive=True, text='Lacie')

默认为True,recursive=False时只在子代寻找,class_下划线是因为class是python内置的类字段
注:soup子代没有a,soup.find('body').find('p',recursive=False)

- find_all,获取匹配的所有标签
soup.find_all(name=['a','div']) 或者的关系

rep = re.compile('p') #找到p标签
rep = re.compile('^p') #以p开头的标签
soup.find_all(name=rep)

rep = re.compile('sister.*') #找到sister开头的,.*表示除换行以外所有的字符,0或多个
soup.find_all(class_=rep)

rep = re.compile('http://www.oldboy.com/static/.*') #找到这个域名开头的
soup.find_all(href=rep)

def func(tag):
return tag.has_attr('class') and tag.has_attr('id')
v = soup.find_all(name=func)
print(v) #如果找到,返回true

-has_attr,检查标签是否具有该属性
tag.has_attr('id')

-get_text,获取标签内部文本内容
tag.get_text()

-index,检查标签在某标签中的索引位置
tag = soup.find('body')
tag.index(tag.find('div'))

-is_empty_element,是否是空标签(是否可以是空)或者自闭合标签
判断是否是如下标签:'br' , 'hr', 'input', 'img', 'meta','spacer', 'link', 'frame', 'base'
tag.is_empty_element

-当前的关联标签
soup.next
soup.next_element
soup.next_elements
soup.next_sibling
soup.next_siblings


tag.previous
tag.previous_element
tag.previous_elements
tag.previous_sibling
tag.previous_siblings


tag.parent
tag.parents
-查找某标签的关联标签
tag.find_next(...)
tag.find_all_next(...)
tag.find_next_sibling(...)
tag.find_next_siblings(...)

tag.find_previous(...)
tag.find_all_previous(...)
tag.find_previous_sibling(...)
tag.find_previous_siblings(...)

tag.find_parent(...)
tag.find_parents(...)

参数同find_all
-select,select_one, CSS选择器
soup.select("title") 括号内填CSS选择器

-标签的内容
tag.string
tag.string = 'new content' # 设置

对比tag.get_text(),只能获取内容,不能设置

tag.stripped_strings # 递归内部获取所有标签的文本,包括嵌套的,类似innerText

-append,在当前标签内部追加一个标签
tag = soup.find('body')
tag.append(soup.find('a')) #添加到body最后,原来标签没了

obj = Tag(name='i',attrs={'id': 'it'})
obj.string = '我是一个新来的'
tag.append(obj) #新增一个添加到后面

-insert,在当前标签内部指定位置插入一个标签
tag.insert(2, obj)

-insert_after,insert_before 在当前标签后面或前面插入
tag.insert_after(obj) #不在内部,和append不同

-replace_with 在当前标签替换为指定标签
tag.replace_with(obj)

- 创建标签之间的关系
a = soup.find('a')
tag.setup(previous_sibling=a) #查找时变为前一个兄弟关系,但soup输出时还是父子关系

-wrap,将指定标签把当前标签包裹起来
tag = soup.find('a')
tag.wrap(obj1) #最后div标签包裹a标签

- unwrap,去掉当前标签,将保留其包裹的标签或者内容
tag = soup.find('a')
tag.unwrap() #对比tag.clear(),整个标签清空(保留标签名)

2.抽屉示例
用第一次的cookie
3.GitHub示例
csrf
# 1. 访问登陆页面,获取 authenticity_token
i1 = requests.get('https://github.com/login')
soup1 = BeautifulSoup(i1.text, features='lxml')
tag = soup1.find(name='input', attrs={'name': 'authenticity_token'})
authenticity_token = tag.get('value')
c1 = i1.cookies.get_dict() #第一次的cookie
i1.close()

# 2. 携带authenticity_token和用户名密码等信息,发送用户验证
form_data = {
"authenticity_token": authenticity_token,
"utf8": "",
"commit": "Sign in",
"login": "wupeiqi@live.com",
'password': 'xxoo'
}
i2 = requests.post('https://github.com/session', data=form_data, cookies=c1) #带着第一次的cookie,以防需要
c2 = i2.cookies.get_dict() #第二次的cookie
c1.update(c2) #将第二次的cookie更新到第一次的cookie,c1就是一个集合

# 3. 带着c1即可访问任意用户网页
i3 = requests.get('https://github.com/settings/repositories', cookies=c1)

soup3 = BeautifulSoup(i3.text, features='lxml')
list_group = soup3.find(name='div', class_='listgroup')

from bs4.element import Tag

for child in list_group.children:
if isinstance(child, Tag):
project_tag = child.find(name='a', class_='mr-1')
size_tag = child.find(name='small')
temp = "项目:%s(%s); 项目路径:%s" % (project_tag.get('href'), size_tag.string, project_tag.string, )
print(temp)
4.知乎
ajax,必须带User-Agent,CSRF

import time
import requests
from bs4 import BeautifulSoup

session = requests.Session()
#1.第一次登陆,带headers,获取csrf
i1 = session.get(
url='https://www.zhihu.com/#signin',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
soup1 = BeautifulSoup(i1.text, 'lxml')
xsrf_tag = soup1.find(name='input', attrs={'name': '_xsrf'})
xsrf = xsrf_tag.get('value')

#2.第二次登陆,拿验证码图片
current_time = time.time() #图片url带着时间窗,可能用于生成验证码,或者其他功能
i2 = session.get(
url='https://www.zhihu.com/captcha.gif', #url开头部分
params={'r': current_time, 'type': 'login'}, #get方式的参数
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
})

with open('zhihu.gif', 'wb') as f:
f.write(i2.content)

captcha = input('请打开zhihu.gif文件,查看并输入验证码:') #input停顿提示输入验证码
#3.第三次登陆,带着验证码
form_data = {
"_xsrf": xsrf,
'password': 'xxooxxoo',
"captcha": 'captcha',
'email': '424662508@qq.com'
}
i3 = session.post(
url='https://www.zhihu.com/login/email',
data=form_data,
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
#4.接着就可以爬取数据
i4 = session.get(
url='https://www.zhihu.com/settings/profile',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
soup4 = BeautifulSoup(i4.text, 'lxml')
tag = soup4.find(id='rename-section')
nick_name = tag.find('span',class_='name').string
print(nick_name)

注:知乎验证码改为倒立的字,要返回点击区域"captcha":{"img_size":[200,44],"input_points":[[,],[,]],}

12306是8张小图组成大图,每张小图有自己的m定值,要返回对的图片的m定值。
建立数据库,一类知道m定值的图片放在数据库一栏里,以后要验证牛的时候,匹配8张图和数据库牛的所有m定值,
然后返回即可

5.博客园
账号密码在js里RSA加密
RSA模块

import re
import json
import base64
import rsa #加密模块
import requests

def js_encrypt(text):
b64der = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCp0wHYbg/NOPO3nzMD3dndwS0MccuMeXCHgVlGOoYyFwLdS24Im2e7YyhB0wrUsyYf0/nhzCzBK8ZC9eCWqd0aHbdgOQT6CuFQBMjbyGYvlVYU2ZP7kG9Ft6YV6oc9ambuO7nPZh+bvXH0zDKfi02prknrScAKC0XhadTHT3Al0QIDAQAB'
der = base64.standard_b64decode(b64der)

pk = rsa.PublicKey.load_pkcs1_openssl_der(der)
v1 = rsa.encrypt(bytes(text, 'utf8'), pk)
value = base64.encodebytes(v1).replace(b'\n', b'')
value = value.decode('utf8')

return value


session = requests.Session()
#第一次登陆获取csrf
i1 = session.get('https://passport.cnblogs.com/user/signin')
rep = re.compile("'VerificationToken': '(.*)'")
v = re.search(rep, i1.text)
verification_token = v.group(1)

form_data = {
'input1': js_encrypt('wptawy'),
'input2': js_encrypt('asdfasdf'),
'remember': False
}
#第二次登陆拿cookie
i2 = session.post(url='https://passport.cnblogs.com/user/signin',
data=json.dumps(form_data),
headers={
'Content-Type': 'application/json; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'VerificationToken': verification_token}
)
#第三次爬取
i3 = session.get(url='https://i.cnblogs.com/EditDiary.aspx')
print(i3.text)

6.web微信
扫图片:
本地浏览器长轮询,1分钟和服务器保持连接,pending,只要手机扫过了,服务器状态改变,浏览器就接收到数据


2018/07/05 day93

应用:开发Web微信

前戏:
轮询:比如一个聊天室,每个人1秒钟打开看看有没有新消息。1秒钟发一次请求,服务器压力大,但代码简单,setIntervel
长轮询:请求不断开,直到服务器返回数据或者时间到,这样服务器并发多,但压力小,浏览器实时接受返回数据
时间到后继续发请求
websocket:http请求是单向的,服务器不能主动发,而socket先创建连接,然后服务器和浏览器相互发请求,
不用每次请求都要创建连接,省掉连接时间

分析:
基于网络请求
1.访问web页面,返回二维码
2.浏览器长轮询发请求给服务器,等待扫码Status=pending
3.手机扫码,手机向服务器发请求 window.code=201,这时还没确认
4.服务器返回头像给浏览器 avatar = re.findall("window.userAvatar = '(.*)';", r1.text)[0]
5.浏览器再发长轮询请求给服务器等待确认登陆
6.手机点确认,发给服务器 window.code=200
7.服务器返回重定向url给浏览器 redirect_url = re.findall('window.redirect_uri="(.*)";',r1.text)[0]
8.浏览器请求新的重定向url,返回凭证 redirect_url = redirect_url + "&fun=new&version=v2&lang=zh_CN"
soup.find('error').children
9.凭证放在url和json中去post发请求个人信息
user_info_url = "xxxr=-1780597526&lang=zh_CN&pass_ticket="+ticket_dict['pass_ticket']
10.接收个人信息 user_init_dict = json.loads(r3.text)
11.有凭证或者cookie即可获取任何信息,所有联系人、发收消息

用户扫码:
from django.shortcuts import render,HttpResponse
import requests
import time #时间窗,用于生成请求url
import re #匹配response.text
import json #序列化字符串为字典
ctime=None #全局变量
qcode = None
tip = 1 #1表示为扫码,0表示已扫码,请求url就变了这个tip
ticket_dict = {} #储存凭证的字典

def login(request):
global ctime
ctime = time.time() #时间窗,用于生成请求url
response = requests.get(
url='https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&fun=new&lang=zh_CN&_=%s' % ctime
#r后面一般是时间窗,redirect_url=xxx完成操作后跳转url
)
code = re.findall('uuid = "(.*)";',response.text) #请求微信服务器返回一个二维码对应的code
global qcode
qcode = code[0] #保存该url码,用于html生成二维码
return render(request,'login.html',{'qcode':qcode})

def check_login(request):
global tip #声明全局变量,保证函数中使用的是全局变量
ret = {'code':408,'data':None}
r1 = requests.get(
url='https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=%s&tip=%s&sr=-1767722401&_=%s' %(qcode,tip,ctime,) #传请求二维码的参数
)
# 这时向微信请求,pending多久看微信什么时候返回
if 'window.code=408' in r1.text:
print('无人扫码')
return HttpResponse(json.dumps(ret))
elif 'window.code=201' in r1.text:
ret['code']=201
avatar = re.findall("window.userAvatar = '(.*)';", r1.text)[0] #扫码后服务器返回头像url
ret['data']=avatar #传到前端显示
tip = 0 #已扫码,tip值改变
return HttpResponse(json.dumps(ret))
elif 'window.code=200' in r1.text:
redirect_url = re.findall('window.redirect_uri="(.*)";',r1.text)[0] #确认后返回重定向url
redirect_url = redirect_url + "&fun=new&version=v2&lang=zh_CN" #新的重定向url去请求
r2 = requests.get(url=redirect_url) #请求重定向url,返回凭证

from bs4 import BeautifulSoup
soup = BeautifulSoup(r2.text,'html.parser')
for tag in soup.find('error').children: #找到所有的登陆凭证
ticket_dict[tag.name]= tag.get_text() #字典类型,引用类型,修改值不用global
# print(ticket_dict)

user_info_url = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1780597526&lang=zh_CN&pass_ticket="+ticket_dict['pass_ticket']
user_info_data={
'BaseRequest':{
'DeviceID':"e459555225169136",
'Sid':ticket_dict['wxsid'],
'Skey':ticket_dict['skey'],
'Uin':ticket_dict['wxuin'],
}
}
r3 = requests.post(
url=user_info_url,
json=user_info_data, #不能data,否则只能拿到key,value传不了
)
r3.encoding = 'utf-8' #编码
user_init_dict = json.loads(r3.text) #loads将text字符串类型转为字典类型
print(user_init_dict) #拿到用户信息
ret['code']=200 #前端即可作判断,跳转重定向,展示个人信息
return HttpResponse(json.dumps(ret))


2018/07/06 day94

微信:
爬取套路:先去请求里分析url,get or post,带cookie或者headers
用requests模块请求,url
params
data:默认带一个请求头application/url-encode-form
json:将字典自动序列化,Content-Type:application/json
headers
cookies
性能相关
多个requests.get()是串型,一个个执行

一、微信开发示例:
def user(request):
"""
个人主页
:param request:
:return:
"""
#获取用户信息
user_info_url = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1780597526&lang=zh_CN&pass_ticket=" + \
ticket_dict['pass_ticket'] #拿着凭证去获取个人信息
user_info_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
}
}
r3 = requests.post(
url=user_info_url,
json=user_info_data, #不能data,否则只能拿到key,value传不了
)
r3.encoding = 'utf-8' #编码
user_init_dict = json.loads(r3.text) #loads将text字符串类型转为字典类型
ALL_COOKIE_DICT.update(r3.cookies.get_dict()) #再次保存cookie,这样就包含了以上所有流程的cookie

注:#USER_INIT_DICT已声明为空字典,内存地址已有,添加值不修改地址,但赋值会改变地址,比如=123,之前要声明global即可。
#USER_INIT_DICT['123']=123,USER_INIT_DICT.update(user_init_dict)两种做法都没改变地址

USER_INIT_DICT.update(user_init_dict) #保存到全局变量,里面有初始化的所有信息,
如个人的UserName数字,SyncKey接收信息的凭证
return render(request,'user.html',{'user_init_dict':user_init_dict})

def contact_list(request):
"""
获取所有联系人
:param request:
:return:
"""
base_url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=zh_CN&pass_ticket=%s&r=%s&seq=0&skey=%s'
ctime = str(time.time()) #加不加str都行,本身是float类型
url = base_url %(ticket_dict['pass_ticket'],ctime,ticket_dict['skey'])
response = requests.get(url=url,cookies=ALL_COOKIE_DICT) #看凭证行不行,不行就带cookie,再不行就带请求头
response.encoding='utf-8'
contact_list_dict = json.loads(response.text)
return render(request,'contact_list.html',{'contact_list_dict':contact_list_dict})

def sendMsg(request):
"""
发送消息
:param request:
:return:
"""
to_user = request.GET.get('toUser')
msg = request.GET.get('msg')
url= 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?lang=zh_CN&pass_ticket=%s' %(ticket_dict['pass_ticket'])
ctime = str(time.time())
post_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
},
'Msg':{
'ClientMsgId': ctime,
'Content':msg,
'FromUserName':USER_INIT_DICT['User']['UserName'],
'LocalID':ctime, #时间窗
'ToUserName': to_user.strip(), #两端可能有空格,清除
'Type':1 #文本类型
},'Scene':0
}
#response = requests.post(url=url,json=post_data) #发消息要看官方要不要带请求头,要相同
#response = requests.post(url=url,data=json.dumps(post_data),headers={'Content-Type':'application/json;charset=utf-8'}) #两种方式写法相等
#注:有时候带着请求头可能会发送不了消息,与sendmsg请求里相同
#发中文,显示unicode编码,需要声明ensure_ascii=False,才能不转化中文,但会触发下面问题
# data字段传的可以是字典,字符串,字节,文件对象,传的时候都会变成字节。json.dumps转为字符串后,由于声明不转换中文,含中文的字符串
# dumps默认用'latin-1',转不了字节,需要主动将中文通过utf-8转为字节

response = requests.post(url=url, data=bytes(json.dumps(post_data,ensure_ascii=False),encoding='utf-8'))
print(response.text) #包含此次发送的ID
return HttpResponse('ok')

def getMsg(request):
"""
获取消息
:param request:
:return:
"""
# 1.登陆成功后,初始化操作时,获取到一个SyncKey,
# 2.带着SyncKey检测是否是否有消息到来,
# 3.如果window.synccheck={retcode:"0",selector:"2"},有消息到来
# 3.1.获取消息,以及新的SyncKey
# 3.2.带着新的SyncKey去检测是否有消息
synckey_list = USER_INIT_DICT['SyncKey']['List'] #获取初始的值
sync_list = []
for item in synckey_list:
temp = "%s_%s" % (item['Key'], item['Val'],) #进行拼接
sync_list.append(temp)
synckey = "|".join(sync_list) #拼接
#base_url = 'https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=%s&skey=%s&sid=%s&uin=%s&deviceid=%s&synckey=%s'
r1 = requests.get(
url='https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck',
params={
'r':time.time(),
'skey':ticket_dict['skey'],
'sid':ticket_dict['wxsid'],
'uin':ticket_dict['wxuin'],
'deviceid':'e843458050524287',
'synckey':synckey
},
cookies = ALL_COOKIE_DICT #不加cookie的话会hold不住,一直发请求,加了后就等待pending
)
if 'retcode:"0",selector:"2"' in r1.text: #返回这个,说明有消息了,发请求获取一下内容
post_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
},
'SyncKey':USER_INIT_DICT['SyncKey'],
'rr':1 #值多少没关系
}
r2 = requests.post(
url='https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync',
params = {
'skey':ticket_dict['skey'],
'sid':ticket_dict['wxsid'],
'pass_ticket':ticket_dict['pass_ticket'],
'lang':'zh_CN'
},
json=post_data
)
r2.encoding='utf-8'
msg_dict=json.loads(r2.text) #返回的所有内容
for msg in msg_dict['AddMsgList']: #AddMsgList包含所有人这个时刻发来的消息
print(msg['Content']) #如果是群聊,content还包含发送人的username,用于标记,根据它获取昵称
USER_INIT_DICT['SyncKey'] = msg_dict['SyncKey'] #更新值
return HttpResponse('ok')

2018/07/07 day95

爬虫性能相关

- 傻等
response = requests.get(....)
- 机智
response = requests.get(....) #批量抛出请求,统一等待回来,哪个回来就回调一下执行
response = requests.get(....) #怎么回调,用的是别人的组件
response = requests.get(....)
response = requests.get(....)

角色:使用者
- 多线程:线程池(线程多了性能降低,上下线程切换耗时)
- 多进程:进程池
===============================================================================================
1.线程:cpu的最小工作单元,存在于进程里,每一个进程,里面的线程共享所有的这个进程的工作资源,
真正的工作者是线程,比如一个教室和里面的学生,多个学生共享教室资源
2.GIL锁:同一时刻一个进程里只能一个线程被cpu调用,计算要用到cpu,IO的话线程只是被cpu调用去操作,
调用时间几乎可以忽略,而且io与cpu没交互,所以不受GIL锁限制
3.每个进程有自己独立的GIL锁
4.http请求属于io操作

总结:IO密集用多线程,计算密集用多进程。两者导入库不同,操作类似,都能实现并发。
===============================================================================================
- 协程(微线程) + 异步IO =》 1个线程发送N个Http请求,哪个请求回来后执行处理
===============================================================================================
协程:一个线程在做很多东西,先执行一个函数,中途触发后执行另一个函数,再中途触发回来执行第一个函数
主要作用是让线程切换。
加上异步IO后自动回调。
协程+异步IO:遇到IO阻塞(请求都没回来),就能并发N个请求(并发是因为发请求速度快,看似并发)
如果同时多个请求回来,只能一个个先后处理
===============================================================================================
- asyncio
- 示例1:asyncio.sleep(5) #不支持发送http请求,支持TCP请求(socket的)
- 示例2:自己封装Http数据包 #构造http请求规则的字符串通过socket发TCP请求即可
- 示例3:asyncio+aiohttp
aiohttp模块:封装Http数据包
- 示例4:asyncio+requests
requests模块:封装Http数据包
- gevent,依赖greenlet协程模块+异步IO,socket级别,需要封装http请求
pip3 install greenlet
pip3 install gevent
- 示例1:gevent+requests
- 示例2:gevent(协程池,即最多发多少个请求)+requests
- 示例3:gevent+requests => grequests

- Twisted
scrapy的并发下载依赖于它,一个优秀网络库,不依赖其他库
- Tornado
既是外部框架,也是爬虫模块,不依赖其他库

顺序:=====> gevent(gevent+协程池+requests) > Twisted > Tornado > asyncio

角色:NB开发者

1. socket客户端、服务端
一般情况下,浏览器请求一个网站时,连接会阻塞
setblocking(0): 这时连接非阻塞,但无数据(连接无响应;数据未返回)就报错。
写一个异常处理,继续执行其他操作,连接成功告知一下即可

2. IO多路复用
客户端:
try:
socket对象1.connet()
socket对象2.connet()
socket对象3.connet()
except Ex..
pass #报错就pass

while True:
r,w,e = select.select([socket对象1,socket对象2,socket对象3,],[socket对象1,socket对象2,socket对象3,],[],0.05)
r = [socket对象1,] #表示有数据返回了,哪个对象有变化放到r中
xx = socket对象1.recv() #接收数据
w = [socket对象1,] #表示和服务器创建连接成功
socket对象1.send('"""GET /index HTTP/1.0\r\nHost: baidu.com\r\n\r\n"""') #发送数据
e:错误信息
0.05:超时时间

3. 自定义socket对象
class Foo:
def fileno(self):
obj = socket()
return obj.fileno() #拿到socket对象的文件描述符

r,w,e = select.select([socket对象?,对象?,对象?,Foo()],[],[])
#不一定是socket对象,但对象必须有:fileno方法,并返回一个文件描述符

========总结===========
a. select内部调用的是对象.fileno()
b. 自定义的Foo()内部必须利用socket创建文件描述符,对象可以写其他对象,对socket进行封装即可

4.自定义异步IO框架

异步IO含义:给出url,自动执行回调,这就是异步操作。编写的时候不是异步。

====>几个重要概念
IO多路复用: r,w,e = while 监听多个socket对象,这个过程是同步,不是异步的。
利用其特性可以开发异步IO模块。

异步IO: 非阻塞的socket+IO多路复用,发IO请求,不等着,完成后自动异步执行回调
- 非阻塞socket
- select[自己对象],w,r
代码:
class HttpRequest:
def __init__(self,sk,host,callback):
self.socket = sk
self.host = host
self.callback = callback
def fileno(self):
return self.socket.fileno() #返回文件描述符

class HttpResponse:
def __init__(self,recv_data):
self.recv_data = recv_data
self.header_dict = {}
self.body = None
self.initialize() #执行方法

def initialize(self):
headers, body = self.recv_data.split(b'\r\n\r\n', 1)#分离请求头请求体,b表示字节,1因为请求体可能有,只找第一个
self.body = body
header_list = headers.split(b'\r\n') #分离请求头
for h in header_list:
h_str = str(h,encoding='utf-8') #先变成字符串
v = h_str.split(':',1)
if len(v) == 2: #部分响应头格式没有冒号
self.header_dict[v[0]] = v[1] #响应头字典形式,所有的外部框架都是这样来做的

class AsyncRequest:
def __init__(self):
self.conn = [] # 用于检测是否已经返回
self.connection = [] # 用于检测是否已经连接成功

def add_request(self,host,callback):
try:
sk = socket.socket()
sk.setblocking(0)
sk.connect((host,80,))
except BlockingIOError as e:
pass
request = HttpRequest(sk,host,callback) #创建HttpRequest对象
self.conn.append(request)
self.connection.append(request)

def run(self):

while True: #事件循环
rlist,wlist,elist = select.select(self.conn,self.connection,self.conn,0.05)
for w in wlist:
print(w.host,'连接成功...')
# 只要能循环到,表示socket和服务器端已经连接成功
tpl = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n" %(w.host,)#通过封装socket对象为httprequest对象,就能传host
w.socket.send(bytes(tpl,encoding='utf-8'))
self.connection.remove(w) #已发送数据的从列表清除
for r in rlist:
# r是HttpRequest对象
recv_data = bytes() #空字节,
while True: #一直接收数据
try:
chunck = r.socket.recv(8096) #8096是大小,超过大小的分为一块块chunks接受,没数据则报错
recv_data += chunck
except Exception as e: #没数据执行这步
break
# print(r.host,'有数据返回',recv_data)
response = HttpResponse(recv_data)
r.callback(response)
r.socket.close()
self.conn.remove(r) #不需再监听
if len(self.conn) == 0:
break

def f1(response):
print('保存到文件',response.header_dict) #打印响应头字典

def f2(response):
print('保存到数据库', response.header_dict)

url_list = [
{'host':'www.baidu.com','callback': f1}, #用用户自己选择哪个回调函数处理返回结果
{'host':'cn.bing.com','callback': f2},
{'host':'www.cnblogs.com','callback': f2},
]

req = AsyncRequest()
for item in url_list:
req.add_request(item['host'],item['callback']) #发送请求,创建HttpRequest对象和检测字典

req.run()

2018/07/10 day96 scrapy

Scrapy框架:
- 下载页面
- 解析
- 并发
- 深度 循环或递归

安装:http://www.cnblogs.com/wupeiqi/articles/6229292.html
Linux
pip3 install scrapy


Windows
a. pip3 install wheel
b. 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. 进入下载目录,执行 pip3 install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
d. pip3 install scrapy
e. 下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/

介绍:
- Spiders:多个spiders,能爬取不同网站,解析,把深度url给队列
- Scrapy engine:写循环递归,深度抓取
- Scheduler:任务队列
- Downloader:下载页面,返回给spiders
- Item Pipeline:储存数据

流程: 1.每个spiders有初始的urls,引擎把url放到队列中
2.队列调度url给下载器进行下载
3.下载器返回结果
4.解析,给项目管道,给队列

使用:
1. 指定初始URL
2. 解析响应内容
- 给调度器,深度url
- 给item格式化和pipeline持久化


scrapy startproject day96

cd day96
scrapy genspider chouti chouti.com

打开chouti.py进行编辑

scrapy crawl chout --nolog #没有日记

window输出:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #标准输出,在输出时设置编码

print(response.text)
# content = str(response.body,encoding='utf-8') #设置完标准输出后,body需要encoding
# print(content)

选择器:
// 所有子代
//a[2] 列表的第二个
.// 当前对象的子代中,obj.xpath()
/ 子代
/div 子代的div标签
/div[@id="i1"] 子代的div标签且id=i1
/div[@id="i1"][@href="link"] 双条件
obj.extract() 列表中的每一个对象转换字符串,返回列表[]
obj.extract_first() 列表中的每一个对象转换字符串,返回列表第一个元素
//div/text() 获取某个标签的文本
//div/@href 获取某个标签的属性
//a[contains(@href, "link")] href包含link的
//a[starts-with(@href, "link")] href以link开头的
//a[re:test(@id, "i\d+")] 正则,id是i+数字的格式

示例:获取新闻标题
def parse(self, response):
# print(response.url)
# print(response.text) #window要设置标准输出格式
# content = str(response.body,encoding='utf-8') #设置完标准输出后,body需要encoding
# print(content)
hxs = Selector(response=response).xpath('//div[@id="content-list"]/div[@class="item"]') #找到所有a标签,列表,extract转字符串,不加的话返回对象
for obj in hxs:
a = obj.xpath('.//a[@class="show-content color-chag"]/text()').extract_first()
print(a.strip()) #去掉空格

示例:获取url
visited_urls=set() #集合,能去重
hxs = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
for item in hxs:
if item in self.visited_urls:
print('已经存在',item)
else:
self.visited_urls.add(item) #集合用add
print(item)

问题:url过长时,索引速度慢
解决:md5等加密转固定长度,再进行比较
def md5(self,url):
import hashlib
obj = hashlib.md5()
obj.update(bytes(url,encoding='utf-8'))
return obj.hexdigest()

深度爬取:
def start_requests(self):
for url in self.start_urls:
yield Request(url, callback=self.parse) #指定开始url的解析函数,能重写指定新的函数

# hxs = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
# hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]/@href').extract()
hxs = Selector(response=response).xpath('//a[re:test(@href, "/all/hot/recent/\d+")]/@href').extract()
for url in hxs:
md5_url = self.md5(url)
if md5_url in self.visited_urls:
pass
else:
print(url)
self.visited_urls.add(md5_url) #集合用add
url = "https://dig.chouti.com%s" %url #添加前缀
yield Request(url=url,callback=self.parse) #访问新页面,下载完成后调用解析,必须写yield,才能发给调度器

1.重写start_requests函数,指定最开始url的解析函数
2.yield的使用
3.DEPTH_LIMIT = 1 来指定“递归”的层数。0表示无限制

格式化和保存:
spiders.py中
from ..items import ChoutiItem
for obj in hxs1:
title = obj.xpath('.//a[@class="show-content color-chag"]/text()').extract_first().strip()
href = obj.xpath('.//a[@class="show-content color-chag"]/@href').extract_first().strip()
item_obj = ChoutiItem(title=title,href=href) #对象化,即格式化
#将item对象传给pipeline
yield item_obj

items.py中
class ChoutiItem(scrapy.Item):
title = scrapy.Field()
href = scrapy.Field()

pipelin.py中
class Day96Pipeline(object):
def process_item(self, item, spider): #spider参数作判断,用于指定爬虫的定制保存
print(spider,item)
tpl = "%s\n%s\n\n" %(item['title'],item['href']) #要字典索引
f = open('news.json','a+')
f.write(tpl)
f.close()

settings.py中
ITEM_PIPELINES = {
'day96.pipelines.Day96Pipeline': 300, 300是优先级,越小越高
}
1.item将数据格式成对象,起到格式化的作用
2.然后将item对象传给pipeline,字典索引,保存
3.pipeline要配置settings.py
4.所有的spider都会经过每一个pipeline,指定时作if判断spider参数


2018/07/10 day97

scrapy框架

上节回顾:
域名:allowed_domains = ['chouti.com',]
响应:response
response.meta = {'depth':'深度值',}:


一、去重url

scrapy默认已经作了筛选,yield Request()有个参数dont_filter=False,通过类RFPDupeFilter(BaseDupeFilter)来完成。
将url放在文件中,来判断访问url是否已经存在。

自己重写:
1.setting配置 DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
2.class RepeatFilter(object)类下面的方法名不改

3.在方法from_settings前@classmethod,不用创建对象执行该方法。
- 内部通过obj = RepeatFilter.from_settings()先执行该方法。
- 该方法的return cls()中cls是当前类名,cls()返回对象,即调用类的构造方法,init()初始化

4.init初识化

5.request_seen方法作if判断
self.visited_url.add(request.url)

6.即可实现去重从单个spider分离出来,统一配置好
7.RepeatFilter从程序开始就运行,结束再执行里面的close()
8.顺序:from_settings->__init__(self)-> open-> request_seen-> close
9.去重url可以放在缓存、数据库、内存、文件等

去重判断:
def request_seen(self, request):
if request.url in self.visited_url:
return True
self.visited_url.add(request.url)
return False

二、pipeline补充

1.多次爬取多次打开文件耗时,分离这个步骤统一打开open_spider和close_spider

def open_spider(self, spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
self.f = open('news.json', 'a+')

2.获取settings配置,并初始化,即可创建数据库连接
@classmethod
def from_crawler(cls, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
val = crawler.settings.getint('DB') #settings即py文件,括号内是变量名,拿到变量,大写
return cls(val)

def __init__(self,conn_str):
self.conn_str = conn_str

self.conn = open(self.conn_str, 'a+') #打开数据库连接

self.conn.write(tpl)

self.conn.close()

3.多个pipeline时,第一个不return item的话,下一个不执行

专业写法:raise DropItem() #这种写法,自定义扩展能监听什么时候丢弃item


三、cookie问题

cookie_obj = CookieJar()
cookie_obj.extract_cookies(response,response.request)
cookie_obj._cookies #即获取到所有的cookie
self.cookie_dict = cookie_obj._cookies #保存到全局变量

示例:自动登陆抽屉并点赞
class ChoutiSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
cookie_dict = None #用于保存cookie信息

def parse(self, response):
cookie_obj = CookieJar()
cookie_obj.extract_cookies(response,response.request)
# print(cookie_obj._cookies) #所有的cookie
self.cookie_dict = cookie_obj._cookies
#带上cookie、账号密码登陆
yield Request(
url='https://dig.chouti.com/login',
method='POST',
body="phone=8613729805358&password=long0486&oneMonth=1",
headers={'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
cookies=cookie_obj._cookies,callback=self.check_login
)

def check_login(self,response):
print(response.text) #登陆成功后打印信息
yield Request(
url='https://dig.chouti.com/',callback=self.good #访问首页,用于解析页面
)

def good(self,response):
id_list = Selector(response=response).xpath('//div[@share-linkid]/@share-linkid').extract()
for id in id_list:
url = 'https://dig.chouti.com/link/vote?linksId=%s' % id
yield Request(url=url,method="POST",cookies=self.cookie_dict,callback=self.show)#点赞带cookie

page_list = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
for page in page_list:
url = 'https://dig.chouti.com%s' % page
yield Request(
url=url,callback=self.good #回调自身
)

def show(self,response):
print(response.text) #打印点赞成功信息

四、扩展

在预留的几个钩子信息上注册自定制函数

from scrapy import signals
class MyExtend:

def __init__(self, crawler):
pass

@classmethod
def from_crawler(cls, crawler):
obj = cls(crawler)
crawler.signals.connect(obj.start, signals.engine_started)
crawler.signals.connect(obj.close, signals.spider_closed) #在指定信息上注册函数
return obj

def start(self):
print('signals.engine_started.')

def close(self):
print('spider_closed')

settings.py中配置
EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
'day96.extensions.MyExtend': 300,
}

五、配置文件

BOT_NAME = 'day96' #爬虫名字

SPIDER_MODULES = ['day96.spiders'] #爬虫路径
NEWSPIDER_MODULE = 'day96.spiders'

USER_AGENT = 'day96 (+http://www.yourdomain.com)' #带着这个去请求,就认为是爬虫,真实爬取时伪造这个

网站建立时的配置文件,里面筛选谁的爬虫可以来

ROBOTSTXT_OBEY = True #true时遵循爬虫规则,网站配置文件允许就爬

CONCURRENT_REQUESTS = 32 #并发请求数,最大值,默认16,依据反爬虫策略来决定

DOWNLOAD_DELAY = 3 #秒,下载延迟

CONCURRENT_REQUESTS_PER_DOMAIN = 16 #每个域名并发16个
CONCURRENT_REQUESTS_PER_IP = 16 #每个IP并发16个,一个域名可能有多台服务器ip

COOKIES_ENABLED = False #是否拿cookie

TELNETCONSOLE_ENABLED = False #是否允许中途暂停,telnet 127.0.0.1 6023进入监听,可以输入命令查看
#当前爬虫的状态,指标

DEFAULT_REQUEST_HEADERS = { #每个请求携带,单独在Request设置
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}

AUTOTHROTTLE_ENABLED = True #智能限速,延迟时间智能化
AUTOTHROTTLE_START_DELAY = 5 #初识延迟,最小延迟是DOWNLOAD_DELAY
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = False

DEPTH_PRIORITY = 0 #0或者1,广度还是深度优先,默认0深度优先



2018/07/11 day98

一、配置

1. 自动限速算法
from scrapy.contrib.throttle import AutoThrottle
自动限速设置
1. 获取最小延迟 DOWNLOAD_DELAY
2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间,上一个请求的
5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY

target_delay = latency / self.target_concurrency #目标延迟
new_delay = (slot.delay + target_delay) / 2.0 #slot.delay表示上一次的延迟时间
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay

二、缓存

目的:解决频繁数据库连接,省时。放于文件或者内存中。

from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage

# 是否启用缓存策略
# HTTPCACHE_ENABLED = True

# 缓存策略1:所有请求均缓存,下次再请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略2:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"

# 缓存超时时间
# HTTPCACHE_EXPIRATION_SECS = 0

# 缓存保存路径
# HTTPCACHE_DIR = 'httpcache'

# 缓存忽略的Http状态码
# HTTPCACHE_IGNORE_HTTP_CODES = []

# 缓存存储的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

三、代理

scrapy代理,依赖python的环境,要想增加,需要在python环境变量中设置
本质:代理即设置请求头meta和headers['Proxy-Authorization']。

from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware

方式一:使用默认
os.environ #键值对,key需以_proxy结尾
{
http_proxy:http://root:woshiniba@192.168.11.11:9999/ #用户名:密码@ip:端口的格式
https_proxy:http://192.168.11.11:9999/
}

方式二:使用自定义下载中间件

def to_bytes(text, encoding=None, errors='strict'):
if isinstance(text, bytes):
return text
if not isinstance(text, six.string_types):
raise TypeError('to_bytes must receive a unicode, str or bytes '
'object, got %s' % type(text).__name__)
if encoding is None:
encoding = 'utf-8'
return text.encode(encoding, errors)

class ProxyMiddleware(object):
def process_request(self, request, spider):
PROXIES = [ #代理池
{'ip_port': '111.11.228.75:80', 'user_pass': ''},
{'ip_port': '120.198.243.22:80', 'user_pass': ''},
{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
{'ip_port': '101.71.27.120:80', 'user_pass': ''},
{'ip_port': '122.96.59.104:80', 'user_pass': ''},
{'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]
proxy = random.choice(PROXIES) #随机选择一个
if proxy['user_pass'] is not None: #有用户和密码
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port']) #python3要转字节
encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))#规定要加密
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)#以Basic开头
print "**************ProxyMiddleware have pass************" + proxy['ip_port']
else:
print "**************ProxyMiddleware no pass************" + proxy['ip_port']
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])

#settings配置一下
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}

四、Https证书

Https访问时有两种情况:
1. 要爬取网站使用的可信任证书(默认支持)
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"

2. 要爬取网站使用的自定义证书
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory" #修改路径

# https.py
from scrapy.core.downloader.contextfactory import ScrapyClientContextFactory
from twisted.internet.ssl import (optionsForClientTLS, CertificateOptions, PrivateCertificate)

# 写一个类继承ScrapyClientContextFactory,重写getCertificateOptions方法
# 增加两个参数,传自定义的证书给参数
class MySSLFactory(ScrapyClientContextFactory):
def getCertificateOptions(self):
from OpenSSL import crypto
v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
return CertificateOptions(
privateKey=v1, # pKey对象,证书文件转对象,找loadfile方法
certificate=v2, # X509对象
verify=False,
method=getattr(self, 'method', getattr(self, '_ssl_method', None))
)

五、下载中间件

顺序:
先顺序执行中间件的process_request,直到scrapy的下载器。
如果某个中间件下载好,就把response给最后一个中间件的process_response
再倒序执行中间件的process_response,直到spider进行处理。
如果某个中间件处理完,就再执行下一个任务。
如果下载process_request过程出现异常,交给process_exception消化,不能消化最后报错。

class DownMiddleware1(object):
def process_request(self, request, spider):
'''
请求需要被下载时,经过所有下载器中间件的process_request调用
:param request:
:param spider:
:return:
None,继续后续中间件去下载;
Response对象,停止process_request的执行,开始执行process_response
Request对象,停止中间件的执行,将Request重新调度器
raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
'''
pass



def process_response(self, request, response, spider):
'''
spider处理完成,返回时调用
:param response:
:param result:
:param spider:
:return:
Response 对象:转交给其他中间件process_response
Request 对象:停止中间件,request会被重新调度下载
raise IgnoreRequest 异常:调用Request.errback #丢弃Request异常
'''
print('response1')
return response #必须有返回值

def process_exception(self, request, exception, spider):
'''
当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
:param response:
:param exception:
:param spider:
:return:
None:继续交给后续中间件处理异常;
Response对象:停止后续process_exception方法
Request对象:停止中间件,request将会被重新调用下载
'''
return None

六、爬虫中间件

下载完成后,response到达spider前,要经过爬虫中间件

class SpiderMiddleware(object):

def process_spider_input(self,response, spider):
'''
下载完成,执行,然后交给parse处理
:param response:
:param spider:
:return:
'''
pass

def process_spider_output(self,response, result, spider):
'''
spider处理完成,返回时调用
:param response:
:param result:返回的两种yield生成器
:param spider:
:return: 必须返回包含 Request 或 Item 对象的可迭代对象(iterable)
'''
return result

def process_spider_exception(self,response, exception, spider):
'''
异常调用
:param response:
:param exception:
:param spider:
:return: None,继续交给后续中间件处理异常;
含 Response 或 Item 的可迭代对象(iterable),交给调度器或pipeline
'''
return None


def process_start_requests(self,start_requests, spider):
'''
爬虫启动时调用
:param start_requests:
:param spider:
:return: 包含 Request 对象的可迭代对象
'''
return start_requests #不返回不知道从哪开始

七、扩展总结

1.下载中间件
DOWNLOADER_MIDDLEWARES = {}
2.爬虫中间件
SPIDER_MIDDLEWARES = {}
3.信号
EXTENSIONS = {
'day96.extensions.MyExtend': 300,
}
4.去重url
DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
5.pipeline
ITEM_PIPELINES
6.https证书
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory" #修改路径
7.代理(使用下载中间件)
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}

八、自定义命令

1.在spiders同级创建任意目录,如:commands
2.在其中创建 crawlall.py 文件 (此处文件名就是自定义的命令)
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings

class Command(ScrapyCommand):
requires_project = True

def syntax(self):
return '[options]'

def short_desc(self):
return 'Runs all of the spiders' #命令提示语

def run(self, args, opts):
spider_list = self.crawler_process.spiders.list() #找到所有的爬虫名称
for name in spider_list:
self.crawler_process.crawl(name, **opts.__dict__)
self.crawler_process.start()

3.在settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称' "day96.commands"
4.在项目目录执行命令:scrapy crawlall

九、源码流程简述

入口:自定义命令

# 1. 执行CrawlerProcess构造方法
# 2. CrawlerProcess对象(含有配置文件)的spiders
# 2.1,为每个爬虫创建一个Crawler
# 2.2,执行 d = Crawler.crawl(...) # ************************ #
# d.addBoth(_done)
# 2.3, CrawlerProcess对象._active = {d,}

# 3. dd = defer.DeferredList(self._active)
# dd.addBoth(self._stop_reactor) # self._stop_reactor ==> reactor.stop()

# reactor.run

即twisted的原理

for url in url_list:
d = task(url)
_active.add(d)

dd = defer.DeferredList(_active)
dd.addBoth(stop)
reactor.run()


2018/07/12 day99 scrapy源码剖析

今日概要
1. 前戏 Twisted 使用


2. Scrapy经验 + Twisted功能
- Low
- High

3. Scrapy源码剖析


一、Twisted框架

1.不包含特殊socket对象的框架,自动结束

from twisted.web.client import getPage #模块功能:socket对象,自动完成移除
from twisted.internet import reactor #模块功能:事件循环(所有的socket对象都移除)
from twisted.internet import defer #模块功能:defer.Deferred,特殊的socket对象,不发请求,需手动移除


# 1.利用getPage创建socket
# 2.将socket添加到事件循环中
# 3.开始事件循环 (内部发送请求,并接受响应;当所有的socekt请求完成后,终止事件循环)

def response(content):
print(content)

@defer.inlineCallbacks #装饰器和yield一下即可添加到事件循环
def task():
url = "http://www.baidu.com"
d = getPage(url.encode('utf-8')) #根据url找到ip创建socket对象,url是字节
d.addCallback(response)
yield d

def done(*args,**kwargs):
reactor.stop()

li=[]
for i in range(10):
d = task() #需先执行一下,添加到循环中
li.append(d) #创建了10个socket放到循环,即之前的异步IO

# d = task() #先执行一下,添加到循环中
dd = defer.DeferredList(li) #列表
dd.addBoth(done) #监听完成没,完成自动执行括号内函数 Both包括Callback和Errback
reactor.run() #开始循环

2.包含特殊socket对象的框架,手动计数器结束

from twisted.web.client import getPage #模块功能:socket对象,自动完成移除
from twisted.internet import reactor #模块功能:事件循环(所有的socket对象都移除)
from twisted.internet import defer #模块功能:defer.Deferred,特殊的socket对象,不发请求,需手动移除

# 1.利用getPage创建socket
# 2.将socket添加到事件循环中
# 3.开始事件循环 (内部发送请求,并接受响应;当所有的socekt请求完成后,终止事件循环)
_close = None
count = 0
def response(content):
print(content)

global count
count += 1
_close.callback(None) #表示特殊对象手动终止

@defer.inlineCallbacks #装饰器和yield一下即可添加到事件循环
def task():
url = "http://www.baidu.com"
d1 = getPage(url.encode('utf-8')) #根据url找到ip创建socket对象,url是字节
d1.addCallback(response)

url = "http://www.baidu.com"
d2 = getPage(url.encode('utf-8')) # 根据url找到ip创建socket对象,url是字节
d2.addCallback(response)

url = "http://www.baidu.com"
d3 = getPage(url.encode('utf-8')) # 根据url找到ip创建socket对象,url是字节
d3.addCallback(response)

global _close
_close = defer.Deferred()
yield _close #返回特殊的socket对象,不终止

def done(*args,**kwargs):
reactor.stop()

#每一个都看为一个爬虫,每个爬虫内都是并发,之间也是并发
#BUG:每个spider有自己的defer.Deferred(),列表或者封装spider成对象,维护自己的_close关闭
spider1 = task() #先执行一下,添加到循环中
spider2 = task()
dd = defer.DeferredList([spider1,spider2]) #列表
dd.addBoth(done) #监听完成没,完成自动执行括号内函数 Both包括Callback和Errback
reactor.run() #开始循环


二、自定义scrapy框架

from twisted.web.client import getPage #模块功能:socket对象,自动完成移除
from twisted.internet import reactor #模块功能:事件循环(所有的socket对象都移除)
from twisted.internet import defer #模块功能:defer.Deferred,特殊的socket对象,不发请求,需手动移除

class Request(object):
#封装请求为一个类,类.xx取url和parse
def __init__(self,url,callback):
self.url = url
self.callback = callback

class HttpResponse(object):
#将下载结果和request封装成一个类,以后方法解析好,类.xxx就能取到所有内容
def __init__(self,content,request):
self.content = content
self.request = request
self.url = request.url

class ChoutiSpider(object):
name = 'chouti'
#爬虫类,生成初始Request,定义parse方法
def start_requests(self):
start_url = ['https://www.baidu.com','https://www.bing.com']
for url in start_url:
yield Request(url,self.parse)

def parse(self,response):
print(response)
#1.crawling移除
#2.获取parser yield值
#3.再次取队列中获取


import queue
Q = queue.Queue() #队列,可以放数据

class Engine(object):
#引擎类,添加初始Request到队列,从队列调度任务,执行回调,再次添加队列
def __init__(self):
self._close = None
self.max = 5 #最大并发数
self.crawling = [] #正在爬取

def get_response_callback(self,content,request):
self.crawling.remove(request) #移除
req = HttpResponse(content,request)
result = request.callback(req) #即调用了parse
import types
if isinstance(result,types.GeneratorType): #解析返回生成器的时候,再放到队列中
for req in result:
Q.put(req)

def _next_request(self):
#取Request对象,并发送请求
if Q.qsize() == 0 and len(self.crawling) == 0:
self._close.callback(None)
return
if len(self.crawling)>=self.max:
return
while len(self.crawling)<self.max:
try:
req = Q.get(block=False) #队列没有不等着
self.crawling.append(req)
d = getPage(req.url.encode('utf-8'))
d.addCallback(self.get_response_callback,req) #下载完成调用这个函数,默认把下载response传给这个函数,手动也能传参
# d.addCallback(self._next_request) #解析完成,再添加队列
d.addCallback(lambda _:reactor.callLater(0,self._next_request)) #防止递归问题,多久后由事件循环reactor调用
except Exception as e:
return

@defer.inlineCallbacks
def crawl(self,spider):
#将初始Request对象添加到调度器
start_requests = iter(spider.start_requests()) #生成器转为迭代器,next取值
while True:
try:
request = next(start_requests) #每一个初始url
Q.put(request) #将请求放到队列
except StopIteration as e: #迭代器没有了会报错
break
#去队列中取任务,下载
reactor.callLater(0, self._next_request)
self._close = defer.Deferred()
yield self._close

_active = set()
engine = Engine()
spider = ChoutiSpider()
d = engine.crawl(spider)
_active.add(d)

dd = defer.DeferredList(_active)
dd.addBoth(lambda _:reactor.stop()) #lambda也是一个函数,即自定义一个函数,_是一个形式参数,a也行

reactor.run()


三、TinyScrapy

from twisted.web.client import getPage #模块功能:socket对象,自动完成移除
from twisted.internet import reactor #模块功能:事件循环(所有的socket对象都移除)
from twisted.internet import defer #模块功能:defer.Deferred,特殊的socket对象,不发请求,需手动移除
from queue import Queue

class Request(object):
"""
用于封装用户请求相关信息
"""
def __init__(self,url,callback):
self.url = url
self.callback = callback

class HttpResponse(object):
#将下载结果和request封装成一个类,以后方法解析好,类.xxx就能取到所有内容
def __init__(self,content,request):
self.content = content
self.request = request

class Scheduler(object):
"""
任务调度器,封装取出、添加队列的方法
"""
def __init__(self):
self.q = Queue()

def open(self):
pass

def next_request(self):
try:
req = self.q.get(block=False)
except Exception as e:
req = None
return req

def enqueue_request(self,req):
self.q.put(req)

def size(self):
return self.q.qsize()

class ExecutionEngine(object):
"""
引擎:所有的调度,回调函数、取出队列、添加初始队列、添加特殊socket开始工作
"""
def __init__(self):
self._close = None
self.scheduler = None
self.max = 5
self.crawlling = []

def get_response_callback(self,content,request): #下载完成,执行回调函数
self.crawlling.remove(request)
response = HttpResponse(content, request)
result = request.callback(response) #即调用了parse
import types
if isinstance(result, types.GeneratorType): #解析返回生成器的时候,再放到队列中
for req in result:
self.scheduler.enqueue_request(req)

def _next_request(self): #取任务、回调、再取任务
if self.scheduler.size() == 0 and len(self.crawlling) == 0:
self._close.callback(None)
return
while len(self.crawlling)<self.max:
req = self.scheduler.next_request()
if not req:
return
self.crawlling.append(req)
d = getPage(req.url.encode('utf-8'))
d.addCallback(self.get_response_callback,req)
d.addCallback(lambda _: reactor.callLater(0, self._next_request))

@defer.inlineCallbacks
def open_spider(self,start_requests): #添加初始队列
self.scheduler = Scheduler()
while True:
try:
req = next(start_requests)
self.scheduler.enqueue_request(req)
except StopIteration as e:
break
yield self.scheduler.open() #必须有yield,否则twisted报错,None表示没有影响,可放任意位置
reactor.callLater(0,self._next_request) #去队列中取任务,下载


@defer.inlineCallbacks
def start(self):
self._close = defer.Deferred()
yield self._close #添加特殊对象,用于手动结束,这时引擎正式开始工作

class Crawler(object):
"""
用户封装调度器以及引擎
"""
def _create_engine(self):
return ExecutionEngine() #创建引擎对象,以引用引擎的方法

def _creat_spider(self,spider_cls_path):
"""
根据spider路径创建spider对象,以导入
:param spider_cls_path:
:return:
"""
module_path,cls_name = spider_cls_path.rsplit('.',maxsplit=1) #模块路径,类名
import importlib #反射
m = importlib.import_module(module_path)
cls = getattr(m,cls_name)
return cls() #爬虫类

@defer.inlineCallbacks
def crawl(self,spider_cls_path):
engine = self._create_engine() #引擎类
spider = self._creat_spider(spider_cls_path) #爬虫类
start_requests = iter(spider.start_requests()) #初始请求的迭代器,迭代每一个Request对象
yield engine.open_spider(start_requests)
#两个yield,上面的yield已经返回值,下面的yield相当于写到open_spider方法中
yield engine.start() #引擎的start方法,创建特殊socket对象

class CrawlerProcess(object):
"""
开启事件循环,爬虫进程
"""
def __init__(self):
self._active = set()

def crawl(self,spider_cls_path):
#创建爬虫对象
crawler = Crawler()
d = crawler.crawl(spider_cls_path) #添加初始请求,取队列,回调函数等,可看作创建爬虫,正式工作
self._active.add(d) #添加到列表

def start(self):
#添加回调函数,事件循环开启,只需执行一下该方法即可启动
dd = defer.DeferredList(self._active)
dd.addBoth(lambda _:reactor.stop())
reactor.run()

class Command(object):

def run(self): #该方法创建多个爬虫,开启事件循环
crawl_process = CrawlerProcess()
spider_cls_path_list = ['spider.chouti.ChoutiSpider',]
for spider_cls_path in spider_cls_path_list:
crawl_process.crawl(spider_cls_path)
crawl_process.start()

if __name__ == '__main__':
cmd = Command()
cmd.run() #执行一下run方法

总结:- Command
- run:创建CrawlerProcess对象,for执行crawl方法创建多个爬虫,执行start方法
- CrawlerProcess
- __init__:self._active
- crawl:创建Crawler对象,执行crawl方法,添加返回值到self._active
- start:创建defer.DeferredList,监听所有爬虫,reactor.run()
- Crawler
- _create_engine:创建ExecutionEngine对象
- _creat_spider:根据spider路径return一个ChoutiSpider对象
- crawl:执行_create_engine()和_creat_spider(),迭代ChoutiSpider的start_requests方法返回值
yield引擎对象的open_spider方法,再yield引擎对象的start方法
- ExecutionEngine
- __init__:self._close特殊socket对象,scheduler队列变量,max最大并发数,crawlling正爬取请求列表
- get_response_callback:移除crawlling,创建HttpResponse对象,执行request.callback,判断返回值类型并
执行scheduler.enqueue_request方法,放入任务
- _next_request:判断是否self._close.callback(None),执行scheduler.next_request取出任务,添加到crawlling
执行getPage方法发送请求,callback(get_response_callback)和callback(_next_request)
- open_spider:创建Scheduler对象,迭代取next(start_requests)以执行scheduler.enqueue_request方法放入任务,
yield scheduler.open(),执行_next_request方法
- start:创建defer.Deferred(),yield self._close
- Scheduler
- __init__:self.q = Queue()
- open:pass,yield open()时相当于yield None
- next_request:取出任务q.get(block=False)
- enqueue_request:放入任务q.put(req)
- size:返回q.qsize(),队列的大小
- HttpResponse
- __init__:self.content和self.request,封装
- Request
- __init__:self.url和self.callback,封装













 

推荐阅读