爬虫的学习

学习书籍《Python爬虫开发与项目实战》
源码地址:https://github.com/qiyeboy/SpiderBook
书上代码部分用的python2所以学的时候要查资料(

概述

HTTP请求的实现

urllib的实现

(虽然现在大多是使用的requests,这里还是简单学习下,详细内容使用时浏览器搜索)
先写一个简单的例子

import urllib.request
#urlopen得到网页内容
response = urllib.request.urlopen('https://www.zhihu.com')
html = response.read()
print(html)

上面是GET方式
下面给一个POST的

POST请求

import urllib.parse
import urllib.request

# urlopen()方法可传递参数:
# url:网站地址,str类型,也可以是一个request对象
# data:data参数是可选的,内容为字节流编码格式的即bytes类型,如果传递data参数,urlopen将使用Post方式请求

data = bytes(urllib.parse.urlencode({'word':'hello'}), encoding='utf8')
#data需要字节类型的参数,使用bytes()函数转换为字节,使用urllib.parse模块里的urlencode()方法来讲参数字典转换为字符串并指定编码
response = urllib.request.urlopen('https://www.zhihu.com', data=data)
print(response.read())

requests 的实现

相比于前种,这种方法更加常用
首先需要安装

pip install requests 

若python3,python2都安装了,则pip3即可,我的电脑就是

简单请求

get

import requests

r = requests.get('http://www.baidu.com')
print(r.content)

post

import requests
postdata={'key':'value'}
r = requests.post('http://www.xxxxxx.com/login',data=postdata)
print r.content

get的带参请求有多种方式实现
第一种:直接通过url+?参数=值&参数=值即可
第二种

import requests
payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
print(r.url)

响应与编码

import requests
r = requests.get('http://www.baidu.com')

print('text-->'+r.text) 
print('encoding-->'+r.encoding)
r.encoding='utf-8'
print('new text-->'+r.text)

content:返回字节形式(代码中未给出)
text:返回文本内容
encoding"返回编码

有时候可能会因为编码的不同导致得到的内容乱码,有一种方式可以对这种情况进行处理
安装chardet库(我电脑上好像直接就有,就没下载了)

pip install chardet

运用:

import requests
import chardet

r = requests.get('http://www.baidu.com')
print(chardet.detect(r.content))
r.encoding = chardet.detect(r.content)['encoding']
print(r.text)

chardet.detect()返回字典,confidence是检测精确度,encoding是编码形式
通过将返回的编码形式赋值给r.encoding实现正常解码

请求头的处理

import requests
r = requests.get('http://www.baidu.com')
if r.status_code == requests.codes.ok:
    print r.status_code#响应码
    print r.headers#响应头
    print r.headers.get('content-type')#推荐使用这种获取方式,获取其中的某个字段
    print r.headers['content-type']#不推荐使用这种获取方式
else:
    r.raise_for_status()

r.headers包含所有的响应头的信息,返回的是一个字典,可以通过字典引用,也可以通过get,这里推荐get,因为如果通过字典引用的话,如果没有该字段会抛出异常,而使用get只会返回None
r.raise_for_status():如果响应码4xx,5xx会抛出异常,响应码为200,返回None

cookie处理

获取Cookie值

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',headers=headers)
#遍历出所有的cookie字段的值
for cookie in r.cookies.keys():
    print cookie+':'+r.cookies.get(cookie)

也可以自定义cookie,只需定义一个cookie字典即可

cookies=dict(name='qiye',age='10')
r=requests.get('url',cookies=cookies)

session

有时候希望访问网页的时候不会断开连接,也希望能自动把cookie带上,可以使用session值

import requests
loginUrl = 'http://www.xxxxxxx.com/login'
s = requests.Session()
#首先访问登录界面,作为游客,服务器会先分配一个cookie
r = s.get(loginUrl,allow_redirects=True)
datas={'name':'qiye','passwd':'qiye'}
#向登录链接发送post请求,验证成功,游客权限转为会员权限
r = s.post(loginUrl, data=datas,allow_redirects= True)

重定向与历史信息

allow-redirects字段处理重定向,true:允许重定Fallow-redirects字段处理重定向,true:允许重定F向,False禁止重定向
r.history可以查看历史信息,即成功前的请求跳转信息

import requests
r = requests.get('http://github.com')
print (r.url)
print (r.status_code)
print (r.history)

超时设置

timeout设置

timeout=2

代理设置

通过Proxy

import requests
proxies = {
  "http": "http://10.10.1.10:3128",
  "https": "http://10.10.1.10:1080",
}
requests.get("http://example.org", proxies=proxies)

HTML解析

python中的正则表达式

python中要运用正则表达式需要使用到re库
(没写的部分使用时搜索即可)

re.compile:将正则表达式的字符串转化为Pattern匹配对象

re.match(pattern,string[,flags])
从string开头匹配pattern,匹配成功返回字符串,否则返回None,且只会匹配一次

#coding:utf-8
import re
# 将正则表达式编译成Pattern对象
pattern = re.compile(r'\d+')
# 使用re.match匹配文本,获得匹配结果,无法匹配时将返回None
result1 = re.match(pattern,'192abc')
if result1:
    print result1.group()
else:
    print '匹配失败1'
result2 = re.match(pattern,'abc192')
if result2:
    print result2.group()
else:
    print '匹配失败2'

结果

192
匹配失败2

abc192开头不符合正则表达式,所以返回None
group()可以获取捕获的值

re.search(pattern,string[,flags])
与match类似,不过扫描的是整个字符串

re.split(pattern,string[,maxsplit])

re.findall(pattern,string[,flags])
搜索整个string,以列表形式返回全部字符串

re.finditer(pattern,string[flags])
搜索整个字符串,以迭代器形式返回

re. finditer (pattern, string[, flags])

import re
pattern = re.compile(r'\d+')
matchiter = re.finditer(pattern,'A1B2C3D4')
for match in matchiter:
    print match.group()

re.sub(pattern,repl,string[,count])
re.subn(pattern,repl,string[,count])

BeautifulSoup

安装

pip3 install Beautifulsoup4 安装
可以使用python内置的解析器,也可以使用lxml解析器,pip3 install lxml 安装

使用

先假定html的文本是

html_str = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2"><!-- Lacie --></a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

BeautifulSoup的调用

from bs4 improt BeautifulSoup

soup = BeautifulSoup(html_str,'lxml', from_encoding='utf-8')
# 如果是文件可以用下面这种方式
# BeautifulSoup(open('index.html'))

#格式化输出
print (soup.prettify())

对象种类

Tag

Tag即是指例如\Name \ 这样 title标签和标签内的内容被称为Tag

#获取title的内容
print(soup.tilte)

输出

<title>The Dormouse's story</title>

Tag有2个重要的属性 name和attributes

print (soup.name)
print (soup.title.name)
# 输出 [document]和title

Tag还可以修改name

soup.title.name = 'mytitle'
print(soup.title)
print(soup.mytitle)
#输出 
# None
# <mytitle>The Dormouse's story</mytitle>

三种获得Tag属性的方式

<p class="title"><b>The Dormouse's story</b></p>
print soup.p['class']
print soup.p.get('class')
print soup.p.attrs #获得所有属性,并且输出键和值

属性也可以像Tag一样改名字

soup.p['class']="myClass"
print(soup.p)
NavigableString

.string可以获取标签的内容

print(soup.p.string)
Comment

如果标签的内容是注释用type返回的是一个comment

遍历文档树

BeautifulSoup将HTML转化为树状结构进行搜索

子节点

.comments:将子节点以列表形式返回
.children:返回一个迭代器
两者仅包含直接子节点
.descendants:对所有tag子孙节点进行递归循环
.string 返回标记内的内容,如果包含多个子节点返回None
.strings 应用于tag中包含多个字符串的情况,可以循环遍历
.stripped_strings:去掉字符串中包含的空格或空行(如\n)

父节点

.parent:得到父节点
.parents:递归得到所有父节点

兄弟节点

.next_sibling:获取该节点的下一个兄弟节点
.previous_sibling:获取上一个兄弟节点
.next_siblings:可以对当前节点的兄弟节点迭代输出
.previous_siblings:

前后节点

.next_element
.previous_element
与兄弟节点的区别在于针对的是所有节点不分层次
.next_elements
.previous_elements
迭代器

搜索文档树

最常用的是find_all
find_all(name,attrs,recursive,text,**kwargs)
参数介绍
name:要查找的标记,并输出标记和标记内得值
如果传入列表,那么会匹配整个列表
如果传入True,匹配任何值(可以根据这个,构造一个方法,匹配返回True,不匹配返回false达到筛选得目的)

kwargs:如果指定名字的参数不是find_all给定的内置函数,那么搜索的时候会把该参数当作指定Tag的属性来搜索

print(soup.find_all(id='link2'))
# 会输出id属性值为link2的标签
print(soup.find_all(id='True'))
# 输出包含有ID的节点
print (soup.find_all("a",class_="sister"))
#利用class的时候徐需要注意class是python的关键词,所以一般会加下划线

text:搜索文档中的字符串内容,与name参数的可选值一样

limit: 限制搜索返回的个数

recursive:默认为True,搜索所有子节点如果设置为False只会搜索直接子节点

CSS选择器

soup.select()
详细内容使用时搜索即可

数据存储

提取路径并下载

使用 urllib下的urlretrieve()函数
urlretrieve(url,filename=None,reporthook=None,data=None)

数据库版

MYSQL

python操作mysql需要下载MYSQLdb模块
注意的是 书上的mysql-python 模块只能用于python2,python3使用mysqlclient模块,并且需要解决包依赖问题不能直接pip3 install mysqlclient,教程网上有

具体操作方法不记录了,使用时可直接搜索教程

动态网站抓

简介

动态网页中,返回的Requests内容和在浏览器中看的HTML内容不一样
根据网页上的内容去F12中的网络中找,特别注意带有ajax的页面,找到所要提取的数据所在的页面,然后根据不同页面的信息判断url如何组成已经哪些会变化

举个例子:
比如http://movie.mtime.com/217130/ 中里面的关于评分信息是无法直接从html中直接抓取的,这时候通过F12的网络模块找到 http://service.library.mtime.com/Movie.api?Ajax_CallBack=true&Ajax_CallBackType=Mtime.Library.Services&Ajax_CallBackMethod=GetMovieOverviewRating&Ajax_CrossDomain=1&Ajax_RequestUrl=http%3A%2F%2Fmovie.mtime.com%2F217130%2F&t=202011281643543696&Ajax_CallBackArgument0=217130
这里面记录了一些有关电影的信息,并且不同电影都有,只是更改了几个参数,可以找到其他电影来查看是通过更改哪几个参数来找到不同电影的,这里是 Ajax_RequestsUrl,t,Ajax_CallBackArgument():分别是当前网页的链接,当前的时间,网页中url最后的编号
所以可以通过更改这几个参数得内容实现不同页面得抓取,然后使用json模块获取对应的内容

selenium

该模块可以将页面动态渲染成HTML
使用时需要安装selenium

pip3 install selenium

同时还要安装对应浏览器的驱动
我这里好像因为firefox版本过高,firefox的驱动用不了,只能用chrome,给出驱动的下载位置,下完以后将文件路径添加到环境变量中
chrome
firefox


不知道为什么,隔了几天firefox好像也能用了,我代码也没变,过了几天重新运行就好了,很奇怪,难道是因为之前我没重启?但好像也没需要重启的地方,不过可以运行了就好


寻找元素

对页面进行操作

通用方法find_element和find_elements
第一个参数指定选取元素的方式,第二个参数选取元素需要传入的值或表达式
第一个参数的取值可以为

页面操作

send_keys() : 向输入框中输入
click() : 点击
clear() : 可以用来清除输入框的内容
switch_to_window("windowName):指定窗口切换
switch_to_frame("frameName):切换页面
弹窗处理
switch_to_alert()
alert.dismiss()
历史记录
driver.forward()
driver.back()

反爬虫突破

UserAgent池 :动态设置User-Agent 伪装存在很多用户
禁用Cookie
设置下载延时与自动限速
代理IP池


2020/12/14

实战

emmm,因为综设需要写爬虫,然后自己写了个很简单的,勉强应付下吧,毕竟期末有一堆DDL没法花太多时间在这上面上,不过写个也用了我一个晚上的时间,因为实在不是很熟悉
这里爬的是知乎一个问题的回答者的相关信息,名字,性别,粉丝数,赞同数,评论数,并且将他们保存到excel中,爬的很慢我没加多线程,后面有时间看能不能加上(问题是随便找的一个,如果要用其他问题需要更改url)
这里直接上代码吧,具体的分析在后面

import requests
import json
import xlwt
from bs4 import BeautifulSoup
import lxml

datalist = []

def get_html(url):
    headers = {

        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
    }
    html = requests.get(url, headers=headers)
    html.encoding = 'UTF-8'
    return html

def get_user_fans(url):
    headers = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
    }
    html = requests.get(url, headers=headers)
    if "该帐号已停用" in html.text:
        return 0
    soup = BeautifulSoup(html.text , "lxml")

    fans_text = soup.find_all(class_="NumberBoard-itemValue" )
    fans=fans_text[1].string.replace(",","")
    return fans

def get_data(html):
    data = html.text
    json_data = json.loads(data)['data']
    json_end = json.loads(data)['paging']['is_end']
    # print(json_data)
    # print(json_data['author']['name'])
    print("正在尝试抓取数据")
    for item in json_data:
        data = []
        data.append(item['author']['name'])
        data.append(item['author']['gender'])
        data.append(item['voteup_count'])
        data.append(item['comment_count'])
        print(item['author']['name'])
        if item['author']['name'] == "匿名用户":
            data.append(0)
        elif item['author']['name'] == "「已注销」":
            data.append(0)
        else:
            url_token=item['author']['url_token']
            url_user="https://www.zhihu.com/people/"+str(url_token)
            fans=get_user_fans(url_user)
            data.append(fans)
        datalist.append(data)
    print("成功抓取一次数据")
    if json_end == False:
        return 0
    else:
        return 1

def save_data():
    book = xlwt.Workbook(encoding='UTF-8')
    sheet = book.add_sheet('test', cell_overwrite_ok=True)
    col = ("作者", "性别", "点赞数", "评论数","粉丝数")
    for i in range(0, 5):
        sheet.write(0, i, col[i])
    len_test = len(datalist)
    for i in range(0, len_test):
        data = datalist[i]
        for j in range(0, 5):
            sheet.write(i + 1, j, data[j])
    book.save('test3.xls')

def main():
    url = "https://www.zhihu.com/api/v4/questions/30178891/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cattachment%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics%3Bsettings.table_of_content.enabled%3B&limit=5&offset={}&platform=desktop&sort_by=default"
    for i in range(0, 1000000):
        i = i * 5
        url_1 = url.format(i)
        html = get_html(url_1)
        if_end = get_data(html)
        if if_end == 1:
            print("全部抓取")
            break

    save_data()

if __name__ == '__main__':
    main()

这里参考了一篇文章,但我忘了在哪儿了emmmm,那我就直接写吧,hhhhhh

分析结构

首先先确定要爬取的对象,我这里是随便找的一个问题https://www.zhihu.com/question/30178891
有哪些让人欲罢不能的学习方法? 这就开始学习如何学习(不是

首先我们要知道知乎是如何显示回答的,同时发现当把页面往下拉的时候才会显示新的回答,说明知乎并不是一次把所有回答都显示完,当用户需要的时候再给你显示,再给用户发送过来,所以我们要找到的就是发送过来的报文

  1. 查找传递的文件
    这里要用到浏览器的开发工具,我用的是火狐,F12打开然后调到network具体过程如图

    XHR可以帮助过滤一些不必要的东西文件,调到响应那栏有助于查看内容
    这里发现一个问题,如果要找到个必须先把页面往下拉,好像是因为前面的回答都是一个个传过来的,后面开始几个一起传过来(全是猜测,有待查证)
    根据右边的响应可以猜测数据就是从这传过来的,并且用了json的数据格式

  2. 分析url构成
    我们拿到了传递数据的url,但显然这么多回答不是通过一个文件传递过来的,我们需要查找其他的文件的url,这里我们将回答页面往下拉,可以看到F12网络中会多一些文件,然后找到同样也是传递数据的url

    第一个
    https://www.zhihu.com/api/v4/questions/30178891/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cattachment%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Cis_labeled%2Cpaid_info%2Cpaid_info_content%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_recognized%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics%3Bdata%5B%2A%5D.settings.table_of_content.enabled&limit=5&offset=5&platform=desktop&sort_by=default
    第二个
    https://www.zhihu.com/api/v4/questions/30178891/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cattachment%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Cis_labeled%2Cpaid_info%2Cpaid_info_content%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_recognized%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics%3Bdata%5B%2A%5D.settings.table_of_content.enabled&limit=5&offset=10&platform=desktop&sort_by=default

    将2个url进行比对,发现只有offset改变了,可以发现每个json文件中只有5条数据,然后offset也是每次增加5,那就可以知道可以根据改变offset的值来得到不同的回答

爬取数据

既然要爬的东西知道了接下来就是实现了,代码写的还算比较清楚,我就简单分析就行了

访问页面

首要先成功访问网址,导入requests库,这里反爬做的不是很严格,毕竟是直接访问的数据文件嘛,改个http头就可以,修改User-agent 修改成火狐,chrome都可以,反正不能用默认的,get_html()中的就是修改的user-agent头,然后编码形式改为utf-8,不然可能会乱码

处理数据

访问成功后,来分析下内容构成,我们先爬取回答者的用户,性别,评论和点赞即可,粉丝数我放后面再说
先看内容

根据内容可以看到用户名在author的name中,其他3个内容也可以在相应位置找到,至于如何处理数据用python的json模块就可,具体实现代码里面应该很清晰了

保存数据

最后要将数据保存再excel表中,使用xlwt模块就可以了,这里定义了一个全局变量datalist,方便数据的存储

其他的一些小问题

在写主函数的时候注意到一个问题,offset的最大值设置为多少最好,我们当然可以根据页面的回答数来设置,但是每次都修改显得有些麻烦,所以我们可以根据这个问题来访问最后几个回答的json数据与之前有啥不同

注意到is_end变成True了,而之前都是False
根据意思也可以猜测这是判断是否结尾的数据,所以代码中get_data返回1和0然后主函数中判断get_data()返回数据是1还是0是用来判断抓取的内容是否到结尾了

抓取用户的粉丝数

这里还想要再抓取回答者的粉丝数,而回答页面上没有显示回答者的粉丝数我们需要进入回答者的主页才能抓取,我先尝试从之前的json数据中查找看是否有数据是给的回答者的主页,发现并没有,那看来得找其他办法了

这里注意到一件事,用户得主页url很有规律他们都是以下格式

https://www.zhihu.com/people/用户名

而用户名在之前json数据的author url_token中,那就简单了可以构造url即可

至于如何从主页中抓取粉丝数,我这里用的是beautifuSoup库,然后粉丝数在

class=NumberBoard-itemValue

直接用find_all即可
只是要注意的是,关注者也在这个class中并且在粉丝数之前,所以find_all要取第二条数据,也就是列表中的[1]

抓取报错的处理

在抓取的过程中抓某些用户发生了报错,经过排查发现有些回答用户没有主页或者说无法找到主页

这里我分为两部分
一部分是用户名为 已注销,和匿名用户
一部分是有用户名,但是进去后提示该账号已停用

很简单,因为比较特殊,写几个判断即可,代码中的判断也是来源于此

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇