网络爬虫-04

Spider03回顾

目前反爬总结

  • 反爬虫梳理

    1】Headers反爬虫
       1.1) 检查: Cookie、Referer、User-Agent
       1.2) 解决方案: 通过F12获取headers,传给requests.get()方法
            
    【2】IP限制
       2.1) 网站根据IP地址访问频率进行反爬,短时间内限制IP访问
       2.2) 解决方案: 
            a) 构造自己IP代理池,每次访问随机选择代理,经常更新代理池
            b) 购买开放代理或私密代理IP
            c) 降低爬取的速度
            
    【3】User-Agent限制
       3.1) 类似于IP限制,检测频率
       3.2) 解决方案: 构造自己的User-Agent池,每次访问随机选择
            a> fake_useragent模块
            b> 新建py文件,存放大量User-Agent
            
    【4】对响应内容做处理
       4.1) 页面结构和响应内容不同
       4.2) 解决方案: 打印并查看响应内容,用xpath或正则做处理
    

requests模块参数总结

1】方法一 : requests.get()2】参数
   2.1) url
   2.2) headers
   2.3) timeout
   2.4) proxies

【3】方法二 :requests.post()4】参数
    data

解析模块总结

  • re正则解析

    import re 
    pattern = re.compile('正则表达式',re.S)
    r_list = pattern.findall(html)
    
  • lxml+xpath解析

    from lxml import etree
    p = etree.HTML(res.text)
    r_list = p.xpath('xpath表达式')
    
    【谨记】只要调用了xpath,得到的结果一定为'列表'
    

xpath表达式

  • 匹配规则

    1】结果: 节点对象列表
       1.1) xpath示例: //div、//div[@class="student"]//div/a[@title="stu"]/span
    
    【2】结果: 字符串列表
       2.1) xpath表达式中末尾为: @src、@href、/text()
    
  • 最常用

    1】基准xpath表达式: 得到节点对象列表
    【2for r in [节点对象列表]:
           username = r.xpath('./xxxxxx')
    
    【注意】遍历后继续xpath一定要以:  . 开头,代表当前节点
    

Spider04笔记

requests.post()

  • 适用场景

    1】适用场景 : Post类型请求的网站
    
    【2】参数 : data={}
       2.1) Form表单数据: 字典
       2.2) res = requests.post(url=url,data=data,headers=headers)3】POST请求特点 : Form表单提交数据
    

控制台抓包

  • 打开方式及常用选项

    1】打开浏览器,F12打开控制台,找到Network选项卡
    
    【2】控制台常用选项
       2.1) Network: 抓取网络数据包
         a> ALL: 抓取所有的网络数据包
         b> XHR:抓取异步加载的网络数据包
         c> JS : 抓取所有的JS文件
       2.2) Sources: 格式化输出并打断点调试JavaScript代码,助于分析爬虫中一些参数
       2.3) Console: 交互模式,可对JavaScript中的代码进行测试
        
    【3】抓取具体网络数据包后
       3.1) 单击左侧网络数据包地址,进入数据包详情,查看右侧
       3.2) 右侧:
         a> Headers: 整个请求信息
            General、Response Headers、Request Headers、Query String、Form Data
         b> Preview: 对响应内容进行预览
         c> Response:响应内容
    

有道翻译破解案例(post)

  • 目标

    破解有道翻译接口,抓取翻译结果
    # 结果展示
    请输入要翻译的词语: elephant
    翻译结果: 大象
    *************************
    请输入要翻译的词语: 喵喵叫
    翻译结果: mews
    
  • 实现步骤

    1】浏览器F12开启网络抓包,Network-All,页面翻译单词后找Form表单数据
    【2】在页面中多翻译几个单词,观察Form表单数据变化(有数据是加密字符串)
    【3】刷新有道翻译页面,抓取并分析JS代码(本地JS加密)
    【4】找到JS加密算法,用Python按同样方式加密生成加密数据
    【5】将Form表单数据处理为字典,通过requests.post()的data参数发送
    
  • 具体实现

1、开启F12抓包,找到Form表单数据如下:

i: 喵喵叫
from: AUTO
to: AUTO
smartresult: dict
client: fanyideskweb
salt: 15614112641250
sign: 94008208919faa19bd531acde36aac5d
ts: 1561411264125
bv: f4d62a2579ebb44874d7ef93ba47e822
doctype: json
version: 2.1
keyfrom: fanyi.web
action: FY_BY_REALTlME

2、在页面中多翻译几个单词,观察Form表单数据变化

salt: 15614112641250
sign: 94008208919faa19bd531acde36aac5d
ts: 1561411264125
bv: f4d62a2579ebb44874d7ef93ba47e822
# 但是bv的值不变

3、一般为本地js文件加密,刷新页面,找到js文件并分析JS代码

【方法1: Network - JS选项 - 搜索关键词salt
【方法2: 控制台右上角 - Search - 搜索salt - 查看文件 - 格式化输出

【结果】 : 最终找到相关JS文件 : fanyi.min.js

4、打开JS文件,分析加密算法,用Python实现

【ts】经过分析为13位的时间戳,字符串类型
   js代码实现)  "" + (new Date).getTime()
   python实现) str(int(time.time()*1000))

【salt】
   js代码实现)  ts + parseInt(10 * Math.random(), 10);
   python实现)  ts + str(random.randint(0,9))

【sign】('设置断点调试,来查看 e 的值,发现 e 为要翻译的单词')
   js代码实现) n.md5("fanyideskweb" + e + salt + "n%A-rKaT5fb[Gy?;N5@Tj")
   python实现)
   from hashlib import md5
   string = "fanyideskweb" + e + salt + "n%A-rKaT5fb[Gy?;N5@Tj"
   s = md5()
   s.update(string.encode())
   sign = s.hexdigest()

5、pycharm中正则处理headers和formdata

1】pycharm进入方法 :Ctrl + r ,选中 Regex
【2】处理headers和formdata
    (.*): (.*)
    "$1": "$2",3】点击 Replace All

5、代码实现

import requests
import time
import random
from hashlib import md5

class YdSpider(object):
  def __init__(self):
    # url一定为F12抓到的 headers -> General -> Request URL
    self.url = 'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
    self.headers = {
      # 检查频率最高 - 3个
      "Cookie": "OUTFOX_SEARCH_USER_ID=970246104@10.169.0.83; OUTFOX_SEARCH_USER_ID_NCOO=570559528.1224236; _ntes_nnid=96bc13a2f5ce64962adfd6a278467214,1551873108952; JSESSIONID=aaae9i7plXPlKaJH_gkYw; td_cookie=18446744072941336803; SESSION_FROM_COOKIE=unknown; ___rl__test__cookies=1565689460872",
      "Referer": "http://fanyi.youdao.com/",
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36",
    }

  # 获取salt,sign,ts
  def get_salt_sign_ts(self,word):
    # ts
    ts = str(int(time.time()*1000))
    # salt
    salt = ts + str(random.randint(0,9))
    # sign
    string = "fanyideskweb" + word + salt + "n%A-rKaT5fb[Gy?;N5@Tj"
    s = md5()
    s.update(string.encode())
    sign = s.hexdigest()

    return salt,sign,ts

  # 主函数
  def attack_yd(self,word):
    # 1. 先拿到salt,sign,ts
    salt,sign,ts = self.get_salt_sign_ts(word)
    # 2. 定义form表单数据为字典: data={}
    # 检查了salt sign
    data = {
      "i": word,
      "from": "AUTO",
      "to": "AUTO",
      "smartresult": "dict",
      "client": "fanyideskweb",
      "salt": salt,
      "sign": sign,
      "ts": ts,
      "bv": "7e3150ecbdf9de52dc355751b074cf60",
      "doctype": "json",
      "version": "2.1",
      "keyfrom": "fanyi.web",
      "action": "FY_BY_REALTlME",
    }
    # 3. 直接发请求:requests.post(url,data=data,headers=xxx)
    html = requests.post(
      url=self.url,
      data=data,
      headers=self.headers
    ).json()
    # res.json() 将json格式的字符串转为python数据类型
   	#    .json() 等价于json.loads()方法
    #   	html = request.post()
   	#		html = json.loads()
    result = html['translateResult'][0][0]['tgt']

    print(result)

  # 主函数
  def run(self):
    # 输入翻译单词
    word = input('请输入要翻译的单词:')
    self.attack_yd(word)

if __name__ == '__main__':
  spider = YdSpider()
  spider.run()

动态加载数据抓取-Ajax

  • 特点

    1】右键 -> 查看网页源码中没有具体数据
    【2】滚动鼠标滑轮或其他动作时加载,或者页面局部刷新
    
  • 抓取

    1】F12打开控制台,页面动作抓取网络数据包
    【2】抓取json文件URL地址
       2.1) 控制台中 XHR :异步加载的数据包
       2.2) XHR -> QueryStringParameters(查询参数)
    

豆瓣电影数据抓取案例

  • 目标

    1】地址: 豆瓣电影 - 排行榜 - 剧情
    【2】目标: 电影名称、电影评分
    
  • F12抓包(XHR)

    1】Request URL(基准URL地址) :https://movie.douban.com/j/chart/top_list?
    【2】Query String(查询参数)
        # 抓取的查询参数如下:
        type: 13 # 电影类型
        interval_id: 100:90
        action: ''
        start: 0  # 每次加载电影的起始索引值 0 20 40 60 
        limit: 20 # 每次加载的电影数量
    
  • 代码实现 - 全站抓取

    """
    豆瓣电影 - 全站抓取
    """
    import requests
    from fake_useragent import UserAgent
    import time
    import random
    import re
    import json
    
    class DoubanSpider:
        def __init__(self):
            self.url = 'https://movie.douban.com/j/chart/top_list?'
            self.i = 0
            # 存入json文件
            self.f = open('douban.json', 'w', encoding='utf-8')
            self.all_film_list = []
    
        def get_agent(self):
            """获取随机的User-Agent"""
            return UserAgent().random
    
        def get_html(self, params):
            headers = {'User-Agent':self.get_agent()}
            html = requests.get(url=self.url, params=params, headers=headers).text
            # 把json格式的字符串转为python数据类型
            html = json.loads(html)
    
            self.parse_html(html)
    
        def parse_html(self, html):
            """解析"""
            # html: [{},{},{},{}]
            item = {}
            for one_film in html:
                item['rank'] = one_film['rank']
                item['title'] = one_film['title']
                item['score'] = one_film['score']
                print(item)
                self.all_film_list.append(item)
                self.i += 1
    
        def run(self):
            # d: {'剧情':'11','爱情':'13','喜剧':'5',...,...}
            d = self.get_d()
            # 1、给用户提示,让用户选择
            menu = ''
            for key in d:
                menu += key + '|'
            print(menu)
            choice = input('请输入电影类别:')
            if choice in d:
                code = d[choice]
                # 2、total: 电影总数
                total = self.get_total(code)
                for start in range(0,total,20):
                    params = {
                        'type': code,
                        'interval_id': '100:90',
                        'action': '',
                        'start': str(start),
                        'limit': '20'
                    }
                    self.get_html(params=params)
                    time.sleep(random.randint(1,2))
    
                # 把数据存入json文件
                json.dump(self.all_film_list, self.f, ensure_ascii=False)
                self.f.close()
                print('数量:',self.i)
            else:
                print('请做出正确的选择')
    
        def get_d(self):
            """{'剧情':'11','爱情':'13','喜剧':'5',...,...}"""
            url = 'https://movie.douban.com/chart'
            html = requests.get(url=url,headers={'User-Agent':self.get_agent()}).text
            regex = '<span><a href=".*?type_name=(.*?)&type=(.*?)&interval_id=100:90&action=">'
            pattern = re.compile(regex, re.S)
            # r_list: [('剧情','11'),('喜剧','5'),('爱情':'13')... ...]
            r_list = pattern.findall(html)
            # d: {'剧情': '11', '爱情': '13', '喜剧': '5', ..., ...}
            d = {}
            for r in r_list:
                d[r[0]] = r[1]
    
            return d
    
        def get_total(self, code):
            """获取某个类别下的电影总数"""
            url = 'https://movie.douban.com/j/chart/top_list_count?type={}&interval_id=100%3A90'.format(code)
            html = requests.get(url=url,headers={'User-Agent':self.get_agent()}).text
            html = json.loads(html)
    
            return html['total']
    
    if __name__ == '__main__':
        spider = DoubanSpider()
        spider.run()
    

json解析模块

  • json.loads(json)

    1】作用 : 把json格式的字符串转为Python数据类型
    
    【2】示例 : html = json.loads(res.text)
    
  • json.dump(python,f,ensure_ascii=False)

    1】作用
       把python数据类型 转为 json格式的字符串,一般让你把抓取的数据保存为json文件时使用
    
    【2】参数说明
       2.1)1个参数: python类型的数据(字典,列表等)
       2.2)2个参数: 文件对象
       2.3)3个参数: ensure_ascii=False 序列化时编码
      
    【3】示例代码
        # 示例1
        import json
    
        item = {'name':'QQ','app_id':1}
        with open('小米.json','a') as f:
          json.dump(item,f,ensure_ascii=False)
      
        # 示例2
        import json
    
        item_list = []
        for i in range(3):
          item = {'name':'QQ','id':i}
          item_list.append(item)
    
        with open('xiaomi.json','a') as f:
            json.dump(item_list,f,ensure_ascii=False)
    
  • json模块总结

    # 爬虫最常用1】数据抓取 - json.loads(html)
        将响应内容由: json 转为 python
    【2】数据保存 - json.dump(item_list,f,ensure_ascii=False)
        将抓取的数据保存到本地 json文件
    
    # 抓取数据一般处理方式1】txt文件
    【2】csv文件
    【3】json文件
    【4】MySQL数据库
    【5】MongoDB数据库
    【6】Redis数据库
    

多线程爬虫

  • 应用场景

    1】多进程 :CPU密集程序
    【2】多线程 :爬虫(网络I/O)、本地磁盘I/O
    

知识点回顾

  • 队列

    1】导入模块
       from queue import Queue
    
    【2】使用
        q = Queue()
        q.put(url)
        q.get()   # 当队列为空时,阻塞
        q.empty() # 判断队列是否为空,True/False3】q.get()解除阻塞方式
       3.1) q.get(block=False)
       3.2) q.get(block=True,timeout=3)
       3.3) if not q.empty():
                q.get()
    
  • 线程模块

    # 导入模块
    from threading import Thread
    
    # 使用流程  
    t = Thread(target=函数名) # 创建线程对象
    t.start() # 创建并启动线程
    t.join()  # 阻塞等待回收线程
    
    # 如何创建多线程
    t_list = []
    
    for i in range(5):
        t = Thread(target=函数名)
        t_list.append(t)
        t.start()
    
    for t in t_list:
        t.join()
    
  • 线程锁

    from threading import Lock
    
    lock = Lock()
    lock.acquire()
    lock.release()
    
    【注意】上锁成功后,再次上锁会阻塞
    
  • 多线程爬虫示例代码

    # 抓取豆瓣电影剧情类别下的电影信息
    """
    豆瓣电影 - 剧情 - 抓取
    """
    import requests
    from fake_useragent import UserAgent
    import time
    import random
    from threading import Thread,Lock
    from queue import Queue
    
    class DoubanSpider:
        def __init__(self):
            self.url = 'https://movie.douban.com/j/chart/top_list?type=13&interval_id=100%3A90&action=&start={}&limit=20'
            self.i = 0
            # 队列 + 锁
            self.q = Queue()
            self.lock = Lock()
    
        def get_agent(self):
            """获取随机的User-Agent"""
            return UserAgent().random
    
        def url_in(self):
            """把所有要抓取的URL地址入队列"""
            for start in range(0,684,20):
                url = self.url.format(start)
                # url入队列
                self.q.put(url)
    
        # 线程事件函数:请求+解析+数据处理
        def get_html(self):
            while True:
                # 从队列中获取URL地址
                # 一定要在判断队列是否为空 和 get() 地址 前后加锁,防止队列中只剩一个地址时出现重复判断
                self.lock.acquire()
                if not self.q.empty():
                    headers = {'User-Agent': self.get_agent()}
                    url = self.q.get()
                    self.lock.release()
    
                    html = requests.get(url=url, headers=headers).json()
                    self.parse_html(html)
                else:
                    # 如果队列为空,则最终必须释放锁
                    self.lock.release()
                    break
    
        def parse_html(self, html):
            """解析"""
            # html: [{},{},{},{}]
            item = {}
            for one_film in html:
                item['rank'] = one_film['rank']
                item['title'] = one_film['title']
                item['score'] = one_film['score']
                print(item)
                # 加锁 + 释放锁
                self.lock.acquire()
                self.i += 1
                self.lock.release()
    
        def run(self):
            # 先让URL地址入队列
            self.url_in()
            # 创建多个线程,开干吧
            t_list = []
            for i in range(1):
                t = Thread(target=self.get_html)
                t_list.append(t)
                t.start()
    
            for t in t_list:
                t.join()
    
            print('数量:',self.i)
    
    if __name__ == '__main__':
        start_time = time.time()
        spider = DoubanSpider()
        spider.run()
        end_time = time.time()
        print('执行时间:%.2f' % (end_time-start_time))
    

selenium+PhantomJS/Chrome/Firefox

  • selenium

    1】定义
        1.1) 开源的Web自动化测试工具
        
    【2】用途
        2.1) 对Web系统进行功能性测试,版本迭代时避免重复劳动
        2.2) 兼容性测试(测试web程序在不同操作系统和不同浏览器中是否运行正常)
        2.3) 对web系统进行大数量测试
        
    【3】特点
        3.1) 可根据指令操控浏览器
        3.2) 只是工具,必须与第三方浏览器结合使用
        
    【4】安装
        4.1) Linux: sudo pip3 install selenium
        4.2) Windows: python -m pip install selenium
    
  • PhantomJS浏览器

    1】定义
        phantomjs为无界面浏览器(又称无头浏览器),在内存中进行页面加载,高效
     
    【2】下载地址
        2.1) chromedriver : 下载对应版本
           http://npm.taobao.org/mirrors/chromedriver/
        
        2.2) geckodriver
           https://github.com/mozilla/geckodriver/releases
                
        2.3) phantomjs
           https://phantomjs.org/download.html
    
    【3】Ubuntu安装
        3.1) 下载后解压 : tar -zxvf geckodriver.tar.gz 
            
        3.2) 拷贝解压后文件到 /usr/bin/ (添加环境变量)
             sudo cp geckodriver /usr/bin/
            
        3.3) 添加可执行权限
             sudo chmod 777 /usr/bin/geckodriver
    
    【4】Windows安装
        4.1) 下载对应版本的phantomjs、chromedriver、geckodriver
        4.2) 把chromedriver.exe拷贝到python安装目录的Scripts目录下(添加到系统环境变量)
             # 查看python安装路径: where python
        4.3) 验证
             cmd命令行: chromedriver
     
    ***************************总结**************************************1】解压 - 放到用户主目录(chromedriver、geckodriver、phantomjs)2】拷贝 - sudo cp /home/tarena/chromedriver /usr/bin/3】权限 - sudo chmod 777 /usr/bin/chromedriver
    
    # 验证
    【Ubuntu | Windows】
    ipython3
    from selenium import webdriver
    webdriver.Chrome()
    或者
    webdriver.Firefox()
    
    【mac】
    ipython3
    from selenium import webdriver
    webdriver.Chrome(executable_path='/Users/xxx/chromedriver')
    或者
    webdriver.Firefox(executable_path='/User/xxx/geckodriver')
    
  • 示例代码

    """示例代码一:使用 selenium+浏览器 打开百度"""
    
    # 导入seleinum的webdriver接口
    from selenium import webdriver
    import time
    
    # 创建浏览器对象
    browser = webdriver.Chrome()
    browser.get('http://www.baidu.com/')
    # 5秒钟后关闭浏览器
    time.sleep(5)
    browser.quit()
    
    """示例代码二:打开百度,搜索赵丽颖,点击搜索,查看"""
    
    from selenium import webdriver
    import time
    
    # 1.创建浏览器对象 - 已经打开了浏览器
    browser = webdriver.Chrome()
    # 2.输入: http://www.baidu.com/
    browser.get('http://www.baidu.com/')
    # 3.找到搜索框,向这个节点发送文字: 赵丽颖
    browser.find_element_by_xpath('//*[@id="kw"]').send_keys('赵丽颖')
    # 4.找到 百度一下 按钮,点击一下
    browser.find_element_by_xpath('//*[@id="su"]').click()
    
  • 浏览器对象(browser)方法

    1】browser.get(url=url)   - 地址栏输入url地址并确认   
    【2】browser.quit()         - 关闭浏览器
    【3】browser.close()        - 关闭当前页
    【4】browser.page_source    - HTML结构源码
    【5】browser.page_source.find('字符串')
        从html源码中搜索指定字符串,没有找到返回:-1,经常用于判断是否为最后一页
    【6】browser.maximize_window() - 浏览器窗口最大化
    
  • 定位节点八种方法

    1】单元素查找('结果为1个节点对象')
        1.1) 【最常用】browser.find_element_by_id('id属性值')
        1.2) 【最常用】browser.find_element_by_name('name属性值')
        1.3) 【最常用】browser.find_element_by_class_name('class属性值')
        1.4) 【最万能】browser.find_element_by_xpath('xpath表达式')
        1.5) 【匹配a节点时常用】browser.find_element_by_link_text('链接文本')
        1.6) 【匹配a节点时常用】browser.find_element_by_partical_link_text('部分链接文本')
        1.7) 【最没用】browser.find_element_by_tag_name('标记名称')
        1.8) 【较常用】browser.find_element_by_css_selector('css表达式')2】多元素查找('结果为[节点对象列表]')
        2.1) browser.find_elements_by_id('id属性值')
        2.2) browser.find_elements_by_name('name属性值')
        2.3) browser.find_elements_by_class_name('class属性值')
        2.4) browser.find_elements_by_xpath('xpath表达式')
        2.5) browser.find_elements_by_link_text('链接文本')
        2.6) browser.find_elements_by_partical_link_text('部分链接文本')
        2.7) browser.find_elements_by_tag_name('标记名称')
        2.8) browser.find_elements_by_css_selector('css表达式')
    
  • 猫眼电影示例

    from selenium import webdriver
    import time
    
    url = 'https://maoyan.com/board/4'
    browser = webdriver.Chrome()
    browser.get(url)
    
    def get_data():
        # 基准xpath: [<selenium xxx li at xxx>,<selenium xxx li at>]
        li_list = browser.find_elements_by_xpath('//*[@id="app"]/div/div/div[1]/dl/dd')
        for li in li_list:
            item = {}
            # info_list: ['1', '霸王别姬', '主演:张国荣', '上映时间:1993-01-01', '9.5']
            info_list = li.text.split('\n')
            item['number'] = info_list[0]
            item['name'] = info_list[1]
            item['star'] = info_list[2]
            item['time'] = info_list[3]
            item['score'] = info_list[4]
    
            print(item)
    
    while True:
        get_data()
        try:
            browser.find_element_by_link_text('下一页').click()
            time.sleep(2)
        except Exception as e:
            print('恭喜你!抓取结束')
            browser.quit()
            break
    
  • 节点对象操作

    1】文本框操作
        1.1) node.send_keys('')  - 向文本框发送内容
        1.2) node.clear()        - 清空文本
        1.3) node.get_attribute('value') - 获取文本内容
        
    【2】按钮操作
        1.1) node.click()      - 点击
        1.2) node.is_enabled() - 判断按钮是否可用
        1.3) node.get_attribute('value') - 获取按钮文本
    

chromedriver设置无界面模式

from selenium import webdriver

options = webdriver.ChromeOptions()
# 添加无界面参数
options.add_argument('--headless')
browser = webdriver.Chrome(options=options)

selenium - 鼠标操作

from selenium import webdriver
# 导入鼠标事件类
from selenium.webdriver import ActionChains

driver = webdriver.Chrome()
driver.get('http://www.baidu.com/')

# 移动到 设置,perform()是真正执行操作,必须有
element = driver.find_element_by_xpath('//*[@id="u1"]/a[8]')
ActionChains(driver).move_to_element(element).perform()

# 单击,弹出的Ajax元素,根据链接节点的文本内容查找
driver.find_element_by_link_text('高级搜索').click()
posted @ 2020-12-08 10:17  野哥李  阅读(12)  评论(0编辑  收藏  举报  来源