爬虫概述

  1. 爬虫:一种程序,用于获取互联网上的数据,执行自动化测试等
  2. robots.txt:一个协议,规定了网站中哪些数据可以被爬取,哪些不可以被爬取。一般位于网站的根目录下,例如知乎网站访问这个文件的路径为https://www.zhihu.com/robots.txt
  3. 爬虫的采集方案分类
    1. 利用HTTP协议采集
    2. 利用api接口采集
    3. 利用目标网站向大众提供的api进行采集
  4. 一个简单的爬虫示例:爬取网站的html文件
from urllib.request import urlopen

url = "http://www.baidu.com"
resp = urlopen(url)

with open("baidu.html",mode="w",encoding="utf-8") as f:
    f.write(resp.read().decode("utf-8"))

HTTP协议

  1. http协议是web开发中以及爬虫需要熟悉的内容,这一部分详见图解HTTP

requests库

  1. 这个第三方库提供了很多的方法,比如说用于模拟客户端浏览器向服务器发送各种不同的请求类型,获取HTTP响应报文的状态行,响应头,响应体等
  2. 简单示例如下:访问百度官网首页
import requests

params = {
    'name': 'NRVCER',
    'age': 23
}
headers = {
    'uid': '1001',
    'date': '2024/1/11',
}

ret = requests.get('http://www.baidu.com', params=params, headers=headers)
print(ret.text)     # Content of the response
print(ret.url)      # path,http://www.baidu.com/?name=NRVCER&age=23
print(ret.headers)  # response headers
  1. 需要特别注意服务器响应的数据的编码。

re模块

1.元字符
  1. .:匹配除"\r","\n"之外的任意单个字符
  2. ^:匹配开始位置,多行模式下匹配每一行的开始
print(re.findall('^h...o','hellftesthello'))   #[]
print(re.findall('^h...o','hellotesthello'))   #['hello']
  1. $:匹配结束位置,多行模式下匹配每一行的结束
  2. *:匹配前一个元字符0到多次
print(re.findall('h*','hellohhhh')) #['h', '', '', '', '', 'hhhh', '']
  1. +:匹配前一个元字符1到多次
print(re.findall('h+','hellohhhh')) #['h', 'hhhh']
  1. ?:匹配前一个元字符0到一次
  2. {m,n}:匹配前一个元字符m到n次
  3. \\:转义字符,跟在其后的字符将失去作为特殊元字符的含义。比如说\\.只能匹配.
  4. []:字符集,可以匹配其中任意一个字符
  5. |:或的意思,比如说a|b表示可以匹配a或者b
  6. \b:匹配一个边界,这个边界指的是单词和特殊字符间的位置
print(re.findall(r'hi\b','hi'))    #['hi']
print(re.findall(r'hi\b','hi$hi testhi^'))    #['hi', 'hi', 'hi']
  1. \B:匹配非边界
print(re.findall(r'hi\B','ti$hitesthi^'))    #['hi']
  1. \d:匹配任意一个数字, 相当于[0-9]
  2. \D:匹配任意一个非数字字符,相当于[^0-9]
  3. \s:匹配任意一个空白字符,空白字符比如说 ,\t,'\n','\r','\f','\v';等同于[ \t\n\r\f\v]
import re
# [' ', ' ', ' ', ' ', ' ', '\r', '\n', '\t', '\x0c', '\x0b']
print(re.findall('\s','test_     \r\n\t\f\vtest'))
  1. \S:匹配任意一个非空白字符,等同于[^ \t\n\r\f\v]
  2. \w:匹配数字、字母、下划线中任意一个字符;等同于 [a-zA-Z0-9_]
  3. \W:匹配非数字、字母、下划线中的任意一个字符,等同于[^a-zA-Z0-9_]
2.模式

模式就是一些RegexFlag

  1. DOTALL:这个模式下.的匹配不受限制,可匹配任何字符,包括换行符
3.函数

python中的内置模块re提供了很多函数。

  1. match:从字符串开始进行匹配,如果开始处不匹配则不再继续寻找
s = '''first line
second line
third line'''

regex = re.compile("s\w+")
print(regex.match(s)) # None


import re

info = "my name is XCER, 生日:2000/1/14,毕业于2024/7/1。"
result = re.match(".*生日.*?(\d{4}).*毕业于.*?(\d{4})", info)

for i in range(len(result.groups())):
    print(result.group(i + 1))

  1. sub:用于替换字符串
  2. search:函数类似于match,不同之处在于不限制正则表达式的开始匹配位置
import re

info = "my name is XCER, 生日:2000/1/14,毕业于2024/7/1。"
result = re.search("生日.*?(\d{4}).*毕业于.*?(\d{4})", info)

for i in range(len(result.groups())):
    print(result.group(i + 1))


  1. findall:返回一个匹配时符合正则表达式规则的列表
4. 方法
5.分组
  1. 被分组的内容可以被单独取出,默认每个分组有个索引,从1开始,按照"("的顺序决定索引值

BeautifulSoup库

  1. 这个库用于解析HTML或者XML文件,从中提取数据。
  2. 简单示例如下:来自官网
from bs4 import BeautifulSoup

html_doc = """
<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>
</body>
</html>
"""

soup = BeautifulSoup(html_doc, 'html.parser')

# represents the document as a string
# print(soup.prettify())

# <title>The Dormouse's story</title>
print(soup.title)

# u'title'
print(soup.title.name)

# u'The Dormouse's story'
print(soup.title.string)

# u'head'
print(soup.title.parent.name)

# <p class="title"><b>The Dormouse's story</b></p>
print(soup.p)

# ['title']
print(soup.p['class'])

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
print(soup.a)

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
print(soup.find_all('a'))

for link in soup.find_all('a'):
    print(link.get('href'))
# http://example.com/elsie
# http://example.com/lacie
# http://example.com/tillie

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
print(soup.find(id="link3"))

# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...
print(soup.get_text())

XPath

1.简介
  1. 全称XML Path Language,即XML路径语言。这个语言可以用于编写解析HTML或者XML文件的语句,方便从中提取数据。
2.XPath语法规则
  1. /:表示从根节点选取。
    1. /html表示选取根元素html
    2. /article/div[1]:选取属于article子元素的第一个div元素
    3. /article/div[last()]:选取属于article子元素的最后一个div元素
    4. /article/div[last()-1]:选取属于article子元素的倒数第二个div元素
    5. /div/*:选取属于div元素的所有子节点
  2. //:表示从子孙节点选取。
    1. //div:表示选取所有div元素
    2. //div[@class]:表示从子孙结点开始选取所有拥有class属性的div元素
    3. //div[@class='value']:表示从子孙结点开始选取所有拥有class属性,其属性值为value的div元素
    4. //*:选取所有元素
    5. //div[@*]:选取所有带属性的div元素
    6. //div/a | //div/p:选取所有div元素的子元素a或者p
    7. //div[@id='value']:表示从子孙结点开始选取所有拥有id属性,其属性值为value的div元素
  3. ./: 表示从当前节点选取
  4. ..:表示从当前节点的父节点选取
  5. @属性名称:选取所有指定属性名的属性
  6. 元素名:表示选取所有元素名的所有子节点
    1. div/a:选取所有属于div元素的子元素a
    2. div//a:选取所有属于div元素的后代元素a
3.XPath中的常用函数
  1. last
  2. contains
  3. text

CSS选择器

这一部分详见CSS基础,主要熟悉CSS中的选择器

1.语法规则
  1. *:选择所有元素
  2. #info:选择所有id为info的元素
  3. .first:选择所有class包含first的元素
  4. div a:选择div元素下的所有后代a元素
  5. ul + li:选择ul元素后面的第一个li元素
  6. div#info > span:选择id为info的div元素的第一个span子元素
  7. span ~ a:选择与span元素相邻的所有a元素
  8. a[href]:选择所有具有href属性的a元素
  9. a[href="http://www.baidu.com"]:选择所有href属性,值为http://www.baidu.com的a元素
  10. a[href*="baidu.com"]:选择所有href属性值包含baidu.com的a元素
  11. a[href^="http"]:选择所有href属性值以http开头的a元素
  12. a[href$="jpg"]:选择所有href属性值以jpg结尾的a元素
  13. input[type="radio"]:checked:选择选中的radio的元素
  14. div:not(#info):选择所有id属性值非info的div
  15. li:nth-child(2):选择第二个li元素
  16. tr:nth-child(2n):选择第偶数个tr元素
  17. a::attr(href):选取a元素的href属性的值

scrapy

这一部分详见scrapy基础

pymysql/peewe/mysqlclient

因为爬虫涉及到数据的存储,需要使用到数据库。因此这里了解一下python如何操作MYSQL数据库

1.pymysql

pymysql是一个包,可以作为一个MySQL客户端访问MySQL数据库

  1. 安装依赖:pip install PyMySQL[rsa]
  2. 简单示例:来自官方文档
    1. 表结构如下:
    CREATE TABLE `users` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `email` varchar(255) COLLATE utf8_bin NOT NULL,
        `password` varchar(255) COLLATE utf8_bin NOT NULL,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
    AUTO_INCREMENT=1 ;
    
    1. 操作users表代码示例如下,MySQL服务器版本为8.0.35
    import pymysql.cursors
    
    # Connect to the database
    connection = pymysql.connect(host='localhost',
                                 user='root',
                                 password='123456',
                                 database='spider',
                                 cursorclass=pymysql.cursors.DictCursor)
    
    with connection:
        with connection.cursor() as cursor:
            # Create a new record
            sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
            cursor.execute(sql, ('2549392021@qq.com', '123456'))
    
        # connection is not autocommit by default. So you must commit to save
        # your changes.
        connection.commit()
    
        with connection.cursor() as cursor:
            # Read a single record
            sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
            cursor.execute(sql, ('2549392021@qq.com',))
            result = cursor.fetchone()
            print(result)
    
2.peewe

peewe是一个ORM框架。

  1. 安装依赖:pip install peewee
  2. peewe中字段的定义和MySQL中字段的对应关系如下:https://docs.peewee-orm.com/en/latest/peewee/models.html
  3. 简单示例:
    1. 创建表:
    from peewee import *
    
    db = MySQLDatabase("spider", host="127.0.0.1", port=3306, user="root", password="123456")
    
    
    # Model definition,一个Model class 等同于一张表,表名为类名小写
    class Person(Model):
        name = CharField(max_length=16)
        birthday = DateField()
    
        class Meta:
            database = db
    
    db.create_tables([Person])
    
    1. 表记录的增删改查
    from peewee import *
    
    db = MySQLDatabase("spider", host="127.0.0.1", port=3306, user="root", password="123456")
    
    # Model definition,一个Model class 等同于一张表,表名为类名小写
    class Person(Model):
        name = CharField(max_length=16)
        birthday = DateField()
    
        class Meta:
            database = db
    
    #记录的增、删、改、查
    if __name__ == "__main__":
        from datetime import date
    
        #增
        user_xcer = Person(name='xcer', birthday=date(2000, 1, 15))
        user_xcer.save()  # user_xcer is now stored in the database
    
        user_nrv = Person(name='nrv', birthday=date(1988, 8, 25))
        user_nrv.save()  # user_nrv is now stored in the database
    
        #查,get方法在取不到数据会抛出异常
        try:
            user = Person.select().where(Person.name == 'nrv').get()
            if user:
                print(user.name)
        except DoesNotExist as e:
            print("查询的用户不存在")
    
        query = Person.select().where(Person.name == 'xcer')
        for person in query:
            # 删
            person.delete_instance()
    
            # person.birthday = date(2023, 1, 17)
            # # 改
            # person.save()
    
    
3.mysqlclient
  1. 安装依赖:pip install mysqlclient
  2. 简单示例如下:
import MySQLdb
from MySQLdb.cursors import DictCursor

conn = MySQLdb.connect(host='localhost', user='root', passwd='123456', db='spider',
                       charset='utf8', cursorclass=MySQLdb.cursors.DictCursor)

c = conn.cursor()
# 创建表
c.execute('''CREATE TABLE users(
  id int NOT NULL AUTO_INCREMENT,
  name varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL,
  password varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL,
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
''')

# 增
c.execute("insert into users values('1','xcer','admin')")
sql = "insert into users values(%s,%s,%s)"
c.executemany(sql, [
    ('2', 'Nrv', '123456'),
    ('3', 'root', '123456'),
    ('4', 'admin', 'admin')
])
# 查
c.execute("select * from users")
for row in c.fetchall():
    print(row)

c.close()

c = conn.cursor()
# 改
sql = "update users set password=%s where name = %s"
c.execute(sql, ('147258369', 'xcer'))

# 删
sql = "delete from users where name=%s"
c.execute(sql, ('admin',))

c.close()

conn.commit()

conn.close()

Selenium

1.安装及配置

一个支持web浏览器自动化的综合项目

  1. 安装依赖:pip install -i https://pypi.douban.com/simple/ selenium,版本为4.16.0
  2. 安装与自己浏览器版本兼容的ChromeDriver
  3. 简单示例如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

# 指定chromedriver的位置
service = Service("D:/chrome/chromedriver-win64/chromedriver-win64/chromedriver.exe")
options = Options()
#指定chrome浏览器二进制文件的位置
options.binary_location = "D:/chrome/Google/Chrome/Application/chrome.exe"

chrome_browser = webdriver.Chrome(options=options, service=service)
chrome_browser.get('http://www.baidu.com')

# 页面HTML代码
# print(chrome_browser.page_source)

chrome_browser.close()
2.反爬

一些站点做了反爬措施,会识别到爬虫使用selenium模拟浏览器。这个时候可以研究一下undetected_chromedriver这个开源项目

3.实现模拟登录
  1. 对于需要账号密码登录的网站,可以使用Selenium实现模拟登录,获取服务器返回的cookie。
  2. 示例如下:模拟登录地址,获取服务器返回的cookie
import time

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

URL = "xxx"


def login(username, password, chrome_browser):
    chrome_browser.get(URL)
    time.sleep(1)

    # 窗口最大化
    chrome_browser.maximize_window()

    # 根据XPath语法查找各个元素
    username_ele = chrome_browser.find_element(By.XPATH, '//input[@placeholder="请输入手机号"]')
    password_ele = chrome_browser.find_element(By.XPATH, '//input[@placeholder="请输入密码"]')
    username_ele.send_keys(username)
    password_ele.send_keys(password)


    login_btn = chrome_browser.find_element(By.XPATH, '//button[@class="login-btn ant-btn ant-btn-primary"]')
    chrome_browser.execute_script("arguments[0].click();", login_btn)

    time.sleep(2)

    cookies = chrome_browser.get_cookies()
    cookie_dict = {}
    for item in cookies:
        cookie_dict[item["name"]] = item["value"]

    chrome_browser.close()
    return cookie_dict


if __name__ == '__main__':
    # 指定chromedriver的位置
    service = Service("D:/chrome/chromedriver-win64/chromedriver-win64/chromedriver.exe")
    options = Options()
    # 指定chrome浏览器二进制文件的位置
    options.binary_location = "D:/chrome/Google/Chrome/Application/chrome.exe"

    chrome_browser = webdriver.Chrome(options=options, service=service)

    username = "xxx"
    password = "xxx"
    cookie_dict = login(username, password, chrome_browser)

反爬/反反爬

1.概念
  1. 反爬:使用某种技术手段,阻止批量获取数据
  2. 反反爬:使用某种技术手段,绕过对方设置的反爬策略
2.反爬及反反爬策略
  1. user-agent字段
    1. 反爬:通过识别HTTP请求头的user-agent字段值,比如说在nginx中可以配置反爬策略:
    # 禁止Scrapy/Curl/HttpClient/python等工具爬取
    if ($http_user_agent ~= (Scrapy|Curl|HttpClient|python)) {
        return 403;
    }
    
    1. 反反爬:在爬虫程序中,需要程序员设置user-agent字段的值。
      1. 使用到fake-useragent库
      pip install fake-useragent
      
      1. 示例如下:
      import requests
      from fake_useragent import UserAgent
      
      
      ua = UserAgent()
      headers = {
          "User-Agent": ua.random
      }
      
      # send a get request
      res = requests.get("http://www.baidu.com", headers=headers)
      print(res.text)
      
  2. IP访问频率限制
    1. 反爬:nginx可以统计IP的访问频率,如果某个IP的访问频率过快会被ban
    2. 反反爬:通过批量的代理IP绕过反反爬。网络上有很多免费或者收费的代理IP
  3. 强制登录才能看到更过信息,比如说京东网
  4. 动态网页,大部分数据通过XHR请求获得
  5. 前端的JS逻辑加密和混淆
  6. 机器学习分析爬虫行为
  7. CSS代码下毒,比如说某个a标签对使用浏览器进行访问的客户进行隐藏,而对爬虫透明。所以访问过该a标签的客户判定为爬虫