爬虫大数据 爬虫Python AI Sql程序员

Python网络爬虫

2018-04-23  本文已影响269人  Lemon_Home

1. 概述

本文主要介绍网络爬虫,采用的实现语言为Python,目的在于阐述网络爬虫的原理和实现,并且对目前常见的爬虫技术进行扩展。
主要的内容有:

2. 基础概念

3. 简单爬虫实例

下面使用Python语言实现一个简单的爬虫程序,爬取的网站是Hi运动。爬取的目标是:

3.1 观察页面DOM结构

在编写爬虫之前,需要先了解所爬页面的大致DOM结构。所谓DOM就是文档对象模型,在网页中把数据组织在一个树形结构中,方便组织管理。通过解析这个树形结构,获取HTML标签内部的值就能够得到我们想要的数据。

首先打开网址四足俯卧撑,然后右键查看源码,可以看到此HTML页面的源码,相关数据都在此数据里。按Ctrl+F搜索字符video查找一下有关视频播放的信息,发现在页面的下方,<script> 标签有关的json数据中包含有对应信息。

3-1.png

之后为了更好查看这段json信息,使用JSON在线解析工具

3-1-2.png

完整的json数据如下:

{
  "id":"1",
  "create_time":"2016-03-29 17:30:46",
  "name":"四足俯卧撑",
  "eng_name":"",
  "description":"1、挺胸收腹,躯干与地面平行
2、双手与肩同宽,始终保持腰背挺直,控制肘部紧贴身体两侧",
  "difficulty":"6",
  "muscle_id":"27",
  "coach_gender":"0",
  "group_id":"2",
  "category_id":"2",
  "created_from":"0",
  "status":"1",
  "ext_info":null,
  "web_mvideo_id":null,
  "web_fvideo_id":null,
  "extra_equipment_type":null,
  "equipment_ids":"3",
  "tpoints":"6",
  "equipments":[
    {
      "id":"3",
      "create_time":"2016-03-29 16:59:17",
      "name":"徒手训练",
      "pic":"",
      "show_type":"0",
      "equipment_group":"0",
      "sort_num":"0"
    }
  ],
  "trainingPoints":"胸部",
  "difficulty_name":"初级",
  "gender_group":{
    "male":true,
    "female":true
  },
  "category_name":"上肢",
  "muscle_name":"胸大肌",
  "otherMuscles":[
    {
      "exercise_id":"1",
      "muscle_id":"17"
    }
  ],
  "exe_explain_pic":[
    {
      "name":null,
      "url":"http://image.yy.com/ojiastoreimage/1462264660160_am__len94877.jpg",
      "desc":"1、挺胸收腹,躯干与地面平行"
    },
    {
      "name":null,
      "url":"http://image.yy.com/ojiastoreimage/1462264665645_am__len98161.jpg",
      "desc":"
1、双手与肩同宽,始终保持腰背挺直,控制肘部紧贴身体两侧"
    }
  ],
  "video_id":"7033",
  "pic":"https://w2.dwstatic.com/yy/ojiastoreimage/20160429_7a5105bb4b153caaf030c490ce36d5a1.jpg",
  "gif":"https://w2.dwstatic.com/yy/ojiastorevideos/a0ced8d3263db3e6cbf774d516c5eec5.gif",
  "detail_video_id":null,
  "muscle_pic":"",
  "video_url":"https://dw-w6.dwstatic.com/49/1/1617/1838324-101-1461899919.mp4",
  "detail_video_url":null,
  "split_time":null,
  "muscle_front_img":"https://w2.dwstatic.com/yy/ojiastoreimage/1477640202013_am_",
  "muscle_back_img":"https://w2.dwstatic.com/yy/ojiastoreimage/1477640203030_am_",
  "definition_group":[
    {
      "definition":1300,
      "name":"超清",
      "url":"https://dw-w6.dwstatic.com/50/1/1617/1838324-100-1461899919.mp4",
      "length":2760,
      "size":"300458",
      "is_default":false
    },
    {
      "definition":"yuanhua",
      "name":"原画",
      "url":"https://dw-w6.dwstatic.com/49/1/1617/1838324-101-1461899919.mp4",
      "length":2760,
      "size":"709332",
      "is_default":true
    }
  ]
}

通过这段json就可以直接获取到我们想要的数据了:

3.2 Requests库使用

当已经了解了所爬取页面的DOM结构之后,下面就开始编写爬虫程序。

首先介绍Python的网络操作库——Requests。
Requests 提供了HTTP很多功能,几乎涵盖了当今 Web 服务的需求,比如:

安装方法pip install requests,简单地使用如下:

>>> r = requests.get('https://www.hiyd.com/dongzuo/1/', timeout=5))
>>> r.status_code
200
>>> r.encoding
'utf-8'
>>> r.text
'<!doctype html>
<html>
<head>
    <meta charset="utf-8">... ...'

更多其他功能,请见官方文档

据此编写爬虫的联网操作:

import requests
class Fitness:

    def get_info(self, url):
        r = requests.get(url, timeout=5)
        print(r.text)


if __name__ == "__main__":
    fitness = Fitness()
    fitness.get_info('https://www.hiyd.com/dongzuo/1/')

分析代码,通过get请求获取response,然后调用text属性获取页面数据,我们就可以根据此数据进行解析。

3.3 BeautifulSoup库使用

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。安装方法pip install beautifulsoup4,简单实用如下:

from bs4 import BeautifulSoup
import requests

r = requests.get('https://www.hiyd.com/dongzuo/1/', timeout=5)
soup = BeautifulSoup(r.text, "html.parser")
print(soup.title)
# <title>四足俯卧撑正确动作要领_四足俯卧撑视频GIF图解_Hi运动健身网</title>
print(soup.a)
# <a class="o-header_logo" href="https://www.hiyd.com/">
# <img src="/static/img/logo3.png?218f9b39b0457ae3"/>
# </a>
print(soup.a['href'])
# https://www.hiyd.com/
print(soup.find_all('a'))
# [<a class="o-header_logo" href="https://www.hiyd.com/">
# <img src="/static/img/logo3.png?218f9b39b0457ae3"/>
# </a>, <a class="item" href="//www.hiyd.com/dongzuo/" target="_self">健...
print(soup.find(id='group_exercise'))
# <div class="menu-group group-tp2 group-expand" id="group_exercise">
# <div class="group-hd">
# <i></i><h3>训练动作</h3><em></em>
# </div>
# <div class="group-bd">
# <div class="menu-item instrument">... ...
print(soup.find_all("script"))
# [<script>
# SITE_URL = "/";
# </script>, <script src="/static/js/libs/seajs.utils.js?7c9ae9ca1b254cde"></script>, <script>
#     seajs.use(['ouj_sdk'], function(sdk) {
#         sdk.init();
#     });
# </script>, <script>... ...

更多其他功能,请见官方文档

接着3.2中的代码,使用BeautifulSoup完成解析数据

from bs4 import BeautifulSoup
import requests
import re
import json


class Fitness:
  headers = {
      'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
  }
    def get_info(self, url):
        r = requests.get(url, headers=self.headers, timeout=5)
        soup = BeautifulSoup(r.text, "html.parser")
        text = str(soup.find_all("script")[-1])
        data = json.loads(re.search(r'e.init\((.+?)\);', text).group(1))
        print(data)

if __name__ == "__main__":
    fitness = Fitness()
    fitness.get_info('https://www.hiyd.com/dongzuo/1/')

其中,data = json.loads(re.search(r'e.init\((.+?)\);', text).group(1))含义是,将获取到的数据通过正则表达式取出中间的json文本数据,再通过json.loads方法将json数据转换为python的dict数据。得到的data数据为:

{'muscle_front_img': 'https://w2.dwstatic.com/yy/ojiastoreimage/1477640202013_am_', 'coach_gender': '0', 'extra_equipment_type': None, 'created_from': '0', 'status': '1', 'difficulty_name': '初级', 'tpoints': '6', 'detail_video_url': None, 'muscle_id': '27', 'trainingPoints': '胸部', 'definition_group': [{'is_default': False, 'url': 'https://dw-w6.dwstatic.com/50/1/1617/1838324-100-1461899919.mp4', 'length': 2760, 'definition': 1300, 'name': '超清', 'size': '300458'}, {'is_default': True, 'url': 'https://dw-w6.dwstatic.com/49/1/1617/1838324-101-1461899919.mp4', 'length': 2760, 'definition': 'yuanhua', 'name': '原画', 'size': '709332'}], 'description': '1、挺胸收腹,躯干与地面平行\r\n2、双手与肩同宽,始终保持腰背挺直,控制肘部紧贴身体两侧', 'video_url': 'https://dw-w6.dwstatic.com/49/1/1617/1838324-101-1461899919.mp4', 'muscle_pic': '', 'video_id': '7033', 'split_time': None, 'eng_name': '', 'otherMuscles': [{'muscle_id': '17', 'exercise_id': '1'}], 'equipments': [{'pic': '', 'show_type': '0', 'create_time': '2016-03-29 16:59:17', 'name': '徒手训练', 'sort_num': '0', 'equipment_group': '0', 'id': '3'}], 'web_mvideo_id': None, 'ext_info': None, 'muscle_back_img': 'https://w2.dwstatic.com/yy/ojiastoreimage/1477640203030_am_', 'gender_group': {'male': True, 'female': True}, 'gif': 'https://w2.dwstatic.com/yy/ojiastorevideos/a0ced8d3263db3e6cbf774d516c5eec5.gif', 'category_name': '上肢', 'difficulty': '6', 'category_id': '2', 'group_id': '2', 'pic': 'https://w2.dwstatic.com/yy/ojiastoreimage/20160429_7a5105bb4b153caaf030c490ce36d5a1.jpg', 'web_fvideo_id': None, 'create_time': '2016-03-29 17:30:46', 'muscle_name': '胸大肌', 'name': '四足俯卧撑', 'id': '1', 'exe_explain_pic': [{'name': None, 'url': 'http://image.yy.com/ojiastoreimage/1462264660160_am__len94877.jpg', 'desc': '1、挺胸收腹,躯干与地面平行'}, {'name': None, 'url': 'http://image.yy.com/ojiastoreimage/1462264665645_am__len98161.jpg', 'desc': '\r1、双手与肩同宽,始终保持腰背挺直,控制肘部紧贴身体两侧'}], 'detail_video_id': None, 'equipment_ids': '3'}

之后,我们就可以通过这个dict数据取出想要的数据了。

3.4 页面跳转

以上实现的功能是取出单一页面的数据,但是可以看到Hi运动动作库中还有很多个视频页面需要提取,此时就需要为爬虫程序添加页面跳转的功能。
我们再继续观察'https://www.hiyd.com/dongzuo/'页面,可以发现源码中带有<div class="cont">的属于当前页所有动作的详细链接,获取这些链接传给get_info方法就像之前实现的逻辑获取视频详细信息了。
实现代码:

def get_pages(self, url):
    r = requests.get(url, headers=self.headers, timeout=5)
    soup = BeautifulSoup(r.text, "html.parser")
    for x in soup.find_all("div", class_="cont"):
        print(x.a.get('href'))

其中,soup.find_all("div", class_="cont")是取得所有带有class="cont"的div标签(因为class是python自有的,所以BeautifulSoup用class_进行替代),然后将得到的list结果进行迭代,获取子标签a的href属性值。得到的结果:

/dongzuo/1/
/dongzuo/2/
/dongzuo/3/
/dongzuo/4/
/dongzuo/5/
/dongzuo/6/
/dongzuo/7/
/dongzuo/8/
/dongzuo/9/
/dongzuo/10/
/dongzuo/11/
/dongzuo/12/
/dongzuo/13/
/dongzuo/14/
/dongzuo/15/
/dongzuo/16/
/dongzuo/17/
/dongzuo/18/
/dongzuo/19/
/dongzuo/20/

上面实现的是获取动作库中第一页的所有视频链接,但是还需要提取其余80页数据。分析动作库页面源码,可以看到<a href="/dongzuo/?page=2" onclick="_pageClick('next')" rel="next" title="下一页">下一页</a>显示了当存在下一页时,下一页的页面链接数据,而在第80页时没有此数据。据此,我们就可以获取到下一页的链接地址。

host = 'https://www.hiyd.com'
def get_pages(self, url):
    r = requests.get(url, headers=self.headers, timeout=5)
    soup = BeautifulSoup(r.text, "html.parser")
    for x in soup.find_all("div", class_="cont"):
        self.get_info(self.host + x.a.get('href'))
    next_page_url = str(soup.find("a", rel="next").get('href'))
    if next_page_url is not None and next_page_url != "/dongzuo/?page=3":
        self.get_pages(self.host + next_page_url)

获取的next_page_url如果不为None就递归调用get_pages方法,此处加的next_page_url != "/dongzuo/?page=3"是加一个页数限制,避免一下取80页。
至此,页面跳转和页面数据解析的功能已经全部实现完成,下面就是对数据进行存储。

3.5 保存数据

下面介绍两种保存方式,一是保存视频文件,二是将信息保存到数据库中。

3.5.1 保存视频文件

requests库支持流下载,查看文档原始响应内容 章节。

3-5-1.png

根据文档,实现视频下载逻辑:

def download(self, name, url):
    if os.path.exists("./video") is False:
        os.makedirs("./video")
    with requests.get(url, stream=True) as response:
        with open("./video/" + name + ".mp4", "wb") as file:
            for data in response.iter_content(chunk_size=1024):
                file.write(data)

首先在当前目录下创建video文件夹,然后用with语句调用requests.get(url, stream=True)(with语句的作用是保证response会调用close方法。在请求中把 stream 设为 True,Requests 无法将连接释放回连接池,除非你消耗了所有的数据,或者调用了 Response.close。)。接着调用open方法设置二进制写入方式,将response的迭代数据写入到文件中。

3.5.2 保存数据库

数据库选择Python自带比较简单的Sqlite数据库,无需安装驱动即可使用。相关使用方法,请见SQLite - Python.

下面是数据库相关操作的实现逻辑:

def __init__(self):
    self.conn = sqlite3.connect('fitness_test.db')
    self.create_db()

def create_db(self):
    self.conn.execute("CREATE TABLE IF NOT EXISTS fitness (id INTEGER PRIMARY KEY, "
                      "name TEXT, "
                      "muscle_name TEXT, "
                      "description TEXT, "
                      "video_url TEXT);")

def save_db(self, data):
    self.conn.execute("INSERT INTO fitness (name, muscle_name, description, video_url) VALUES(?, ?, ?, ?)", data)

def close_db(self):
    self.conn.commit()
    self.conn.close()

__init__初始化方法中连接数据库,如果没有会在当前目录下创建数据库,然后创建对应的表结构。save_db方法,接收一个list参数,对应sql插入语句中的参数。close_db方法,提交数据库事务,并关闭数据库。

3.6 完整代码

至此,Hi运动爬虫demo的功能已经开发完成,下面是完整的代码:

import os

from bs4 import BeautifulSoup
import requests
import re
import sqlite3
import json


class Fitness:
    host = 'https://www.hiyd.com'

    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
    }

    def __init__(self):
        self.conn = sqlite3.connect('fitness_test.db')
        self.create_db()

    def get_pages(self, url):
        r = requests.get(url, headers=self.headers, timeout=5)
        soup = BeautifulSoup(r.text, "html.parser")
        for x in soup.find_all("div", class_="cont"):
            self.get_info(self.host + x.a.get('href'))
        next_page_url = str(soup.find("a", rel="next").get('href'))
        if next_page_url is not None and next_page_url != "/dongzuo/?page=3":
            self.get_pages(self.host + next_page_url)
        else:
            self.close_db()

    def get_info(self, url):
        r = requests.get(url, headers=self.headers, timeout=5)
        soup = BeautifulSoup(r.text, "html.parser")
        text = str(soup.find_all("script")[-1])
        data = json.loads(re.search(r'e.init\((.+?)\);', text).group(1))
        self.download(data['name'], data['video_url'])
        self.save_db([data['name'], data['muscle_name'], data['description'], data['video_url']])
        print("done " + data['name'])

    def download(self, name, url):
        if os.path.exists("./video") is False:
            os.makedirs("./video")
        with requests.get(url, stream=True) as response:
            with open("./video/" + name + ".mp4", "wb") as file:
                for data in response.iter_content(chunk_size=1024):
                    file.write(data)

    def create_db(self):
        self.conn.execute("CREATE TABLE IF NOT EXISTS fitness (id INTEGER PRIMARY KEY, "
                          "name TEXT, "
                          "muscle_name TEXT, "
                          "description TEXT, "
                          "video_url TEXT);")

    def save_db(self, data):
        self.conn.execute("INSERT INTO fitness (name, muscle_name, description, video_url) VALUES(?, ?, ?, ?)", data)

    def close_db(self):
        self.conn.commit()
        self.conn.close()


if __name__ == "__main__":
    fitness = Fitness()
    fitness.get_pages('https://www.hiyd.com/dongzuo/')

4. 拓展

从上面的章节来看,可能会觉得爬虫程序很简单,但是这只是对于个人学习的demo,到真正的工程化项目来说还有很大的差距。困难主要在于:

可以看到,工程化的爬虫程序需要解决很多的实际问题,这些实际问题往往很难解决。所以入门爬虫的门槛很低,但是成长曲线很陡峭。接下来,介绍一些常见的爬虫进阶知识。

4.1 Scrapy框架

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。安装方式pip install Scrapy(需要翻墙,并且根据提示安装依赖库),或者通过Anaconda来便捷安装scrapy。

下面将Hi运动爬虫程序转换成Scrapy项目。

4.1.1 创建项目

进入打算存储代码的目录中,运行下列命令
scrapy startproject fitness
该命令将会创建包含下列内容的SpiderExercise 目录:

SpiderExercise/
    scrapy.cfg
    fitness/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...

这些文件分别是:

4.1.2 定义Item

Item 是保存爬取到的数据的容器;其使用方法和python字典类似,并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。类似在ORM中做的一样,可以通过创建一个 scrapy.Item 类,并且定义类型为 scrapy.Field 的类属性来定义一个Item。

首先根据需要从http://www.hiyd.com/获取到的数据对item进行建模。 我们需要从hiyd中获取肌肉id、动作库名称等字段。对此,在item中定义相应的字段。编辑 fitness目录中的 items.py 文件:

from scrapy.item import Item, Field


class FitnessItem(Item):
    muscle_id = Field()
    name = Field()
    difficulty_name = Field()
    training_points = Field()
    category_name = Field()
    muscle_name = Field()
    equipments = Field()
    description = Field()
    video = Field()
    gif = Field()
    muscle_pic = Field()
    muscle_front_img = Field()
    muscle_back_img = Field()
    other_muscles = Field()

一开始这看起来可能有点复杂,但是通过定义item,可以很方便的使用Scrapy的其他方法。而这些方法需要知道item的定义。

4.1.3 提取Item

从网页中提取数据有很多方法。Scrapy使用了一种基于XPathCSS 表达式机制。
这里给出XPath表达式的例子及对应的含义:

上边仅仅是几个简单的XPath例子,XPath实际上要比这远远强大的多。具体请参考XPath 教程

为了配合XPath,Scrapy除了提供了 Selector 之外,还提供了方法来避免每次从response中提取数据时生成selector的麻烦。

Selector有四个基本的方法:

下面就通过XPath提取Hi运动网站提取数据:

for href in response.xpath('//div[@class="cont"]/a[@target="_blank"]/@href').extract():
    url = response.urljoin(href)

4.1.4 编写爬虫

Spider是用户编写用于从单个网站(或者一些网站)爬取数据的类。其包含了一个用于下载的初始URL,如何跟进网页中的链接以及如何分析页面中的内容, 提取生成 item 的方法。
为了创建一个Spider,必须继承 scrapy.Spider 类,且定义以下三个属性:

以下为我们的第一个Spider代码,保存在fitness/spiders 目录下的 jirou.py 文件中:

import json

import scrapy

from fitness.items import FitnessItem


class DmozSpider(scrapy.Spider):
    name = "fitness"
    allowed_domains = ["hiyd.com"]
    start_urls = [
        "http://www.hiyd.com/dongzuo"
    ]

    def parse(self, response):
        for href in response.xpath('//div[@class="cont"]/a[@target="_blank"]/@href').extract():
            url = response.urljoin(href)
            yield scrapy.Request(url, callback=self.parse_info)
        next_page_url = response.xpath('//a[@rel="next"]/@href').extract_first()
        if next_page_url is not None and next_page_url != "/dongzuo/?page=2":
            yield scrapy.Request(response.urljoin(next_page_url))

    def parse_info(self, response):
        item = FitnessItem()
        text = response.xpath('//script').extract()[-1]
        data_text = text.split("e.init(")[1].split(");")[0]
        json_text = json.loads(data_text)
        other_muscle = json_text["otherMuscles"]
        temp = []
        if len(other_muscle) != 0:
            for x in other_muscle:
                temp.append(self.change_muscle_id(x["muscle_id"]))

        item['name'] = json_text["name"]
        item['difficulty_name'] = json_text["difficulty_name"]
        item['training_points'] = json_text["trainingPoints"]
        item['category_name'] = json_text["category_name"]
        item['muscle_name'] = json_text["muscle_name"]
        item['muscle_id'] = self.change_muscle_id(json_text["muscle_id"])
        item['other_muscles'] = ",".join(temp)
        item['equipments'] = ("徒手训练" if json_text["equipments"][0] is None else json_text["equipments"][0]["name"])
        item['description'] = json_text["description"]
        item['video'] = json_text["video_url"]
        item['gif'] = json_text["gif"]
        item['muscle_pic'] = json_text["muscle_pic"]
        item['muscle_front_img'] = json_text["muscle_front_img"]
        item['muscle_back_img'] = json_text["muscle_back_img"]
        item['file_urls'] = [json_text["video_url"]]
        yield item

    def change_muscle_id(self, muscle_id):
        """
        网站数据中,有些肌肉的id有错误,此方法是把错误的肌肉id纠正。
        :param muscle_id:
        :return:
        """
        unknown = ["27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37"]
        known = ["9", "21", "12", "19", "10", "16", "23", "20", "20", "16", "16"]
        if muscle_id in unknown:
            muscle_id = known[unknown.index(muscle_id)]
        return muscle_id

可以看到parse方法对每一页动作库的链接进行了递归处理,并且提取了下一页的链接,yield返回了一个函数生成器,方便Scrapy框架内部进行迭代处理。然后调用parse_info方法解析每个动作的详细信息。

Item 对象是自定义的python字典。可以使用标准的字典语法来获取到其每个字段的值。(字段即是我们之前用Field赋值的属性),一般来说,Spider将会将爬取到的数据以 Item 对象返回。

4.1.5 保存爬取到的数据

采用 JSON 格式对爬取的数据进行序列化,生成 items.json 文件。编写fitness/pipelines.py,将之前返回的item数据输入到管道中,进行保存。


from scrapy.pipelines.files import FilesPipeline
from urllib.parse import urlparse
from os.path import basename, dirname, join
from scrapy.conf import settings


class FitnessPipeline(object):

    def __init__(self):
        self.file = open('jirou.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + '\n'
        self.file.write(line)
        return item

    def spider_closed(self, spider):
        self.file.close()

可以看到,重写了process_item方法,将传入的参数item进行json格式转化,并且写入到文件中。重写spider_closed方法关闭文件流。

之后将管道配置到settings.py并且写入到文件中:

BOT_NAME = 'fitness'
BOT_VERSION = '1.0'

SPIDER_MODULES = ['fitness.spiders']
NEWSPIDER_MODULE = 'fitness.spiders'
#USER_AGENT = '%s/%s' % (BOT_NAME, BOT_VERSION)
USER_AGENT = 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 ' \
             '(KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
ITEM_PIPELINES = {
    'fitness.pipelines.FitnessPipeline': 300
}

4.1.6 开始爬取

执行命令scrapy crawl fitness, 可以看到生成了包含数据的json文件。

更多Scrapy框架功能,请见Scrapy文档

4.2 分布式爬虫

所谓的分布式爬虫,就是多台机器合作进行爬虫工作,提高工作效率。
分布式爬虫需要考虑的问题有:

Redis数据库是一种key-value数据库,所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行,单个操作是原子性的。而且Redis中自带的“消息队列”,方便来解决分布式爬虫的任务分配问题。

架构设计如下:

4-1.png
在Master端跑一个程序去生成所有任务(Request/url/ID)。Master端负责的是生产任务,并把任务去重、加入到待爬队列。Slaver只管从Master端拿任务去爬。
其中:

具体实现比较复杂,感兴趣的可以通过Scrapy+Redis的方式进一步实现。

4.3 反爬虫策略

对于大型网站来说,如果有很多爬虫应用在短时间内大规模爬取的话,会对这个网站系统的运维造成很大的压力。而且有些网站的信息是不希望被他人爬取的,所以各个网站都会有自己的一套反爬策略。常见的反爬策略有:

爬虫与反爬虫是一个斗智斗勇的过程,每个网站的反爬策略都会有所不同,需要对其具体情况具体分析。

4.4 模拟登录

有些网站会强制登录才能查看一下信息,此时就需要爬虫程序去模拟用户登录。下面我们尝试模拟登录百度贴吧获取当前用户关注的贴吧列表。

首先打开百度贴吧,然后输入用户名和密码登录。然后打开浏览器的开发者模式F12,转到network选项卡。在左边的Name一栏找到当前的网址,选择右边的Headers选项卡,查看Request Headers,这里包含了该网站颁发给浏览器的cookie。最好运行你的程序前再登录。如果太早登录,或是把浏览器关了,很可能复制的那个cookie就过期无效了
cookie是保存在发起请求的客户端中,服务器利用cookie来区分不同的客户端。因为http是一种无状态的连接,当服务器一下子收到好几个请求时,是无法判断出哪些请求是同一个客户端发起的。而“访问登录后才能看到的页面”这一行为,恰恰需要客户端向服务器证明:“我是刚才登录过的那个客户端”。于是就需要cookie来标识客户端的身份,以存储它的信息(如登录状态)。当然,这也意味着,只要得到了别的客户端的cookie,我们就可以假冒成它来和服务器对话。我们先用浏览器登录,然后使用开发者工具查看cookie。接着在程序中携带该cookie向网站发送请求,就能让你的程序假扮成刚才登录的那个浏览器,得到只有登录后才能看到的页面。

4-4-1.png

然后我们在源代码中找到进入“我的贴吧”链接,<a class="media_left" style="" href="/home/main?un=fobkbmdo&fr=index" target="_blank">,里面的href属性是跳转链接,fobkbmdo是我的贴吧用户名。然后分析一下在“我的贴吧”页面的源码,

</span>爱逛的吧</h1><div class="clearfix u-f-wrap" id="forum_group_wrap">         <a data-fid="59099" target="_blank" locate="like_forums#ihome_v1" href="/f?kw=%E6%9D%8E%E6%AF%85&fr=home"         class="u-f-item unsign"><span>李毅</span><span class="forum_level lv2"></span></a>         <a data-fid="407248" target="_blank" locate="like_forums#ihome_v1" href="/f?kw=%E5%8D%95%E6%9C%BA%E6%B8%B8%E6%88%8F&fr=home"         class="u-f-item unsign"><span>单机游戏</span><span class="forum_level lv1"></span></a>         <a data-fid="113893" target="_blank" locate="like_forums#ihome_v1" href="/f?kw=%E6%98%BE%E5%8D%A1&fr=home"         class="u-f-item unsign"><span>显卡</span><span class="forum_level lv1"></span></a>         <a data-fid="543521" target="_blank" locate="like_forums#ihome_v1" href="/f?kw=%E6%8E%A8%E7%90%86&fr=home"         class="u-f-item unsign"><span>推理</span><span class="forum_level lv1"></span></a>         <a data-fid="825" target="_blank" locate="like_forums#ihome_v1" href="/f?kw=%E8%80%83%E7%A0%94&fr=home"         class="u-f-item unsign"><span>考研</span>

可以根据此提取出相关的信息,实现代码如下:

import requests
from bs4 import BeautifulSoup

url = 'http://tieba.baidu.com/home/main?un=fobkbmdo&fr=index'

cookie_str = '填充自己的cookie数据'
cookies = {}
for line in cookie_str.split(';'):
    key, value = line.split('=', 1)
    cookies[key] = value

headers = {
    'User-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'}

resp = requests.get(url, headers=headers, cookies=cookies)
soup = BeautifulSoup(str(resp.content.decode('utf-8')), "html.parser")
for x in soup.find_all("a", class_="u-f-item unsign"):
    print(x.span.text)

4.5 模拟请求

下面介绍通过爬虫模拟网络请求,以百度贴吧自动发帖为例。

首先选择进入一个贴吧,选择一个帖子,然后打开F12开发者模式,转到network选项卡。手动发表回复,查看network变化:

4-5-1.png

发现是多了一个add请求,点击查看发现具体的联网请求如下:

Request URL: https://tieba.baidu.com/f/commit/post/add
Request Method: POST
Status Code: 200 OK
Remote Address: 127.0.0.1:50578
Referrer Policy: no-referrer-when-downgrade

可以发现“发表”功能其实是发送了https://tieba.baidu.com/f/commit/post/addPOST请求。接着查看一下请求的数据:

4-5-2.png

上面这些数据就是我们需要用代码模拟的数据,其中我们只需要关心修改下面的数据即可:

下面我们使用代码来模拟发表的功能:

import requests
from bs4 import BeautifulSoup
from selenium import webdriver

url_send = 'https://tieba.baidu.com/f/commit/post/add'

cookie_str = '输入你的登录cookie'
cookies = {}
for line in cookie_str.split(';'):
    key, value = line.split('=', 1)
    cookies[key] = value

headers = {
    'User-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'}


def send_posts():
    data = str2dic('ie=utf-8&kw=%E5%8D%95%E6%9C%BA%E6%B8%B8%E6%88%8F&fid=407248&tid=5649760137&vcode_md5=&floor_num=23&rich_text=1&tbs=f0f598aecd004a1c1524122217&content=%5Bbr%5D%E9%AD%94%E5%85%BD%E4%BA%89%E9%9C%B8&basilisk=1&files=%5B%5D&mouse_pwd=99%2C99%2C97%2C121%2C98%2C103%2C96%2C98%2C92%2C100%2C121%2C101%2C121%2C100%2C121%2C101%2C121%2C100%2C121%2C101%2C121%2C100%2C121%2C101%2C121%2C100%2C121%2C101%2C92%2C103%2C102%2C99%2C109%2C98%2C92%2C100%2C108%2C103%2C101%2C121%2C100%2C101%2C109%2C101%2C15241222546850&mouse_pwd_t=1524122254685&mouse_pwd_isclick=0&__type__=reply&_BSK=JVwSUGcLBF83AFZzQztEElBFCwIWaVcbHD5WBH8Vd2BpAHxNUnpmR1VVPDsXSAZLKBx9Dl42WSstG01BMAAGEWc4XCUIIBpRRVJrZ3oNIl5QKQhRNhUIBHEGfl4IaRMyIgVpfkRsWhklTQhRHBtRM3FLUlRrAhILbT57CywAWhAGDi4yemIQVFUiTgYGXTQ%2FPFV%2FWQZndFNGAG9iShwSWGl7PlIfIlFoa01WTXYHBgdnFAF9W2xHAF5XaXU0FFcNHn9XF3IFc2FgHHIYAGt%2BXRNSMSQBAQgHegplFExgCXBuS1BSaRNUGmdPEXpWDlMCWBMnJH8XRxINDEIUd1N2NmQJaA5WKiBNRQc8ZgccH1t9GW0GTGEPYmxMRFQBEwgJIEQRZUF%2BRgFdV3BmawlXWQxvXQYxRzM1fRIxXhFzZExMAW17Rl0ZS3MIOVUSI11rfAlVQX8TVEQ2AX46Ej8XVw9LJzsvV1lRUS4SVWlWKj8iVXwJQSgpGAYfLjIISwYeIEY7Wwl8SCYsGw8XaV5UTisQQXMVIwYcBgIrMC5NWVRSIhRDIRkqPzJRJAZcJ2gZGlAoOgFDXkUmWjZTFz4UKT8TBE8tWFdfKgdKcw0jFVEeDio5OEQHG1MoCVMnVDR8IVUiHFwnJREXUi97F05YBiVEPVUMIxprfA5TQX8Tc0IrRgF9TW4CAUhdZzEvSxZDVyIJBjFaFSQjWT4IG2BkBlVoMzYQRFwMaUswUBsNGDp8UkMKdBMeCzEHRjpNbgUCSF1lZmMXRRscIFYEfxckMSJZPAZAIhscOUVtPQMPBks9Gn0OXmENdWpPU1F3BhwHZxkBfVtsAkIfAml1PhRXDRwDMmoJF2pyPQFyVREzLFA2fX97Rl8bS3MKOUEQM0wuMRBBESRfQEQoXRp%2FGmwtXgsTLCE%2FBRZYWig6BjgXanImAXJVEQcRMTkRcXUHHAhTaVwtQRst')
    data['kw'] = '单机游戏'
    # 当前层数 - 1
    data['floor_num'] = 23
    data['content'] = '星际'
    data['title'] = '单机游戏'
    resp = requests.post(url_send, headers=headers, cookies=cookies, data=data)
    print(resp.text)


def str2dic(text):
    idic = {}
    ilist = text.split('&')
    for item in ilist:
        name, value = item.split('=', 1)
        idic[name] = value
    return idic

send_posts()

当输出的内容为{"no":0,"err_code":0,"error":null,"data":{"autoMsg":"","fid":407248,"fname":"\u5355\u673a\u6e38\u620f","tid":5649760137,"is_login":1,"content":"\u661f\u9645","access_state":null,"vcode":{"need_vcode":0,"str_reason":"","captcha_vcode_str":"","captcha_code_type":0,"userstatevcode":0},"is_post_visible":0}}时,证明已经发表成功。

4-5-3.png

除了模拟发送请求外,还有另外一种方式:使用无头浏览器访问
在Python中可以使用Selenium库来调用浏览器,写在代码里的操作(打开网页、点击……)会变成浏览器忠实地执行。这个被控制的浏览器可以是Firefox,Chrome等,但最常用的还是PhantomJS这个无头(没有界面)浏览器。也就是说,通过模拟浏览器的点击、提交等操作来实现模拟人工处理。

安装selenium库:pip install seleniumPhantomJS浏览器下载

下面我们以百度贴吧的签到来体验一下。首先需要找到“签到”的标签:

4-5-4.png

然后右键,Copy, Copy XPath,获取到控件的XPath路径。

from selenium import webdriver

url_sign = 'https://tieba.baidu.com/f?kw=%E6%8E%A8%E7%90%86&fr=home'

cookie_str = '输入你的登录cookie'
cookies = {}
for line in cookie_str.split(';'):
    key, value = line.split('=', 1)
    cookies[key] = value

headers = {
    'User-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'}


def sign():
    browser = webdriver.PhantomJS('D:/phantomjs-2.1.1-windows/bin/phantomjs.exe')
    for key in cookies:
        c = {}
        c['name'] = key
        c['value'] = cookies[key]
        c['domain'] = '.baidu.com'
        c['path'] = '/'
        c['httponly'] = False
        c['secure'] = False
        browser.add_cookie(c)

    browser.get(url_sign)
    browser.implicitly_wait(3)

    sign_btn = browser.find_elements_by_xpath('//*[@id="signstar_wrapper"]/a')[0]
    sign_btn.click()
    sign_btn.click()

    print(browser.page_source.encode('utf-8').decode())

    browser.quit()

PhantomJS的cookie需要单独进行配置,填充一些必要的参数。接着就获取了“签到”的标签,并开始点击(不知道为什么,代码签到必须点击两下),然后退出浏览器。
执行完成后,刷新查看是否已经签到

4-5-5.png

5. 爬虫应用畅想

经过以上的介绍,可能已经对爬虫有所了解了。对于爬虫来说,其核心是数据源的获取,如果没有数据源就没有爬虫存在的意义。未来的生活可能离不开各种各样的数据,我们每天从不同的渠道获取数据,也会在各种场景中生产数据,对于数据的收集和分析可以帮助我们更好地行业规划和了解行业规律。所以,数据源的选取会给爬虫的应用插上想象的翅膀。下面我们就脑洞大开,想象一下应用爬虫技术会给我们提供什么样的帮助。

上一篇下一篇

猜你喜欢

热点阅读