Python网络爬虫

Python网络爬虫—给爪巴

    • 1.网络爬虫的安全性
    • 2.网络爬虫的工作原理
    • 3.requests库
        • 1.requests库的常用函数
        • 2.request操作步骤
        • 3.response返回响应
    • 4.beautifulsoup4库
        • 1.常用的解析技术
        • 2.beautifulsoup4的操作步骤
    • 5.数据解析技术Xpath
        • 1.爬虫提取数据方法
        • 2.概述
        • 3.Xpath语法选取数据逻辑
    • 6.lxml库
        • 1.概述
        • 2.具体过程
        • 3.示例
    • 7.多线程爬虫技术
        • 1.实例:爬取斗图啦整个网站的所有图片
        • 2.多线程
        • 3.线程锁
        • 4.运用多线程的实例:爬取斗图啦整个网站的所有图片
    • 8.爬虫框架——scrapy
        • 1.概述
        • 2.scrapy原理分析
        • 3.scrapy框架的基本操作

网络爬虫,又被称为 网页蜘蛛,网络机器人,是 一种按照一定的规则,自动请求万维网网站并提取网络数据的程序或脚本
网络爬虫的相关库有:

  • urllib:Python标准库,urllib3是第三方库
  • requests:用来抓取网页数据
  • beautifulsoup4:用来解析网页数据
  • Scrapy:一个爬虫框架

1.网络爬虫的安全性

既然要爬取别人的网站的数据,就存在安全性问题。关于网络爬虫的建议,每个网站可能会有robots.txt文件,这个文件一般放在网站的根目录下下。该文件建议了网站的哪些目录允许爬取,哪些目录不允许爬取。当然这个Robots协议只是一种建议,它没有法律的约束力,但是作为一个遵纪守法的良好公民还是要遵循它的建议。
可以通过网站名/robots.txt来查看Robots协议,例如:
Python网络爬虫_第1张图片

注意,在上面的robots.txt文件中的User-agent指明的爬取的对象,也就是说第一部分的意思就是:对于百度的爬取,哪些是允许Allow的,哪些是不允许Disallow的。

当然有些网站没有robots.txt的话会出现404,那就说明开发人员没有设定哪些可爬哪些不可爬,我们默认就全部可爬

2.网络爬虫的工作原理

3.requests库

Requests最友好的网络爬虫库之一

  • 提供了简单易用的类HTTP协议网络爬虫功能
  • 支持连接池,SSL,Cookies,HTTP(S)代理
  • Python最主要的页面级网络爬虫功能库

1.requests库的常用函数

requests基于Python开发的HTTP库

2.request操作步骤

  1. 导入request库
  2. 准备URL
  3. 准备参数
  4. 准备请求头
  5. 发送请求,获取响应数据

例如:爬取搜狐页面上的数据

import requests
url='http://www.sohu.com'
response=requests.get(url)
#当输出的文本中出现中文乱码时需要设置encoding属性
response.encoding='utf-8'
text=response.text
print(text)

另外,还可能出现的一种情况是:利用百度搜索"xxx",爬取搜索页,例如百度搜索库里,其中的wd就是指关键字库里,就相当于是一个键值对,键名是wd,值名是库里
百度搜索库里
对于这种类型的爬取,如果写成:

import requests

url='http://www.baidu.com/s?'
params={
     
    'wd':'库里'
}
response=requests.get(url,params=params)
response.encoding='utf-8'
text=response.text
print(text)

这样是无法显示出正确的页面的,在Pycharm控制台Ctrl+F,并查询关键字:库里查询不到的。会弹出类似于百度安全验证的信息,这就是反爬虫机制,因为服务器检测到是一个Python程序在爬取它的数据而不是正常的网页搜索,那么要反反爬虫,就需要添加伪装头信息,让服务器以为我就是网页搜索,不是Python爬虫:

import requests
#下面的com后面的/s?是用来分隔地址和参数的,在网页地址栏中就能看到
url='http://www.baidu.com/s?'
params={
     
    'wd':'库里'
}
headers={
     
    'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01Accept-Encoding: gzip, deflate,Accept-Language: zh-CN,zh;q=0.9',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36'
}
#Accept:能接收的文件类型;User-agent:请求的发送者
response=requests.get(url,params=params,headers=headers)
response.encoding='utf-8'
text=response.text
print(text)

这样爬取到的内容就是我们百度搜索页面搜索关键字得到的页面

3.response返回响应

响应客户端的请求,动态生成响应,包括状态码,网页的内容等。有以下几种响应属性:
Python网络爬虫_第2张图片
例如,content的使用方法:

...
t=response.content
print(t.decode())
#decode()中默认的参数,即转码形式就是'utf-8'

4.beautifulsoup4库

爬取到网页的数据之后,可以借助网页解析器从网页中解析和提取出有价值的信息,或者是新的URL列表。前面通过requests库得到的仅仅是文本形式的网页源码

1.常用的解析技术

  • 针对文本的解析:正则表达式;
  • 针对HTML/XML的解析:XPath,BeautifulSoup,正则表达式;
  • 针对JSON的解析:JSONPath;

2.beautifulsoup4的操作步骤

  1. 创建BeautifulSoupp类对象
  2. 搜索节点(find_all方法):按照节点的名称,属性值或文本
  3. 访问节点:包括名称,属性,文本

通过.标签名就可以获取到标签的所有值,如果有多个标签只能获取到第一个元素,常用的标签名和属性有:

,在每一行中的标签中放的是该学校的排名,名称,位置,总分等等信息,每一行有多个标签。
然后直接上代码:

import requests
from bs4 import BeautifulSoup
import pandas as pd

allUniv=[]
def getHtmlText(url):
    r=requests.get(url)
    r.encoding='utf-8'
    return r.text
def fillUnivList(bs):
    data_lt=bs.find_all('tr')
    #一个t就是其中的一行也就是一个大学
    for t in data_lt:
        tdd=t.find_all('td',limit=4)
        single_lt=[]
        for x in tdd:
        	#每一个tdd中除了内容还包含标签信息,因此需要.string来获取内容
            single_lt.append(x.string)
        allUniv.append(single_lt)
def saveToExcel(num):
    x=pd.DataFrame(allUniv[1:num],columns=['排名','学校','省市','总分'])
    print(x)
    x.to_csv('a1.csv',index=False)
    x.to_excel('a1.xlsx',index=False)
def main(num):
    url='http://www.zuihaodaxue.com/zuihaodaxuepaiming2019.html'
    html=getHtmlText(url)
    bs=BeautifulSoup(html,'html.parser')
    fillUnivList(bs)
    saveToExcel(num)
main(20)

最终运行结果:
a1.csv

排名,学校,省市,总分
1,清华大学,北京,94.6
2,北京大学,北京,76.5
3,浙江大学,浙江,72.9
4,上海交通大学,上海,72.1
5,复旦大学,上海,65.6
6,中国科学技术大学,安徽,60.9
7,华中科技大学,湖北,58.9
7,南京大学,江苏,58.9
9,中山大学,广东,58.2
10,哈尔滨工业大学,黑龙江,56.7
11,北京航空航天大学,北京,56.3
12,武汉大学,湖北,56.2
13,同济大学,上海,55.7
14,西安交通大学,陕西,55.0
15,四川大学,四川,54.4
16,北京理工大学,北京,54.0
17,东南大学,江苏,53.6
18,南开大学,天津,52.8
19,天津大学,天津,52.3

a1.xlsx
Python网络爬虫_第4张图片

5.数据解析技术Xpath

谷歌浏览器插件Xpath,该插件仅仅用于获取网页元素的调试:
Xpath谷歌浏览器插件

1.爬虫提取数据方法

获取到想要的html页面之后,数据提取的三种方式:Xpath语法正则表达式bs4库。关于这三种方法的比较:

属性 描述
head HTML页面的内容
title HTML页面标题,在之中,由</code>标记</td> </tr> <tr> <td>body</td> <td>HTML页面的<code><body></code>内容</td> </tr> <tr> <td>p</td> <td>HTML页面的第一个<p>内容</p></td> </tr> <tr> <td>strings</td> <td>HTML页面所有呈现在Web上的字符串,即标签的内容</td> </tr> <tr> <td>stripped_strings</td> <td>HTML页面所有呈现在Web上的非空格字符串</td> </tr> </tbody> </table> <p>每一个Tag标签在beautifulsoup4库中也是一个对象,称为<strong>Tag对象</strong>:</p> <table> <thead> <tr> <th>属性</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>name</td> <td>字符串,标签的名字,比如div</td> </tr> <tr> <td>attrs</td> <td>字典,包含了原来页面Tag所有的属性,比如href,class</td> </tr> <tr> <td>contents</td> <td>列表,这个Tag下所有子Tag的内容</td> </tr> <tr> <td>string</td> <td>字符串,Tag所包含的文本,网页中真实的文字</td> </tr> </tbody> </table> <p>另外,还有一个<code>find_all</code>方法可以根据参数找到<strong>对应标签</strong>,返回<strong>列表类型</strong>,具体使用方法为:<br> <code>BeautifulSoup.find_all(name,attrs,recursive,string,limit)</code></p> <table> <thead> <tr> <th>参数</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>name</td> <td>按照Tag标签名字检索,名字用字符串形式表示,例如div,li</td> </tr> <tr> <td>attrs</td> <td>按照Tag标签属性值检索,需要列出属性名称和值,采用JSON表示</td> </tr> <tr> <td>recursive</td> <td>设置查找层次,只查找当前标签的下一层时使用recursive =False</td> </tr> <tr> <td>string</td> <td>按照关键字检索string属性内容,采用string=开始</td> </tr> <tr> <td>limit</td> <td>返回结果的个数,比如你要取前几名就可以用到这个参数,默认返回全部结果</td> </tr> </tbody> </table> <p>例如:</p> <pre><code class="prism language-python"><span class="token keyword">from</span> bs4 <span class="token keyword">import</span> BeautifulSoup <span class="token comment">#准备HTML字符串数据</span> html<span class="token operator">=</span><span class="token triple-quoted-string string">''' <html><head><title>it is title 百度 hao123 搜狐 ''' bs=BeautifulSoup(html) #在运行程序时可能会出现警告,因此需要加上一个解析器,就是下面的'html.parser' #bs=BeautifulSoup(html,'html.parser') #bs.prettify()就是将整个网页按照有缩进的格式进行输出 print(bs.prettify()) #输出页面的header信息 print(bs.header) #获取第一个a标签的各种属性,名字,属性,内容,文本 print(bs.a.name) print(bs.a.attrs) print(bs.a.contents) print(bs.a.string) print(bs.html.head.title.string) new_lt=bs.find_all('a') for lt in new_lt: print(lt.string) print(bs.find('a',{ 'href':'http://www.baudu.com'}))

运行结果如下所示:

<html>
 <head>
  <title>
   it is title
  </title>
 </head>
 <body>
  <a class="mnav" href="http://www.baudu.com">
   百度
  </a>
  <a class="mnav" href="http://www.hao123.com">
   hao123
  </a>
  <a class="mnav" href="http://www.sohu.com">
   搜狐
  </a>
 </body>
</html>
a
{
     'class': ['mnav'], 'href': 'http://www.baudu.com'}
['百度']
百度
it is title
百度
hao123
搜狐
<a class="mnav" href="http://www.baudu.com">百度</a>

来一个实战:利用网络爬虫爬取全国最好大学的排名:
中国最好大学排名2019
Python网络爬虫_第3张图片
分析上图后可以知道,表格的每一行就是一个大学,每一行对应的标签元素是

解析方式 解析速度 困难程度
Xpath 中等
bs4 容易
re(正则表达式) 最快 困难

2.概述

Xpath是一门在XML和HTML文档中查找信息的语言,可用来在XML和HTML在文档中对元素和属性进行遍历。就要按照一定规则来进行数据的获取,这种规则就叫做Xpath语法

3.Xpath语法选取数据逻辑

HTML页面是由标签构成的,这些标签像这个族谱一样排列有序。所有的HTML标签都有很强的层级关系,正是基于这种层次关系,Xpath语法能够选择出我们想要的数据。每个标签都可能有这这样的关系:xxx>>太爷爷>>爷爷>>爸爸>>儿子>>孙子

节点的选取

表达式 描述 用法 说明
nodename 选取此节点的所有子节点 div 选取div下的所有标签
// 从全局节点中选取节点 //div 选取整个HTML页面的div标签
/ 选取某个节点下的节点 //head/title 选取head标签下的title标签
@ 选取带某个属性的节点 //div[@id] 选择带有id属性的div标签
. 当前节点下 ./span 选择当前节点下的span标签[代码中威力强大]

可以在谷歌浏览器的Xpath插件中尝试一下节点的选取
Python网络爬虫_第5张图片

在插件左侧输入选取语句右侧就会出现相应结果,同时选取的元素被被高亮显示(其实就是在元素增加了一个高亮的class样式)

谓语

表达式 用法说明
//head/meta[1],//head/meta[k] 选择所有head下的第一个meta标签(下标从1开始),选择所有head下的第K个meta标签
//head/meta[last()] 选择所有head下的最后一个meta标签
//head/meta[position()< 3] 选择所有head下的前两个meta标签
//div[@id] 选择带有id属性的div标签
//div[@id=‘u1’] 选择所有拥有id=u1的div标签

补充,如果想要获取到标签中的文本内容,可以用/text()

6.lxml库

1.概述

lxml库对html的字符串进行解析,将它还原为一个HTML页面,供Xpath语法进行数据提取
字符串还原为html页面的方法为:

from lxml import etree
html=etree.HTML('text')

2.具体过程

  1. 通过request来获取到页面的字符串形式的文件
  2. 通过lxml将字符串文件转化为html的文件
  3. 解析网页获取到目标数据
  4. 显示输出,保存为文件

3.示例

例如想要抓取百度首页的新闻等标签和对应的链接,打印输出并保存为csv文件:

from lxml import etree
import requests
import pandas as pd

url="https://www.baidu.com/"
headers={
     
    'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01Accept-Encoding: gzip, deflate,Accept-Language: zh-CN,zh;q=0.9',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36'
}
response=requests.get(url,headers=headers)
response.encoding='utf-8'
text=response.text
html=etree.HTML(text)
conttext=html.xpath('//div[@id="s-top-left"]/a/text()')
conthref=html.xpath('//div[@id="s-top-left"]/a/@href')
daa=zip(conttext,conthref)
datapd=pd.DataFrame(daa,columns=['栏目','超链接'])
print(datapd)
datapd.to_csv('百度首页爬取1.csv',index=False)

运行结果:
Python网络爬虫_第6张图片

在上面的百度首页爬取时目标只有2个关键字,所以我们可以用zip函数将它们直接打包,但是当碰到需要爬取的数据3,4列甚至更多时,用zip函数就比较麻烦。

再看另一个例子:爬取世界大学排名,打印输出并保存为csv文件,网站为:世界最好大学,代码如下:

from lxml import etree
import requests
import pandas as pd

url="http://www.zuihaodaxue.cn/ARWU2019.html"
headers={
     
    'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01Accept-Encoding: gzip, deflate,Accept-Language: zh-CN,zh;q=0.9',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36'
}
response=requests.get(url,headers=headers)
response.encoding='utf-8'
text=response.text
html=etree.HTML(text)
uni_level=html.xpath('//tbody/tr/td[1]/text()')
uni_name=html.xpath('//tbody/tr/td/a/text()')
uni_area=html.xpath('//tbody/tr//a/img/@src')
uni_img=[s1.replace('image/flag/','').replace('.png','')for s1 in uni_area]
print(uni_level)
print(uni_name)
print(uni_img)
# 当数据不止两列时可以用字典的方式传入
datapd=pd.DataFrame(data={
     '世界排名':uni_level[:20],'学校名称':uni_name[:20],'所在国家/地区':uni_img[:20]})
datapd.to_csv('世界最好大学排名.csv',index=False)

运行结果:
Python网络爬虫_第7张图片

7.多线程爬虫技术

1.实例:爬取斗图啦整个网站的所有图片

先看一个实例:爬取斗图啦整个网站的所有图片
Python网络爬虫_第8张图片

经过一番缜密的分析,斗图啦这个网站的表情包均在最新表情这个专栏中,并且在该专栏中,每一页的地址都有固定格式及规律(地址栏中会标注页码):Python网络爬虫_第9张图片

因此只需要设置一个for循环将需要爬取的页面地址都存放在一个列表变量中即可。实现代码如下:

import requests
from lxml import etree
import urllib

PAGE_URL_LIST=[]
base_url="https://www.doutula.com/photo/list/?page="
#获取到所有页面的URL(这里暂时爬取100页)
for i in range(0,101):
    x=base_url+str(i)
    PAGE_URL_LIST.append(x)
#这里的pop方法可以直接取出元素并将它从列表中删除
page_url=PAGE_URL_LIST.pop()
#伪装请求头信息
headers={
     
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
}
response=requests.get(page_url,headers=headers)
response.encoding='utf-8'
text=response.text
html=etree.HTML(text)
img_list=html.xpath("//div[@class='page-content text-center']//img/@data-original")
for img in img_list:
    #为了避免下载图片命名重复,直接获取到页面的图片名字保存
    filename=img.split('/')[-1]
    path='./image/'+filename
    #img参数是地址,path参数是保存位置
    urllib.request.urlretrieve(img,path)

2.多线程

在上面的实例中,如果需要爬取的页数很多,整个运行过程会变得很慢,因此需要用多线程来解决这个问题。所谓多线程,就是指同时执行多个不同的程序,它是利用标准库import threading来实现的。常用的方法和函数有:

  • 创建一个新线程th=threading.Thread(target=fun,args=[...])
  • 启动线程活动th.start()
  • 线程同步th.join(),主线程任务结束之后,进入阻塞状态,一直等待其它的子线程执行结束之后,主线程再终止。

例如:

import threading
import time

def hello(x):
    print("hello{}".format(x))
    time.sleep(0.5)
def line_fun():
    for x in range(10):
        hello(x)
line_fun()

与用到线程的程序:

import threading
import time

def hello(x):
    print("hello{}".format(x))
    time.sleep(0.5)
def thread_fun():
    for x in range(10):
        th=threading.Thread(target=hello,args=[x])
        th.start()
thread_fun()

对于前者(没有用到线程的程序)来讲,运行结果应该是hello0—hello9依次出现,且每个间隔0.5s,而对于后者(用到线程的程序),hello0—hello9几乎同时很快出现,因为前者仅仅用到了一个线程,因此每次输出需要排队等待0.5s,而后者一共有10个线程,分别用于输出hello0—hello9,因此它们是同时开始执行的,所以很快就输出全部。这个就是多线程的应用。有了多线程的应用,在许多项目中能够提高运行速度

多线程有一个生产者消费者模式,即:
Python网络爬虫_第10张图片

生产者不断的生产资源并放到资源池中,而消费者不断的消耗资源池中的资源。资源池在实际项目中可以理解为全局变量。如果没有应用多线程,那么假如某个消费者需要用到的资源大于资源池中的资源,那么它就必须停下来等待直到资源池中有足够的资源够它使用。假如应用了多线程,在同一时间内,多个线程可以同时生产资源,然后将资源放到资源池中,这样资源池中的资源量肯定比单个线程来的多,项目整体运行速度也会更快。

下面来看一个例子:

import threading
g_num=1
def test1():
    global g_num
    for i in range(1000000):
        g_num+=1
    print("test1={}".format(g_num))
def test2():
    global g_num
    for i in range(1000000):
        g_num+=1
    print("test2={}".format(g_num))
def main():
    print("g_num原始值为{}".format(g_num))
    t1=threading.Thread(target=test1)
    t1.start()
    t2=threading.Thread(target=test2)
    t2.start()
    t1.join()
    t2.join()
    print("--- main  end ---")
    print("g_num现在的值为{}".format(g_num))
main()

test1()test2()方法都是对全局变量g_num进行+1操作,分别循环1000000次,t1线程执行test1,t2线程执行test2,这两个线程同步执行,按道理全部执行完之后结果应该是2000001,但是运行之后的结果却不是2000001:
Python网络爬虫_第11张图片
并且每次运行得到的结果都不一样,而且这两个线程的执行先后也不一样,上图来看是test2先执行完。为什么每次运行得到的结果都不是预期中的2000001呢?存在的问题就是:对资源池同时做生产者消费者操作时,数据会出错。就好比初始值10,线程一要执行+3操作,线程二要执行-5操作,在不知道线程一和二哪个先执行的情况下,假如同时执行两个线程会得到13和5这两个结果就会出错,实际上正确的结果应该是8。

针对上面这种数据出错的情况,解决方法就是加入线程锁

3.线程锁

所谓线程锁就是某一线程对资源池操作时,加锁,禁止其它线程操作。也就是同一时间只允许一个线程对资源池进行操作。该线程对资源池操作完毕之后,解锁。
操作方法:

  • 创建一个线程锁:glock=threading.Lock()
  • 上锁:glock.acquire()
  • 解锁:glock.release()

将上述例子加上线程锁之后:

import threading
g_num=1
glock=threading.Lock()
def test1():
    global g_num
    for i in range(1000000):
        glock.acquire()
        g_num+=1
        glock.release()
    print("test1={}".format(g_num))
def test2():
    global g_num
    for i in range(1000000):
        glock.acquire()
        g_num+=1
        glock.release()
    print("test2={}".format(g_num))
def main():
    print("g_num原始值为{}".format(g_num))
    t1=threading.Thread(target=test1)
    t1.start()
    t2=threading.Thread(target=test2)
    t2.start()
    #t1.join()
    #t2.join()
    print("--- main  end ---")
    print("g_num现在的值为{}".format(g_num))
main()

这样一来运行之后的结果:
Python网络爬虫_第12张图片

4.运用多线程的实例:爬取斗图啦整个网站的所有图片

Python网络爬虫_第13张图片
将实例整个过程可以分为两个部分,生产者就是从网站上的页面获取到每张图片的下载地址,资源池里放的就是所有图片的下载地址,而消费者就是从资源池中获取下载地址,下载图片。这就将这个项目与多线程(生产者消费者模式)结合在了一起,可以提高整个项目的运行速度及效率。
具体代码如下:

import requests
from lxml import etree
import urllib
import threading

g=threading.Lock()
PAGE_URL_LIST=[]
FACE_URL_LIST=[]
base_url="https://www.doutula.com/photo/list/?page="
for i in range(0,101):
    x=base_url+str(i)
    PAGE_URL_LIST.append(x)

# 生产者
def p():
    while True:
    	#这里需要判断一下是否已经把所有页面取完了
        if len(PAGE_URL_LIST)==0:
            break
        else:
            page_url = PAGE_URL_LIST.pop()
            headers = {
     
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'
            }
            response = requests.get(page_url, headers=headers)
            response.encoding = 'utf-8'
            text = response.text
            html = etree.HTML(text)
            img_list = html.xpath("//div[@class='page-content text-center']//img/@data-original")
            g.acquire()
            for img_url in img_list:
                FACE_URL_LIST.append(img_url)
            g.release()
def c():
    while True:
    	#这里需要判断资源池中的资源是否消耗完了
        if len(FACE_URL_LIST)==0:
            # 消费者消费太快就等一会,不是break
            continue
        else:
            g.acquire()
            face_url=FACE_URL_LIST.pop()
            # 为了避免下载图片命名重复,直接获取到页面的图片名字保存
            filename = face_url.split('/')[-1]
            path = './image/' + filename
            # img参数是地址,path参数是保存位置
            urllib.request.urlretrieve(face_url,path)
            g.release()
def main():
    for i in range(5):
        th1=threading.Thread(target=p)
        th1.start()
    for i in range(4):
        th2=threading.Thread(target=c)
        th2.start()
    th1.join()
    th2.join()

8.爬虫框架——scrapy

1.概述

scrapy是基于Twisted的异步处理框架,是纯Python实现的爬虫框架。另外常用的Python爬虫框架有:Scrapy,Crawley,Portia,Newspaper

2.scrapy原理分析

网络爬虫的工作步骤可大致分为四步

URL列表
发送请求 获取响应
数据解析 提取数据
数据存储

在数据解析的过程中做两件事情,第一就是获取到想要的数据,第二就是在解析的过程中如果碰到新的URL再返回给URL列表

上面的工作流程图有一个缺点是:当中间某个模块出错的时候,其他模块都会受到影响。也就是各个模块之间的依赖性很高。为了解决这个问题,scrapy就引入了一个中间模块——协调员,来协调各个模块之间的工作。引入协调员之后,上图就变成了:

URL列表
协调员
发送请求 获取响应
数据解析 提取数据
数据存储

Python网络爬虫_第14张图片

  • 协调员:引擎,负责连接各个模块。
  • URL列表:调度员,协调员会发送请求给调度员,询问是否有需要爬取的URL地址,有的话调度员就会发送给协调员。
  • 发送请求 获取响应:下载器,协调员获取到URL时会询问下载器是否空闲,空闲的话就会将URL发送给下载器来获取URL上的页面数据。下载器获取到页面数据也会返回给协调员。
  • 数据解析 提取数据:爬虫,整个过程最关键的部分。协调员收到下载器发来的页面数据之后将它发送给爬虫,爬虫进行数据的解析,得到的有用的数据再返回给协调员。如果得到了新的URL,就通过协调员再送到调度器中去。
  • 数据存储:数据管道,协调员将有用的数据送到数据管道中最后保存下来。

scrapy工作流程如下图:

3.scrapy框架的基本操作

新建项目
明确目标
制作爬虫
存储数据

1.新建项目:创建一个新的爬虫项目
在终端使用命令创建项目,命令格式为:scrapy startproject 项目名称,创建完了之后项目格式应该如下图所示:
Python网络爬虫_第15张图片

scrapy.cfg整个项目的配置文件settings.py整个项目的各种设置,包括请求头,爬虫约定robotstxt等等
Python网络爬虫_第16张图片
pipelines.py数据管道,用来处理数据(保存数据,去重数据,产生新的URL送回到调度器)
middlewares.py:中间件,items.py:实体文件,用来定义项目的实体,就比如我要建立一个数据库的表,就要先定义表中的字段名(姓名,性别,年龄…),_init_.py:初始化文件。

2.明确目标:明确想要抓取的目标
实例:爬取赶集网上面的租房信息
对页面分析之后,想要获取的数据如下图所示:
Python网络爬虫_第17张图片
然后编写items.py,定义实体:

import scrapy

class MyspiderItem(scrapy.Item):
	# 租房标题
    title = scrapy.Field()
    # 价格
    price = scrapy.Field()
    # 房型
    h_type = scrapy.Field()
    # 装修程度
    dec = scrapy.Field()

3.制作爬虫:制作爬虫,开始爬取网页
第三步制作爬虫中又可以分为以下三步:

创建爬虫
运行爬虫
提取数据

1.创建爬虫
终端输入命令:scrapy genspider 爬虫名称 "爬取域",注意要进入项目目录之后再输入。在本例中,可以输入scrapy genspider ganji ganji.com,爬取域可以选择范围最大的主页面,这样还可以爬取到其它页面的数据。
创建爬虫命令
爬虫创建好了之后,在spiders文件夹下就会多了一个ganji.py,它的文件格式为:

class GanjiSpider(scrapy.Spider):
	# 爬虫的名称
    name = 'ganji'
    # 爬虫搜索的域名范围
    allowed_domains = ['ganji.com']
    # 这是爬取的起始URL,在这里可以修改成租房的那个页面
    start_urls = ['http://hz.ganji.com/zufang/']
    # parse方法:用于处理响应,1.解析提取的数据 2.提取新的URL
    def parse(self, response):
    	pass

2.运行爬虫
使用命令:scrapy crawl 爬虫名称,在输出的时候会附带很多其它日志信息,可以用命令scrapy crawl 爬虫名称 --nolog运行爬虫之后就会执行parse方法,第一次运行是可以的,但是运行多次之后就发现不行了,因为网站存在反爬机制,因此我们需要反反爬,在settings.py中将USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36'USER_AGENT这一段的注释给去掉,使之生效(具体的USER_AGENT的内容需要在网页上右键检查来获取)。
3.提取数据
整个ganji.py解析网页,提取数据的代码如下:

import scrapy

from myspider.items import MyspiderItem
class GanjiSpider(scrapy.Spider):
    name = 'ganji'
    allowed_domains = ['ganji.com']
    start_urls = ['http://hz.ganji.com/zufang/']
    def parse(self, response):
        # print(response,'-'*100)
        divs=response.xpath("//div[@class='f-main-list']/div/div[position()>1]")
        item = MyspiderItem()
        # 遍历divs的时候,把要提取的数据放入事先定义好的数据模型中,导入数据模型文件
        for div in divs:
            # 默认返回的是selector的对象列表,可以使用extract()获取对象列表,extract_first()获取对象列表的第一个元素
            item['title']=div.xpath("./dl/dd[1]/a/text()").extract_first()
            price=div.xpath("./dl/dd[@class='dd-item info']/div/span/text()").extract()
            item['price']=''.join(price)
            item['h_type']=div.xpath("./dl/dd[@class='dd-item size']/span[1]/text()").extract_first()
            item['dec']=div.xpath("./dl/dd[@class='dd-item size']/span[@class='last']/text()").extract_first()
            # 为了避免有些户主没有提交装修信息,以及便于用户的直观了解,可以做一下转换
            # item['dec']没有信息,可能是0也可能为空
            if not item['dec'] or item['dec']==0:
                item['dec']='暂无装修信息'
            print(item)
            yield item
            # 与 return 不同的是yield不会结束程序

scrapy自带有多线程,也是在settings.py中设置开启,如下图:
Python网络爬虫_第18张图片
4.存储数据:存储爬取内容
数据的保存放在pipeslines.py数据管道文件中,首先,这是两个页面,所以需要在ganji.py中将item返回出去,用到的语句是:yield item(这与return不同的是当每次循环需要返回一个对象的时候,return无法做到,yield能不让程序结束)。接着,在pipeslines.py中,

当然要实现数据的保存,运行爬虫时的命令要改为:scrapy crawl 爬虫名称 -o zf.json,zj.json就是要保存的文件名及格式,具体代码如下:

import json

class MyspiderPipeline:
    def process_item(self, item, spider):
        json.dump(dict(item),self.f,ensure_ascii=False)
        # 换行
        self.f.write('\n')
        return item
    def open_spider(self,spider):
        print('open')
        self.f = open('zf11.txt', 'a', encoding='utf-8')
    def close_spider(self, spider):
        print('close')
        self.f.close()

因为在ganji.py中的租房对象是一个一个返回的,所以为了不让文件的打开,关闭重复执行,调用了open_spiderclose_spider这两个内置函数(只要写了就会自动执行,分别是在爬虫运行时爬虫关闭时执行),f是自定义的文件对象。文件采用追加'a'的方式添加数据ensure_ascii=False为了让保存的文件中不显示ASC码

最终运行结果:
Python网络爬虫_第19张图片

你可能感兴趣的:(Python,python)