爬虫基础

ooowl
  • python🐍
  • 爬虫🕷
  • 爬虫🕷
  • python🐍
About 16 min

📌Tip

基本知道的爬虫 好多自己看吧

。。。 #TODO 重整爬虫 爬虫的本质,是把数据从服务器请求到本地,提取解析出有用的东西,主要是http请求

网页数据的处理

请求的反爬机制和突破

爬虫效率和监控

  • 多线程多进程和异步加速效率
    • 一般是多线程因为是IO密集型任务
    • 异步生态还不是特别完善
  • 大批量作业分布式爬虫
  • 任务队列和定时
  • 集成fastapi框架 存储到DB
  • 各种存储方案,从队列取出类似pipline的处理

uinotes扒站爬虫

爬虫的基本原理

基本就是三部分

爬虫攻防

请求就是http,把网页的源码下载到本地(内存)进行访问的,经常用的有urlib(太老一般只用到urljoin),requests,scrapy(框架),twisted(异步事件驱动),aiohttp(异步) 请求一般用get(直接获取资源)post(表单验证)set等后面不常用
这里会涉及到反爬

  • 对网站感兴趣,分析网络请求,用scrapy写爬虫爬取数据
    监控发现某个时间段访问陡增,ip相同,userag-ent都是python,直接限制访问(不能封ip)
  • User-agen模拟firefox,获取ip代理
    发现ip变化,直接要求登录才能访问
  • 注册账号,每次请求带cookie或者token
    健全账号体系,即A只能访问好友的信息
  • 注册多个账号,多个账号联合爬取
    请求过于频繁,进一步加剧ip访问频率限制
  • 模仿人请求,限制请求速度
    弹出验证码,让识别验证码
  • 通过各种手段识别验证码
    增加动态网站,数据通过js动态加载,增加网络分析复杂度,发现大量请求只请求html,不请求image和CSS
  • 通过selenium和phantomjs完全模拟浏览器操作
    12成本太高,放弃了

处理

BS4,pyQuery,lxml等解析DOM和文件涉及到寻找属性定位xpath,seletor,css定位 最后产生一个自己想要的数据结构

存储

拿到这个结构的时候,存到数据库。
pymysql(推荐),aiomysql(异步),存为文件(FileIO)scrapy的pipline,推送消息等等 twisted+pymysql(手动异步)while+eventloop驱动

去重

爬虫去重策略

  1. 将访问过的url保存到数据库中
  2. 将访问过的url保存到set中,只需要O(1)的代价就可以查询url极限100000000*2byte*50个字符/1024/1024/1024=9G
  3. url经过md5等方法哈希后保存到set中
  4. 用bitmap方法,将访问过的url通过hash函数映射到某一
  5. bloomfilter方法对bitmap进行改进,多重hash函数降低冲突

留坑

布隆过滤器

请求

requests

request的基本使用
返回的是一个 一般post之前要查看一下正常的post有没有加密,要不要重写,

留坑

js断点调试fiddler抓包

get()&post()

  1. get(url,参数),写图片 返回的是一个response对象,包含状态码请求成功与否等等
    data={
        "name":"zhizhi",
        "age":20
    }
    files={"files":open("baidu.png","rb")}
    res=requests.get("http://www.httpbin.org/get",params=data)#构造一个参数并传进去
    print(res.text)#直接返回字符串
    print(res.json())#直接解析json
    print(res.contnet)#返回网页原始内容
    print(res0.status_code)#请求的状态码
    res0.encoding="utf-8"#把网页的编码转换
    res1=requests.get("https://www.baidu.com/img/bd_logo1.png")
    len(res1.content)#图片的二进制
    with open("baidu.png","wb") as f:#写二进制图片
        f.write(res1.content)
  1. 使用header
headers={"Referer": "https://www.zhihu.com/signup?next=%2F","User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36"}
res3=requests.get("https://www.zhihu.com",headers=headers)#添加headers,像某呼这种不加header就不给访问
print(res3.text)

  1. 传入文件并使用cookie
    data被传入了cookie中可以访问了
res4 = requests.post("http://www.httpbin.org/post", data=data,headers=headers, files=files)
 # 构造一个form并传进去
pprint(res4.text)
print(res4.cookies)
for k,v in res4.cookies:
    print(k,":",v)
  1. session维持会话,一般session会很久
s=requests.session()#维持会话
s.get("http://www.httpbin.org/cookies/set/abc/svsvdhasb",verify=False)
# 不进行证书验证,可使用cert=("path/server.crt","path/ket")进行证书验证
res5=s.get("http://www.httpbin.org/cookies")
print(res5.text)
  1. 使用ip代理 常用的代理有西刺也可以自己买
proxies={"https":"127.0.0.1:9900"}#使用代理
res6=requests.get("http://www.httpbin.org/get",proxies=proxies)
print(res6.content)

selenium

selenium浏览器驱动。
当某些网页动态加载js的时候(淘宝)我们需要程序去执行js,(效率自然会低一些)这就是selenium的作用 phantomjs(无界面浏览器)也行但是在多进程的情况下性能下降严重。
使用之前先下载一个driver淘宝镜像

from selenium import webdriver
broswer.get('http://www.baidu.com')#打开
print(broswer.page_source) # 拿到F12的代码(经过渲染的)
broswer.maximize_window()#最大化窗口
broswer.set_window_size(600,800)#设置窗口大小
broswer.back()#后退一步
broswer.forward()#前进一步
broswer.refresh()#刷新
print(broswer.title)#拿到浏览器的标题
print(broswer.current_url)#拿到url
broswer.get_cookies()#拿到cookie
broswer.delete_all_cookies()#删除所有的cookie
broswer.add_cookie({k-v})#增加cookie

可用的驱动

webdriver.Firefox
webdriver.FirefoxProfile
webdriver.Chrome
webdriver.ChromeOptions
webdriver.Ie
webdriver.Opera
webdriver.PhantomJS
webdriver.Remote
webdriver.DesiredCapabilities
webdriver.ActionChains
webdriver.TouchActions

查找元素

不建议使用自带的,因为会拖慢速度,性能会变差。
可以用其他的包lxml bs等代替,不过有时候不能代替

print(broswer.find_element_by_id(id_="lg"))
  1. id定位:find_element_by_id()
  2. name定位:find_element_by_name()
  3. class定位:find_element_by_class()
  4. tag定位:find_element_by_tag_name()
  5. link定位:find_element_by_link_text()
  6. partial link 定位: find_element_by_partial_link_text()
  7. Xpath定位:
    • 绝对路径:find_element_by_xpath("绝对路径")
    • 元素属性:find_element_by_xpath("//unput[@id='kw']")
    • 层级与属性结合:find_element_by_xpath("//form[@id='loginForm']/ul/input[1]")
    • 逻辑运算符:find_element_by_xpath("//input[@id='kw' and@class='s_ipt']")
  8. CSS定位:find_element_by_css_selector()

通过selenium模拟登陆微博

失败了 留坑

后面再试,填值的时候填不上

往元素中填值并登陆

ele0=broswer.find_element_by_id(id_="lg")
ele=broswer.find_element_by_id(id_="kw")
ele.send_keys(u"qwe")#模拟按键输入:
ele.clear()#清除文本:
ele.send_keys(u"abc")#模拟按键输入:
ele1=broswer.find_element_by_class_name("bg s_btn")
ele1.click()#单击元素
ele1.submit()#提交表单(相当于"回车")
broswer.execute_script('alert("123");')#执行js

加载的时候不加载图片(节省性能),如下设置

from selenium import webdriver

chrome_options = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}
chrome_options.add_experimental_option("prefs",prefs)

driver = webdriver.Chrome(chrome_options=chrome_options)

aiohttp

异步请求的http库

留坑

异步等过一两天再整理

处理

数据处理就是把DOM的元素内容拿出来

BeautifulSoup

对dom进行解析
from bs4 import BeautifulSoup as BS
dom例子

      <li id="note-36396362" data-note-id="36396362" class="have-img">
          <a class="wrap-img" href="www.baidu.com" target="_blank">
            <img class="  img-blur-done" src="//upload-images.jianshu.io/upload_images/12240181-bb9399878eae6ddd?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240" alt="120">
          </a>
        <div class="content">
          <a class="title" target="_blank" href="www.baidu.com">罗永浩最爱的“软件”下凡了! 让你体验高效记录的感觉</a>
          <p class="abstract">
            听说锤子又发布了新产品! 锤子科技不是一个卖手机的好公司,却是一个搞软件的领头军,旗下的闪念胶囊和BigBang一直被赞扬,网友戏称: 锤子有三...
          </p>
          <div class="meta">
            <a class="nickname" target="_blank" href="/u/00385c37e5de">Tools指南</a>
              <a target="_blank" href="www.baidu.com#comments">
                <i class="iconfont ic-list-comments"></i> 3
      </a>      <span><i class="iconfont ic-list-like"></i> 46</span>
          </div>
        </div>
      </li>
from bs4 import BeautifulSoup as BS
soup=BS(dom,'lxml')#生成了一个soup对象使用unicode编码

注意指定编码

默认lxml
BeautifulSoup(markup, "lxml")速度快
BeautifulSoup(markup, "html.parser")系统自带
BeautifulSoup(markup, "xml")唯一支持xml
BeautifulSoup(markup, "html5lib")生成h5浏览器方式解析

tag.name标签名称可嵌套soup.head.tittle
tag.attributes标签后面的属性,可添加或者修改,和字典一样
多值属性,class最多,每个tag可能用多个css
其他的rel , rev , accept-charset , headers , accesskey返回一个list非多值属性会返回str
将tag转换为str的时候多值属性会自动合为由空格隔开的字符串,xml没有多值

print(soup.name,soup.attrs)#获取名字和属性
print(soup.find(class_="meta"),#获取了class为meta的第一个元素
soup.find_all_next(class_="meta"),#获这也元素的下一个邻居
soup.find_all(class_="meta"))#列表获取所有meta的元素
tag.parent#获得父节点beautifulsoup父节点为None
tag.descendants#子孙节点的递归迭代对象
tag.chlidren#是包含子节点对象对象对象的可迭代对象
方法参数返回
find(self, name, attrs, recursive, text,.… Tag1
find_all(self, name, attrs, recursive, te.. Tag2
find_all_previous(self, name, att.. PageElement3
find_next(self, name, attrs, text.. PageElement4
find_next_sibling(self, name, att.. PageElement5
find_next_siblings(self, name, at.. PageElement6
find_parent(self, name, attrs, kw.. PageElement7
find_parents(self, name, attrs,1.. PageElement8
find_previous(self, name, attrs,.. PageElement9
find_previous_sibling(self, name,.… PageElement10
find previous siblings(self, name.. PageElement11

这是节点的一些属性,可以获取内容

Note

注意顿号以及换行也算节点
class_用来标识css可以部分匹配也可全匹配,全匹配的时候注意顺序若不一样无法匹配

print(soup.find(class_="meta"),#获取了class为meta的第一个元素
soup.find_all_next(class_="meta"),#获这也元素的下一个邻居
soup.find_all(class_="meta"),)
soup1=soup.find(name='p',class_="abstract")#获取标签为p,class="abstract"的元素
print(soup.string)#没有子节点的情况下得到字符串
print(soup1.strings)#得到多个字符串 列表
soup1=soup.find(name="div",class_="content")
print(soup1.contents)#节点的元素,是个列表
soup.get_text()#返回所有的及其孩子的文本
soup.prettify()#自动对齐补全html

pyQuery

pip安装pyQuery

打开

pq=PyQuery("http://www.baidu.comopen in new window")#也可以直接打开文件pq=PyQuery("aa.html")

类型形式含义
.class.color选择class="color”的所有元素
#id#info选择id="info”的所有元素
**选择所有元素
elementp选择所有的p元素
element,elementdiv,pe选择所有div元素和所有p元素
elemente lementdiv pe选择所有div元素内所有p元素
[attribute][target]选择带有targe属性的所有元素。
[arrtibute=value][target=_blank]选择 target="_blank”的所有元素

可以使用伪类
可调用的函数

  • pq.find()#发现标签
  • pq.parent()#发现标签的父标签
  • pq.parents()#递归发现父标签
  • pq.children()#发现子标签
  • pq.text()#获取所有文本,忽略标签

Note

pq本身是具有标签的字符串 pq.html()不含本身的标签

程序如下

使用上面的dom
x=pq('li a')
pq.find('a')
pq.parent('a')
pq.parents('a')
pq.children('a')
print(x.attr("target","_blank"))
print(x.attr.target)
print(x.text())

结果

<a class="wrap-img" href="www.baidu.com" target="_blank">
      <img class="  img-blur-done" src="//upload-images.jianshu.io/upload_images/12240181-bb9399878eae6ddd?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240" alt="120"/>
    </a>
  <a class="title" target="_blank" href="www.baidu.com">罗永浩最爱的“软件”下凡了! 让你体验高效记录的感觉</a>
    <a class="nickname" target="_blank" href="/u/00385c37e5de">Tools指南</a>
        <a target="_blank" href="www.baidu.com#comments">
          <i class="iconfont ic-list-comments"/> 3
</a>
_blank
 罗永浩最爱的“软件”下凡了! 让你体验高效记录的感觉 Tools指南 3

Xpath&CssSelector

利用这些语法定位dom的元素 F12开发者模式,直接复制注意js渲染会扰乱xpath和css

Xpath

类型形式
article选取所有article元素的所有子节点
/article选取根元素article
article/a选取所有属于article的子元素的a元素
//div选取所有div子元素(不论出现在文档任何地方)
article//div选取所有属于article元素的后代的div元素,不管它出现在article之下的任何位置
//@class选区这个标签下所有名为class的属性
/a[@href="link2.html"]选取所有href为"link2.html"的a标签
@href拿到href标签
?/text()?元素下的文本

css选择器

类型形式
*选择所有节点
#container选择id为container的节点
.container选取所有class包含container的节点
li a选取所有i下的所有a节点
ul+p选择l后面的第一个p元素
div#container>ul选取id为container的div的第一个ul子元素
ul~p选取与ul相邻的所有p元素
a[title]选取所有有title属性的a元素
a[href="http:/jobbole.com"]选取所有href属性为jobbole.com值的a元素
a[href*="jobole"]选取所有href属性包含jobbole的a元素
a[href^="http"]选取所有href属性值以http开头的a元素
a[href$=".jpg”]选取所有href属性值以.jpg结尾的a元素
input[type=radio]:checked选择选中的radio的元素
div:not(#container)选取所有id非container的div属性
li:nth-child(3)选取第三个i元素
tr:nth-child(2n)第偶数个tr
a::text拿到标签的文本
a::attr(href)拿到标签href属性
from lxml import etree
html=etree.HTML(dom)#初始化,也可初始化本地网页etree.parse('test.html')
result = etree.tostring(html)#放入lxml中
result.decode("utf-8")#对这个html进行编码

x=html.xpath('//div[@class="content"]/text()')
print(x)
y=html.cssselect('#note-36396362 > div >.title')#拿到所有class为title的元素
for i in y:#是个列表
    print(i.text,i.attrib['href'])

注意

空格和换行符也算一个文本,看结果

结果

['\n  听说锤子又发布了新产品! 锤子科技不是一个卖手机的好公司,
却是一个搞软件的领头军,旗下的闪念胶囊和BigBang一直被赞扬,网友戏称:
锤子有三...\n    ', '\n    ', '\n    ', '\n  ']
罗永浩最爱的“软件”下凡了! 让你体验高效记录的感觉 
www.baidu.com

存储

pymysql

  • 创建连接 参数如下 800
  • 创建游标
    可选择字典形式
  • 构造sql语句
    善用str.format
  • 执行sql
    注意错误回滚
  • 拿回结果
    注意指针的位置
import pymysql

conn=pymysql.connect(host='127.0.0.1',
                    user='root',
                    password='不给看!!!',
                    database='aiosql',
                    charset='utf8mb4')
cur=conn.cursor(cursor=pymysql.cursors.DictCursor)
#使用DictCursor返回不再是元组是一个代映射的字典
sql='select * from aio'
try:
    reslen=cur.execute(sql)#执行sql语句
    conn.commit()#注意要提交
except:#发生错误
    conn.rollback()#回滚
    cur.close()
    conn.close()#关闭
#行为单位移动
cur.description#光标详情描述
print(cur.lastrowid)#光标
cur.scroll(2,mode='absolute')#指针指向第二条
cur.scroll(1,mode='relative')#指针相对向后移动两条
one=cur.fetchone()#结果的一条光标后移1
all=cur.fetchmany(3)#拿出三条后移3
many=cur.fetchall()#拿出三条,移到底
print(one,all,many)

有坑

插入数据库要保证健壮性,特殊字符提前处理比如去掉单引号,反斜杠,编码字符等
四字节字符入库时\xF0\x9F\x99\x8F像这样
首先修改 my.ini 编码集为utf8mb4 数据库和表改为utf8mb4
还不行的话删库重新新建选择字符集utf8mb4
连接数据库的时候改为utf8mb4重启mysqld服务

PyMongo

pip install pymongo
mongo的操作看这里

连接

简写(默认连接本地)
client=MongoClient()
指定端囗和地址
client=MongoClient(localhost,27017)
使用URI
client=MongoClient(mongodb://localhost:27017/")
client.PORT,client.HOST,client.list_database_names()
db=client.mydb # 使用mydb,没有就自动创建
myset=db.my_set

关闭

关闭数据库的时候调用client.close()之后仍然能使用属性,比如在打印一次也能打印出来,yinweimongo默认有个连接池,只要没满随时都能连

增删改查

  • 插入
    my_set.insert([{"name":"zhangsan","age":18},{"name":"lilin","age":18}])
    my_set.save({"name":"zhangsan","age":18})
    save的时候他会遍历,但是insert不会,所以insert快一些
  • 查询这个集合所有的数据(需要用for遍历) my_set.find()
  • 查找名字为zhangsan的数据(需要用for遍历)
    my_set.find({"name":"zhangsan"})
  • 取出这条记录的'id'依此类推可以往后取
    id=my_set.find({"name":"zhangsan"})
    ['id']

  • 移除这个记录的id
    my_set.remove(id)
  • 把name为zhangsan的age改为20
    my_set.update({"name":"zhangsan"},{'$set':{"age":20}})
  • 移除张三这个数据的全部记录
    my_set.remove({'name': 'zhangsan'})
  • 删掉这个库
    db.remove()

操作和查询符

  1. 查询符
    • > 大于 - $gt
    • < 小于 - $lt
    • >= 大于等于 - $gte
    • <= 小于等于 - $lte
  • age大于25的记录 my_set.find({"age":{"$gt":25}})
  1. 类型判断
    发现字符串类型的name
    my_set.find({'name':{'$type':2}})
类型对照表

类型
Double类型代码 1
String类型代码 2
Object类型代码 3
Array类型代码 4
Binary data类型代码 5
Boolean类型代码 8
Date类型代码 9
Null类型代码 10
Regular Expression类型代码 11
JavaScript类型代码 13
Symbol类型代码 14
JavaScript (with scope)类型代码 15
32-bit integer类型代码 16
Timestamp类型代码 17
64-bit integer类型代码 18
Min key类型代码 255
Max key类型代码 127
  1. 条件查询
- 排序<br/>
age升序查询<br>
<strong> *my_set.find().sort([("age",1)])* </strong><br>
- 读取
跳过两条之后读六条<br/>
<strong> *my_set.find().skip(2).limit(6)* </strong><br>

- in<br/>
发现年龄(20,30,53)中的数据<br/>


<strong> *my_set.find({"age":{"$in":(20,30,35)}})* </strong><br>
- or<br/>

发现age是20或35的数据
<br/>


<strong> *my_set.find({"$or":[{"age":20},{"age":35}]})* </strong><br>
- all<br/>
查询序列的包含<br/>

dic = {"name":"xiahong","age":18,"li":[1,2,3]}<br/>
dic2 = {"name":"xiaoming","age":18,"li":[1,2,3,4,5,6]}<br/>

<strong> *my_set.find({'li':{'$all':[1,2,3,4]}})* </strong><br>
结果只会出现dic2
- push[ALL]
更改序列<br/>
单个值: my_set.update({'name':"xiaohong"}, {'$push':{'li':[4]}})<br/>
多个追加: my_set.update({'name':"xiaohong"}, {'$pushAll':{'li':[4,5]}})<br/>
dic = {"name":"xiahong","age":18,"li":[1,2,3,4,4,5]}
- pop/pullAll
单个值: my_set.update({'name':"xiaohong"}, {'$pop':{'li':[4]}})<br/>
去除多个: my_set.update({'name':"xiaohong"}, {'$pullAll':{'li':[4,5]}})<br/>
dic = {"name":"xiahong","age":18,"li":[1,2,3]}
- 多级目录操作<br/>
取的时候用.连接,结果用['字典调用']方式连接<br/>
查找a属性的b属性的c属性值为5的数据<br/>
<strong> *my_set.find(a.b.c:5)* </strong><br>
拿到结果的a属性的b属性的c属性值<br/>
<strong> *res['a']['b']['c']* </strong>

:::tip 提示 update,$set等也是这么寻找 ::: ### redis

pip install redis


### aiomysql 依赖
- Python3.5+ - asyncio - PyMySQL
eventloop+while True模式进行插入异步进行
import asyncio
from aiomysql import create_pool

import aiohttp

loop=asyncio.get_event_loop()

async def go():
    async with create_pool(host='127.0.0.1',port=3306,user='root',password='1118',db='aiosql',charset='utf8mb4',loop=loop) as pool:
        async with pool.acquire() as conn:#获得pool
            async with conn.cursor() as cur:#创建游标
                for i in range(100):#异步运行了一百次这个sql语句
                    await cur.execute('select * from aio limit 1;')
                    value= await cur.fetchone()
                    print(value)

loop.run_until_complete(go())

通过连接池来连接

async def test(loop):
    pool=await create_pool(host='127.0.0.1',port=3306,user='root',password='1118',db='aiosql',charset='utf8mb4',loop=loop)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            for i in range(1000000):
                await cur.execute('select * from aio limit 1;')
                print(cur.description)
                (r,)=await cur.fetchone()
                print(r)
    pool.close()
    await pool.wait_closed()

loop=asyncio.get_event_loop()
loop.run_until_complete(test(loop))

Scrapy

这里或者下一篇

rad爬虫 you-get

我看看能不能执

Loading...