2022.07.28

学习资料:https://www.bilibili.com/video/BV12E411A7ZQ

Python爬虫基础部分

urllib库创建请求

get方式

    使用 urllib.request.urlopen() 方法(需要引包),指定请求链接(需要加http://)和超时阈值等,然后使用 read() 方法将得到的返回信息打印,decode()方法用于进行格式化。
    此处使用了 try/except进行错误捕获,主要是为了不在超时的情况下报一大堆错。

#使用get方式请求
import urllib.request
#超时处理
try:
    timeout = 0.01
    response = urllib.request.urlopen("http://www.baidu.com",timeout=timeout)
    print(response.read().decode('utf-8')) #对获取到的网页进行utf-8解码
except urllib.error.URLError as e:
    print("ERROR! Time out of "+str(timeout)+"s!")

post方式

    使用post方式请求时,必须要附带一些表单信息,否则无法请求。此处使用 urllib.parse.urlencode() 方法并套一个 bytes 方法来创建附带的数据,附带的这些信息会附带在请求头中。

#使用post方式请求,post不能不传参数直接请求
import urllib.parse
#bytes:创建二进制数据,{"a":"b"}:键值对,其中a为key,b为value
data = bytes(urllib.parse.urlencode({"hello":"world"}),encoding="utf-8")
response = urllib.request.urlopen("http://httpbin.org/post",data=data)
print(response.read().decode("utf-8"))#解码可以顺便解码\n等,保证格式好看

创建请求头

    对请求头进行伪装以避开某些网站的检测
    使用 urllib.request.Request() 方法来使用创建好的请求头,其中可以指定url、附带信息、请求头、请求种类等
    之后还是得用 urlopen 打开链接

#创建请求头
#url="http://httpbin.org/post"
url="http://www.douban.com"
#若不设置header,会被豆瓣认出来并说我是个茶壶
headers={
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
}
data=bytes(urllib.parse.urlencode({'name':'eric'}),encoding="utf-8")
req=urllib.request.Request(url=url,data=data,headers=headers,method="POST")
response=urllib.request.urlopen(req)
print(response.read().decode("utf-8"))

查看请求状态和响应头等

#看看请求状态和请求头
response = urllib.request.urlopen("https://www.baidu.com")
print(response.status)#200
#print(response.getheaders())#获取整个响应头
print(response.getheader("Date"))#注意headers和header

Beautiful Soup 4 库提取响应内容

from bs4 import BeautifulSoup

#创建BS实例
file = open("testPackages\douban.html","rb")#rb是读字节流
html=file.read().decode("utf-8")#打开html文件
bs=BeautifulSoup(html,"html.parser")#第一个参数是要解析的文件,第二个参数是解析方式

Tag

    标签及其中的所有内容(包括其中的低级标签),会找到第一个符合的标签

print(bs.title)#如果输出bs.title的类型,则是Tag
print(bs.title.string)#只取标签中的文字内容
print(type(bs.title))
'''输出:
<title>豆瓣电影 Top 250</title>
豆瓣电影 Top 250
<class 'bs4.element.Tag'>
'''

&nbsp;&nbsp;&nbsp;&nbsp;获取标签里的内容(字符串)

print(bs.a.string)#
print(type(bs.a.string))
'''
登录/注册
<class 'bs4.element.NavigableString'>
'''

BeautifulSoup

&nbsp;&nbsp;&nbsp;&nbsp;获取整个文档

#print(bs.head.contents)#返回一个字典类型,内容是head下面的所有标签
print(bs.head.contents[1])#获取字典中的第一个元素(下标从0开始)
'''
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
(这是html中的第一个标签,上述字典中第0个元素是'\n')
'''

Comment

&nbsp;&nbsp;&nbsp;&nbsp;特殊的NavigableString,但是会自动过滤注释符号

print(bs.a.string)#如果其中有形如<!--abc-->的注释内容,其会被自动转换为abc

提取标签下的所有属性

print(bs.a.attrs)#字典类型
'''
{'href': 'https://accounts.douban.com/passport/login?source=movie', 'class': ['nav-login'], 'rel': ['nofollow']}
'''

文档的搜索

&nbsp;&nbsp;&nbsp;&nbsp;使用bs.find_all()或者bs.select()方法定位某些标签或者文字内容。

(1)以字符串匹配或者方法查找

&nbsp;&nbsp;&nbsp;&nbsp;理解:bs.find_all方法首先会获取到文档中的所有标签,然后根据方法传入的参数来筛选符合要求的标签,当参数是方法名时,将每一个标签传入方法,根据返回值判断这个标签是否可用。

#①字符串查找:返回与传入字符串完全匹配的内容
t_list = bs.find_all("span")#返回所有span标签(包括其子标签)

#②正则表达式:调用正则对象的search()方法来匹配内容
t_list = bs.find_all(re.compile("s"))#返回所有包含s的标签(包括其子标签)

#③函数:传入函数名,根据函数的内容来查找(类似C++和C#中sort函数排序时可以传入自定义比较函数)
def value_is_exist(tag):
    return tag.has_attr("value")

t_list = bs.find_all(value_is_exist)#返回所有具有value属性的标签

(2)kwargs:查找某属性为某特定值的标签

&nbsp;&nbsp;&nbsp;&nbsp;参数中指定想要的标签中的属性内容

t_list = bs.find_all(value="unwatched")#找出所有value属性值为"unwatched"的标签
t_list = bs.find_all(class_=True)#找到所有拥有class属性的标签(class关键字为了消除歧义,需要使用class_来代替)

(3)text:查找符合某规则的标签中的文本

&nbsp;&nbsp;&nbsp;&nbsp;使用text时只会返回标签中的文本,不会返回整个标签
&nbsp;&nbsp;&nbsp;&nbsp;理解:一个标签的内容也被视作属性,其key为text,value为其中的内容

t_list = bs.find_all(text="豆瓣")#完全匹配,注意使用text时只会返回标签中的文本,不会返回整个标签
t_list = bs.find_all(text=["豆瓣","让子弹飞","后页"])#同时找多个,返回顺序是其在文档中本来的顺序
t_list = bs.find_all(text = re.compile("\d"))#使用正则表达式找出所有包含数字的文本

(4)limit参数:限定查找出的条目数量

t_list = bs.find_all(text=["豆瓣","让子弹飞","后页"],limit=3)#总之就是在原来的基础上截断了,与上面那个的结果相比只有其前三条

(5)css选择器:使用select()方法按照css选择器语法定位标签

t_list = bs.select("title")#所有的title
t_list = bs.select(".quote")#所有class为quote的标签
t_list = bs.select("#footer")
t_list = bs.select("a[href='https://movie.douban.com/tv/']")#找a标签中href属性为特定值的
t_list = bs.select("head > title")#找head下面的title
t_list = bs.select(".rating45-t ~ .rating_num")#找前一标签同级的后一标签

实例测试:提取豆瓣TOP250中电影的链接、片名等信息

&nbsp;&nbsp;&nbsp;&nbsp;以下是一部电影的信息

<div class="item">
    <div class="pic">
      <em class="">1</em>
      <a href="https://movie.douban.com/subject/1292052/">
        <img width="100" alt="肖申克的救赎" src="https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp" class="">
      </a>
    </div>
    <div class="info">
      <div class="hd">
        <a href="https://movie.douban.com/subject/1292052/" class="">
          <span class="title">肖申克的救赎</span>
          <span class="title">&nbsp;/&nbsp;The Shawshank Redemption</span>
          <span class="other">&nbsp;/&nbsp;月黑高飞(港)  /  刺激1995(台)</span>
        </a>
        <span class="playable">[可播放]</span>
      </div>
      <div class="bd">
        <p class="">
          导演: 弗兰克·德拉邦特 Frank Darabont&nbsp;&nbsp;&nbsp;主演: 蒂姆·罗宾斯 Tim Robbins /...<br>
          1994&nbsp;/&nbsp;美国&nbsp;/&nbsp;犯罪 剧情
        </p>
        <div class="star">
          <span class="rating5-t"></span>
          <span class="rating_num" property="v:average">9.7</span>
          <span property="v:best" content="10.0"></span>
          <span>2649976人评价</span>
        </div>
        <p class="quote">
          <span class="inq">希望让人自由。</span>
        </p>
    </div>
  </div>
</div>

&nbsp;&nbsp;&nbsp;&nbsp;分别对需要的每一种类信息创建各自的正则表达式

#影片详情链接
linkPat = re.compile(r'<a href="(.*?)">')
#注意:findall()中,如果传入的正则表达式含有括号,则只会返回括号内的内容


def getData(baseurl):
    datalist = []
    
    for i in range(0,10):#共10页
        url=baseurl+str(i*25)
        html = askUrl(url)#访问链接获取到目标html文件

    #2.逐一解析数据
        bs = BeautifulSoup(html,"html.parser")
        list = bs.find_all("div",class_="item")#解析发现,每个电影的相关信息都被class为item的div包裹
        for item in list:
            #print(item) #测试:获取全部电影的全部信息
            data=[]
            item = str(item)#re.findall要求传入字符串

            #影片详情的链接
            link = re.findall(linkPat ,item)[0]#如果会获取到多个,则可以只取第一个,否则会得到一个列表
            print(link)

    return datalist

&nbsp;&nbsp;&nbsp;&nbsp;解析完成后,将其存储到excel表格中

import xlwt
def SaveData(datalist, savePath):
    print("saving...")
    # 先创建好表格
    workBook = xlwt.Workbook(encoding="utf-8")  # 创建WorkBook对象
    workSheet = workBook.add_sheet('豆瓣电影')  # 添加工作表

    # 添加表头
    colName = ("影片详情链接", "影片图片链接", "中文片名", "外文片名", "分数", "评价人数", "一句话概括", "其他信息")
    for i in range(0, 8):
        workSheet.write(0, i, colName[i])

    for i in range(0, 250):
        for j in range(0, 8):  # 隐患:最好将8修改为len(colName)
            workSheet.write(i+1, j, datalist[i][j])
        print("第%d个写入完成" % (i+1))

    workBook.save(savePath)
    return None

将数据存储到sqlite数据库中

&nbsp;&nbsp;&nbsp;&nbsp;注意:在创建sql语句时,字符串类型的数据必须加双引号,数值类型可加可不加。

def SaveData2DB(datalist, savePath):
    Init_DB(savePath=savePath)

    conn = sqlite3.connect(savePath)
    cur = conn.cursor()

    for data in datalist:
        for index in range(len(data)):
            data[index] = '"'+str(data[index])+'"'
        sql = '''
            insert into tblMovie250 (link,image,cTitle,oTitle,rating,judgeNum,generalization,info) 
            values (%s)
        ''' % ",".join(data)
        cur.execute(sql)
        conn.commit()

    cur.close()
    conn.close()
    return None

异步爬取(以bilibili评论区内容为例)

&nbsp;&nbsp;&nbsp;&nbsp;某些网站的数据并不会一次性加载完(喜欢我AJAX吗),而是在用户做出某些特定操作后才会加载(如b站评论区,滚动条滑到底时才会继续加载下面的评论)。这种异步加载的实现逻辑是在触发需要加载的行为时,浏览器向AJAX发送请求,并由AJAX从服务器处获取到新加载的内容(JSON串)。在异步爬取中,我们只需要分析AJAX向服务器获取新内容时的请求链接,然后使用urllib来请求这个链接即可。

0.查看返回的JSON串格式

json-1
json-2

1.AskUrl

def AskUrl(url):
    '''
    根据访问链接获取对应的html页面
    return: html页面
    '''
    # 若不设置header,会被豆瓣认出来并说我是个茶壶
    head = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
    }
    req = urllib.request.Request(url=url, headers=head)
    try:
        response = urllib.request.urlopen(req)
        html = response.read().decode("utf-8")
        # print(html)
    except urllib.error.URLError as e:
        if hasattr(e, "code"):
            print(e.code)
        if hasattr(e, "reason"):
            print(e.reason)

    return html

2.AskJsonByPage,按页号获取JSON串

无评论页面将页数设置为1000可以看到能够获取一个JSON串,但是没有评论内容,可以将此作为循环结束标志。

baseUrlFront = "https://api.bilibili.com/x/v2/reply/main?csrf=d2f1171678cefe4350b2a43a023f6a4c&mode=3&next="
baseUrlAfter = "&oid=800272415&plat=1&type=1"
def AskJsonByPage(index):
    '''
    根据页号来访问评论区,页号从1开始
    return: 获取到的JSON串
    '''
    url = baseUrlFront+str(index)+baseUrlAfter
    # 分析JSON串得知,replies:"null"代表无内容
    resp = AskUrl(url=url)

    jsonStr = json.loads(resp)
    if(jsonStr["data"]["replies"] is None):
        return "Empty"
    else:
        commentLlist = jsonStr["data"]["replies"]
        for item in commentLlist:
            print(item["member"]["uname"] + " : " + item["content"]["message"])
        return "Success"

3.循环爬取页面

&nbsp;&nbsp;&nbsp;&nbsp;注意:如果短时间请求次数过多,即使伪装了请求头也可能会被ban IP,一个解决方案是每爬一页停一秒。
&nbsp;&nbsp;&nbsp;&nbsp;此处设置循环上限为1000,防止死循环,如果某视频评论页数大于1000则再调整即可。

def main():
    index = 1
    while(AskJsonByPage(index=index) != "Empty" and index <= 1000):
        index += 1
    # AskJsonByPage(1000)
    return None

爬取结果

其他内容

&nbsp;&nbsp;&nbsp;&nbsp;包括WordCloud词云、Flask后端服务组件、Sqlite数据库等,写在石墨文档