分享

python网络爬虫笔记

 小生凡一 2021-11-30

网络爬虫

1. 网络请求

1. urllbi库

1.1 urlopen

将返回一个类文件句柄对象,解析网页

resp=request.urlopen('http://www.baidu.com')  
print(resp.read())

1.2 urlretrieve

将页面保存到本地中,名字叫’baidu.html’

request.urlretrieve('http://www.baidu,com','baidu.html')

1.3 urlencode

将字典数据转换为URL编码数据

若网址上是中文的话,浏览器是会将中文编码成%+十六进制数,浏览器发送给服务器,服务器是收不了中文的

data={'name':'爬虫','great':'hello world','age':100}
qs=parse.urlencode(data)
print(qs)

1.4 parse_qs

可以将经过编码后的url参数进行解码

qs='xxxxx'
print(parse.parse_qs(qs))

1.5 urlparse & urlsplit

urlparse & urlsplit 对url进行分割,分成若干个部分,返回这些部分

urlparse多返回了一个参数params,其他和urlsplit一样

1.6 request.Request类

用于进行添加请求头的时候,增加一些数据(为了防止反爬虫),比如增加User-Agent

headers={
        'User-Agent':'xxx'                                  #这是让服务器知道这个浏览器而不是一个爬虫.
        }
req=request.Request('http://www.baidu.com',headers=headers) #加上请求头所余姚的信息发送请求.

1.7 ProxyHandler处理器(代理设置)

代理的原理:在请求目的服务器之前,先请求代理服务器,然后让代理服务器去请求目的服务器网站,代理服务器拿到目的网站的数据后,在转发给我们的代码

handler=request.ProxyHandler({"http":"xxxxxx"})   
#传入代理,要构建的代理是要字典的形式表示用ProxyHandler传入代理构建一个handler
opener=request.build_opener(handler)
#用handler创立一个opener
req=request.Request("http:xxxxxx")
resp=opener.open(req)
#调用这个opener去发送请求,就可以以代理的ip地址进行页面的访问请求
print(resp.read())

常用的代理请求:
1.西刺免费代理ip(免费的代理不太可行,容易失败)
2.快代理
3.代理云

1.8 Cookie

将数据给服务器,然后用户的数据再返回给浏览器,让浏览器知道这个用户的身份(大小一般4KB)

Set-Cookie:NAME=VALUE;Expires/max-age=DATE;Path=PATH;Domain=DOMAIN_NAME;SECURE
参数意义:
NAME:cookie的名字
VALUE:cookie的值
Expires:cookie过期的时间
Path:cookie作用的路径
Domain:cookie作用的域名 (作用的范围)
SECURE:是否旨在https协议下起作用

使用Cookie:

from urllib import request
request_url="http://xxxxxxx"
headers={
'User-Agent':"xxxx",                               
#将这个请求模拟成浏览器,而不是一个爬虫机制,防止反爬虫
'cookie':'xxxx'                                    
#加入cookie,将用户信息放入,进行模拟包装,将其更像一个爬虫
}
request.Request(url=request_url,headers=headers)   #发送请求
resp=request.urlopen(req)                          #解析网页
print(resp.read().decode('utf-8'))                 
#将其读取下来,但同时要记得解码!不然会返回的都是经过编码的
with open('xxx.html','w',encoding='utf-8') as fp: 
#注意要加上encoding将str变成bytes,是因为str要以bytes才能写入硬盘当中
#毕竟是机器读写进去的
    #write函数必须写入一个str的数据类型
    #resp.read()读出来的是一个bytes数据类型
    #bytes要通过decode变成str
    #str要通过encode变成bytes
    fp.write(resp.read().decode('utf-8'))          
    #通过utf-8进行解码才能将其中的东西能让人能看的懂

1.9 http.CookieJar模块

1.CookieJar

管理储存cookie对象,将其中都存放到内存当中

2.FileCookieJar(filename, delayload=None, policy=None)

从CookieJar派生而来,用来创建一个文件以来储存cookie,dalayload是表示可以支持延迟访问文件(有需要的时候才去访问文件)

3.MozillaCookieJar(filename, delayload=None, policy=None)

FileCookieJar派生而来,创建与Mozilla浏览器cookies.

4. 例子模板

from urllib import request,parse
from http.CookieJar import CookieJar
headers={
        'User-Agent':'xxxxx'
        }
#1.登陆页面
def get_opener():
    cookiejar=CookieJar()                                      
    #1.1 创建一个CookieJar对象 支持HTTP的请求
    handler=request.HTTPCookieProcessor(cookiejar)             
    #1.2 使用CookieJar创建一个HTTPCookieProcess对象
#HTTPCookieProcess主要是处理cookie对象,并构建handler对象,这里的handler只是一个承接的作用
    opener=request.bulid_opener(handler)                       
    #1.4 使用上一步创建的handler,调用build_opener()的方法创建一个opener对象,参数是构建的处理对象                                                
    #1.5 使用opener发送登陆的请求
    return opener

def login_the_url(opener):
    data={"name":"xxxxx","password":"xxxxxx"}
    data=parse.urlencode(data).encode('utf-8')                 
    #注意发送请求的信息一定要经过编码才能被服务器接受
    login_url='http//:xxxx'                                    
    #这个页面是有登陆的那个页面
    req=request.Request(login_url,headers=headers,data=data)   
    #在获取个人网页的页面的时候,不要新建一个opener,
    opener.open(req)                                           
    #使用之前的opener就可以了,之前的那个opener已经包含了登陆所需要的cookie

#2.访问主页
def visit_profile(opener):                                     
    #这里的opener的信息也已经包含了cookie,就不需要进行再一次的创建新opener
    url="http://xxxxxx"                                        
    #这个页面是要所爬取信息的页面
    req=request.Request(url,headers=headers)
    resp=opener.open(req)                                      
    #这里不能用request.urlopen,这个发送请求是不支持带参数的,是请求不了里面的
    with open('xxx.html','w',encoding='utf-8') as fp:
        fp.write(resp.read().decode("utf-8"))                  
        #注意写入是要解码显示出来的

if __name__='main':
    opener=get_opener()
    login_the_url(opener)
    visit_profile(opener)

2. request库

网络爬虫的请求之request库

import request

2.1 get请求:

2.1.1 无参数

response=request.get("http//:xxx")    #这样就可以进行请求访问网页了

2.1.2 带参数

import request
kw={
"wd":"xxx"
}
headers={
"User-Agent":"xxx"
}
response=request.get("http//:xxx",params=kw,headers=headers)
#这边这个params是接受一个字典或是字符串的查询参数,字典类型自动转换为url编码,不需要urlencode()
print(response.text)
#查看响应内容,response.text返回的是Unicode格式的数据,即经过Unicode编码的字符串,中文可能会乱码
print(response.content)
#查看响应内容,response.content 返回的是字节流数据
#后面response.content.decode('utf-8')才能看见中文的显示

2.1.3 response.text&response.content

  • response.content:这个是直接从网络上面抓取到的数据,没有经过任何的解码,所以是bytes类型,在硬盘和网络上传输的字符串都是bytes类型
  • response.text:这个是requests将response.content进行解码的字符串。解码现需要制定一种编码方式,requests会根据自己的猜测来判断编码的方式。所以有时候可能会猜测错误,就会导致解码产生乱码。这时候就应该使用response.content.decode('utf-8’)进行手动解码。

2.1.4 other

print(response.encoding)
#查看响应头部的字符编码
print(response.status_code)
#查看其中响应头部的响应码

2.2 post请求:

2.2.1 带参数

import request
url='http://xxx'
headers={
     'User-Agent':'http//:xxx',      
#这里是用户代理,让服务器知道这里一个浏览器,而不是一个爬虫
      'Referer':'http//:xxx'          
#用来表示从哪儿链接到当前的网页,服务器因此可以获得一些信息用于处理,这样服务器就不会不将其认为是爬虫
}
data={          #这个是在浏览上面的数据
   'first':'true',
   'pn':1,
   'kd':'python'
}
resp=request.post(url,headers=headers,data=data)
print(resp.json)      #将其转换成json格式

2.2.2 加入代理机制:

import requests
proxy={
     'http':'xxx'                        #代理ip地址
}
response=requests.get("http//:xxx",proxies=proxy)
print(response.text)

2.2.3 关于session

注意:(这个session不是web开发那个session):

import request
url="http//:xxx"
data={
  "name":"xxx","password":"xxx"
}
headers={
    'User-Agent':"xxx"
}
session=requests.session()                             
#session的不同就是可以自带cookie
session.post(url,data=data,headers=headers)
response=session.get('http//:xxx')
print(response.text)

如果想要在多次请求中共享cookie,那么应该使用session

2.2.4 处理不信任的SSL证书

有一些网站的证书是不会被信任的,网址会有红色的不安全,
对于已经信任的证书就可以直接进行request的访问就行了。

注意下面是没有的http是没有s

resp=requests.get('http://xxxxxx',verify=False)
print(resp.content.decode('utf-8'))

2. 数据解析

解析工具解析速度使用难度
BeautifulSoup最慢最简单
lxml简单
正则最快最难

xpath可以将其xmlhtml的文档中查找所需要的信息

安装浏览器插件

xpath helper(chrome)

2.1 XPath语法:

2.1.1 选取节点:

  • nodename (选取此节点的所有子节点)

eg: bookstore

就会选取bookstore下所有的子节点

  • / (如果在最前面,代表从根节点选区。否则选择某节点下的某个节点)
    局部
    ​eg:/bookstore 就选取到了根元素下所有的bookstore节点
    ​eg: 在网页上 /div 是找不到的,因为这个是在根节点上找的,而在根节点html上面是没有 div 的
    div是在其中的孙节点body中,/html是可以找到的,但是/html/div 就是找不到的

  • // (从全局节点中选择节点,随便在哪个位置)全局
    eg: //book 从全局节点中找到所有的book节点
    eg: //head/script 从head中选中局部的script就是单单是head中的script
    eg: //script 从全局当中选中script,不单单是局限与head中的script,也有可以能是body当中的script

  • @ (选区某个节点的属性) 有点类似面向对象的类的属性

<book price="xx">     这个price就是book的属性
eg: //book[@price]     选择所有拥有price属性的book节点
<div id="xxx">     这个id就是div的属性
eg: //div[@id]     选择所有拥有id属性的div节点

2.1.2 谓点

用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中
1)

eg:/bookstore/book[1]   
选取bookstore下的第一个子元素
eg://body/div[1]        
获取body当中的第一个div元素
eg:/bookstore/book[last()]  
选取bookstore下的倒数第二个book元素
eg:bookstore/book[position()< 3]    
选取bookstore下前面两个子元素
​eg://body/div[position()< 3]               
选取body元素的div下的前两个position元素
eg://book[@price]                           
选取拥有price属性的book元素
eg://book[@price=10]                      
选取所有属性price等于10的book元素节点
eg://div[@class='s_position_list'] 
可以获取div下的有s_position_list的class节点
  1. 模糊匹配contains:
 eg:<div class="content_1 f1">            
 只选取其中的f1属性则有
//div[contains(@class,"f1")]              
使用contains进行模糊匹配,匹配到class下的f1属性

2.1.3 通配符

(*表示通配符)

  1. *匹配任意节点
​eg:/bookstore/*
选取bookstore下的所有子元素
  1. @*匹配节点中的任何属性
eg://book[@*]
选取所有带有属性的book元素

2.1.4 选取多个路径

(通过 | 运算符来选取多个路径)
1)

​eg: //bookstore/book | //book/title
​#选取所有bookstore元素下的book元素以及book元素下的所有所有title元素
​eg: //dd[@class="job_bt"] | //dd[@class="job-advantage"]#选取所有dd下的class的job_bt和job-advantage的所有属性
​还有其他运算符 and or之类的

2.1.5 总结:

​1. 使用//获取整个页面当中的元素,然后写标签名,然后在写谓词进行提取。

​eg: //div[@class='abc']

2 . / 只是直接获取子节点,而//是获取子孙节点
​3. contains: 有时候某个属性中包含了多个值,那么可以使用contains函数

eg: //div[contains(@class,'xxx')]

2.1.6 lxml库

1. 基本使用:

  • 解析html字符串:使用lxml.etree.HTML进行解析
from lxml import etree    (这是用c语言写的)
text="这里就是代码"                                   
#这里的代码是不规范的不完整的html
html=etree.HTML(text)                                
#利用etree.HTML类,将字符串变成为HTML文档再进行解析,但是这是一个对象
result=etree.tostring(text,encoding='utf-8')         
#按字符串序列化HTML文档,但是这个是bytes类型,为了防止乱码,加上encoding='utf-8'
#那么就是说解析这个网页的时候要用utf-8的形式来进行编码,防止乱码,因为默认是unicode编码
result.decode('utf-8') 
#要解码为了使人可以看懂
  • 解析html文件

​使用lxml.etree.parse继续解析

parser=etree.HTMLParser(encoding='utf-8')               
#构建HTML解析器,防止网页的源代码的缺失
html=etree.parse("tencent.html(放地址)",parser=parser)  
#可以进行这parse就可以直接对其进行解析,但是有时候有些网页不完整
#少一个div之类的,这时候就是会报错,解决方法就是加上parser解析器
result=etree.tostring(text,encoding='utf-8')
result.decode('utf-8')

效果是和上面的一样的
这个方法是默认使用XML解析器,所以如果碰到一些不规范的HTML代码的时候就会解析错误,这时候就要自己创建HTML解析器

from lxml import etree
parser = etree.HTMLParser(encoding="utf-8")  # 构造HTML解析器,防止网页不完整而解析不了
html = etree.parse("tencent.html", parser=parser)


# xpath函数是返回一个列表
# 1.获取所有的tr标签   //tr
trs = html.xpath("//tr")
for tr in trs:
    # print(tr)  
    # 这样的话是直接返回一个迭代器对象,人是看不懂的,要经过解码才行.
    print(etree.tostring(tr, encoding="utf-8").decode("utf-8"))
    # 就是直接用etree.tostring变成字符串,然后再进行编码,再进行解码
    # 可以用先不用decode试试再加上decode

    
    
# 2.获取第二个tr标签
trs = html.xpath("//tr[2]")     # 这是返回一个元素,迭代器元素
print(trs)
trs = html.xpath("//tr[2]")[0]  # 这就是取这里的第一个元素
print(trs)
print(etree.tostring(trs, encoding='utf-8').decode("utf-8"))
# 以字符串的形式,utf-8的编码方式再解码才能让这个迭代器元素呈现出来,即是网页的源代码



# 3.获取所有class等于even的tr标签
evens = html.xpath("//tr[@class='even']")
for even in evens:
    print(etree.tostring(even, encoding="utf-8").decode("utf-8"))
# 先是写tr标签,再写符合class属性等于even的所有标签



# 4.获取所有a标签的href属性,这边这个是属性,返回属性,href属性其实就是网址域名后面的那一串东西
ass = html.xpath("//a/@href")
print("http://hr.tencent.com/" + ass)  # 就可以直接进行点击网页
# 4.1获取所有href属性的a标签,这边这个是显示a这个容器中的所有东西,毕竟[]
ass = html.xpath("//a[@href]")



# 5.获取所有的职位信息(纯文本)
"""<tr>
<td class="xxx"><a target="xxx" href="xxx">我是第一个文本</a></td>
<td>我是第二个文本</td>
<td>我是第三个文本</td>
</tr>"""
words = html.xpath("//tr[position()>1]")  # 除了第一个tr标签,其他全获取
all_things=[]
for word in words:
    # href=tr.xpath("a")                   
    # 获取a标签,但是这样是默认tr下的直接a标签,但是这时候是获取不到的,
    # 因为a不是tr的直接子标签,td才是直接子标签
    # href=tr.xpath("//a")                 
    # 这样是相当于忽视了前面的tr.的默认,因为加了//就是全局的a标签了
    href = tr.xpath(".//a")                
    # 在某个标签下,再执行xpath函数,获取这个标签的子孙元素,那么//前加了一个点就是相当于是当前这个tr.并且是仅限于该tr.标签下的a标签
    href = tr.xpath(".//a/@href")          
    # 得到第一个a标签的href属性,href就是页面后面的网址的那一部分
    title = tr.xpath(".//a/text()")        
    # 这样就可以获取到a标签下的所有文本即"我是第一个文本"
    title = tr.xpath("./td/text()")        
    # 这样就可以获取到td标签下的所有文本,但是这里只是获取到"我是第二个文本",所以上面的那个"我是第一个文本"这个信息是在a标签下的并不是直接属于td的
    title1 = tr.xpath("./td[1]//text()")   
    # 这里就是第一个td标签,注意这是和python的索引不一样的,这个是从1开始的,python的是从0开始的
    # 因为这里面的文本并不是td的直接子元素,a才是td的直接子元素,所以我们就是要将器变成//text(),而不是/text()
    title2 = tr.xpath("./td[2]//text()")   # 就可以拿到第二个文本,即"我是第三个文本"
    all_thing={
        "first": title1,                   # 将其变成列表形式
        "second": title2
    }
    all_things.append(all_thing)           # 将其放给列表当中
    print(href)
    break

# lxml结合xpath注意事项:
# 反复练习才有用

2.1.7 xpath实战之豆瓣

import requests
from lxml import etree
# 1.将目标网站上的页面抓取下来

headers={
    'User-Agent': "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.16 Safari/537.36",
    # 仿照浏览器,将该爬虫包装成一个浏览器
    'Referer': "https://www.baidu.com/s?wd=%E8%B1%86%E7%93%A3&rsv_spt=1&rsv_iqid=0xded42b9000078acc&issp=1&f=8&rsv_bp"
               "=1&rsv_idx=2&ie=utf-8&tn=62095104_19_oem_dg&rsv_enter=1&rsv_dl=ib&rsv_sug3=8&rsv_sug1=5&rsv_sug7=100"
               "&rsv_sug2=0&inputT=1250&rsv_sug4=1784 "
    # 告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理,一般用于多网页的爬取
}
url = 'https://movie.douban.com/'
response = requests.get(url, headers=headers)
text = response.text                              #将其网页爬取下来了
#text=open("Douban.text",'r',encoding="utf-8")
# print(response.text)

# response.text: 返回的是一个经过解码后的字符串,是str(unicode)类型,有可能会发生乱码,因为解码方式可能不一样而导致乱码
# response.content: 返回的是一个原生的字符串,就是从网页上抓取下来,没有经过处理,bytes类型

# 2.将抓取的数据根据一定的规则进行提取
html = etree.HTML(text)                      # 对网页进行解析,对text进行解码
print(html)
#html = html.xpath("//ul/li/a/@href")获取a标签下的href属性值             
#html = html.xpath("//ul/li/a/text()")获取a标签下的文本

ul = html.xpath("//ul")[0]
print(ul)
lts=ul.xpath("./li")
for li in lts:
    title=li.xpath("@data-title")
    data_release=li.xpath("@data-release")
    #data_duration=li.xpath("@data-ticket data-duration")
    data_region=li.xpath("@data-region")
    data_actors=li.xpath("@data-actors")
    post=li.xpath(".//img/@scr")
    print(data_actors)
    print(post)
    movie={
        'title':title,
        'data_release':data_release
    }

2.1.8 xpath实战之电影天堂

# 爬取电影天堂
import requests
from lxml import etree

BASE_URL='https://www./'
url = 'https://www./html/gndy/dyzz/index.html'
HEADERS = {
        'Referer': 'https://www./',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.16 Safari/537.36'
    }
def get_detail_urls(url):
    response = requests.get(url, headers=HEADERS)
    # print(response.text)          
    #requests库,默认会使用自己猜测的编码方式将爬取下来的网页进行解码,,然后存到text属性上面
    # 在电影天堂的网页中,因为编码方式,requests库猜错了,所以就会乱码  print(response.content.decode(encoding='gbk', errors='ignore'))    
    #F12 在console输入document.charset 查看编码方式,要加上这个errors才能让程序跑通 response.content 会是将其中的解码方式改成自己所需要的解码方式
    text = response.content.decode(encoding='gbk', errors='ignore')
    html = etree.HTML(text)  # 解析网页
    detail_urls = html.xpath("//table[@class='tbspan']//a/@href")  
    #在含有class=tbspan属性的table标签,因为一个网页有很多的class,
    #这个class=tbspan就是所需要爬取的数据的table的特征特定
    #然后就是这个table属性下的所有a标签中的所有href属性
    #for detail_url in detail_urls:
        #print(BASE_URL + detail_url)
    detail_urls=map(lambda url:BASE_URL+url,detail_urls)
    return detail_urls
    #以上代码就是相当于:
    #def abc(url):
    #    return BASE_URL+url
    #index=0
    #for detail_url in detail_urls:
    #    detail_url=abc(detail_url)
    #    detail_urls[index]=detail_url
    #    index+=1


def spider():
    movies = []
    base_url="https://www./html/gndy/dyzz/list_23_{}.html"    
    # 留一个{}所以就是会将其中槽填上
    for x in range(1,7):                                               
        # for中找到其中的网页的几页
        print("==================================")
        print(x)
        print("==================================")                    
        # 如果有gbk识别不了的编码的话,就是会有出现错误,因为有一个特殊的字符是gbk识别编译不了                                                           
        # 那么解析网页的时候text=response.content.decode('gbk',errors='ignore')
        url=base_url.format(x)
        detail_urls=get_detail_urls(url)
        for detail_url in detail_urls:             
            # 这个for循环是为了遍历一个页面中的全部电影详情的url
            # print(detail_url)
            movie = parse_detail_page(detail_url)
            movies.append(movie)
    print(movies)                     #爬完之后才会全部显示出来,时间有点慢的               



def parse_detail_page(url):
    movie={}
    response = requests.get(url,headers=HEADERS)
    text = response.content.decode('gbk')     #解码
    html=etree.HTML(text)                     #返回元素
    #titles=html.xpath("//font[@color='#07519a']")    
    #将详情页面上面的标题爬取下来,但是单单这样的话就是会将其中的其他的一样的标准的也是会爬取下来,那么就是将其独一无二的标签限定出来
    title=html.xpath("//div[@class='title_all']//font[@color='#07519a']/text()")[0]       # 这样规定的div就可以爬取下特定的标题,加上text就会将对象编码的东西里面的文字打印出来
    #print(titles)         
    #这样是把获取到的对象列表给打印出来
    #for title in titles:
        #print(etree.tostring(title,encoding='utf-8').decode('utf-8'))  
        #以字符串的形式输出,不然就会以字节流的形式
    movie['titile']=title
    zoomE=html.xpath("//div[@id='Zoom']")[0]   
    #zoom中含有很多所需要爬取的信息,而xpath中是返回一个列表所以就是要将其取第一个元素
    post_imgs=zoomE.xpath(".//img/@src")
    movie['post_imgs']=post_imgs
    #print(post_imgs)
    infos=zoomE.xpath(".//text()")     
    #将zoom下的所有信息拿到
    #print(infos)

    def parse_info(info,rule):
        return info.replace(rule,"").strip()  
    #定义一个函数,传入原来的字符串,输出后来修改后的字符串


    #for info in infos:
    for index,info in enumerate(infos):   
        # 这样将对应的下表和元素给打印出来
        if info.startswith("◎年  代"):
            # print(info)
            #info = info.replace("◎年  代", "").strip()  
            # 这个代码和下面那一行函数执行额代码是一样的 
            # 将年代替换了之后,再将其中年代左右空格给替换掉
            info=parse_info(info,"◎年  代")
            movie["year"]=info
        elif info.startswith("◎产  地"):
            #info=info.replace("◎产  地","").strip()
            info = parse_info(info, "◎产  地")
            movie["country"]=info
        elif info.startswith("◎类  别"):
            #info = info.replace("◎类  别", "").strip()
            info = parse_info(info, "◎类  别")
            movie["category"]=info
        elif info.startswith("◎豆瓣评分"):
            info=parse_info(info,"◎豆瓣评分")
            movie["douban_score"]=info
        elif info.startswith("◎片  长"):
            info=parse_info(info,"◎片  长")
            movie["duration"]=info
        elif info.startswith("◎导  演"):
            info=parse_info(info,"◎导  演")
            movie["director"]=info
        elif info.startswith("◎主  演"):
            info=parse_info(info,"◎主  演")      
            #因为这个源代码是一行一个列表下标,所以就是比较特殊,要按照下标来进行数据的获取
            actors=[info]     
            #要将第一个也搞进去
            for x in range(index+1,len(infos)):   
            #index是主演中第一行的位置,那么我们就是应该从第二行开始进行遍历,
            #上面的第一行已经包括进去了
                actor=infos[x].strip()    
                #去除两边的空格
                if actor.startswith("◎标  签"):
                    break
                actors.append(actor)  
                #把处理第一个全搞进去
            movie['actors']=actors
        elif info.startswith("◎简  介"):
            info = parse_info(info, "◎简  介")     
            #这个简介也是和上面演员的一样的
            movie["director"] = info
            for x in range(index+1,len(infos)):
                profile=infos[x].strip()

                if profile.startswith("【下载地址】"):
                    break
            movie["profile"]=profile
    download_url=html.xpath("//td[@bgcolor='#fdfddf']/a/@href")
    movie["download_url"]=download_url
    return movie

if __name__ == '__main__':
    spider()

2.2 BeautifulSoup4

lxml一样,BeautifulSoup也是一个htmlxml的解析器,主要功能也是如何提取其中的数据。

lxml只是会局部遍历,而BeautifulSoup是基于HTML DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多,所以性能要比lxml低。

BS用来解析HTML比较简单,API非常人性化,支持CSS选择器,python标准库中的HTML解析器,也支持lxml的XML解析器。

但是BeautifulSoup的底层还是lxml,就像python的底层还是C,所以解析还是要依照第三方的解析器。

解析器使用方法优势劣势
python标准库BeautifulSoup(markup,“html.parser”)python内置标准库 执行速度快 容错能力强python3.3之前的版本的效果比较差
lxml HTML解析器BeautifulSoup(markup,“lxml”)速度快,容错能力强需要安装C语言,就是pip install
lxml XML解析器BeautifulSoup(markup,[“lxml”,“xml”]) BeautifulSoup(markup,“xml”)速度快,唯一支持XML解析器需要安装C语言,就是pip install
html5libBeautifulSoup(markup,“html5lib”)最好的容错性 以浏览器的方式解析文本,生成HTML5格式的文档速度慢 不依赖外部扩展

如果是比较奇葩的网页,建议就用html5lib来进行解析网页,防止报错,他是会自动修复错误的存在的。

简单使用:

from bs4 import BeautifulSoup
html="""

xxxxxx

"""
bs=BeautifulSoup(html,"lxml")#将其变成html模式,补上缺失的成分
print(bs.prettify())#以比较美观的方式打印出来

2.2.1 四个常用的对象:

BeautifulSoup将复杂的HTML文档换成一个复杂的树形节点,每个节点都是Python对象,所有对象都可以归结为4种:

  1. Tag
    Tag就是HTML的一个个的标签
  2. NavigatebleString
  3. BeautifulSoup
  4. Comme

2.2.2 find&find_all

  • find:
    只能提取第一个的标签,只是找到一个就返回了

  • find_all:

  1. 可以提取所有的标签,以列表的形式返回多个元素

  2. 在提取标签的时候,第一个参数就是标签的名字。然后如果在提取标签的时候想要使用属性进行过滤,那么可以在这个方法中通过关键字参数的形式,将属性的名字以及对应的值传进去。或者是使用’attrs’属性,将所有的属性以及对应的值放在一个字典中传给’attrs’属性

  3. 有些时候,在提取标签的时候,不想提取那么多,那么可以使用’limit’ 限制提取多少个

2.2.3 string,strings,stripped_strings,get_test

  • string:
    获取某个标签下的非标签字符串,只是一个,以普通字符串的形式返回

  • strings:
    获取某个标签下的所有子孙非标签字符串,返回生成器,可以加上list变成列表形式

  • stripped_strings:
    获取某个标签下的所有子孙标签的字符串并且去掉空格,返回生成器,方法上同

  • get_text:
    获取某个标签下的所有子孙非标签字符串,但是不是以列表返回,以普通字符串返回

from bs4 import BeautifulSoup

html="""
xxxxxx
"""
soup=BeautifulSoup(html,"lxml")


#1.获取所有的tr标签
trs=soup.find_all('tr')
for tr in trs:
    print(tr)
    print(type(tr))  
    #这是一个Tag类型,但是BeautifulSoup里面的repr方法将Tag以字符串的形式打印出来


#2.获取2个tr标签
trs=soup.find_all('tr',limit=2)  
#limit最多获取两个元素,返回列表,最后加上[1]才是返回第二个元素


#3.获取所有class等于even的tr标签

trs=soup.find_all('tr',class_='even')  #class是python的关键字,所以bs4当中加上下划线加以区分
for tr in trs:
    print(tr)

trs=soup.find_all('tr',attrs={'class':"even"})  #可以用attrs里面的信息作为参数
for tr in trs:
    print(tr)

#4.将所有id等于test,class也等于test的a标签提取出来
aList=soup.find_all('a',id='test',class_='test')   #有多少个特点也可以一直上去
for a in aList:
    print(a)

aList=soup.find_all('a',attrs={"id":"test","class":"test"})   #有多少个特点也可以一直上去
for a in aList:
    print(a)


#5.获取所有a标签的href属性
aList=soup.find_all('a')    #找到所有的a标签
for a in aList:
    # 1.通过下标的操作
    href=a['href']     #这种方式比较简单
    print(href)
    #2.通过attrs属性
    href=a.attrs['href']    #获取a标签下的href属性
    print(href)

#6.获取所有的职位信息(纯文本)
trs=soup.find_all('tr')[1:]    #职位信息都在tr标签以内,第一个不是,所以就是要到一以后的就行了
infos_=[]
for tr in trs:
    info={}
    #方法一
    tds=tr.find_all("td")      #找到tr标签下所有的td标签
    title=tds[0]               #title元素都是藏在其中的
    print(title.string)        #就可以将其中的字符串提取出来了
    title=tds[0].string        #tds中的第一个元素就是标题
    category=tds[1].string     #tds中的第二个元素就是分类
    nums=tds[2].string         #tds中的第三个元素就是个数
    city=tds[3].string         #tds中的第四个元素就是城市
    pubtime=tds[4].string      #tds中的第五个元素就是发布时间
    info['title']=title
    info['category']=category
    info['nums']=nums
    info['city']=city
    info['pubtime']=pubtime
    infos_.append(info)

    #方法二
    #infos=tr.strings             
    #可以将其中的纯文本(非标签)给全都爬取下来,这样的话是拿到一个生成器,一个对象
    #for info in infos:
    #    print(info)                  #就可以打印出来了
    #infos = list(tr.string)
    infos=list(tr.stripped_strings)   #可以将其中的字符串中的空格去掉
    info['title']=infos[0]
    info['category']=infos[1]
    info['nums']=infos[2]
    info['city']=infos[3]
    info['pubtime']=infos[4]
    infos_.append(info)               #更加简洁简单
import requests
from bs4 import BeautifulSoup
import html5lib
from pyecharts.charts import Bar

ALL_Data = []


def parse_page(url):
    headers = {
        'User-Agent': 'Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 79.0.3945.16Safari / 537.36',
        'Referer': 'http: // www.weather.com.cn / forecast / index.shtml'
    }
    response = requests.get(url, headers=headers)
    text = response.content.decode('utf-8')
    soup = BeautifulSoup(text, 'html5lib')
    conMidTab = soup.find('div', class_='contentboxTab1')  # 找到div中第一个class='contentboxTab1'里面的第一个
    # print(conMidTab)
    tables = conMidTab.find_all('table')
    for table in tables:
        trs = table.find_all('tr')[2:]
        for index, tr in enumerate(trs):  # 这里会返回下标和值
            tds = tr.find_all('td')  # 获取其中的td标签,这个是返回一个列表
            city_td = tds[0]  # 城市就是第一个标签
            if index == 0:
                city_td = tds[1]  # 由于其中的结构问题,要将第一个下标的值变成第一个(哈尔滨 市) 而不是(黑龙江 省)
            city = list(city_td.stripped_strings)[0]  # 由于返回的是生成器所以就是要将其转换成列表的形式,然后就是将其中的第零个元素取出来
            temp_td = tds[-2]  # 最低气温就是td标签的倒数第二个
            min_temp = list(temp_td.stripped_strings)[0]  # 将其中的其中所有的文字都抓取下来
            ALL_Data.append({"city": city, "min_temp": int(min_temp)})
            # print({"city": city, "min_temp": min_temp})


def main():
    urls = ["hb", "db", "hd", "hn", "xb", "xn", "gat"]
    for id in urls:
        url = f'http://www.weather.com.cn/textFC/{id}.shtml#'
        # 港澳台的from,table比较不同,与其他相比不一样,不太规范 table标签不完整源代码没有,只是浏览器自动补充了,所以要用html5lib来进行完善
        parse_page(url)
    # 分析数据
    # 根据最低气温进行排序
    # def sort_key(data):
    #    min_temp = data['min_temp']
    #    return min_temp
    # ALL_Data.sort(key=sort_key)   
    # 将其中的key=sort_key,将其返回的值作为key进行排序
    
    
    #下面的数据可视化有点问题啊
    ALL_Data.sort(key=lambda data: data['min_temp'])  # 这个和上面那个函数一样,冒号后面是返回的值
    data = ALL_Data[0:10]
    # for value in data:
    # city=value['city']
    # cities.append(city)   
    # 将城市名字提取出来
    cities_ = list(map(lambda x: x['city'], data))  
    # 列表data当中的每一项都传给lambda表达式然后将其分解
    temps_ = list(map(lambda x: x['min_temp'], data))
    chart = Bar()  # 标题
    chart.add_yaxis(series_name="thetitle",xaxis_index=cities_,yaxis_data=temps_)
    #chart.add_dataset('', cities_, temps_)  # 数据
    chart.render('temperature.html')  # 渲染


if __name__ == '__main__':
    main()

2.2.4 CssSelect方法

有时候选择css选择器会可以更加的方便。
1)通过标签名查找

soup.select('a')   #寻找a标签

2)通过类名查找

通过类名就是要加上一个.。比如要查找class='sister'
soup.select('.sister')

3)通过id查找

通过id查找就是要加上一个#。比如要查找id='link’

soup.select('#link')

4)组合查找

soup.select("p #link1")  #这里会找到p中所有的含有link1属性的id标签

soup.select("head>titile")#这里就是会将其中的head下的直接子元素获取到,而不会获取到孙元素

5)通过属性查找

查找时还可以加入属性元素,属性需要用中括号括起来。

soup.select('a[href="http://www.baidu.com"]')
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
        .line1{
            background-color: pink;
        }
        #line2{
            background-color: rebeccapurple;
        }
        .box p{ /*会将全部的子孙元素选取*/
            background-color: azure;
        }
        .box > p{ /*这里是将其中的子元素给搞了,孙元素没有被搞*/
            background-color: aqua;
        }
        input[name='username']
        {
            background-color: coral;
        }

    </style>
</head>
<body>
<div class="box">
    <div>
        <p>the zero data</p>  /*这是孙元素*/
    </div>
    <p class="line1">the first data,class可以出现无数次</p>
    <p class="line1">the second data,而class就是要用.</p>
    <p id="line2">the third data,一个网页的id不能一样,这个id就是要用#</p>
    /*这是直接的子元素*/
</div>
<p>
        the fourth data
</p>
<from>
    <input type="text" name="username">
    <input type="text" name="password">
</from>
</body>
</html>

5.soup+select

在使用css选择器的时候就是要用到soup中的select中的soup.select

2.3 正则表达式

关于正则表达式: 按照一定规则,从某个字符串中匹配到想要的数据。

2.3.1 匹配单个字符

text='hello'
ret=re.match('he',text)  #这里就是在hello中匹配he,但是只能是在第一个匹配,如果是ahello就会报错匹配不到
print(ret.group()) #group可以将其中的值打出来
>>he

点(.)匹配任意的字符:

text="ab"
ret=re.match('.',text)  #match只能匹配到一个字符
print(ret.group())
>>a

但是(.)不能匹配到换行符 text="\n" 就是会报错
\d匹配到任意的数字:

text="123"
ret=re.match('\d',text)   #只能匹配到一个字符
print(ret.group())
>>1

\D匹配任意的非数字

text="2a"
ret=re.match('\d',text)   #只能匹配到一个字符
print(ret.group())
>>a

\s匹配到是空白字符(\n,\t,\r,空格)

text=" "
ret=re.match('\s',text)   #只能匹配到一个字符
print(ret.group())
>> 

这里是有匹配到的,只是匹配到了空的字符
\w匹配到的是a-zA-Z以及数字下划线

text="_"
ret=re.match('\w',text)   #只能匹配到一个字符
print(ret.group())
>>_

而如果是要匹配到一个其他字符,那么就匹配不到

text="+"
ret=re.match('\w',text)   #只能匹配到一个字符
print(ret.group())
>>报错

\W匹配的适合\w是相反的

text="+"
ret=re.match('\W',text)   #只能匹配到一个字符
print(ret.group())
>>+

[] 组合的方式,只要满足中括号里面的字符就可以匹配到

text="0888-88888"
ret=re.match('[\d\-]+',text)   #匹配到数字和-,加了个+号之后就是会匹配到所有的符合的,直到不满足条件为止
print(ret.group())
>>0888-88888
代替
\d:[0-9]       [^0-9]^这是非
\D:0-9
\w:[0-9a-zA-Z_]     [^0-9a-zA-Z_]
\W:[0-9a-zA-Z_]
text="0888-88888"
ret=re.match('[^0-9]',text)   
print(ret.group())
>>-

2.3.2 匹配多个字符

*可以匹配0或是任意多个字符,没有不会报错

text="0888-88888"
ret=re.match('\d*',text)   
print(ret.group())
>>0888

+可以匹配1或是任意多个字符 至少要一个,不然报错

text="abcd"   #text="+abcd"
ret=re.match('\w+',text)   
print(ret.group())
>>abcd     #>>ab  

匹配一个或者0个(要么没有,要么就只有一个)

text="abcd"  #text="+abcd"
ret=re.match('\w?',text)   
print(ret.group())
>>a#>>   匹配到0个

{m}匹配到m个

text="abcd"  #text="+abcd"
ret=re.match('\w{2}',text)   
print(ret.group())
>>ab   #只是会匹配到两个

{m,n}:匹配m-n个字符

text="abcd"  #text="+abcd"
ret=re.match('\w{1,5}',text)    #匹配最多的
print(ret.group())
>>abcd    #>>报错

2.3.3 小案例

1.验证手机号码:

text="13070970070" 
ret=re.match('1[34578]\d{9}',text)    #验证,第一位是1,第二位是34578里面当中的一个后面九个随便
print(ret.group())

2.验证邮箱:

text="together13_@11.com" 
ret=re.match('\w+@[a-z0-9]+\.[a-z]+',text)    #第一位w匹配到任意的字符,然后就是至少要有一位,所以要是有+号,直到匹配到异常@即不属于w的匹配,然后就是要有@而只有一个@,然后再匹配@后面的一个或者多个字符,然后就是\.匹配任意字符来匹配.最后的com就是用一个[a-z]来匹配,也可能会有+号
print(ret.group())

3.验证url:

text="http://www.baidu.com" 
ret=re.match('(http|https|ftp)://[^\s]+',text)    #前面用圆括号阔了起来,然后就是http,https,ftp三个里面的选择一个,然后就是//匹配到非空的就行了
print(ret.group())

4.验证身份证

text="12345678909876543x" 
ret=re.match('\d{17}[\dxX]',text)    #前面的十七位可以是数字,然后后面一个可能是数字也可能是数字,也可能是x或是X,就用一个中括号括起来
print(ret.group())

2.3.4 零碎知识

^脱字号:表示以…开始

text="hello"

ret=re.match('^a',text)   # 这个match是自带脱字号的

print(ret.group())

>>h
text="hello"

ret=re.search('o',text)   # 这个search是全局去找

print(ret.group())

>>o
text="hello"

ret=re.match('^o',text)   # 脱字号第一个不是o如果是^h就可以找到h

print(ret.group())

>>报错

如果是在中括号当中就是取反的意思

$ 表示以。。。结尾

text="xxx@163.com"

ret=re.match('\w+@163.com$',text)   # 以163.com结尾就可以更好验证邮箱

print(ret.group())

| 匹配多个字符串或是表达式

text="https"

ret=re.match('http|https|ftp',text)   # 如果组合要用()括起来

print(ret.group())

贪婪模式与非贪婪模式

text="https"
ret=re.match('\d+',text)   # 这里就是会贪婪模式,匹配尽可能多的字符
ret1=re.match('\d+?',text)  # 这里就是非贪婪模式,匹配到一个就行了
print(ret.group())
text="<h1>标题</h1>"
ret=re.match('<.+>',text)   # 这里就是会贪婪模式,匹配到了全部<h1>标题</h1> 
ret1=re.match('<.+?>',text)  # 这里就是非贪婪模式,匹配到一个就行了,就只是匹配到h1
print(ret.group())

匹配到0-100之间的数字

text="99"
ret=re.match('[1-9]\d?$|100$',text)#第一位不可为0,所以一到九,第二位中的就是可有可无的就要加一个? ,而且要以这个数字为结尾,而100是最特殊的,所以单独选择考虑,而100的话就是100为结尾就可以了  
print(ret.group())

原生字符串与转义字符

text="the mac book pro is $1999"
ret=re.match('\$\d+',text)# 所以说就是其中的$就是其中的1999要加上\将其进行转义
print(ret.group())
>>$299

r’\n’ raw 原生
打印出\n

text="\\n"
ret=re.match('\\\\n',text)  #\\n就是会变成\n因为两个\就是会转义成一个\ 
#要加上\\\\才会让正则识别出n
print(ret.group())
>>\n     

group分组

text="apple's price $99,orange's price is $10"

ret=re.search('.*(\$\d+).*(\$\d+)',text)

print(ret.group())  
#匹配出整个字符串整个正则就是圆括号一个大的分组ret.group()和ret.group(0)是一样的
print(ret.group(1))  #匹配出第一个分组 99
print(ret.group(2))  #匹配出第一个分组 10
print(ret.groups())  #将所有的子分组

findall

找到所有满足条件的一个列表

text="apple's price $99,orange's price is $10"

ret=re.findall('\$\d+',text)  #就可以找到所有的满足的,返回一个列表

sub

text="apple's price $99,orange's price is $10"

ret=re.sub('\$\d+',"0",text)  #将匹配到的都替换成0

print(ret)   #返回一个新的字符串apple's price 0,orange's price is 0

可以将<h1>xxx</h1>就可以将标签替换成空格

text="<h1>xxx</h1>"
ret=re.sub('<.+?>',"",text)

split函数

text"hello world ni hao"
ret=re.split(' ',text)
print(ret)  #['hello','world','ni','hao']

comlie:

如果要经常用到可以使其保存下来,以后用

text="the number is 20.50"
r=re.compile('\d+\.?\d*')

r=re.compile("""
\d+  #小数点前面的数
\.?  #小数点本身
\d*  #小数点后面的数字
""",re.VERBOSE)

ret=re.search(r,text)   #re.VERBOSE可以写注释

print(ret.group())

2.3.5 正则实战爬取古诗网

#正则实例爬取古诗文网
import re,requests

def parse_page(url):
    headers={
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.16 Safari/537.36'
    }
    response=requests.get(url,headers=headers)
    text=response.text
    titles=re.findall(r'<div\sclass="cont">.*?<b>(.*?)</b>',text,re.DOTALL)  #由于网页当中的是有\n的,所以就是会有.就匹配不到这个\n就是停止就是会返回一个空
    # 后面加上一个re.DOTALL 就可以让这个.去匹配所有的字符包括\n   加上?防止非贪婪模式不加的话只能匹配到一个题目
    dynasties=re.findall(r'<p\sclass="source">.*?<a.*?>(.*?)</a>',text,re.DOTALL)  #这个findall是将括号当中的数字给括起来的给爬取下来的
    authors=re.findall(r'<p\sclass="source">.*?<a.*?>.*?<a.*?>(.*?)</a>',text,re.DOTALL)  #因为这里是第二个a标签所以就是要将其中的第一个先获取到,然后再将其中的第二个标签给整好
    content_tag=re.findall(r'<div class="contson" .*?>(.*?)</div>',text,re.DOTALL)     #使用正则表达式就是将其看出字符串而不是网页,就会有什么子元素父元素
    contents=[]
    for content in content_tag:
        #print(content)
        x=re.sub(r'<.*?>',"",content)  # 将其中的标签替换掉
        contents.append(x.strip())
    poems=[]
    for value in zip(titles,dynasties,authors,contents):
        title, dynasty, author, content=value
        more_peoms={
            'title':title,
            'daynastie':dynasty,
            'authors':author,
            'content':content
        }
        poems.append(more_peoms)
    for poem in poems:
        print(poem)


def main():
    for page in range(10):
        url=f"https://www.gushiwen.org/default_{page}.aspx"
        parse_page(url)

if __name__ == '__main__':
    main()

3. 数据储存

3.1 json文件处理

json是一个轻量级的数据交换格式。
支持对象(字典),数组(列表),整形字符串,字符串要用双引号,不能用单引号

import json
#将python对象转换成json字符串
persons=[
{
'username':"zhilioa",
'age':18,
'country':"china"
},
{
'username':"zhaxiaolie",
'age':20,
'country':"china"
}
]
json_str=json.dumps(persons)
print(json_str)
print(type(json_str))   # json实际是一个字符串

with open('person.json','w',encoding='utf-8') as fp:
    fp.write(json_str)
    # 或者也可以json,dump(person,fp,ensure_ascii=False)   直接保存再fp指向的文件,最后一个是将其变成asckii将其关掉,防止转换

class Person (object):
    country='china'

a={
    'person':Person
}
json.dumps(a)  #这里会报错,不能转,这个类型不能变json格式

如果是json变成列表就是

persons=json.load(xxxx)

3.2 CSV文件处理

逗号分隔
1.读取csv文件

import csv

def read_csv_demo1():
with open('stock.csv','r') as fp:
    reader =csv.reader(fp)   #可以读取csv文件,返回迭代器
    next(reader)    #第二个开始,跳过表头
    for x in reader:  
        print(x)    #打印出列表
      
def read_csv_dome2():
    with open('stock.csv','r') as fp:
        #这样的话就是不会包括到了标题的那一行
        reader=csv.DictReader(fp)
        for x in reader:
            print(x)
            value={"name"=x['secShortname'],"volume"=x['turnoverVol']}
print(value)
if __name__=='__main__'
read_csv_demo2()

2.写入csv文件:

import csv

header=['username','age','height']

def writer_csv_demo1():
    values=[('zhanghan',12,1800),
       ('wangwu',16,170),     #列表元祖的方式
       ('lisi',14,111)]
with open('class.csv','w',encoding="utf-8",newline='') as fp:
    writer = csv.writer(fp)    #newline是默认换行的,为了防止空行
    writer.writerow(headers)  #这个可以写一行
    writer.writerows(values)#这个可以写多行

        
def writer_csv_demo2():
    values=[{'username':'zhanghan','age':12,'height':180},    #列表字典的方式
       {'username':'lisi','age':11,'height':130}, 
       {'username':'wangwu','age':15,'height':140}]
    with open('class1.csv','w',encoding='utf-8') as fp:
        writer=csv.DictWriter(fp,headers)  #写入表头信息的时候就是要用到writeheader的方法
    writer.writeheader()  #手动将表头放入其中,不然没有写进去
        writer.writerrows(values)

if __name__=='__main__'
writer_csv_demo2()        

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多