应用python+和讯进行长距离关机

在二次工作中,出现了三个小意外,叁个设有redis写与读的法门中搜查缉获的结果再而三和预期的不等同,前边仔细考虑了后来,发现是因为集群中逐一节点都选取共享的缓存、队列这么些,有个别场景中逐条节点之间恐怕会产生产资料源竞争,只怕会发各样节点之间的“线程不安全难题”,单机中,能够利用锁来化解,在分布式环境下,就要用到分布式锁了,当时网上查了查,发现了许多大神给出的思绪和代码,本身试着实现了弹指间,在那把思路分享给大家。

不长一段时间没有立异简书的始最后,打算把天涯论坛爬虫完善得大概之后,再系统的把做博客园爬虫的各种模块和阶段都记录下来。在那之中腾讯网页面抓取和分析、用户页面抓取和剖析等模块,都是足以复用的。未来还只是单机单线程,因为天涯论坛的反爬虫机制还没完全研商透,等找到抓取的阈值后再考虑分布式只怕多进程。这里是今日头条扩散分析的项目地址,有趣味的能够看看,喜欢的话不防点个star,如何?

Redis能够做分布式锁的准绳

因为redis是单线程的,并行访问的下令在他在那之中会是串行执行的,所以redis能够看狠抓现分布式锁的技能。


所用的吩咐

在前几篇作品中,已经介绍了Redis的焦点项目与其相应的操作,有不太领悟的情人能够去《Redis常用项目知多少?》去看望。在落到实处分布式锁中,首要接纳了redis的字符串类型数据结构,以及以下的操作:

那篇文章写的是依照依傍登陆和讯的一个小工具。使用境况是人不在办公室,不过电脑没有关闭,供给长途关闭电脑。对模拟登陆天涯论坛有失常态的同室,请移步作者的那篇作品。上边进入正题。

set key value [ex 秒数] [px 毫秒数] [nx/xx] : 保存key-value

ex、px指的是key的有效期。
nx:表示当key不设有时,创立key并保留value
xx:表示当key存在时,保存value

localhost:0>set hello world
OK
localhost:0>set hello world ex 100
OK
localhost:0>set hello world px 1000
OK
这里主要看看 nx与xx的效果
localhost:0>set hello worldnx nx 
NULL
localhost:0>set hello worldxx xx 
OK
因为hello这个key已经存在,所以带上参数nx后,返回了NULL,没有操作成功。
带上xx这个参数后,现在hello这个key的value已经被替换成了worldxx

兑现中用到了set指令的nx参数,成效是保存3个key-value,假使key不设有时,创制key并保存value,要是key存在是,不做操作重临NULL,在java代码中,那个命令被包裹成了Jedis的多少个艺术,笔者本人在代码中又做了三个包裹:

    /**
     * 如果key不存在则建立返回1
     * 如果key存则不作操作,返回0
     * @param key
     * @param value
     * @return
     */
    public Long setNx(final String key,final String value){
        return this.execute(new Function<Long, Jedis>() {
            @Override
            public Long callback(Jedis jedis) {
                //set命令带nx参数在Jedis中被封装成了setnx方法
                return jedis.setnx(key, value);
            }
        });
    }

首要用来得到锁,setnx重回1,注明获取到了锁,再次来到0注脚锁已经存在。

思路

expire key 整数值:设置key的生命周期以秒为单位

最主要用以设置锁的逾期时间,贴出java中封装的方法expire:

   /**
     * 设置key的过期时间
     * @param key
     * @param exp
     * @return
     */
    public Long expire(final String key,final int exp){
        return this.execute(new Function<Long, Jedis>() {
            @Override
            public Long callback(Jedis jedis) {
                return jedis.expire(key, exp);
            }
        });
    }
  • 定时模拟登陆(定时是因为天涯论坛cookie24小时失效),关于模拟登陆详细步骤可参看我的博文,代码可参看github项目
  • 定时(10分钟)获取最新一条新浪,并把公布时间和系统时间做比较,假使距离在半个时辰以内,大家就觉着命令有效,那么就让系统进行关机命令
get key : 取出key对应的value

取出存进去的锁值,贴出java中封装的艺术get:

   /**
     * 获取String类型的值
     *
     * @param key
     * @return
     */
    public String get(final String key) {
        return this.execute(new Function<String, Jedis>() {
            @Override
            public String callback(Jedis jedis) {
                return jedis.get(key);
            }
        });
    }

项目依赖

getset key newvalue:返回key的旧value,把新值newvalue存进去
localhost:0>getset hello wolrd
wogldxx
localhost:0>get hello
wolrd

用于获取到锁以往把新的锁值存进去,贴出java中封装的点子getset:

   /**
     * 获取并返回旧值,在设置新值
     * @param key
     * @param value
     * @return
     */
    public String getSet(final String key, final String value){
        return this.execute(new Function<String, Jedis>() {
            @Override
            public String callback(Jedis jedis) {
                return jedis.getSet(key, value);
            }
        });
    }
  • 宪章登陆+页面解析:
  • requests+pyexecjs+beautifulsoup
  • pip install requests
  • pip install bs4
  • pip install PyExecJS
  • 一声令下行解析docopt
  • pip install docopt
  • phantomjs
  • windows:在phantomjs官网下载它,并且把它的门道添加到环境变量
  • ubuntu:sudo apt-get install phantomjs
    或许到官网下载并且累加到环境变量

心想事成的笔触

1.先利用近来的时辰戳与锁的超时时间相加后得出锁将要过期的时间,并把那几个时刻作为value,锁作为key调用setnx方法,假诺回到1,表明原来这一个key,也正是锁不存在,获取锁成功,则调用expire方法设置锁的逾期时间,重临获取锁成功。
2.比方setnx再次来到0,注明原来锁存在,没有取得到锁,然后阻塞等锁的拿走。(小编原先本人的笔触就考虑到第二步就终止了,后边看了大神的兑现思路,发现自家的想法欠妥,下边说一下大神的第2步)。
3.如若setnx重临0,注解原来锁存在,没有收获到锁,然后调用get方法,获取第贰步存进去的value,就是锁将要过期的年月,与当前的年月戳相比,若是只要当前的岁月戳大于锁将要过期的小时,注脚锁已经过期,调用getset方法,把新的时间存进去,再次回到获取锁成功。那样做首若是应对expire执行破产,恐怕服务注重启的气象下冒出的锁不可能自由的景观。
上边把依据大神思路达成的代码贴出来,完成了堵截机制的锁,也兑现了1个排它锁:

import com.eduapi.common.component.RedisComponent;
import com.eduapi.common.util.BeanUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * @Description: 利用redis实现分布式锁.
 * @Author: ZhaoWeiNan .
 * @CreatedTime: 2017/3/20 .
 * @Version: 1.0 .
 */
public class RedisLock {

    private RedisComponent redisComponent;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    private String lockKey;

    /**
     * 锁超时时间,防止线程在入锁以后,无限的执行等待
     */
    private int expireMillisCond = 60 * 1000;

    /**
     * 锁等待时间,防止线程饥饿
     */
    private int timeoutMillisCond = 10 * 1000;

    private volatile boolean isLocked = false;

    public RedisLock(RedisComponent redisComponent, String lockKey) {
        this.redisComponent = redisComponent;
        this.lockKey = lockKey;
    }

    public RedisLock(RedisComponent redisComponent, String lockKey, int timeoutMillisCond) {
        this(redisComponent, lockKey);
        this.timeoutMillisCond = timeoutMillisCond;
    }

    public RedisLock(RedisComponent redisComponent, String lockKey, int timeoutMillisCond, int expireMillisCond) {
        this(redisComponent, lockKey, timeoutMillisCond);
        this.expireMillisCond = expireMillisCond;
    }

    public RedisLock(RedisComponent redisComponent, int expireMillisCond, String lockKey) {
        this(redisComponent, lockKey);
        this.expireMillisCond = expireMillisCond;
    }

    public String getLockKey() {
        return lockKey;
    }

    /**
     * 获得 lock. (把大神的思路粘过来了)
     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
     * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
     * 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMillisCond;

        boolean flag = false;

        while (timeout > 0){
            //设置所得到期时间
            Long expires = System.currentTimeMillis() + expireMillisCond;
            String expiresStr = BeanUtils.convertObject2String(expires);

            //原来redis里面没有锁,获取锁成功
            if (this.redisComponent.setNx(lockKey,expiresStr) > 0){
                //设置锁的过期时间
                this.redisComponent.expire(lockKey,expireMillisCond);
                isLocked = true;
                return true;
            }

            flag = compareLock(expiresStr);

            if (flag){
                return flag;
            }

            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
                延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进程,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
                使用随机的等待时间可以一定程度上保证公平性
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
        }
        return false;
    }

    /**
     * 排他锁。作用相当于 synchronized 同步快
     * @return
     * @throws InterruptedException
     */
    public synchronized boolean excludeLock() {

        //设置所得到期时间
        long expires = System.currentTimeMillis() + expireMillisCond;
        String expiresStr = BeanUtils.convertObject2String(expires);

        //原来redis里面没有锁,获取锁成功
        if (this.redisComponent.setNx(lockKey,expiresStr) > 0){
            //设置锁的过期时间
            this.redisComponent.expire(lockKey,expireMillisCond);
            isLocked = true;
            return true;
        }

        return compareLock(expiresStr);
    }

    /**
     * 比较是否可以获取锁
     * 锁超时时 获取
     * @param expiresStr
     * @return
     */
    private boolean compareLock(String expiresStr){
        //假如两个线程走到这里
        //因为redis是单线程的获取到
        // A线程获取  currentValueStr = 1 B线程获取 currentValueStr = 1
        String currentValueStr = this.redisComponent.get(lockKey);

        //锁
        if (StringUtils.isNotEmpty(currentValueStr) && Long.parseLong(currentValueStr) < System.currentTimeMillis()){

            //获取上一个锁到期时间,并设置现在的锁到期时间,
            //只有一个线程才能获取上一个线程的设置时间,因为jedis.getSet是同步的
            //只有A线程 把 2 存进去了。 取出了 1, 对比获得了锁
            //B线程 吧 2存进去了。 获取 2.。对比 没有获得锁,
            String oldValue = this.redisComponent.getSet(lockKey,expiresStr);

            if (StringUtils.isNotEmpty(oldValue) && StringUtils.equals(oldValue,currentValueStr)){

                //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
                //[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
                isLocked = true;
                return true;
            }
        }
        return false;
    }

    /**
     * 释放锁
     */
    public synchronized void unlock(){
        if (isLocked){
            this.redisComponent.delete(lockKey);
            isLocked = false;
        }
    }

}

代码中,把大神写的思路站进去了,大神讲的还是很了然的,看看调用的代码:

        //创建锁对象, redisComponent 为redis组件的对象   过期时间  锁的key
        RedisLock redisLock = new RedisLock(redisComponent,1000 * 60,RedisCacheKey.REDIS_LOCK_KEY + now_mm);
        //获取锁
        if (redisLock.excludeLock()){
            try {
                //拿到了锁,读取定时短信有序集合
                set = this.redisComponent.zRangeByScore(RedisCacheKey.MSG_TIME_LIST,0,end);

                if (set != null && set.size() > 0){
                    flag = true;
                }
            } catch (Exception e) {
                LOGGER.error("获取定时短信有序集合异常,异常为{}",e.toString());
            }
...

利用redis实现分布式锁的一种思路就为大家介绍到此处,欢迎我们来调换,提出文中部分说错的地方,让自个儿加深认识。
感激我们!

梯次模块和代码

login.py

该模块代码负责模拟登陆,之前业已详尽讲过那有的代码了,在此地就不啰嗦了,最后回到的是session和uid(和讯ID,用于拼凑主页ULX570L)

** weibo_parser.py**

浅析搜狐主页,并且重回最新一条今日头条和宣布时间

具体代码如下
<pre>
def get_newest(session, uid):
# 获取只包蕴原创内容的个人主页
url =
http://weibo.com/’ +
uid + ‘/profile?profile_ftype=1&is_ori=1#_0’
page = session.get(url).text

soup = BeautifulSoup(page, 'html.parser')    
scripts = soup.find_all('script')    

status = ' '    
for s in scripts:        
    if 'pl.content.homeFeed.index' in s.string:            
            status = s.string    
#用正则表达式获取微博原创内容
pattern = re.compile(r'FM.view\((.*)\)')    
rs = pattern.search(status)    
if rs:       
    cur_status = rs.group(1)        
    html = json.loads(cur_status).get('html')        
    soup = BeautifulSoup(html, 'html.parser') 
    # 获取最新一条微博所有信息       
    newest = soup.find(attrs={'action-type': 'feed_list_item'})        
    # 获取最新发布内容
    post_cont = newest.find(attrs={'node-type': 'feed_list_content'}).text.strip()     
    # 获取最新发布时间
    post_stamp = int(newest.find(attrs={'node-type': 'feed_list_item_date'}).get('date')[:-3])                            
    post_time = datetime.fromtimestamp(post_stamp)        
    now = datetime.now() 
    # 计算此刻和发布时间的时间差(单位为秒)       
    t = (now - post_time).total_seconds()        
    return post_cont, t    
else:        
    return None  

</pre>

那里面用到的学问蕴涵beautifulsoup和正则表达式,它们的切切实举行使本身就不细说了,关李樯则表明式,search()函数小编是用得最多的,beautifulsoup我用得最多的是find(attrs={key:
value})
,attrs这一个参数真心好用!这么些是beautifulsoup的官方文书档案:bs中文文书档案.关于页面解析,大概笔者会专门写一篇小说详细说,那里就略去了。

pc_shutdown.py
<pre>
“””Resolvewang

Usage:
pc_shutdow.py name <name> password <password>
pc_shutdow.py (-h | –help)
pc_shutdow.py –version

Options:
-h –help Show this screen.
–version Show version
“””
from login import get_cur_session
from weibo_parser import get_newest
from docopt import docopt
from os import system
import platform
import time

def shutdown(name, password):
session, uid = get_cur_session(name, password)
return get_newest(session, uid)

if name == ‘main‘:
# 从命令行获取登陆账号和密码
args = docopt(doc, version=’ShutdownMyPC 1.0′)
login_name = args.get(‘<name>’)
login_pass = args.get(‘<password>’)
# 循环用于定时查看是还是不是有新博客园揭橥
while True:
# 获取发布内容和岁月,内容用 ” “隔断,比如“关机 10”
cont, ptdelta = shutdown(login_name, login_pass)
info = cont.split(‘ ‘)
# 判断是关机命令依旧如常腾讯网
if info[0] == ‘关机’ and ptdelta < 30 * 60:
shut_time = 0
try:
shut_time = int(info[1])
except Exception:
print(‘登时自动关机’)
else:
print(‘{time}分钟后自动关机’.format(time=info[1]))
finally:
# 判断操作系统平台,由于没有mac实验环境,所以那里没添加mac的连锁判断
os_system = platform.system().lower()
if os_system == ‘windows’:
command = ‘shutdown -s -t
{shut_time}’.format(shut_time=shut_time60)
else:
command = ‘shutdown -h {shut_time}’.format(shut_time=shut_time)
# 执行关机命令
system(command)
time.sleep(10
60)
</pre>

那段代码的逻辑基本都写在诠释里了,当中有个docopt模块,是关于命令行参数的,假如有不晓得的校友能够看看那篇博客,也能够看看它的github,里面有不少例证


有关利用天涯论坛展开长距离关机的讲课都完了,上述代码还有能够改进的地点,特别是pc_shutdown.py,比如

  • 接纳定时器举办查看新今日头条
  • session复用直到24刻钟失效,那样就毫无每隔十分钟就再也登陆1回了,能够通过多进度或许四线程共享变量达成
  • 能够把那一个小工具修改成二个开机运行脚本(linux平台)恐怕服务(win平台)。

吾生也有涯,而知也开阔。大家加油,共勉!

发表评论

电子邮件地址不会被公开。 必填项已用*标注