一、爬虫的概念
- 爬虫是模拟浏览器发送请求,获取响应
二、爬虫的流程
- url--->发送请求,获取响应--->提取数据---》保存
- 发送请求,获取响应--->提取url
- 爬虫要根据当前url地址对应的响应为准 ,当前url地址的elements的内容和url的响应不一样
三、Python 爬虫的入门
Python 爬虫是一种常用的技术,用于从网站上自动获取信息。下面是一个简单的 Python 爬虫入门教程:
1. 环境准备
首先,确保你已经安装了 Python。然后,安装以下库:
- requests:用于发送 HTTP 请求
- BeautifulSoup:用于解析 HTML 文档
- lxml:用于解析 HTML 和 XML 文档(可选,但推荐使用)
安装方法:
bash
pip install requests beautifulsoup4 lxml
2. 爬虫基础知识
在开始编写代码之前,了解一些基础知识:
- 网页结构:HTML、CSS 和 JavaScript
- HTTP 请求:GET、POST 等
- 响应状态码:200、404、500 等
- 反爬虫:一些网站会通过各种方式阻止爬虫,如 IP 封禁、验证码、动态加载等
3. 编写爬虫代码
以一个简单的例子为例,爬取百度搜索结果。
python
import requests
from bs4 import BeautifulSoup
def search_baidu(keyword):
url = f'https://www.baidu.com/s?wd={keyword}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'lxml')
results = soup.select('.result.c-container')
for result in results:
title = result.select_one('.t a')
if title:
print(title.text)
else:
print('无标题')
else:
print('请求失败')
if __name__ == '__main__':
keyword = input('请输入搜索关键词:')
search_baidu(keyword)
这个例子中,我们使用 requests 库发送 GET 请求,获取百度搜索结果页面。然后,使用 BeautifulSoup 库解析 HTML 文档,提取搜索结果。
4. 处理异常和限制
在实际爬虫中,可能会遇到各种异常和限制。例如,网络请求失败、反爬虫策略等。因此,我们需要编写代码处理这些情况。
python
import time
from random import randint
def search_baidu(keyword):
url = f'https://www.baidu.com/s?wd={keyword}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f'请求失败:{e}')
return
soup = BeautifulSoup(response.text, 'lxml')
results = soup.select('.result.c-container')
for result in results:
title = result.select_one('.t a')
if title:
print(title.text)
else:
print('无标题')
# 休眠一段时间,避免频繁请求
time.sleep(randint(1, 5))
if __name__ == '__main__':
keyword = input('请输入搜索关键词:')
search_baidu(keyword)
在这个例子中,我们添加了 try-except 语句,捕获网络请求失败等异常。同时,我们使用 time.sleep() 函数让爬虫在每次请求后休眠一段时间,避免频繁请求。
四、Python 爬虫进阶
判断请求否是成功
python
assert response.status_code==200
url编码
https://www.abc123.com/s?wd=%E4%BC%A0%E6%99%BA%E6%92%AD%E5%AE%A2
字符串格式化的另一种方式
python
"爬{}虫教程".format(1)
使用代理ip
-
准备一堆的ip地址,组成ip池,随机选择一个ip来时用
-
如何随机选择代理ip,让使用次数较少的ip地址有更大的可能性被用到
- {"ip":ip,"times":0}
- [{},{},{},{},{}],对这个ip的列表进行排序,按照使用次数进行排序
- 选择使用次数较少的10个ip,从中随机选择一个
-
检查ip的可用性
- 可以使用requests添加超时参数,判断ip地址的质量
- 在线代理ip质量检测的网站
携带cookie请求
- 携带一堆cookie进行请求,把cookie组成cookie池
使用requests提供的session类来请求登陆之后的网站的思路
- 实例化session
- 先使用session发送请求,登录对网站,把cookie保存在session中
- 再使用session请求登陆之后才能访问的网站,session能够自动的携带登录成功时保存在其中的cookie,进行请求
不发送post请求,使用cookie获取登录后的页面
- cookie过期时间很长的网站
- 在cookie过期之前能够拿到所有的数据,比较麻烦
- 配合其他程序一起使用,其他程序专门获取cookie,当前程序专门请求页面
字典推导式,列表推到是
python
cookies="anonymid=j3jxk555-nrn0wh; _r01_=1; _ga=GA1.2.1274811859.1497951251; _de=BF09EE3A28DED52E6B65F6A4705D973F1383380866D39FF5; ln_uact=mr_mao_hacker@163.com; depovince=BJ; jebecookies=54f5d0fd-9299-4bb4-801c-eefa4fd3012b|||||; JSESSIONID=abcI6TfWH4N4t_aWJnvdw; ick_login=4be198ce-1f9c-4eab-971d-48abfda70a50; p=0cbee3304bce1ede82a56e901916d0949; first_login_flag=1; ln_hurl=http://hdn.xnimg.cn/photos/hdn421/20171230/1635/main_JQzq_ae7b0000a8791986.jpg; t=79bdd322e760beae79c0b511b8c92a6b9; societyguester=79bdd322e760beae79c0b511b8c92a6b9; id=327550029; xnsid=2ac9a5d8; loginfrom=syshome; ch_id=10016; wp_fold=0"
cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split("; ")}
python
[self.url_temp.format(i * 50) for i in range(1000)]
获取登录后的页面的三种方式
- 实例化session,使用session发送post请求,在使用他获取登陆后的页面
- headers中添加cookie键,值为cookie字符串
- 在请求方法中添加cookies参数,接收字典形式的cookie。字典形式的cookie中的键是cookie的name对应的值,值是cookie的value对应的值
寻找登录的post地址
-
在form表单中寻找action对应的url地址
- post的数据是input标签中name的值作为键,真正的用户名密码作为值的字典,post的url地址就是action对应的url地址
-
抓包,寻找登录的url地址
- 勾选perserve log按钮,防止页面跳转找不到url
- 寻找post数据,确定参数
- 参数不会变,直接用,比如密码不是动态加密的时候
- 参数会变
- 参数在当前的响应中
- 通过js生成
定位想要的js
- 选择会触发js时间的按钮,点击event listener,找到js的位置
- 通过chrome中的search all file来搜索url中关键字
- 添加断点的方式来查看js的操作,通过python来进行同样的操作
安装第三方模块
- pip install retrying
- 下载源码解码,进入解压后的目录,
python setup.py install
***.whl
安装方法pip install ***.whl
json使用注意点
- json中的字符串都是双引号引起来的
- 如果不是双引号
- eval:能实现简单的字符串和python类型的转化
- replace:把单引号替换为双引号
- 如果不是双引号
- 往一个文件中写入多个json串,不再是一个json串,不能直接读取
- 一行写一个json串,按照行来读取
正则表达式使用的注意点
-
re.findall("a(.*?)b","str")
,能够返回括号中的内容,括号前后的内容起到定位和过滤的效果 -
原始字符串r,待匹配字符串中有反斜杠的时候,使用r能够忽视反斜杠带来的转义的效果
-
点号默认情况匹配不到
\n
-
\s
能够匹配空白字符,不仅仅包含空格,还有\t|\r\n
xpath学习重点
-
使用xpath helper或者是chrome中的copy xpath都是从element中提取的数据,但是爬虫获取的是url对应的响应,往往和elements不一样
-
获取文本
a/text()
获取a下的文本a//text()
获取a下的所有标签的文本//a[text()='下一页']
选择文本为下一页三个字的a标签
-
@符号
a/@href
//ul[@id="detail-list"]
-
//
- 在xpath最前面表示从当前html中任意位置开始选择
li//a
表示的是li下任何一个标签
lxml使用注意点
-
lxml能够修正HTML代码,但是可能会改错了
- 使用etree.tostring观察修改之后的html的样子,根据修改之后的html字符串写xpath
-
lxml 能够接受bytes和str的字符串
-
提取页面数据的思路
- 先分组,渠道一个包含分组标签的列表
- 遍历,取其中每一组进行数据的提取,不会造成数据的对应错乱
xpath的包含
//div[contains(@class,'i')]
实现爬虫的套路
-
准备url
- 准备start_url
- url地址规律不明显,总数不确定
- 通过代码提取下一页的url
- xpath
- 寻找url地址,部分参数在当前的响应中(比如,当前页码数和总的页码数在当前的响应中)
- 准备url_list
- 页码总数明确
- url地址规律明显
- 准备start_url
-
发送请求,获取响应
- 添加随机的User-Agent,反反爬虫
- 添加随机的代理ip,反反爬虫
- 在对方判断出我们是爬虫之后,应该添加更多的headers字段,包括cookie
- cookie的处理可以使用session来解决
- 准备一堆能用的cookie,组成cookie池
- 如果不登录
- 准备刚开始能够成功请求对方网站的cookie,即接收对方网站设置在response的cookie
- 下一次请求的时候,使用之前的列表中的cookie来请求
- 如果登录
- 准备多个账号
- 使用程序获取每个账号的cookie
- 之后请求登录之后才能访问的网站随机的选择cookie
- 如果不登录
-
提取数据
- 确定数据的位置
-
如果数据在当前的url地址中
- 提取的是列表页的数据
- 直接请求列表页的url地址,不用进入详情页
- 提取的是详情页的数据
*- 确定url
-
- 发送请求
-
- 提取数据
-
- 返回
- 提取的是列表页的数据
-
如果数据不在当前的url地址中
- 在其他的响应中,寻找数据的位置
*- 从network中从上往下找
-
- 使用chrome中的过滤条件,选择出了js,css,img之外的按钮
-
- 使用chrome的search all file,搜索数字和英文
- 在其他的响应中,寻找数据的位置
-
- 数据的提取
- xpath,从html中提取整块的数据,先分组,之后每一组再提取
- re,提取max_time,price,html中的json字符串
- json
- 确定数据的位置
-
保存
- 保存在本地,text,json,csv
- 保存在数据库
验证码的识别
-
url不变,验证码不变
- 请求验证码的地址,获得相应,识别
-
url不变,验证码会变
-
思路:对方服务器返回验证码的时候,会和每个用户的信息和验证码进行一个对应,之后,在用户发送post请求的时候,会对比post请求中法的验证码和当前用户真正的存储在服务器端的验证码是否相同
-
1.实例化session
-
2.使用seesion请求登录页面,获取验证码的地址
-
3.使用session请求验证码,识别
-
4.使用session发送post请求'
-
-
使用selenium登录,遇到验证码
- url不变,验证码不变,同上
- url不变,验证码会变
- 1.selenium请求登录页面,同时拿到验证码的地址
- 2.获取登录页面中driver中的cookie,交给requests模块发送验证码的请求,识别
- 3.输入验证码,点击登录
selenium使用的注意点
-
获取文本和获取属性
- 先定位到元素,然后调用
.text
或者get_attribute
方法来去
- 先定位到元素,然后调用
-
selenium获取的页面数据是浏览器中elements的内容
-
find_element和find_elements的区别
- find_element返回一个element,如果没有会报错
- find_elements返回一个列表,没有就是空列表
- 在判断是否有下一页的时候,使用find_elements来根据结果的列表长度来判断
-
如果页面中含有iframe、frame,需要先调用driver.switch_to.frame的方法切换到frame中才能定位元素
-
selenium请求第一页的时候回等待页面加载完了之后在获取数据,但是在点击翻页之后,hi直接获取数据,此时可能会报错,因为数据还没有加载出来,需要time.sleep(3)
-
selenium中find_element_by_class_name智能接收一个class对应的一个值,不能传入多个
python
db.stu.aggregate({$group:{_id:"$name",counter:{$sum:2}}})
db.stu.aggregate({$group:{_id:null,counter:{$sum:1}}})
db.stu.aggregate({$group:{_id:"$gender",name:{$push:"$name"}}})
db.stu.aggregate({$group:{_id:"$gender",name:{$push:"$$ROOT"}}})
db.tv3.aggregate(
{$group:{_id:{"country":"$country",province:"$province",userid:"$userid"}}},
{$group:{_id:{country:"$_id.country",province:"$_id.province"},count:{$sum:1}}},
{$project:{country:"$_id.country",province:"$_id.province",count:"$count",_id:0}}
)
db.stu.aggregate(
{$match:{age:{$gt:20}}},
{$group:{_id:"$gender",count:{$sum:1}}}
)
db.t2.aggregate(
{$unwind:"$size"}
)
db.t3.aggregate(
{$unwind:"$tags"},
{$group:{_id:null,count:{$sum:1}}}
)
db.t3.aggregate(
{$unwind:{path:"$size",preserveNullAndEmptyArrays:true}}
)
五、MongoDB数据库操作
mongodb插入数据
- db.collecion.insert({}) 插入数据,
_id
存在就报错 - db.collection.save({}) 插入数据,
_id
存在会更新
mongodb的更新操作
db.test1000.update({name:"xiaowang"},{name:"xiaozhao"})
- 把name为xiaowang的数据替换为
{name:"xiaozhao"}
db.test1000.update({name:"xiaohong"},{$set:{name:"xiaozhang"}})
- 把name为xiaowang的数据name的值更新为xiaozhang
db.test1000.update({name:"xiaozhang"},{$set:{name:"xiaohong"}},{multi:true})
{multi:true}
达到更新多条的目的
mongodb删除
db.test1000.remove({name:"xiaohong"},{justOne:true})
- 默认情况会删除所有满足条件的数据,
{justOne:true}
能达到只删除一条的效果
mongodb的count方法
db.collection.find({条件}).count()
db.collection.count({})
mongodb的投影
- 投影:选择返回结果的字段
db.collection.find({条件},{name:1,_id:0})
- 1.
_id
默认会显示,置为0不显示 - 2.出了
_id
之外的其他字段,如果不显示,不写,不能写为0
- 1.
$group的注意点
$group
对应的字典中有几个键,结果中就有几个键- 分组依据需要放到
_id
后面 - 取不同的字段的值需要使用 , ' ,` ,'gender
,
$age` - 取字典嵌套的字典中的值的时候
$_id.country
- 能够同时按照多个键进行分组
{$group:{_id:{country:"$country",province:"$province"}}}
- 结果是:
{_id:{country:"",province:""}
- 结果是:
编辑器写mongodb语句
sql
db.stu.find(
{$or:[{age:{$gte:20}},{hometown:{$in:["桃花岛","华⼭"]}}]}
)
#按照gender进行分组,获取不同组数据的个数和平均年龄
db.stu.aggregate(
{$group:{_id:"$gender",count:{$sum:1},avg_age:{$avg:"$age"}}},
{$project:{gender:"$_id",count:1,avg_age:"$avg_age",_id:0}}
)
# 按照hometown进行分组,获取不同组的平均年龄
db.stu.aggregate(
{$group:{_id:"$hometown",mean_age:{$avg:"$age"}}}
)
#使用$group统计整个文档
db.stu.aggregate(
{$group:{_id:null,count:{$sum:1},mean_age:{$avg:"$age"}}}
)
#选择年龄大于20的学生,观察男性和女性有多少人
db.stu.aggregate(
{$match:{$or:[{age:{$gt:20}},{hometown:{$in:["蒙古","⼤理"]}}]}},
{$group:{_id:"$gender",count:{$sum:1}}},
{$project:{_id:0,gender:"$_id",count:1}}
)
#page37页练习
db.tv3.aggregate(
{$group:{_id:{country:"$country",province:"$province",userid:"$userid"}}},
{$group:{_id:{country:"$_id.country",province:"$_id.province"},count:{$sum:1}}},
{$project:{country:"$_id.country",province:"$_id.province",count:1,_id:0}}
)
mongodb mysql redis的区别和使用场景
- mysql是关系型数据库,支持事物
- mongodb,redis非关系型数据库,不支持事物
- mysql,mongodb,redis的使用根据如何方便进行选择
- 希望速度快的时候,选择mongodb或者是redis
- 数据量过大的时候,选择频繁使用的数据存入redis,其他的存入mongodb
- mongodb不用提前建表建数据库,使用方便,字段数量不确定的时候使用mongodb
- 后续需要用到数据之间的关系,此时考虑mysql
爬虫数据去重,实现增量式爬虫
-
使用数据库建立关键字段(一个或者多个)建立索引进行去重
-
根据url地址进行去重
- 使用场景:
- url地址对应的数据不会变的情况,url地址能够唯一判别一个条数据的情况
- 思路
- url存在redis中
- 拿到url地址,判断url在redis的url的集合中是够存在
- 存在:说明url已经被请求过,不再请求
- 不存在:url地址没有被请求过,请求,把该url存入redis的集合中
- 布隆过滤器
- 使用多个加密算法加密url地址,得到多个值
- 往对应值的位置把结果设置为1
- 新来一个url地址,一样通过加密算法生成多个值
- 如果对应位置的值全为1,说明这个url地址已经抓过
- 否则没有抓过,就把对应位置的值设置为1
- 使用场景:
-
根据数据本省进行去重
- 选择特定的字段,使用加密算法(md5,sha1)讲字段进行假面,生成字符串,存入redis的集合中
- 后续新来一条数据,同样的方法进行加密,如果得到的字符串在redis中存在,说明数据存在,对数据进行更新,否则说明数据不存在,直接插入
练习
python
db.tv1.aggregate(
{$project:{title:1,_id:0,count:"$rating.count",rate:"$rating.value",country:"$tv_category"}},
{$match:{rate:{$gt:8}}},
{$group:{_id:"$country",count:{$sum:1}}},
{$project:{_id:0,country:"$_id",count:1}}
)
logging 模块的使用
-
scrapy
- settings中设置LOG_LEVEL="WARNING"
- settings中设置LOG_FILE="./a.log" #设置日志保存的位置,设置会后终端不会显示日志内容
- import logging,实例化logger的方式在任何文件中使用logger输出内容
-
普通项目中
- import logging
- logging.basicConfig(...) #设置日志输出的样式,格式
- 实例化一个
logger=logging.getLogger(__name__)
- 在任何py文件中调用logger即可
crawlspider的使用
-
常见爬虫 scrapy genspider -t crawl 爬虫名 allow_domain
-
指定start_url,对应的响应会进过rules提取url地址
-
完善rules,添加Rule
Rule(LinkExtractor(allow=r'/web/site0/tab5240/info\d+\.htm'), callback='parse_item'),
-
注意点:
- url地址不完整,crawlspider会自动补充完整之后在请求
- parse函数不能定义,他有特殊的功能需要实现
- callback:连接提取器提取出来的url地址对应的响应交给他处理
- follow:连接提取器提取出来的url地址对应的响应是否继续被rules来过滤
request对象什么时候入队
- dont_filter = True ,构造请求的时候,把dont_filter置为True,该url会被反复抓取(url地址对应的内容会更新的情况)
- 一个全新的url地址被抓到的时候,构造request请求
- url地址在start_urls中的时候,会入队,不管之前是否请求过
- 构造start_url地址的请求时候,dont_filter = True
python
def enqueue_request(self, request):
if not request.dont_filter and self.df.request_seen(request):
# dont_filter=False Ture True request指纹已经存在 #不会入队
# dont_filter=False Ture False request指纹已经存在 全新的url #会入队
# dont_filter=Ture False #会入队
self.df.log(request, self.spider)
return False
self.queue.push(request) #入队
return True
scrapy_redis去重方法
- 使用sha1加密request得到指纹
- 把指纹存在redis的集合中
- 下一次新来一个request,同样的方式生成指纹,判断指纹是否存在reids的集合中
生成指纹
python
fp = hashlib.sha1()
fp.update(to_bytes(request.method)) #请求方法
fp.update(to_bytes(canonicalize_url(request.url))) #url
fp.update(request.body or b'') #请求体
return fp.hexdigest()
判断数据是否存在redis的集合中,不存在插入
python
added = self.server.sadd(self.key, fp)
return added != 0
六、Python爬虫项目总结
-
项目名字
- request+selenium爬虫
-
项目周期
-
项目介绍
- 爬了XXXXX,XXX,XXX,等网站,获取网站上的XXX,XXX,XXX,数据,每个月定时抓取XXX数据,使用该数据实现了XXX,XXX,XX,
-
开发环境
- linux+pycharm+requests+mongodb+redis+crontab+scrapy_redis+ scarpy + mysql+gevent+celery+threading
-
使用技术
- 使用requests...把数据存储在mongodb中
- 使用crontab实现程序的定时启动抓取
- url地址的去重
- 使用redis的集合,把request对象的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现基于url地址的增量式爬虫
- 布隆过滤
- 对数据的去重
- 把数据的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现增量式爬虫
- 反扒
- 代理ip
- 购买了第三的代理ip,组成代理ip池,其中的ip没两天更新一次,同时使用单独的程序来检查代理ip的可用
- cookie
- 准备了XX个账号,使用requests获取账号的对应的cookie,存储在redis中,后续发送请求的时候随机选择cookie
- 使用selenium来进行模拟登陆,获取cookie,保存在Redis中
- 数据通过js生成
- 分析js,通过chrome浏览器定位js的位置,寻找js生成数据的方式
- 通过selenium来模拟页面的加载内容,获取页面动态加载后的数据
- 代理ip
- 提高爬虫效率
- 使用多线,线程池,协程,celery来完成爬虫
- 使用scrapy框架来实现爬虫,
- 不能断点续爬,请求过的url地址不能持久化
- 使用scrapy_redis
- 不能对数据进行去重
- 把数据的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现增量式爬虫
- 不能断点续爬,请求过的url地址不能持久化
- scrapy_redis
- domz实现增量式,持久化的爬虫
- 实现分布式爬虫
-
项目名字
- scarpy爬虫
-
项目周期
-
项目介绍
- 爬了XXXXX,XXX,XXX,等网站,获取网站上的XXX,XXX,XXX,数据,每个月定时抓取XXX数据,使用该数据实现了XXX,XXX,XX,
-
开发环境
- linux+pycharm+requests+mongodb+redis+crontab+scrapy_redis+ scarpy + mysql+gevent+celery+threading
-
使用技术
- 使用requests...把数据存储在mongodb中
- 使用crontab实现程序的定时启动抓取
- url地址的去重
- 使用redis的集合,把request对象的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现基于url地址的增量式爬虫
- 布隆过滤
- 对数据的去重
- 把数据的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现增量式爬虫
- 反扒
- 代理ip
- 购买了第三的代理ip,组成代理ip池,其中的ip没两天更新一次,同时使用单独的程序来检查代理ip的可用
- cookie
- 准备了XX个账号,使用requests获取账号的对应的cookie,存储在redis中,后续发送请求的时候随机选择cookie
- 使用selenium来进行模拟登陆,获取cookie,保存在Redis中
- 数据通过js生成
- 分析js,通过chrome浏览器定位js的位置,寻找js生成数据的方式
- 通过selenium来模拟页面的加载内容,获取页面动态加载后的数据
- 代理ip
- 提高爬虫效率
- 使用多线,线程池,协程,celery来完成爬虫
- 使用scrapy框架来实现爬虫,
- 不能断点续爬,请求过的url地址不能持久化
- 使用scrapy_redis
- 不能对数据进行去重
- 把数据的XXX字段通过sha1生成指纹,放入redis的集合中进行去重,实现增量式爬虫
- 不能断点续爬,请求过的url地址不能持久化
- scrapy_redis
- domz实现增量式,持久化的爬虫
- 实现分布式爬虫