白露未晞

白露未晞 查看完整档案

海外编辑  |  填写毕业院校  |  填写所在公司/组织 meiyou.com 编辑
编辑

微信公众号:Charles的皮卡丘

个人动态

白露未晞 发布了文章 · 4月12日

要不一起来写个淘宝商品数据小爬虫吧?

原文链接

要不一起来写个淘宝商品数据小爬虫吧?​mp.weixin.qq.com图标

导语

好久没给自己开源的模拟登录库添加爬虫小案例了,周末就给大家带来一个淘宝商品数据小爬虫吧。顺便按老规矩把抓取到的数据可视化一波。废话不多说,让我们愉快地开始吧~

开发工具

Python版本:3.6.4
相关模块:
DecryptLogin模块;
pyecharts模块;
以及一些Python自带的模块。

数据爬取

既然说了是模拟登录相关的爬虫小案例,首先自然是要实现一下淘宝的模拟登录啦。这里还是利用我们开源的DecryptLogin库来实现,只需三行代码即可:

'''模拟登录淘宝'''
@staticmethod
def login():
    lg = login.Login()
    infos_return, session = lg.taobao()
    return session

另外,顺便提一句,经常有人想让我在DecryptLogin库里加入cookies持久化功能。其实你自己多写两行代码就能实现了:

if os.path.isfile('session.pkl'):
    self.session = pickle.load(open('session.pkl', 'rb'))
else:
    self.session = TBGoodsCrawler.login()
    f = open('session.pkl', 'wb')
    pickle.dump(self.session, f)
    f.close()

我真不想在这个库里添加这个功能,后面我倒是想添加一些其他爬虫相关的功能,这个之后再说吧。好的,偏题了,言归正传吧。接着,我们去网页版的淘宝抓一波包吧。比如F12打开开发者工具后,在淘宝的商品搜索栏里随便输入点东西,就像这样:

全局搜索一下诸如search这样的关键词,可以发现如下链接:

看看它返回的数据是啥:

看来应该没错了。另外,如果小伙伴们自己实战的时候没有找到这个接口api,可以尝试再点击一下右上角的下一页商品按钮:

这样就肯定能抓到这个请求接口啦。简单测试一下,可以发现尽管请求这个接口所需携带的参数看上去很多,但实际上必须要提交的参数只有两个,即:

q: 商品名称
s: 当前页码的偏移量

好啦,根据这个接口,以及我们的测试结果,现在就可以愉快地开始实现淘宝商品数据的抓取啦。具体而言,主代码实现如下:

'''外部调用'''
def run(self):
    search_url = 'https://s.taobao.com/search?'
    while True:
        goods_name = input('请输入想要抓取的商品信息名称: ')
        offset = 0
        page_size = 44
        goods_infos_dict = {}
        page_interval = random.randint(1, 5)
        page_pointer = 0
        while True:
            params = {
                        'q': goods_name,
                        'ajax': 'true',
                        'ie': 'utf8',
                        's': str(offset)
                    }
            response = self.session.get(search_url, params=params)
            if (response.status_code != 200):
                break
            response_json = response.json()
            all_items = response_json.get('mods', {}).get('itemlist', {}).get('data', {}).get('auctions', [])
            if len(all_items) == 0:
                break
            for item in all_items:
                if not item['category']:
                    continue
                goods_infos_dict.update({len(goods_infos_dict)+1: 
                                            {
                                                'shope_name': item.get('nick', ''),
                                                'title': item.get('raw_title', ''),
                                                'pic_url': item.get('pic_url', ''),
                                                'detail_url': item.get('detail_url', ''),
                                                'price': item.get('view_price', ''),
                                                'location': item.get('item_loc', ''),
                                                'fee': item.get('view_fee', ''),
                                                'num_comments': item.get('comment_count', ''),
                                                'num_sells': item.get('view_sales', '')
                                            }
                                        })
            print(goods_infos_dict)
            self.__save(goods_infos_dict, goods_name+'.pkl')
            offset += page_size
            if offset // page_size > 100:
                break
            page_pointer += 1
            if page_pointer == page_interval:
                time.sleep(random.randint(30, 60)+random.random()*10)
                page_interval = random.randint(1, 5)
                page_pointer = 0
            else:
                time.sleep(random.random()+2)
        print('[INFO]: 关于%s的商品数据抓取完毕, 共抓取到%s条数据...' % (goods_name, len(goods_infos_dict)))

就是这么简单,我们已经大功告成啦。最后,我们再来看下代码的运行效果呗:

见:https://zhuanlan.zhihu.com/p/...

数据可视化

这里我们来可视化一波我们抓到的奶茶数据呗。先来看看在淘宝上卖奶茶的商家在全国范围内的数量分布情况呗:

没想到啊,奶茶店铺最多的地方竟然是广东。T_T

再来看看淘宝上卖奶茶的店铺的销量排名前10名呗:

以及淘宝上评论数量前10名的奶茶店铺:

再看看在这些店铺要运费和不要运费的商品比例呗:

最后,再看看奶茶相关商品的售价区间呗:

差不多今天就这样呗。

相关文件

https://github.com/CharlesPika

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 4月5日

写个桌面挂件 | 手把手带大家做只桌面宠物呗

原文链接

网页链接​mp.weixin.qq.com图标

效果展示

见:https://zhuanlan.zhihu.com/p/...

导语

前段时间有小伙伴留言说想让我带大家写写桌面小挂件,今天就满足一下留过类似言的小伙伴的请求呗~不过感觉写桌面的挂历啥的没意思,就简单带大家做一只桌面宠物吧~

废话不多说,让我们愉快地开始吧~

开发工具

Python版本:3.6.4
相关模块:
PyQt5模块;
以及一些Python自带的模块。

原理简介

既然要写个桌面宠物,首先当然是要找宠物的图片素材啦。这里我们使用的是来自shimiji这款手机APP上的宠物图片素材,例如皮卡丘:

我下了大约60多种宠物的图片素材供大家选择:

在相关文件里都打包一起提供了,所以这里就不分享爬虫代码了(我挑选了一下,只要不是我觉得特别丑的,我基本都保留了),别给人家服务器带来不必要的压力。

接下来,我们就可以开始设计我们的桌面宠物啦。鉴于网上用python写的桌面挂件基本都是基于tkinter的,为了突出公众号的与众不同,这里我们采用PyQt5来实现我们的桌面宠物。

首先,我们来初始化一个桌面宠物的窗口组件:

class DesktopPet(QWidget):
    def __init__(self, parent=None, **kwargs):
        super(DesktopPet, self).__init__(parent)
        self.show()

它的效果是这样子的:

接下来,我们设置一下窗口的属性让更适合作为一个宠物的窗口:

# 初始化
self.setWindowFlags(Qt.FramelessWindowHint|Qt.WindowStaysOnTopHint|Qt.SubWindow)
self.setAutoFillBackground(False)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.repaint()

并随机导入一张宠物图片来看看运行效果:

# 随机导入一个宠物
self.pet_images, iconpath = self.randomLoadPetImages()
# 当前显示的图片
self.image = QLabel(self)
self.setImage(self.pet_images[0][0])

其中随机导入一个宠物的所有图片的函数代码实现如下:

'''随机导入一个桌面宠物的所有图片'''
def randomLoadPetImages(self):
    pet_name = random.choice(list(cfg.PET_ACTIONS_MAP.keys()))
    actions = cfg.PET_ACTIONS_MAP[pet_name]
    pet_images = []
    for action in actions:
        pet_images.append([self.loadImage(os.path.join(cfg.ROOT_DIR, pet_name, 'shime'+item+'.png')) for item in action])
    iconpath = os.path.join(cfg.ROOT_DIR, pet_name, 'shime1.png')
    return pet_images, iconpath

当然,我们也希望宠物每次在桌面上出现的位置是随机的,这样会更有趣一些:

'''随机到一个屏幕上的某个位置'''
def randomPosition(self):
    screen_geo = QDesktopWidget().screenGeometry()
    pet_geo = self.geometry()
    width = (screen_geo.width() - pet_geo.width()) * random.random()
    height = (screen_geo.height() - pet_geo.height()) * random.random()
    self.move(width, height)

现在,运行我们的程序时,效果是这样子的:

好像蛮不错的呢~等等,好像有问题,重新设置了窗口属性之后,这玩意咋退出啊?在宠物右上角加个×这样的符号又好像很奇怪?

别急,我们可以给我们的桌面宠物添加一个托盘图标,以实现桌面宠物程序的退出功能:

# 设置退出选项
quit_action = QAction('退出', self, triggered=self.quit)
quit_action.setIcon(QIcon(iconpath))
self.tray_icon_menu = QMenu(self)
self.tray_icon_menu.addAction(quit_action)
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(QIcon(iconpath))
self.tray_icon.setContextMenu(self.tray_icon_menu)
self.tray_icon.show()

效果是这样子的:

OK,这样好像有模有样了呢~但是好像还是不太对的样子,这宠物每次在桌面生成的位置是随机的,但是我们却无法调整这个宠物的位置,这显然不合理,作为一个桌面宠物,你肯定不能在妨碍主人工作的位置啊!要不我们来写一下鼠标按下、移动以及释放时的函数吧,这样就可以用鼠标拖动它了:

'''鼠标左键按下时, 宠物将和鼠标位置绑定'''
def mousePressEvent(self, event):
    if event.button() == Qt.LeftButton:
        self.is_follow_mouse = True
        self.mouse_drag_pos = event.globalPos() - self.pos()
        event.accept()
        self.setCursor(QCursor(Qt.OpenHandCursor))
'''鼠标移动, 则宠物也移动'''
def mouseMoveEvent(self, event):
    if Qt.LeftButton and self.is_follow_mouse:
        self.move(event.globalPos() - self.mouse_drag_pos)
        event.accept()
'''鼠标释放时, 取消绑定'''
def mouseReleaseEvent(self, event):
    self.is_follow_mouse = False
    self.setCursor(QCursor(Qt.ArrowCursor))

效果如下:

哈哈,越来越像样了呢~最后,作为一个活泼的宠物,你不能这么呆板,一动也不动吧?好歹要学会做做表情逗主人开心吧?OK,我们先来设置一个定时器:

# 每隔一段时间做个动作
self.timer = QTimer()
self.timer.timeout.connect(self.randomAct)
self.timer.start(500)

定时器每隔一段时间切换一下选中的宠物的图片,以达到宠物做表情动作的动画效果(视频是一帧帧的图片组成的这种基础内容就不需要我来科普了吧T_T)。当然,这里我们必须对图片进行动作分类(在做同一个动作的图片属于同一类),保证宠物做表情动作时的连贯性。具体而言,代码实现如下:

'''随机做一个动作'''
def randomAct(self):
    if not self.is_running_action:
        self.is_running_action = True
        self.action_images = random.choice(self.pet_images)
        self.action_max_len = len(self.action_images)
        self.action_pointer = 0
    self.runFrame()
'''完成动作的每一帧'''
def runFrame(self):
    if self.action_pointer == self.action_max_len:
        self.is_running_action = False
        self.action_pointer = 0
        self.action_max_len = 0
    self.setImage(self.action_images[self.action_pointer])
    self.action_pointer += 1

OK,大功告成了~完整源代码详见相关文件。

相关文件

见:https://github.com/CharlesPikac

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 4月2日

之前发布的音乐下载器又全新升级啦~

原文链接

哇,公众号发布的音乐下载器又全新升级啦~​mp.weixin.qq.com图标

效果展示

见:https://zhuanlan.zhihu.com/p/...

项目文档

musicdl中文文档 - musicdl 2.1.0 文档

项目地址

https://github.com/CharlesPikachu/Music-Downloader

更新简介

公众号的老粉丝应该还记得公众号之前分享的音乐下载器项目吧:

Python版音乐下载器更新啦~

转眼间项目又一年多没什么更新了,而自己也用了一年多,感觉这项目还是存在很多缺点的,于是根据自己一年多来的使用体验,对这款音乐下载器又做了一些更新升级,顺便分享给大家。废话不多说,让我们愉快地开始吧。

1.添加了项目文档

为了方便大家使用,尤其是方便一些喜欢鼓捣的人使用(例如想在树莓派上弄个音乐下载器,可以声控下载和播放音乐?没错,那个人就是我,当然希望你也可以

),我花了点时间整理并上线了一份项目文档:

https://musicdl.readthedocs.io/zh/latest/

目前文档里的内容还比较简单,后续会逐渐完善这个文档的。

2.修复了一些失效的api和bugs

目前这款音乐下载器支持的平台情况如下:

3.支持全平台搜索

之前发布的版本都是先选择平台,然后再根据用户输入的关键词进行歌曲搜索。自己用了一年多后的感觉就是,我**怎么知道哪首歌在哪个平台上搜索比较好?!这设计也太愚蠢了!

于是在默认情况下,我们的音乐下载器现在直接支持全平台搜索了,而不需要一个个平台地去尝试了,效果大概是这样的:

里面添加了更详细的歌曲信息,以方便大家使用。

4.支持pip安装

效果如下:

很多小伙伴可能会问啦,支持pip安装有什么用呢?对于一般的小伙伴,当然是用不上的。支持pip主要是为了给爱鼓捣的小伙伴使用,例如某些小伙伴不想使用全平台搜索功能,那么安装了该库之后,你可以自己写一个脚本,实现指定平台的歌曲搜索。例如指定网易云音乐和咪咕音乐进行搜索并下载搜索结果:

from musicdl import musicdl

config = {'logfilepath': 'musicdl.log', 'savedir': 'downloaded', 'search_size_per_source': 5, 'proxies': {}}
target_srcs = ['netease', 'migu']
client = musicdl.musicdl(config=config)
search_results = client.search('说好不哭', target_srcs)
for key, value in search_results.items():
    client.download(value)

其中config是一个字典对象,其各参数含义为:

logfilepath: 日志文件保存路径
proxies: 设置代理, 支持的代理格式参见https://requests.readthedocs.io/en/master/user/advanced/#proxies
search_size_per_source: 在各个平台搜索时的歌曲搜索数量
savedir: 下载的音乐保存路径

target_srcs是一个列表对象,用来指定搜索平台:

baiduFlac: 百度无损音乐
kugou: 酷狗音乐
kuwo: 酷我音乐
qq: qq音乐
qianqian: 千千音乐
netease: 网易云音乐
migu: 咪咕音乐
xiami:虾米音乐

更多功能可以参见项目文档里说明。

对了,有些小伙伴嫌弃GitHub慢,你也可以pip安装项目,然后写一个脚本:

from musicdl import musicdl

config = {'logfilepath': 'musicdl.log', 'savedir': 'downloaded', 'search_size_per_source': 5, 'proxies': {}}
target_srcs = ['baiduFlac', 'kugou', 'kuwo', 'qq', 'qianqian', 'netease', 'migu']
client = musicdl.musicdl(config=config)
client.run(target_srcs)

然后运行这个脚本,就愉快地使用我们最新版的音乐下载器了。

5.支持添加代理

之前有国外的同学说下载了我的项目后用不了,我一看,原来是有些平台只有国内用户可以使用,因此,加了个添加代理功能。对于爱鼓捣的用户,添加代理的方式参见上一条。对于不爱鼓捣的用户,你可以修改下图中所示的配置文件:

里面有配置代理的选项:

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 3月18日

随便玩玩,要不要试试大吼一声来发条微博

原文链接

随便玩玩,要不要试试大吼一声来发条微博​mp.weixin.qq.com图标

效果展示

见:https://zhuanlan.zhihu.com/p/...

导语

好多天没写推文了,出来冒个泡吧。听说很多小伙伴因为疫情在家快憋坏了,要不试试一起对着电脑吼两声发泄一下?吼的够大声就可以自动发条微博庆祝一下?废话不多说,让我们愉快地开始吧~

原理简介

既然要实现自动发微博,首先自然要实现微博模拟登录啦。这里我们还是借助公众号开源的模拟登录包DecryptLogin来实现:

'''利用DecryptLogin模拟登录'''
@staticmethod
def login(username, password):
    lg = login.Login()
    infos_return, session = lg.weibo(username, password, 'pc')
    return infos_return.get('nick'), infos_return.get('uid'), session

接着我们来新建一个文件夹(例如weibo),在这个文件夹里放我们需要发的微博的内容:

weibo文件夹里的内容如下:

其中pictures文件夹里放我们微博的配图:

http://weixin.qq.com/r/Pi-y6qjEqPXXreyJ93os(二维码自动识别)

weibo.md里写我们微博的文字内容:

写个函数来自动解析一下上面的微博内容:

'''待发送微博内容解析'''
def __parseWeibo(self, weibopath):
    text = open(os.path.join(weibopath, 'weibo.md'), 'r', encoding='utf-8').read()
    pictures = []
    for filename in sorted(os.listdir(os.path.join(weibopath, 'pictures'))):
        if filename.split('.')[-1].lower() in ['jpg', 'png']:
            pictures.append(open(os.path.join(weibopath, 'pictures', filename), 'rb').read())
    if len(pictures) > 9:
        print('[Warning]: 一条微博最多只能有9张配图, 程序现在将自动剔除多出的图片')
        pictures = pictures[:9]
    return text, pictures

该函数可以根据传入的微博文件夹路径(例如我们刚刚在当前目录新建了一个名为weibo的文件夹,那么我们可以传入./weibo)来自动解析待发送的微博内容(文字+配图)。

成功解析微博内容后,进入我们的重头戏,对着电脑大吼一声来自动发微博。原理说起来其实很简单,就是利用python的pyaudio包来检测当前电脑麦克风输入的声音音量,当音量足够大时(需要在30s内达到设定的最小音量),就自动把刚刚的微博发送出去:

# 大吼一声确定是该微博
print('微博内容为: %s\n配图数量为: %s' % (text, len(pictures)))
print('如果您确认想发这条微博, 请在30s内对着电脑大吼一声')
stream = pyaudio.PyAudio().open(format=pyaudio.paInt16, 
                                channels=1, 
                                rate=int(pyaudio.PyAudio().get_device_info_by_index(0)['defaultSampleRate']), 
                                input=True, 
                                frames_per_buffer=1024)
is_send_flag = False
start_t = time.time()
while True:
    time.sleep(0.1)
    audio_data = stream.read(1024)
    k = max(struct.unpack('1024h', audio_data))
    # --声音足够大, 发送这条微博
    if k > 8000:
        is_send_flag = True
        break
    # --时间到了还没有足够大的声音, 不发这条微博
    if (time.time() - start_t) > 30:
        break
# 发送微博
if is_send_flag:
    print('大吼成功! 准备开始发送该条微博~')
    if self.__sendWeibo(text, pictures):
        print('[INFO]: 微博发送成功!')

有小伙伴可能会问了,咋自动发微博?和网上的教程一样去注册SDK么?当然,不是啦,注册SDK多麻烦(还经常通不过注册T_T)。直接试着发条微博抓个包不就行了么:

我们可以很容易发现,图片上传用的是这个接口:

发微博用的则是这个接口:

于是我们可以很轻松地写出自动发微博的函数来:

'''发微博'''
def __sendWeibo(self, text, pictures):
    # 上传图片
    pic_id = []
    url = 'https://picupload.weibo.com/interface/pic_upload.php'
    params = {
                'data': '1',
                'p': '1',
                'url': 'weibo.com/u/%s' % self.uid,
                'markpos': '1',
                'logo': '1',
                'nick': '@%s' % self.nickname,
                'marks': '1',
                'app': 'miniblog',
                's': 'json',
                'pri': 'null',
                'file_source': '1'
            }
    for picture in pictures:
        res = self.session.post(url, headers=self.headers, params=params, data=picture)
        res_json = res.json()
        if res_json['code'] == 'A00006':
            pid = res_json['data']['pics']['pic_1']['pid']
            pic_id.append(pid)
        time.sleep(random.random()+0.5)
    # 发微博
    url = 'https://www.weibo.com/aj/mblog/add?ajwvr=6&__rnd=%d' % int(time.time() * 1000)
    data = {
                'title': '',
                'location': 'v6_content_home',
                'text': text,
                'appkey': '',
                'style_type': '1',
                'pic_id': '|'.join(pic_id),
                'tid': '',
                'pdetail': '',
                'mid': '',
                'isReEdit': 'false',
                'gif_ids': '',
                'rank': '0',
                'rankid': '',
                'pub_source': 'page_2',
                'topic_id': '',
                'updata_img_num': str(len(pictures)),
                'pub_type': 'dialog'
            }
    headers = self.headers.copy()
    headers.update({'Referer': 'http://www.weibo.com/u/%s/home?wvr=5' % self.uid})
    res = self.session.post(url, headers=headers, data=data)
    is_success = False
    if res.status_code == 200:
        is_success = True
    return is_success

OK,大功告成~完整源代码详见相关文件。

相关文件

https://github.com/CharlesPik...

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 3月3日

听说想了解一个人要从爬取她(他)的所有微博开始呢~

原文链接

听说想了解一个人要从爬取她(他)的所有微博开始呢~​mp.weixin.qq.com图标

导语

既然你已经点进来了,那我就直说吧,标题其实是我瞎编的,但套路你点进来学习的我是真心的。

前两天发了篇文章:

嘿嘿,之前开源的模拟登录工具包开源文档上线啦~

从阅读量可以看出来了,很多小伙伴对这玩意并不感兴趣。看来还是得多回归回归老本行,写点有趣的脚本,才能套路各位过来学习呀。今天的小目标是写个小爬虫,爬取目标用户发的所有微博数据。废话不多说,让我们愉快地开始吧~

原理简介

这里简单讲讲整个爬取的流程吧。首先,当然是模拟登录新浪微博啦,这里还是借助我们之前开源的模拟登录包来实现微博的模拟登录。具体而言,代码实现如下:

'''利用DecryptLogin模拟登录'''
@staticmethod
def login(username, password):
  lg = login.Login()
  _, session = lg.weibo(username, password, 'mobile')
  return session

然后,让程序使用者输入想要爬取的目标用户id。那么如何获取这个微博用户id呢?以刘亦菲的微博为例,首先,进入她的主页,然后可以看到链接里有:

所以刘亦菲的微博用户id为3261134763。

根据使用者输入的微博用户id,我们用已经实现模拟登录的会话来访问以下链接:

# 链接1
url = f'https://weibo.cn/{user_id}'
res = self.session.get(url, headers=self.headers)
# 链接2
url = f'https://weibo.cn/{user_id}/info'
res = self.session.get(url, headers=self.headers)

这个链接在浏览器里显示大概是这样的:

显然,在这,我们可以利用xpath提取到目标用户的一些基本信息:

# 链接1
selector = etree.HTML(res.content)
base_infos = selector.xpath("//div[@class='tip2']/*/text()")
num_wbs, num_followings, num_followers = int(base_infos[0][3: -1]), int(base_infos[1][3: -1]), int(base_infos[2][3: -1])
num_wb_pages = selector.xpath("//input[@name='mp']")
num_wb_pages = int(num_wb_pages[0].attrib['value']) if len(num_wb_pages) > 0 else 1
# 链接2
selector = etree.HTML(res.content)
nickname = selector.xpath('//title/text()')[0][:-3]

xpath是啥我就不多废话了,看下网页源码,很容易就可以写出来:

提取完之后打印出来让程序使用者确认一下用自己输入的用户id获得的用户信息是不是和自己想要爬取的用户信息一样,等使用者确认信息无误再开始爬该用户的微博数据:

# 使用者确认是否要下载该用户的所有微博
tb = prettytable.PrettyTable()
tb.field_names = ['用户名', '关注数量', '被关注数量', '微博数量', '微博页数']
tb.add_row([nickname, num_followings, num_followers, num_wbs, num_wb_pages])
print('获取的用户信息如下:')
print(tb)
is_download = input('是否爬取该用户的所有微博?(y/n, 默认: y) ——> ')
if is_download == 'y' or is_download == 'yes' or not is_download:
  userinfos = {'user_id': user_id, 'num_wbs': num_wbs, 'num_wb_pages': num_wb_pages}
  self.__downloadWeibos(userinfos)

爬用户微博数据也基本用的xpath来提取数据,而查看用户的微博只需要访问以下这个链接就可以了:

url = f'https://weibo.cn/{user_id}?page={page}'
page代表访问用户的第page页微博

没啥特别的技巧,值得一提的处理只有两点:

  • 每爬20页微博数据,就保存一次数据,以避免爬虫意外中断,导致之前已经爬到的数据“人去楼空”;
  • 每爬n页数据就暂停x秒,其中n是随机生成的,且n一直在变化,x也是随机生成的,且x也一直在变化。

思路就是这么个思路,一些细节的处理就自己看源代码吧,代码运行的效果见:
https://zhuanlan.zhihu.com/p/...

代码运行的命令格式为:

python weiboSpider.py --username 用户名 --password 密码

大功告成啦,完整源代码详见相关文件。

数据可视化

老规矩,把爬到的数据拿来可视化一波呗,方便起见,就看看刘亦菲的微博数据可视化效果吧。

先来看看用她发的所有微博做的词云吧(仅原创微博):

然后看看她原创和转发的微博数量?

以及每年发的微博数量?

果然现在发的微博数量变少了很多。看看她发的第一条微博呗,微博id是zoaIU7o2d,😀:

“大家好,我是刘亦菲”

统计一下她每年发的原创微博都拿到了多少赞?

多少转发量?

以及多少评论?

爬的数据不多,就先这样呗,完整源代码和数据详见相关文件~

相关文件

https://github.com/CharlesPikac

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 3月1日

之前开源的模拟登录工具包开源文档上线啦~

原文链接

嘿嘿,之前开源的模拟登录工具包开源文档上线啦~​mp.weixin.qq.com图标

导语

各位小伙伴还记得之前开源的模拟登录工具包嘛,就是这个:

前排造轮子|分享一个最近自己开源的小项目

前段时间上传到pypi之后:

来个小教程吧 | 发布自己的python库到pypi

就一直想着再给它搞个开源项目文档,让它变得更“正规”一些。今天整了一上午,终于实现了这个小目标,过来再简单推销一下这个项目以及记录下制作文档的过程。

DecryptLogin简介

DecryptLogin是我自己开源的一个用于实现网站模拟登录的python第三方工具包。目前支持以下网站的模拟登录:

项目地址(欢迎stars呀):

https://github.com/CharlesPikachu/DecryptLogin

项目的开源文档地址:

DecryptLogin中文文档 - DecryptLogin 0.1.0 文档

https://httpsgithubcomcharlespikachudecryptlogin.readthedocs.io/zh/latest/ (In Chinese)
https://httpsgithubcomcharlespikachudecryptlogin.readthedocs.io/en/latest/ (In English)

简单截个图(其实还有些功能没写,以后慢慢完善吧)

项目中也包含了之前发的关于该库的使用案例的所有源代码,例如:

模拟登录系列 | 来写个网易云个人歌单下载器?

模拟登录系列 | 下载B站指定UP主的所有视频

等等(后面还会添加更多有趣的案例~)。

简单聊聊文档的制作过程

首先当然是找资料啦,第一反应是在Github上搜索一下开源文档的制作教程,结果找到了这个:

http://www.wbh-doc.com.s3.amazonaws.com/Python-OpenSource-Project-Developer-Guide/index.html

上面介绍了三种部署开源文档的方式:

(1) 使用PyPI Host
(2) 使用ReadTheDocs
(3) 使用AWS S3
因为ReadTheDocs可以自动关联Github账户实现网站自动更新,
而且完全免费,于是我选择了第二种方案。

以及创建开源文档网站可以使用的工具:

sphinx是Python社区用于自动生成文档网站的工具。
可以用来生成纯文档, 也可以自动从Python代码中提取文档。

确定了工具之后,就去网上找了个模板(用的港中文和商汤联合开源的mmdetection里的文档模板):

https://github.com/open-mmlab/mmdetection/tree/master/docs

顺便去官网简单地浏览了一下sphinx的使用教程

http://www.sphinx-doc.org/en/stable/

然后就是照着教程把下载的模板改成我自己的开源文档网站。但是制作过程中遇到了一个问题,我想把文档搞成中英文双版本的,一时间在官方给的说明里没找到对应的教程,于是去Google搜索了一下,发现了这个网站:

https://readthedocs-demo-zh.readthedocs.io/zh_CN/latest/index.html

里面介绍了一个解决方案(大概就是项目导入两次然后设置一下翻译版本就OK了,当然如果需要两种以上的语言,操作就比这麻烦些了),试了下,发现成功了:

大概是运气比较好,没遇到什么大坑,就这么直接部署成功了:

就是这样,同样需要部署文档的小伙伴可以参考一下上面提到的几个文章链接,感觉还是有帮助的。

总之,欢迎感兴趣的小伙伴关注一下这个项目呀~

https://github.com/CharlesPikachu/DecryptLogin

后面会不断完善这个库的功能以及增加更多有趣的小例子哒~

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 2月28日

随便聊聊pyinstaller打包和它的安全性吧

原文链接

答个疑,随便聊聊pyinstaller打包以及安全性吧​mp.weixin.qq.com图标

导语

感觉经常有小伙伴问我关于pyinstaller打包的问题,不回吧,可能会被说咋这么高冷不理人,一个个回吧,又感觉太浪费时间了,干脆写篇文章聊聊被问的比较多的两个问题吧。

相关文件

本期文章中涉及到的所有相关文件可通过关注微信公众号“Charles的皮卡丘”,在公众号内回复“pyinstaller打包”获取。

打包程序素材

关于pyinstaller如何把图片,音乐,字体等素材文件也打包进exe文件中。很久以前其实有篇文章讲过,不过可能是我没讲清楚,以至于到现在也经常有人问我类似的问题。这里就不整那些花里胡哨的东西了,直接讲讲我们该怎么做才能实现这个功能吧,先声明一下,其实这东西官网里就有教程,不明白且想明白为什么这么做的自己看官网的介绍吧:

https://pyinstaller.readthedocs.io/en/v3.3.1/runtime-information.html

还是以之前的表白小软件为例:

情人节到了,是时候把祖传小程序拿出来皮一波啦?

相关文件在这可以下载到:

https://github.com/CharlesPikachu/Tools/tree/master/NaughtyConfession

有用的就这三个文件:

其中love.py是主程序,cfg.py是配置文件,resources文件夹里是一些类似字体,音乐等的素材文件。

先直接试试运行如下命令打包:

pyinstaller -F love.py -w

打包结束后根目录变成了这样:

dist文件夹里有打包好的exe文件。打开文件夹,直接双击运行一下,会发现报错:

原因很简单,因为你没把相关的素材文件打包进这个exe文件,而在该目录下根据程序本身的设定是无法读取到这些素材文件的。你需要先把该exe文件移动到love.py这个主程序所在的目录,然后双击运行:

想要把素材文件也打包进exe文件的话,得先修改下程序,把程序中关于素材资源加载路径的相关代码从(在cfg.py文件里):

# 背景音乐路径
BGM_PATH = os.path.join(os.getcwd(), 'resources/music/bgm.mp3')
# 字体路径
FONT_PATH = os.path.join(os.getcwd(), 'resources/font/STXINGKA.TTF')
# 背景图片路径
BG_IMAGE_PATH = os.path.join(os.getcwd(), 'resources/images/bg.png')
# ICON路径
ICON_IMAGE_PATH = os.path.join(os.getcwd(), 'resources/images/icon.png')

改成:

if getattr(sys, 'frozen', False):
  cur_path = sys._MEIPASS
else:
  cur_path = os.path.dirname(__file__)
# 背景音乐路径
BGM_PATH = os.path.join(cur_path, 'resources/music/bgm.mp3')
# 字体路径
FONT_PATH = os.path.join(cur_path, 'resources/font/STXINGKA.TTF')
# 背景图片路径
BG_IMAGE_PATH = os.path.join(cur_path, 'resources/images/bg.png')
# ICON路径
ICON_IMAGE_PATH = os.path.join(cur_path, 'resources/images/icon.png')

然后新建一个.spec文件,当然,为了方便,你可以直接打开刚刚生成的那个.spec文件(就是运行最前面那个打包命令时,也会根据你的命令来生成一个love.spec文件),类似这样:

打开该文件,可以发现该文件里的内容是这样的(为了方便某些懒癌患者复制粘贴,我就不截图而是直接把内容copy下来了):

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['love.py'],
             pathex=['C:\\Users\\ThinkPad\\Desktop\\NaughtyConfession'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='love',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=False )

通过修改该文件,可以将指定的素材资源全部打包进exe文件中,具体而言,修改后的文件如下:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


added_files = [('C:\\Users\\ThinkPad\\Desktop\\NaughtyConfession\\resources', 'resources')]
a = Analysis(['love.py'],
             pathex=['C:\\Users\\ThinkPad\\Desktop\\NaughtyConfession'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='love',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=False )

其实就加了一行代码(第六行):

added_files = [('C:\\Users\\ThinkPad\\Desktop\\NaughtyConfession\\resources', 'resources')]

然后把(第十行):

datas=[],

改成了:

datas=added_files,

就这么简单就完事了,很意外吧?最后在命令行运行:

pyinstaller -F love.spec

同样地,在dist文件夹里会生成打包好的exe文件,双击运行一下,可以发现这个exe文件竟然可以直接运行啦:

至此,我们轻松地实现了将python程序的素材文件一起打包进exe文件的目标。当然,上面只是介绍了一种个人比较习惯且相对简单方便的解决方案,想了解更多相关内容以及原理,各位小伙伴还是自己去查阅官方文档吧:

https://pyinstaller.readthedocs.io/en/v3.3.1/index.html

打包安全性

一样地,我们舍弃那堆花里胡哨的前言,直接开干,毕竟实践出真知,讲一堆有的没的废话,还不如搞一波事,感兴趣的小伙伴自然会自己进一步探究下去。以我们刚刚打包好的exe文件为例,就是它:

假设我们只把这个exe文件发给了心仪的小姐姐/小哥哥(然后人家拉黑了你

)。那么对方能不能通过这个exe文件来获得你的源代码呢?先公布答案,可以。让我们一步步操作下去来实现这个目的。

先到这下载个解包工具:

https://sourceforge.net/projects/pyinstallerextractor/

下载后长这样:

然后运行如下命令:

python pyinstxtractor.py love.exe

运行后发现根目录变成了这样:

多了一个文件夹,打开后发现里面一堆ddl,pyd文件:

在这里面我们可以找到三个比较关键的文件:

其中love就是你之前打包的那个py文件对应的pyc文件。注意,如果exe文件名被改动过,比如一开始打包好的love.exe被改成了pig.exe,那么你找到的文件仍然是:

love.exe.manifest

而不是:

pig.exe.manifest

struct也是一个pyc文件。于是我们现在只需要反编译这些pyc文件就行了,随便搜索下就可以发现一堆相关的网站:

随便选一个就OK了:

http://tools.bugscaner.com/decompyle/

啊,对了,还要下载个十六进制编辑器,比如:

https://wxmedit.github.io/downloads.html

或者直接用notepad++也行。打开love和struct文件(重命名一下加个后缀就变成pyc文件了,这个总不用我教了吧T_T):

把struct.pyc文件里的前12个字节复制到love.pyc文件里,说人话就是我们都知道:

1B = 8bit
表示一个16进制数需要4bit

剩下数数的活就不需要我来教了吧

总之,一顿操作之后,love.pyc文件变成了这样:

保存,然后拿去在线反编译:

可以发现我们已经成功地通过单个exe文件获得了程序的源代码。

不过pyinstaller提供了--key这个选项,可以实现加密打包,但实际上它只对依赖库进行了加密,并没有对主程序做加密处理。所以,不希望自己代码泄露的小伙伴应该知道写程序和打包的时候该怎么做了吧。

查看原文

赞 1 收藏 1 评论 0

白露未晞 发布了文章 · 2月20日

尝试用强化学习算法来玩下FlappyBird?

原文链接

https://mp.weixin.qq.com/s/nm...

效果展示

参见:https://zhuanlan.zhihu.com/p/...

原理简介

原理其实在这篇文章里讲过:

长文预警 | 利用DQN玩吃豆人(Pacman)小游戏

不过今天我们将尝试只用Q-Learning算法,而不是DQN来玩FlappyBird这款经典的小游戏。有些懒癌患者可能不想点击上面的超链接去看那么长的文章,所以这里我们先简单介绍一下Q-Learning算法,然后再说下如何用这个算法玩FlappyBird这个游戏,以及我们的代码实现。

1.Q-Learning算法

好像直接讲算法有点突兀,那就先举一个网上比较经典的例子吧:

假设需要创建一个智能体agent,他需要去捡右上角的蓝宝石,agent每次可以移动一个方块的距离,若agent踩到陷阱就会死掉,那么如何设计策略才能让agent学会去捡右上角的蓝宝石同时不会在路途中因为踩到陷阱而死掉呢?

一个比较直观的方案就是创建类似下图这样的表格:

即对于当前的状态S(也就是agent现在在哪个方块中),我们都可以为agent计算出每种动作A(即上下左右移动)最大的未来期望奖励R,从而可以知道每个状态应当采取的最佳动作。将上面的表格弄成这样,就是我们熟悉的Q-table啦(Q代表动作的质量):

那么我们如何获得Q-table中的值呢?这时候就需要Q-Learning算法闪亮登场啦。该算法的基本流程如下:

Q-Learning的思想基于价值迭代,直观地理解就是每次利用新得到的reward和原本的Q值来更新现在的Q值。其数学形式表示为:

其中Q是当前的table,Q*是更新后的table,r是在状态s时采取动作a后获得的奖励,α可以当作是学习率,γ一般称为折扣因子,用于定义未来奖励的重要性。max{Q(s', a')}用于计算进行动作a后进入新的状态s'时可以获得的最大奖励。至于这个公式咋来的,还是请参见:

长文预警 | 利用DQN玩吃豆人(Pacman)小游戏

感觉再打下去还不如你们自己跳转去看了。

2.FlappyBird游戏实现

学习了Q-learning算法之后,我们需要先实现一下我们的FlappyBird小游戏,然后再考虑怎么把算法用在这个游戏上?

显然,我们之前已经写过一个这样的游戏了:

重做一波之前的flappybird呗~

当然,为了方便实现后面的算法,我们对游戏做了一些微小的改动,即我们把小鸟上下移动的速度都做了取整化处理(也就是速度每次加1或者减1了,并且假设每帧的时间为单位1,该帧内小鸟的速度仍然假设为保持不变)。

3.如何用Q-Learning玩FlappyBird?

其实很简单,只需要明确状态state,动作action和奖赏reward,然后往算法里套就OK啦~

对于状态,我们假设小鸟当前的状态定义为:

s = (delta_x, delta_y, speed)
--delta_x:小鸟和即将通过那组管道的下半部分,水平方向上的距离
--delta_y:小鸟和即将通过那组管道的下半部分,竖直方向上的距离
--speed:小鸟当前的速度

一个丑陋的示意图:

当然delta_x和delta_y也可以用其他方式定义,这个无所谓的。

动作的话无非是这样:

a = 1 向上飞一下
a = 0 啥都不做

奖赏的话,可以这样:

reward = 1 平安无事
reward = -1000000 小鸟死掉了
reward = 5 小鸟成功通过了一组管道

反正只要合理,应该大差不差。

接下来的事情就是套算法了,具体而言,其核心代码实现如下:

'''q learning agent'''
class QLearningAgent():
    def __init__(self, mode, **kwargs):
        self.mode = mode
        # learning rate
        self.learning_rate = 0.7
        # discount factor(also named discount rate)
        self.discount_factor = 0.95
        # store the necessary history data, the format is [previous_state, previous_action, state, reward]
        self.history_storage = []
        # store the q values, the last dimension is [value_for_do_nothing, value_for_flappy]
        self.qvalues_storage = np.zeros((130, 130, 20, 2))
        # store the score for each episode
        self.scores_storage = []
        # previous state
        self.previous_state = []
        # 0 means do nothing, 1 means flappy
        self.previous_action = 0
        # number of episode
        self.num_episode = 0
        # the max score so far
        self.max_score = 0
    '''make a decision'''
    def act(self, delta_x, delta_y, bird_speed):
        if not self.previous_state:
            self.previous_state = [delta_x, delta_y, bird_speed]
            return self.previous_action
        if self.mode == 'train':
            state = [delta_x, delta_y, bird_speed]
            self.history_storage.append([self.previous_state, self.previous_action, state, 0])
            self.previous_state = state
        # make a decision according to the qvalues
        if self.qvalues_storage[delta_x, delta_y, bird_speed][0] >= self.qvalues_storage[delta_x, delta_y, bird_speed][1]:
            self.previous_action = 0
        else:
            self.previous_action = 1
        return self.previous_action
    '''set reward'''
    def setReward(self, reward):
        if self.history_storage:
            self.history_storage[-1][3] = reward
    '''update the qvalues_storage after an episode'''
    def update(self, score, is_logging=True):
        self.num_episode += 1
        self.max_score = max(self.max_score, score)
        self.scores_storage.append(score)
        if is_logging:
            print('Episode: %s, Score: %s, Max Score: %s' % (self.num_episode, score, self.max_score))
        if self.mode == 'train':
            history = list(reversed(self.history_storage))
            # penalize last num_penalization states before crash
            num_penalization = 2 
            for item in history:
                previous_state, previous_action, state, reward = item
                if num_penalization > 0:
                    num_penalization -= 1
                    reward = -1000000
                x_0, y_0, z_0 = previous_state
                x_1, y_1, z_1 = state
                self.qvalues_storage[x_0, y_0, z_0, previous_action] = (1 - self.learning_rate) * self.qvalues_storage[x_0, y_0, z_0, previous_action] +\
                                                                       self.learning_rate * (reward + self.discount_factor * max(self.qvalues_storage[x_1, y_1, z_1]))
            self.history_storage = []
    '''save the model'''
    def saveModel(self, modelpath):
        data = {
                'num_episode': self.num_episode,
                'max_score': self.max_score,
                'scores_storage': self.scores_storage,
                'qvalues_storage': self.qvalues_storage
            }
        with open(modelpath, 'wb') as f:
            pickle.dump(data, f)
        print('[INFO]: save checkpoints in %s...' % modelpath)
    '''load the model'''
    def loadModel(self, modelpath):
        print('[INFO]: load checkpoints from %s...' % modelpath)
        with open(modelpath, 'rb') as f:
            data = pickle.load(f)
        self.num_episode = data.get('num_episode')
        self.qvalues_storage = data.get('qvalues_storage')

OK,大功告成,完整源代码详见相关文件~

参考文献:

[1].https://blog.csdn.net/qq_30615903/article/details/80739243
[2].https://www.zhihu.com/question/26408259
[3].https://zhuanlan.zhihu.com/p/35724704

相关文件

https://github.com/CharlesPik...

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 2月14日

情人节到了,是时候把祖传小程序拿出来皮一波啦?

原文链接

https://mp.weixin.qq.com/s/wM...

效果展示

视频见:https://zhuanlan.zhihu.com/p/...
image.png

原理简介

原理和之前的代码类似,只不过之前那个代码写的丑,效果也丑(每次当前月看上个月的代码我都会觉得写的好丑,更别说这么“远古”的代码了,不知道大家是不是也这么觉得T_T)。

具体而言,首先我们来定义一个按钮类,其功能是可以根据初始化参数生成一个界面上按钮,且这个按钮是否可以被点击到也由传入的初始化参数决定,具体而言代码实现如下:

'''
Function:
  按钮类
Initial Args:
  --x, y: 按钮左上角坐标
  --width, height: 按钮宽高
  --text: 按钮显示的文字
  --fontpath: 字体路径
  --fontsize: 字体大小
  --fontcolor: 字体颜色
  --bgcolors: 按钮背景颜色
  --is_want_to_be_selected: 按钮是否想被玩家选中
  --screensize: 软件屏幕大小
'''
class Button(pygame.sprite.Sprite):
  def __init__(self, x, y, width, height, text, fontpath, fontsize, fontcolor, bgcolors, edgecolor, edgesize=1, is_want_to_be_selected=True, screensize=None, **kwargs):
    pygame.sprite.Sprite.__init__(self)
    self.rect = pygame.Rect(x, y, width, height)
    self.text = text
    self.font = pygame.font.Font(fontpath, fontsize)
    self.fontcolor = fontcolor
    self.bgcolors = bgcolors
    self.edgecolor = edgecolor
    self.edgesize = edgesize
    self.is_want_tobe_selected = is_want_to_be_selected
    self.screensize = screensize
  '''自动根据各种情况将按钮绑定到屏幕'''
  def draw(self, screen, mouse_pos):
    # 鼠标在按钮范围内
    if self.rect.collidepoint(mouse_pos):
      # --不想被选中
      if not self.is_want_tobe_selected:
        while self.rect.collidepoint(mouse_pos):
          self.rect.left, self.rect.top = random.randint(0, self.screensize[0]-self.rect.width), random.randint(0, self.screensize[1]-self.rect.height)
      pygame.draw.rect(screen, self.bgcolors[0], self.rect, 0)
      pygame.draw.rect(screen, self.edgecolor, self.rect, self.edgesize)
    # 鼠标不在按钮范围内
    else:
      pygame.draw.rect(screen, self.bgcolors[1], self.rect, 0)
      pygame.draw.rect(screen, self.edgecolor, self.rect, self.edgesize)
    text_render = self.font.render(self.text, True, self.fontcolor)
    fontsize = self.font.size(self.text)
    screen.blit(text_render, (self.rect.x+(self.rect.width-fontsize[0])/2, self.rect.y+(self.rect.height-fontsize[1])/2))

其实就是看看鼠标的当前位置有没有在按钮所在的范围内,如果在且设置的不让用户可以点击到该按钮,就自动地移动按钮的位置,使鼠标位置不在移动后的按钮所在的范围内。

然后写个主循环,把界面大小,配色,布局啥的弄的稍微走心一点:

'''主函数'''
def main():
  # 初始化
  pygame.init()
  screen = pygame.display.set_mode(cfg.SCREENSIZE, 0, 32)
  pygame.display.set_icon(pygame.image.load(cfg.ICON_IMAGE_PATH))
  pygame.display.set_caption('来自一位喜欢你的小哥哥')
  # 背景音乐
  pygame.mixer.music.load(cfg.BGM_PATH)
  pygame.mixer.music.play(-1, 30.0)
  # biu爱心那个背景图片
  bg_image = pygame.image.load(cfg.BG_IMAGE_PATH)
  bg_image = pygame.transform.smoothscale(bg_image, (150, 150))
  # 实例化两个按钮
  button_yes = Button(x=20, y=cfg.SCREENSIZE[1]-70, width=120, height=35, 
            text='好呀', fontpath=cfg.FONT_PATH, fontsize=15, fontcolor=cfg.BLACK, edgecolor=cfg.SKYBLUE, 
            edgesize=2, bgcolors=[cfg.DARKGRAY, cfg.GAINSBORO], is_want_to_be_selected=True, screensize=cfg.SCREENSIZE)
  button_no = Button(x=cfg.SCREENSIZE[0]-140, y=cfg.SCREENSIZE[1]-70, width=120, height=35, 
             text='算了吧', fontpath=cfg.FONT_PATH, fontsize=15, fontcolor=cfg.BLACK, edgecolor=cfg.DARKGRAY, 
             edgesize=1, bgcolors=[cfg.DARKGRAY, cfg.GAINSBORO], is_want_to_be_selected=False, screensize=cfg.SCREENSIZE)
  # 是否点击了好呀按钮
  is_agree = False
  # 主循环
  clock = pygame.time.Clock()
  while True:
    # --背景图片
    screen.fill(cfg.WHITE)
    screen.blit(bg_image, (cfg.SCREENSIZE[0]-bg_image.get_height(), 0))
    # --鼠标事件捕获
    for event in pygame.event.get():
      if event.type == pygame.QUIT:
        # ----没有点击好呀按钮之前不许退出程序
        if is_agree:
          pygame.quit()
          sys.exit()
      elif event.type == pygame.MOUSEBUTTONDOWN and event.button:
        if button_yes.rect.collidepoint(pygame.mouse.get_pos()):
          button_yes.is_selected = True
          root = Tk()
          root.withdraw()
          messagebox.showinfo('', '❤❤❤么么哒❤❤❤')
          root.destroy()
          is_agree = True
    # --显示文字
    showText(screen=screen, text='小姐姐, 我观察你很久了', position=(40, 50), 
         fontpath=cfg.FONT_PATH, fontsize=25, fontcolor=cfg.BLACK, is_bold=False)
    showText(screen=screen, text='做我女朋友好不好?', position=(40, 100), 
         fontpath=cfg.FONT_PATH, fontsize=25, fontcolor=cfg.BLACK, is_bold=True)
    # --显示按钮
    button_yes.draw(screen, pygame.mouse.get_pos())
    button_no.draw(screen, pygame.mouse.get_pos())
    # --刷新
    pygame.display.update()
    clock.tick(60)

没啥关键点,代码啥意思自己看注释吧T_T。记得设置个flag,对方没点击“好呀”按钮之前,不要让对方可以关闭这个小程序就好啦~

完整源代码

https://github.com/CharlesPik...

查看原文

赞 0 收藏 0 评论 0

白露未晞 发布了文章 · 2月12日

支持联机对战的五子棋小游戏

原文链接

https://mp.weixin.qq.com/s/79...

效果展示

1.png
2.png
3.png

原理简介

每次都写单机游戏自嗨好像没啥意思,这次我们来写个支持联机对战的游戏吧,省的有人在issue里说:

好吧,联机和对手比赛输了总不能怪我了吧

OK,跑题了,这明明是一个学习用的公众号。因为我之前也没写过可以联机对战的游戏,所以先整个简单的游戏试试吧,支持局域网联机对战的五子棋小游戏。废话不多说,让我们愉快地开始吧~

这里简单介绍下原理吧,代码主要用PyQt5写的,pygame只用来播放一些音效。首先,设计并实现个游戏主界面:

代码实现如下:

'''游戏开始界面'''
class gameStartUI(QWidget):
  def __init__(self, parent=None, **kwargs):
    super(gameStartUI, self).__init__(parent)
    self.setFixedSize(760, 650)
    self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘')
    self.setWindowIcon(QIcon(cfg.ICON_FILEPATH))
    # 背景图片
    palette = QPalette()
    palette.setBrush(self.backgroundRole(), QBrush(QPixmap(cfg.BACKGROUND_IMAGEPATHS.get('bg_start'))))
    self.setPalette(palette)
    # 按钮
    # --人机对战
    self.ai_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('ai'), self)
    self.ai_button.move(250, 200)
    self.ai_button.show()
    self.ai_button.click_signal.connect(self.playWithAI)
    # --联机对战
    self.online_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('online'), self)
    self.online_button.move(250, 350)
    self.online_button.show()
    self.online_button.click_signal.connect(self.playOnline)
  '''人机对战'''
  def playWithAI(self):
    self.close()
    self.gaming_ui = playWithAIUI(cfg)
    self.gaming_ui.exit_signal.connect(lambda: sys.exit())
    self.gaming_ui.back_signal.connect(self.show)
    self.gaming_ui.show()
  '''联机对战'''
  def playOnline(self):
    self.close()
    self.gaming_ui = playOnlineUI(cfg, self)
    self.gaming_ui.show()

会pyqt5的应该都可以写出这样的界面,没啥特别的,记得把人机对战和联机对战两个按钮触发后的信号分别绑定到人机对战和联机对战的函数上就行。

然后分别来实现人机对战和联机对战就行了。这里人机对战的算法抄的公众号之前发的那篇AI五子棋的文章里用的算法,所以只要花点心思用PyQt5重新写个游戏界面就行了,效果大概是这样的:

主要的代码实现如下:

'''人机对战'''
class playWithAIUI(QWidget):
    back_signal = pyqtSignal()
    exit_signal = pyqtSignal()
    send_back_signal = False
    def __init__(self, cfg, parent=None, **kwargs):
        super(playWithAIUI, self).__init__(parent)
        self.cfg = cfg
        self.setFixedSize(760, 650)
        self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘')
        self.setWindowIcon(QIcon(cfg.ICON_FILEPATH))
        # 背景图片
        palette = QPalette()
        palette.setBrush(self.backgroundRole(), QBrush(QPixmap(cfg.BACKGROUND_IMAGEPATHS.get('bg_game'))))
        self.setPalette(palette)
        # 按钮
        self.home_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('home'), self)
        self.home_button.click_signal.connect(self.goHome)
        self.home_button.move(680, 10)
        self.startgame_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('startgame'), self)
        self.startgame_button.click_signal.connect(self.startgame)
        self.startgame_button.move(640, 240)
        self.regret_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('regret'), self)
        self.regret_button.click_signal.connect(self.regret)
        self.regret_button.move(640, 310)
        self.givein_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('givein'), self)
        self.givein_button.click_signal.connect(self.givein)
        self.givein_button.move(640, 380)
        # 落子标志
        self.chessman_sign = QLabel(self)
        sign = QPixmap(cfg.CHESSMAN_IMAGEPATHS.get('sign'))
        self.chessman_sign.setPixmap(sign)
        self.chessman_sign.setFixedSize(sign.size())
        self.chessman_sign.show()
        self.chessman_sign.hide()
        # 棋盘(19*19矩阵)
        self.chessboard = [[None for i in range(19)] for _ in range(19)]
        # 历史记录(悔棋用)
        self.history_record = []
        # 是否在游戏中
        self.is_gaming = True
        # 胜利方
        self.winner = None
        self.winner_info_label = None
        # 颜色分配and目前轮到谁落子
        self.player_color = 'white'
        self.ai_color = 'black'
        self.whoseround = self.player_color
        # 实例化ai
        self.ai_player = aiGobang(self.ai_color, self.player_color)
        # 落子声音加载
        pygame.mixer.init()
        self.drop_sound = pygame.mixer.Sound(cfg.SOUNDS_PATHS.get('drop'))
    '''鼠标左键点击事件-玩家回合'''
    def mousePressEvent(self, event):
        if (event.buttons() != QtCore.Qt.LeftButton) or (self.winner is not None) or (self.whoseround != self.player_color) or (not self.is_gaming):
            return
        # 保证只在棋盘范围内响应
        if event.x() >= 50 and event.x() <= 50 + 30 * 18 + 14 and event.y() >= 50 and event.y() <= 50 + 30 * 18 + 14:
            pos = Pixel2Chesspos(event)
            # 保证落子的地方本来没有人落子
            if self.chessboard[pos[0]][pos[1]]:
                return
            # 实例化一个棋子并显示
            c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
            c.move(event.pos())
            c.show()
            self.chessboard[pos[0]][pos[1]] = c
            # 落子声音响起
            self.drop_sound.play()
            # 最后落子位置标志对落子位置进行跟随
            self.chessman_sign.show()
            self.chessman_sign.move(c.pos())
            self.chessman_sign.raise_()
            # 记录这次落子
            self.history_record.append([*pos, self.whoseround])
            # 是否胜利了
            self.winner = checkWin(self.chessboard)
            if self.winner:
                self.showGameEndInfo()
                return
            # 切换回合方(其实就是改颜色)
            self.nextRound()
    '''鼠标左键释放操作-调用电脑回合'''
    def mouseReleaseEvent(self, event):
        if (self.winner is not None) or (self.whoseround != self.ai_color) or (not self.is_gaming):
            return
        self.aiAct()
    '''电脑自动下-AI回合'''
    def aiAct(self):
        if (self.winner is not None) or (self.whoseround == self.player_color) or (not self.is_gaming):
            return
        next_pos = self.ai_player.act(self.history_record)
        # 实例化一个棋子并显示
        c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
        c.move(QPoint(*Chesspos2Pixel(next_pos)))
        c.show()
        self.chessboard[next_pos[0]][next_pos[1]] = c
        # 落子声音响起
        self.drop_sound.play()
        # 最后落子位置标志对落子位置进行跟随
        self.chessman_sign.show()
        self.chessman_sign.move(c.pos())
        self.chessman_sign.raise_()
        # 记录这次落子
        self.history_record.append([*next_pos, self.whoseround])
        # 是否胜利了
        self.winner = checkWin(self.chessboard)
        if self.winner:
            self.showGameEndInfo()
            return
        # 切换回合方(其实就是改颜色)
        self.nextRound()
    '''改变落子方'''
    def nextRound(self):
        self.whoseround = self.player_color if self.whoseround == self.ai_color else self.ai_color
    '''显示游戏结束结果'''
    def showGameEndInfo(self):
        self.is_gaming = False
        info_img = QPixmap(self.cfg.WIN_IMAGEPATHS.get(self.winner))
        self.winner_info_label = QLabel(self)
        self.winner_info_label.setPixmap(info_img)
        self.winner_info_label.resize(info_img.size())
        self.winner_info_label.move(50, 50)
        self.winner_info_label.show()
    '''认输'''
    def givein(self):
        if self.is_gaming and (self.winner is None) and (self.whoseround == self.player_color):
            self.winner = self.ai_color
            self.showGameEndInfo()
    '''悔棋-只有我方回合的时候可以悔棋'''
    def regret(self):
        if (self.winner is not None) or (len(self.history_record) == 0) or (not self.is_gaming) and (self.whoseround != self.player_color):
            return
        for _ in range(2):
            pre_round = self.history_record.pop(-1)
            self.chessboard[pre_round[0]][pre_round[1]].close()
            self.chessboard[pre_round[0]][pre_round[1]] = None
        self.chessman_sign.hide()
    '''开始游戏-之前的对弈必须已经结束才行'''
    def startgame(self):
        if self.is_gaming:
            return
        self.is_gaming = True
        self.whoseround = self.player_color
        for i, j in product(range(19), range(19)):
            if self.chessboard[i][j]:
                self.chessboard[i][j].close()
                self.chessboard[i][j] = None
        self.winner = None
        self.winner_info_label.close()
        self.winner_info_label = None
        self.history_record.clear()
        self.chessman_sign.hide()
    '''关闭窗口事件'''
    def closeEvent(self, event):
        if not self.send_back_signal:
            self.exit_signal.emit()
    '''返回游戏主页面'''
    def goHome(self):
        self.send_back_signal = True
        self.close()
        self.back_signal.emit()

整个逻辑是这样的:

设计并实现游戏的基本界面之后,先默认永远是玩家先手(白子),电脑后手(黑子)。然后,当监听到玩家鼠标左键点击到棋盘网格所在的范围内的时候,捕获该位置,若该位置之前没有人落子过,则玩家成功落子,否则重新等待玩家鼠标左键点击事件。玩家成功落子后,判断是否因为玩家落子而导致游戏结束(即棋盘上有5颗同色子相连了),若游戏结束,则显示游戏结束界面,否则轮到AI落子。AI落子和玩家落子的逻辑类似,然后又轮到玩家落子,以此类推。

需要注意的是:为保证响应的实时性,AI落子算法应当写到鼠标左键点击后释放事件的响应中(感兴趣的小伙伴可以试试写到鼠标点击事件的响应中,这样会导致必须在AI计算结束并落子后,才能显示玩家上一次的落子和AI此次的落子结果)。

开始按钮就是重置游戏,没啥可说的,这里为了避免有些人喜欢耍赖,我实现的时候代码写的是必须完成当前对弈才能重置游戏(毕竟小伙子小姑娘们要学会有耐心地下完一盘棋呀)。

因为是和AI下,所以悔棋按钮直接悔两步,从历史记录列表里pop最后两次落子然后从棋盘对应位置取下这两次落子就OK了,并且保证只有我方回合可以悔棋以避免出现意料之外的逻辑出错。

认输按钮也没啥可说的,就是认输然后提前结束游戏。

接下来我们来实现一下联机对战,这里我们选择使用TCP/IP协议进行联机通信从而实现联机对战。先启动游戏的一方作为服务器端:

通过新开一个线程来实现监听:

threading.Thread(target=self.startListen).start()
'''开始监听客户端的连接'''
def startListen(self):
    while True:
       self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘 ——> 服务器端启动成功, 等待客户端连接中')
       self.tcp_socket, self.client_ipport = self.tcp_server.accept()
       self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘 ——> 客户端已连接, 点击开始按钮进行游戏')

后启动方作为客户端连接服务器端并发送客户端玩家的基本信息:

self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.connect(self.server_ipport)
data = {'type': 'nickname', 'data': self.nickname}
self.tcp_socket.sendall(packSocketData(data))
self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘 ——> 已经成功连接服务器, 点击开始按钮进行游戏')

当客户端连接到服务器端时,服务器端也发送服务器端的玩家基本信息给客户端:

data = {'type': 'nickname', 'data': self.nickname}
self.tcp_socket.sendall(packSocketData(data))

然后客户端和服务器端都利用新开的线程来实现网络数据监听接收:

'''接收客户端数据'''
def receiveClientData(self):
    while True:
        data = receiveAndReadSocketData(self.tcp_socket)
        self.receive_signal.emit(data)
'''接收服务器端数据'''
def receiveServerData(self):
    while True:
        data = receiveAndReadSocketData(self.tcp_socket)
        self.receive_signal.emit(data)

并根据接收到的不同数据在主进程中做成对应的响应:

'''响应接收到的数据'''
def responseForReceiveData(self, data):
    if data['type'] == 'action' and data['detail'] == 'exit':
        QMessageBox.information(self, '提示', '您的对手已退出游戏, 游戏将自动返回主界面')
        self.goHome()
    elif data['type'] == 'action' and data['detail'] == 'startgame':
        self.opponent_player_color, self.player_color = data['data']
        self.whoseround = 'white'
        self.whoseround2nickname_dict = {self.player_color: self.nickname, self.opponent_player_color: self.opponent_nickname}
        res = QMessageBox.information(self, '提示', '对方请求(重新)开始游戏, 您为%s, 您是否同意?' % {'white': '白子', 'black': '黑子'}.get(self.player_color), QMessageBox.Yes | QMessageBox.No)
        if res == QMessageBox.Yes:
            data = {'type': 'reply', 'detail': 'startgame', 'data': True}
            self.tcp_socket.sendall(packSocketData(data))
            self.is_gaming = True
            self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘 ——> %s走棋' % self.whoseround2nickname_dict.get(self.whoseround))
            for i, j in product(range(19), range(19)):
                if self.chessboard[i][j]:
                    self.chessboard[i][j].close()
                    self.chessboard[i][j] = None
            self.history_record.clear()
            self.winner = None
            if self.winner_info_label:
                self.winner_info_label.close()
            self.winner_info_label = None
            self.chessman_sign.hide()
        else:
            data = {'type': 'reply', 'detail': 'startgame', 'data': False}
            self.tcp_socket.sendall(packSocketData(data))
    elif data['type'] == 'action' and data['detail'] == 'drop':
        pos = data['data']
        # 实例化一个棋子并显示
        c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
        c.move(QPoint(*Chesspos2Pixel(pos)))
        c.show()
        self.chessboard[pos[0]][pos[1]] = c
        # 落子声音响起
        self.drop_sound.play()
        # 最后落子位置标志对落子位置进行跟随
        self.chessman_sign.show()
        self.chessman_sign.move(c.pos())
        self.chessman_sign.raise_()
        # 记录这次落子
        self.history_record.append([*pos, self.whoseround])
        # 是否胜利了
        self.winner = checkWin(self.chessboard)
        if self.winner:
            self.showGameEndInfo()
            return
        # 切换回合方(其实就是改颜色)
        self.nextRound()
    elif data['type'] == 'action' and data['detail'] == 'givein':
        self.winner = self.player_color
        self.showGameEndInfo()
    elif data['type'] == 'action' and data['detail'] == 'urge':
        self.urge_sound.play()
    elif data['type'] == 'action' and data['detail'] == 'regret':
        res = QMessageBox.information(self, '提示', '对方请求悔棋, 您是否同意?', QMessageBox.Yes | QMessageBox.No)
        if res == QMessageBox.Yes:
            pre_round = self.history_record.pop(-1)
            self.chessboard[pre_round[0]][pre_round[1]].close()
            self.chessboard[pre_round[0]][pre_round[1]] = None
            self.chessman_sign.hide()
            self.nextRound()
            data = {'type': 'reply', 'detail': 'regret', 'data': True}
            self.tcp_socket.sendall(packSocketData(data))
        else:
            data = {'type': 'reply', 'detail': 'regret', 'data': False}
            self.tcp_socket.sendall(packSocketData(data))
    elif data['type'] == 'reply' and data['detail'] == 'startgame':
        if data['data']:
            self.is_gaming = True
            self.setWindowTitle('五子棋-微信公众号: Charles的皮卡丘 ——> %s走棋' % self.whoseround2nickname_dict.get(self.whoseround))
            for i, j in product(range(19), range(19)):
                if self.chessboard[i][j]:
                    self.chessboard[i][j].close()
                    self.chessboard[i][j] = None
            self.history_record.clear()
            self.winner = None
            if self.winner_info_label:
                self.winner_info_label.close()
            self.winner_info_label = None
            self.chessman_sign.hide()
            QMessageBox.information(self, '提示', '对方同意开始游戏请求, 您为%s, 执白者先行.' % {'white': '白子', 'black': '黑子'}.get(self.player_color))
        else:
            QMessageBox.information(self, '提示', '对方拒绝了您开始游戏的请求.')
    elif data['type'] == 'reply' and data['detail'] == 'regret':
        if data['data']:
            pre_round = self.history_record.pop(-1)
            self.chessboard[pre_round[0]][pre_round[1]].close()
            self.chessboard[pre_round[0]][pre_round[1]] = None
            self.nextRound()
            QMessageBox.information(self, '提示', '对方同意了您的悔棋请求.')
        else:
            QMessageBox.information(self, '提示', '对方拒绝了您的悔棋请求.')
    elif data['type'] == 'nickname':
        self.opponent_nickname = data['data']

对战过程实现的基本逻辑和人机对战是一致的,只不过要考虑数据同步问题,所以看起来代码略多了一些。当然对于联机对战,我也做了一些小修改,比如必须点击开始按钮,并经过对方同意之后,才能正式开始对弈,悔棋按钮只有在对方回合才能按,对方同意悔棋后需要记得把落子方切换回自己。然后加了一个催促按钮,同样必须在对方回合才能按。其他好像也没什么特别的改动了。

All done~完整源代码详见相关文件~

相关文件

https://github.com/CharlesPik...

查看原文

赞 8 收藏 3 评论 0

认证与成就

  • 获得 88 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-24
个人主页被 447 人浏览