Loading...

Django 爬虫入门

此前已经对 Python 基础和 Python Web 框架 Django 有了一定的了解,通过手动实践自主搭建一个 Web 程序,相信已经掌握了大部分 Python Web 相关知识。但这对我来说大概是不够的,因此,耐不住寂寞的我又继续了下一项学习,这也是随手在网络上找了一篇学习路线,原文在这里,知道是时候了解一下爬虫相关的知识了。

概述

什么是爬虫

相信在正式接触之前,其实大多数开发者都早已对这个名词有所听闻。

爬虫(网络爬虫),又被称为网页蜘蛛,网络机器人,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

quote from 网络爬虫_百度百科

简单来说,爬虫就是一种自动获取网络内容的程序或脚本。因此,爬虫程序其实并不限于 Python,其他语言如 Java,PHP 等,也均可以编写爬虫程序,只是 Python Spider 相较于其他语言来说,它具有更为简单和使用等特点。

网络爬虫主要用途是采集数据,它是数据分析不可或缺的工具之一,我们日常使用的一些列搜索引擎,如百度,搜狗,谷歌等,其实都是大型的网络爬虫。

爬虫分类

网络爬虫通常可以分为如下三类:

  1. 通用网络爬虫

    通用网络爬虫是搜索引擎的重要组成部分,通用网络爬虫需要遵守 robots 协议,网站通过此协议告知搜索引擎允许被爬取的页面。

    robots 协议:是一种“约定俗称”的协议,并不具备法律效力,它体现了互联网人的“契约精神”。行业从业者会自觉遵守该协议,因此它又被称为“君子协议”。

    网站通常都会公开 robots.txt 文件,其查看地址一般为:域名 + robots.txt,例如,豆瓣(https://www.douban.com/robots.txt)。

  2. 聚焦网络爬虫

    聚焦网络爬虫是一种面向特定需求的网络爬虫程序,它与通用网络爬虫的区别在于聚焦网络爬虫在抓取网页时会对内容进行筛选和处理,尽量保证只抓取与需求相关的网页信息,极大地节省了硬件和网络资源。

  3. 增量式网络爬虫

    增量式网络爬虫是指对已下载网页采取增量式更新,它是一种只爬取新产生的或者已经发生变化网页的爬虫程序,能够在一定程度上保证所爬取的页面是最新的页面。

面向监狱编程

爬虫是一把双刃剑,它给我们带来便利的同时,也给网络安全带来了隐患。有些不法分子利用爬虫在网络上非法搜集网民信息,或者利用爬虫恶意攻击他人网站,从而导致网站瘫痪的严重后果。因此,为了自身安全,请务必合法使用爬虫程序,关于爬虫的如何合法使用,推荐阅读《中华人民共和国网络安全法》。

学习前置

在正式学习 Python 爬虫之前,你需要掌握如下基本知识或:

  • 掌握网页基本知识:html,css,js,query。

  • 熟悉 HTTP 协议相关知识。

  • 掌握 MySQL 基本语法。

  • 掌握 Python 基础知识,如此项仍有疑问,请前往Python 基础知识全通关进行补习(也可以对照着阅读)。

  • 熟悉浏览器开发者相关工具的使用,例如检查元素,查看网络请求信息,查看网站缓存等。

这些都是学习 Python 爬虫必须的,且是极简单的知识,本文后续不会对相关基础点做过多说明。

此外,在开始学习前,请自主搭建相关环境:

  • 准备好 Python 相关的开发环境。

  • 准备好 MySQL 环境。

  • 集成开发工具,推荐 Pycharm,VSCode 等也可。

其实学习了 Python 基础之后,Python 爬虫入门还是相当简单的,下文中很多知识点,其实在 Python 基础中都有详细叙述,如认真学习了 Python 基础,本文基本一读就懂。

基础知识

通过学习 Python 基础知识,我们可以使用如下方式实现一个简单的爬虫程序:

import urllib.request

# 向指定URL发起请求,并获取响应体
response = urllib.request.urlopen('https://www.douban.com')
print(response)
# 从响应体中提取内容
html = response.read().decode('utf-8')
print(html)

这里使用到的是 Python 提供的网络工具包 urllib,Python 基础知识全通关中已有相关示例和说明。

User-Agent

User-Agent 即用户代理,简称 UA,网站服务器通过用户代理来识别用户所使用的操作系统版本、CPU 类型、浏览器版本等信息,并根据不同客户端为用户返回不同内容。

对于一个网站的所有请求,都会在请求头中包含有用户代理信息,如下所示:

User-Agent示例

针对不同浏览器环境,User-Agent 内容自然会有所不同,可自行尝试查看。

网站通过识别请求头中 User-Agent 信息来判断是否是爬虫访问网站,我们可以通过如下示例查看当前爬虫程序的 UA 信息:

import urllib.request

response = urllib.request.urlopen('http://httpbin.org/get')
html = response.read().decode()
print(html)

执行该脚本,可以发现该脚本打印出的 UA 信息为:Python-urllib/x.x.x,因此,若要将爬虫程序伪装成浏览器,则需要对请求头进行重写,如下:

from urllib import request

# 重构请求头,伪装成windows chrome浏览器访问
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'}
req = request.Request(url='http://httpbin.org/get',headers=headers)
res = request.urlopen(req)
html = res.read().decode('utf-8')
print(html)

从伪装 UA 信息出发,我们在编写爬虫程序时,通常会构建一个 User-Agent 池,放入多个浏览器 UA 信息,当运行爬虫程序时,则从池中随机选择用户代理,以此避免同一个 UA 高频访问某个网站,因而引起警觉而被封杀。我们既可以通过定义一个 list 来存储代理信息,并自主实现切换,也可以使用专门的第三方组件来获取随机的 UA 信息,例如:

pip3 install fake-useragent

使用示例如下:

from fake_useragnet import UserAgent

ua = UserAgent()
# 随机获取IE浏览器代理
print(ua.ie)
# 随机获取火狐浏览器代理
pring(ua.firefox)

编码与解码

Python 的标准库 urllib.parse 模块提供了用于编码和解码的方法,分别是 urlencode()unquote() 方法。

正则匹配

在 Python 爬虫过程中,实现网页元素解析的方法有很多,正则解析只是其中之一,常见的还有 BeautifulSoup 和 lxml,它们都支持网页 HTML 元素的解析操作。re 模块则是 Python 提供的正则解析模块,它提供了如下几种常用方法:

# 生成正则表达式对象,flags代表功能标志位,扩展正则表达式的匹配
regex = re.compile(pattern, flags=0)
# 正则匹配目标字符串内容
re.findall(pattern,string,flags=0)
# 根据正则对象匹配目标字符串内容
regex.findall(string,pos,endpos)
# 使用正则表达式匹配内容,切割目标字符串,返回值是切割后的内容列表
re.split(pattern,string,flags=0)
# 使用一个字符串替换正则表达式匹配到的内容,返回值是替换后的字符串
re.sub(pattern,replace,string,max,flags=0)
# 匹配目标字符串第一个符合的内容,返回值为匹配的对象
re.search(pattern,string,flags=0)

此处只做了简单的陈列,详细信息请另行了解。

使用正则匹配抓取需要的网页信息,需要编程人员从目标内容中发现规律,从而编写匹配表达式。

文件读写

Python 内置的文件读写主要通过 csv 实现,CSV 文件又称为逗号分隔值文件,是一种通用的、相对简单的文件格式,用以存储表格数据,包括数字或者字符。CSV 是电子表格和数据库中最常见的输入、输出文件格式,可参考《CSV介绍》。

通过爬虫将数据抓取的下来,然后把数据保存在文件,或者数据库中,这个过程称为数据的持久化存储。

  1. CSV 文件写入

    csv 模块中的 writer 类可用于读写序列化的数据,其语法格式如下:

    writer(csvfile, dialect='excel', **fmtparams)

    参数说明:

    • csvfile:必须是支持迭代的对象,可以是文件对象或列表对象。

    • dialect:编码风格,默认为 excel 风格,即使用逗号分隔。

    • fmtparam:格式化参数,用于覆盖之前 dialect 对象指定的编码风格。

    使用示例:

    import csv
    
    with open('file.csv', 'w', newline='') as csvfile:
        # delimiter 指定分隔符,默认为逗号,这里指定为空格
        # quotechar 表示引用符
        # writerow 单行写入,列表格式传入数据
        spamwriter = csv.writer(csvfile, delimiter=' ',quotechar='|')
        spamwriter.writerow(['Python'] * 5 + ['how are you'])
        spamwriter.writerow(['Python Spider', 'Chinmoku\'s Blog', 'www.chinmoku.cc'])

    此外,也可以使用 DictWriter 类以字典的形式读写数据,示例如下:

    import csv
    with open('dictfile.csv', 'w', newline='') as csvfile:
        #构建字段名称,也就是key
        fieldnames = ['first_name', 'last_name']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        # 写入字段名,当做表头
        writer.writeheader()
        # 多行写入
        writer.writerows([{'first_name': 'Baked', 'last_name': 'Beans'},{'first_name': 'Lovely', 'last_name': 'Spam'}])
        # 单行写入
        writer.writerow({'first_name': 'Wonderful', 'last_name': 'Spam'})
  2. CSV 文件读取

    同样地,csv 模块中的 reader 类和 DictReader 类则可用于读取文件中的数据,其中 reader 类使用示例如下:

    import csv
    with open('file.csv', 'r', newline='') as csvfile:
        spamreader = csv.reader(csvfile, delimiter=' ', quotechar='|')
        for row in spamreader:
            print(', '.join(row))

    DictReader 类使用示例如下:

    import csv
    with open('dictfile.csv', newline='') as csvfile:
        reader = csv.DictReader(csvfile)
        for row in reader:
            print(row['first_name'], row['last_name'])

数据存储

通常情况下,我们通过爬虫程序从网络上爬取到数据,并不是通过文件进行存储,而是将数据持久化到数据库。Python 内置了 Pymysql 模块,可以实现数据库连接和数据持久化处理,此外,也可以安装第三方模块,如 mysql-connector 等(在 Python 基础相关文章中已有详述)。

Pymysql 使用示例:

import pymysql

db = pymysql.connect('localhost','root','123456','testdb')
cursor = db.cursor()
# sql语句执性,单行插入
info_list = ['第一人称单数','村上春树','2021-11']
# info_list = [('第一人称单数','村上春树','2021-11'),('最后的大象','唐·皮诺克','2022-04')]  # 批量插入
sql = 'insert into book_info values(%s,%s,%s)'
# 列表传参
cursor.execute(sql,info_list)
# cursor.executemany(sql,info_list)  # 批量插入
db.commit()
# 关闭
cursor.close()
db.close()

Requests 库

Requests 库包含七个常用的请求方法,如下:

  1. requests.request

  2. requests.get

  3. requests.head

  4. requests.post

  5. requests.put

  6. requests.patch

  7. requests.delete

Requests 库基本使用,在Python 基础知识全通关常用第三方模块部分已有说明,不在赘述。

对于一些没有经过 CA 机构认证的 HTTPS 类型的网站,可以通过设置 verify 参数指定不校验 SSL,其使用格式如下:

response = requests.get(url=url,params=params,headers=headers,verify=False)

另外,一些网站为了限制爬虫,也会针对访问 IP 做一定的限制,例如同一 IP 访问频率超过一定范围,或某个时间频繁更换浏览器访问等,而被识别为异常流量,因而被封禁。针对固定 IP 的局限性,于是出现了 IP 代理,它突破了 IP 地址的访问限制,因此了本地网络真实 IP,而使用第三方 IP 进行访问。

  1. 代理 IP 池:构建 IP 池可以使爬虫程序更加稳定,当需要爬取一个网站时,则从 IP 池中随机选取一个 IP 进行访问。

  2. proxies 参数,使用示例如下:

    import requests
    
    url = 'http://httpbin.org/get'
    headers = {
        'User-Agent':'Mozilla/5.0'
    }
    # 网上找的免费代理ip
    proxies = {
        'http':'http://191.231.62.142:8000',
        'https':'https://191.231.62.142:8000'
    }
    html = requests.get(url,proxies=proxies,headers=headers,timeout=5).text
    print(html)

通常免费代理 IP 质量和稳定性都较差,真实应用场景可以选择使用一些付费 IP 代理服务。

此外,Requests 模块还提供了一个 auth 参数,用于支持用户认证功能,适用于那些需要验证用户名、密码等场景的网站,其使用示例如下:

class xxxSpider(object):
  def __init__(self):
    self.url = 'http://127.0.0.1:31671/rabbitmq/'
    # 网站使用的用户名,密码
    self.auth = ('guest','guest')

  def get_headers(self):
      headers = {'User-Agent':"Mozilla/5.0"}
      return headers

  def get_html(self,url):
      res = requests.get(url,headers=self.get_headers(),auth=self.auth)
      html = res.content
      return html
#...

关于 IP 代理,可扩展了解一下 Proxy SwitchyOmega,这是一款比较使用的浏览器插件,可以对当前用户的 IP 进行代理,在日常生活中其实也很实用。

Python 多线程爬虫

网络爬虫程序是一种 IO 密集型程序,程序中设计了很多网络 IO 以及本地磁盘 IO 操作,这些都会消耗大量时间,从而降低程序执行效率,使用 Python 多线程,能够在一定程度上提升 Python 爬虫程序执行效率。

Python 提供了两个支持多线程的模块,分别是 _thread 和 threading。其中 _thread 模块偏底层,它相比于 threading 模块功能有限,因此推荐使用 threading 模块。

Python 线程相关知识,在Python 基础知识全通关进程和线程模块部分已有说明,可进行参考。

多线程爬虫程序实例:

# -*- coding:utf8 -*-
import requests
from threading import Thread
from queue import Queue
import time
from fake_useragent import UserAgent
from lxml import etree
import csv
from threading import Lock
import json

class XiaomiSpider(object):
    def __init__(self):
        self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'
        # 存放所有URL地址的队列
        self.q = Queue()
        self.i = 0
        # 存放所有类型id的空列表
        self.id_list = []
        # 打开文件
        self.f = open('XiaomiShangcheng.csv','a',encoding='utf-8')
        self.writer = csv.writer(self.f)
        # 创建锁
        self.lock = Lock()

    def get_cateid(self):
        # 请求
        url = 'http://app.mi.com/'
        headers = { 'User-Agent': UserAgent().random}
        html = requests.get(url=url,headers=headers).text
        # 解析
        parse_html = etree.HTML(html)
        xpath_bds = '//ul[@class="category-list"]/li'
        li_list = parse_html.xpath(xpath_bds)
        for li in li_list:
            typ_name = li.xpath('./a/text()')[0]
            typ_id = li.xpath('./a/@href')[0].split('/')[-1]
            # 计算每个类型的页数
            pages = self.get_pages(typ_id)
            #往列表中添加二元组
            self.id_list.append( (typ_id,pages) )
        # 入队列
        self.url_in()

    # 获取count的值并计算页数
    def get_pages(self,typ_id):
        # 获取count的值,即app总数
        url = self.url.format(0,typ_id)
        html = requests.get(
          url=url,
          headers={'User-Agent':UserAgent().random}
        ).json()
        count = html['count']
        pages = int(count) // 30 + 1
        return pages

    # url入队函数,拼接url,并将url加入队列
    def url_in(self):
        for id in self.id_list:
            # id格式:('4',pages)
            for page in range(1,id[1]+1):
                url = self.url.format(page,id[0])
                # 把URL地址入队列
                self.q.put(url)

    # 线程事件函数: get() -请求-解析-处理数据,三步骤
    def get_data(self):
        while True:
            # 判断队列不为空则执行,否则终止
            if not self.q.empty():
                url = self.q.get()
                headers = {'User-Agent':UserAgent().random}
                html = requests.get(url=url,headers=headers)
                res_html = html.content.decode(encoding='utf-8')
                html=json.loads(res_html)
                self.parse_html(html)
            else:
                break

    # 解析函数
    def parse_html(self,html):
        # 写入到csv文件
        app_list = []
        for app in html['data']:
            # app名称 + 分类 + 详情链接
            name = app['displayName']
            link = 'http://app.mi.com/details?id=' + app['packageName']
            typ_name = app['level1CategoryName']
            # 把每一条数据放到app_list中,并通过writerows()实现多行写入
            app_list.append([name,typ_name,link])
            print(name,typ_name)
            self.i += 1
        # 向CSV文件中写入数据
        self.lock.acquire()
        self.writer.writerows(app_list)
        self.lock.release()

    # 入口函数
    def main(self):
        # URL入队列
        self.get_cateid()
        t_list = []
        # 创建多线程
        for i in range(1):
            t = Thread(target=self.get_data)
            t_list.append(t)
            # 启动线程
            t.start()
        for t in t_list:
            # 回收线程   
            t.join()
        self.f.close()
        print('数量:',self.i)

if __name__ == '__main__':
    start = time.time()
    spider = XiaomiSpider()
    spider.main()
    end = time.time()
    print('执行时间:%.1f' % (end-start))

反爬虫

如何黑客攻防一样,爬虫与反爬虫也是相互的,存在众所周知的爬虫手段,就必然存在众所周知的反爬虫手段。

君子协议

前文已经提到 robots.txt 文件,该文件是各大网站约定俗成的一种爬虫规范限制,其本身并未从技术层面进行限制,是否遵守该协议,也却决于爬虫程序的编写者。

反爬虫思路

爬虫反制策略有很多,总体可归纳为基于 IP 的反爬虫和基于爬行的反爬虫两大类,基于者两大类,出现了如下几种策略思路:

  1. 限制 IP

  2. 限制 User-Agent

  3. 限制 Cookie

  4. javascript 动态渲染

  5. 验证码机制

  6. ajax 异步传输

  7. 图片伪装

  8. CSS 偏移

  9. SVG 映射

但总体来说,爬虫与反爬虫始终处于攻防之间,目前也一直未曾分出高下。

爬虫框架推荐

相比与其他语言,使用 Python 编写爬虫程序更有优势,因此也诞生了许多 Python 爬虫框架,较为推荐的 Python 爬虫框架有:

  • Scrapy

  • Crawley

  • Beautiful Soup

  • Selenium

  • PySpider

文末总结

在还未开始学习前,一直觉得 Python 爬虫是一种很遥远的技术,通过本文的了解,如今觉得 Python 爬虫其实本质上就是一个 HTTP 客户端,这个客户端通过各种网络工具,通过程序来模拟一个正常用户行为,实现自动爬取数据。基于这一点理解,其实只要能够发送网络请求,理论上就都可以实现爬虫功能。

当然,基于本文的了解仍旧是极其浅显的,只是就 Python 爬虫入门而言,目前自我感觉还是挺简单的。

参考

版权声明

本文链接:https://www.chinmoku.cc/python/python-spider-base/

本博客中的所有内容,包括但不限于文字、图片、音频、视频、图表和其他可视化材料,均受版权法保护。未经本博客所有者书面授权许可,禁止在任何媒体、网站、社交平台或其他渠道上复制、传播、修改、发布、展示或以任何其他方式使用此博客中的任何内容。

Press ESC to close