Hahally's BLOG

- 只想做个无关紧要的副词 -

0%

I know nothing but my ignorance.

5月18日

今天是单片机实习的第一天,因为疫情原因变成了线上开展。

上午,老师把六个选题都一一讲解了一遍,并告诉我们一些在实习过程中可能会出现的一些问题和注意事项。老师讲完后,我们就开始组队和选题了。

在经过一番并不激烈的讨论后,我们组选择了第二个项目:数字频率计设计

一、基本要求:

测量待测 TTL 电平信号的频率

  1. 频率范围:10 Hz ~ 50kHz,全测量范围误差不大于 1%;
  2. 用液晶屏 LCD1602 显示数值和单位,可显示频率及周期;
  3. 数据刷新率每秒三次以上;
  4. 在同一台单片机上自行设计测试用信号源;
  5. 通过串口通信在远程单片机(仿真环境)显示测量结果。

二、扩展要求

  1. 占空比测量
  2. 在保证数据刷新率和精度的基础上拓展测量频率范围

下午,我们组四个人开始查文献讨论总体方案和分工。又是一番并不激烈的讨论后,我们确定好了分工。我选择了串口通信功能的实现。

5月19日

上午确定分工合作的一些细节部分的设计,资源的分配,引脚的功能等等。然后,听老师讲一些知识点。

下午,我开始查找串口通信的资料文献。最终确定采用异步通信的方式实现串口通信。接着开始着重异步通信方面的资料查询。经过一番并不困难的翻阅后,大致了解了异步通信的 4 种方式以及定时器的知识。

52单片机有三个定时器/计数器,与串口通信有关的就是 T1定时器了。定时器/计数器的实质是加 1 计数器(高 8 位 和低 8 位 两个寄存器组成)。TMOD 是确定工作方式和功能的寄存器;TCON 是控制 T0、T1 的启动和停止及设置溢出标志的寄存器。

TMOD:

位序号 D7 D6 D5 D4 D3 D2 D1 D0
位符号 GATE $C/\overline{\text{T}}$ M1 M0 GATE $C/\overline{\text{T}}$ M1 M0

D7~D4T1 定时器,D3~D0T0 定时器。$C/\overline{\text{T}}$ 为 0 时为定时器模式,为 1 时为计数器模式,M1M0 是工作方式选择位。

M1 M0 工作方式
0 0 方式0,为13位定时器/计数器
0 1 方式1,为16位定时器/计数器
1 0 方式2,8位初值自动重装的8位定时器/计数器
1 1 方式3,仅适用于 T0,分成两个8位计数器,T1停止计数

TCON

位序号 D7 D6 D5 D4 D3 D2 D1 D0
位符号 TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0

5月20日

经过昨天一天的查阅学习,已经对串口通信有了比较全面的了解。所以,今天就是先把串口通信的一些必要环境准备好。下载串口助手和虚拟串口工具,然后测试串口助手。在一番并不花里胡哨的操作后,基本操作都会了。接着就是准备仿真下的串口测试了。在 protues 上,点来点去,又是一番行云流水且不花里胡哨的操作后,仿真环境也搭建好了。然后开始敲代码,在 keil 这个外表朴实无华但功能强大的软件上,敲下第一个简单串口通信的代码。然后结合串口助手观察效果。这样一个发送单个字符的例子就测试成功了。

5月21日

今天是周四,上午老师照常开了小小的线上例会。交我们一些小小的调试技巧之类的。在昨天的基础上,今天继续修改代码,实现发送和接收功能。上午把整个代码框架写了出来,本以为会是顺顺利利的一天。但是下午的调试,着实不太顺利。先是,接收的数据并不是期望的数据,在一顿微操后,发现是接收数据的数组出现了溢出现象。调好后,又发现接收的数据有些错位了。又是一顿 Debug 后,自己定义一个简单通信协议,在数据头尾加上标志位,以确保接收方正确接收到期望数据。来来回回这样折腾后,仿真串口通信是没有问题了。

5月22日

一觉醒来还是周五,是的,昨晚熬到了十二点。精力显然有些不充沛的我,还是打开了电脑,接着干了。队友那边的频率测量 和LCD 显示功能已经就绪了。今天可以整一块测试一下了。拿到他们的代码后,便开始调试。红红火火恍恍惚惚,就这样一顿朴实无华而实用的 CV大法过后,加以小小微操作为辅助,就调好了。

5月25日

短暂的周末一晃就过去了。新的一周,继续干。现在只剩下信号源了。查资料,敲代码,仿真。陷入循环状态。进度十分缓慢。浑浑噩噩,一天就这样结束了。

5月26日

距离实习结束,迫在眉睫。但是,实在有些精神疲惫了。打开和昨天一样的软件一样的文件,开始改代码。一番操作,以为成了。结果,一到高频信号,就不受调制了,无法准确控制信号频率的改变。

5月27日

今天开始在开发板上进行真机测试,果然不出所料,仿真和真机不一样。一天的疯狂调试,勉强调好,差强人意的样子。

交互式Python爬虫分析实例小项目

先抛出项目地址吧: 厦门大学数据库实验室

项目简述

实现一个简单的交互式的租房信息分析展示 web 平台。

数据来源 : http://www.xhj.com/zufang/

技术栈

  • python 爬虫
  • pyspark 数据分析
  • flask web 后端
  • pyecharts 可视化

最终呈现效果

image-20200517220605897
image-20200517220645343

租房信息爬取

地址: http://www.xhj.com/zufang/

image-20200517222251986

网页分析

地址分析

区域找房 一栏找到长沙的各个区域。这里选取了长沙的六个区:【天心区、芙蓉区、开福区、岳麓区、雨花区、望城】

逐个点击 六个区可以观察到每个区域都对应有 40 页 ,而且地址可以简单按下面这样方式拼接:

1
http://www.xhj.com/zufang/ + 区域+/pg + 页码/

所以代码中可以这样构造地址:

1
2
3
4
base_url = 'http://www.xhj.com/zufang/'
param = ['tianxinqu','furongqu','kaifuqu','yueluqu','yuhuaqu','wangcheng']
# region in param
url = base_url + region + '/pg%d/'%i # region为区域,i 为页码

这样很容易就可以通过两个循环来爬取我们需要的数据了。

源码分析

通过开发者工具查看源码:

image-20200517223429820

显然,这个网站的前端非常的给力,结构一目了然,而且没有动态加载。要是能改为 ajax 的请求方式加载数据或许会更友好。

不用仔细观察都可以看见,每个租房信息都被一个 <div​ class="lp_wrap">...</div>​ 包裹着。

我们很容易就可以通过 xpath 定位到每个租房信息,像这样:

1
2
# html is the source code of webpage.
div = html.xpath('//*/div[@class="lp_wrap"]')

这样提取的 div​ 对象为一个 list 。最后将结果保存在 rent_info.csv 中。

补充

经过这么一波并不花里胡哨的简单操作和分析,基本上可以写出对应的代码了。但是,这看似普普通通的网站,还是会封你的 ip 的。所以,一般的加 headers['User-Agent']​ 已经不行了。这里,我选择了添加代理来绕过它的反爬机制。【备注:很久之前爬过免费高匿代理存放在 mongodb 中】

(此处省略个几百字)一顿花里胡哨的操作后,数据库中的代理 ip 果然已经基本失效了。毕竟一年多了。

后面发现,这个网站只会封你半分钟不到好像(应该是的,被禁后,刷新了好几下网页,然后刷回来了)。所以说,代码中是不是可以通过设置休眠时间来降低访问速度呢。三思过后,放弃的这个想法,这样的做法好像一点都不干脆利落。还是决定自己做个代理池算了。

于是开启了免费代理的寻找之路,又是一顿的花里胡哨操作后(此处省略几百字)。很多 西刺代理 这样的免费代理网站已经迭代升级了,不在是曾经那个亚子了。它也开始封我 ip 了。差点当场炸裂开来……因为它不是封你一两分钟酱紫玩玩。

不过没关系,多爬几个这样的网站就可以有比较多得代理了。如果不想爬也不打紧,不妨逛一逛这里小幻http代理

支持批量提取,十分友好,可以帮我们省十几行代码了。

一点建议

记得用我们的目标网站测试一下这些免费代理是否失效。

下面是我选出来的比较好的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
proxies = [
{'https':"https://221.6.201.18:9999"},
{'http': 'http://39.137.69.9:80'},
{'https': 'https://221.122.91.64:80'},
{'http': 'http://39.137.69.8:8080'},
{'http': 'http://125.59.223.27:8380'},
{'http':'http://118.212.104.22:9999'},
{'https':'https://47.106.59.75:3128'},
{'http':'http://221.180.170.104:8080'},
{'http': 'http://113.59.99.138:8910'},
{'http':'http://123.194.231.55:8197'},
{'https':'https://218.60.8.99:3129'},
{'http': 'http://218.58.194.162:8060'},
{'https': 'https://221.122.91.64:80'}
]

pyspark 数据分析

这一步主要使用 pyspark.sql.SparkSession 来操作。从 rent_info.csv 中读取数据获得一个 DataFrame 对象,然后通过一系列动作(过滤筛选,聚合,统计)完成简单分析。

flask 后端

使用 flask_socketio.SocketIO 来注册一个 flask app 对象。调用 run 方法启动服务。

1
2
3
4
5
6
app = Flask(__name__)
app.config['SECRET_KEY'] = 'xmudblab'
socketio = SocketIO(app)

if __name__ == '__main__':
socketio.run(app, debug=True)

剩下的就是一些简单的路由配置(通过装饰器来实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 客户端访问 http://127.0.0.1:5000/,可以看到index界面
@app.route("/")
def handle_mes():
return render_template("index.html")

# 对客户端发来的start_spider事件作出相应
@socketio.on("start_spider")
def start_spider(message):
print(message)
run_spider()
socketio.emit('get_result', {'data': "请获取最后结果"})

# 对客户端发来的/Get_result事件作出相应
@app.route("/Get_result")
def Get_result():
return render_template("result.html")

socketio 补充

使用 socketio 可以轻松实现 web 后台和前端的信息交互,这种连接是基于 websocket 协议的全双工通信。

前端 socketio

1
<script src="static/js/socket.io.js"></script>

未完待续…..

改进空间

整个项目中,spark 的强大好像并没有发挥出来。毕竟 spark 在实时数据处理方面可是碾压 mapreduce的,好像一套组合拳,只使出了一点花拳绣腿。不妨大点想象一下,能不能实现一个实时房租信息交互系统,通过可视化工具在地图上直观的显示租房信息,每隔一小段时间更新数据,同时发送邮件提醒。甚至结合微信小程序在移动端也能查看。

嗯,想一想,挺好的。但是,这里的数据来源的可信度还有待考察。或许应该去 贝壳找房 看看(当事人非常后悔)。怎么开始就没想到去贝壳找。【不是打广告/手动滑稽】


写在前面

在很久之前就已经学过了爬虫。那时还是懵懵懂懂的小白,学了一点基础,就买来一本书,然后就开干。代码倒是写了不少,但是没有什么拿的出手的。之后,便又匆匆忙忙的转战 web ,学起了 Django 。这一入坑,不知不觉差不多快一年了。最后发现自己知道的依旧凤毛麟角。没有基础的计算机网络知识,没有良好的代码编写规范……

意识到问题后,开始试着阅读官方文档,去看协议,看源码。这些天看了 http 协议,计算机网络基础,python 文档,以及 Scrapy 文档。不得不说,看完后虽然记住的不多,但是大致是怎么一回事,多多少少还是了解了。比如,当初的爬虫程序,为什么要设置 headercookiesession 什么的。还有 requestresponse 的含义。

这些天看了一下 Scrapy 的 官方文档,对这个框架有了一些了解。正如文档中所提到的,scrapy 框架很大程度上借鉴了 Django ,这也是为什么现在的我重新来看待它时,比之前要轻松太多了。

关于 Scrapy

Scrapy is an application framework for crawling web sites and extracting structured data which can be used for a wide range of useful applications, like data mining, information processing or historical archival.

学习一个框架,得明白,它是什么?怎么做?更深入为什么要这样做?

是什么?

简而言之,就是一个支持分布式的,可扩展的,用于批量爬取网站并提取结构化数据的异步应用程序框架。值得一提的是,Scrapy 是用 Twisted 编写的,Twisted 是一种流行的 Python 事件驱动的网络框架。因此,它是使用非阻塞(又称为异步)代码并发实现的。

Scrapy 有着丰富的命令行工具,交互式控制台,内置支持以多种格式(json、xml、csv)等。

怎么做?

要使用 Scrapy ,我们不得不先安装它。文档为我们提供的良好的 安装指南

我们只需要这样做:

1
pip install Scrapy

不过我们不得不知道下面文档中提到的:

Scrapy is written in pure Python and depends on a few key Python packages (among others)

Scrapy 需要一些依赖包:

  • lxml,高效的XML和HTML解析器
  • parsel,是在lxml之上编写的HTML / XML数据提取库
  • w3lib,用于处理URL和网页编码的多功能帮助器
  • twisted,异步网络框架
  • cryptographypyOpenSSL ,以处理各种网络级安全需求

其中还有一些版本要求:

  • Twisted 14.0
  • lxml 3.4
  • pyOpenSSL 0.14

如果你没有这些依赖包,那你不得不考虑先安装依赖。在此建议使用清华源下载,这样可以避免不必要的 Time out 。如下:

1
pip install [example_modul] -i https://pypi.tuna.tsinghua.edu.cn/simple/

安装完成后,就可以开始按接下来的教程 学习了。

像这样创建一个项目:

1
> scrapy startproject tutorial

编写自己的爬虫类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scrapy

class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)

运行项目:

1
> scrapy crawl quotes

至此,一个基本可以运行的 Scrapy 项目就成型了。

框架概述

在依葫芦画瓢的完成一个 Scrapy 项目的编写后,要想明白为什么要这样编写我们的爬虫程序,就不得不了解这个框架的一些细节。

Scrapy的体系结构及组件如下图所示:

对照着 Scrapy 的项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tutorial/
scrapy.cfg # deploy configuration file

tutorial/ # project's Python module, you'll import your code from here
__init__.py

items.py # project items definition file

middlewares.py # project middlewares file

pipelines.py # project pipelines file

settings.py # project settings file

spiders/ # a directory where you'll later put your spiders
__init__.py
quotes_spider.py # a spider written by yourself

学过 Django 就会发现,这个框架简直就是套着它的设计模式来的。全局设置的 settings.py 、项目的管道 pipelines.py 、强大可扩展的中间件 middlewares.py 、以及类似模型的 items.py 。从图中我们不难发现,spiders可以对 requests 和 response 进行处理。而中间件 middlewares还可以对 items 进行处理。 管道 pipelines 对输出的 items 进行最后的清洗。所以,在我们明白要对数据做怎样处理时,只需要在对应的地方按要求编写我们的代码来达到我们的目的即可。

一个例子:如果我们需要对最后清洗的数据保存到一个文件(如:json文件)中,那么你可能就要在管道 pipelines.py 中编写合适代码来实现。像这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import json

class JsonWriterPipeline(object):

@classmethod
def from_crawler(cls, crawler):

return cls(crawler)

def open_spider(self, spider):
self.file = open('items.jl', 'w')

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

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

    每个项目管道组件均调用此方法,返回一个 item 对象,返回 Twisted Deferred 或引发 DropItem 异常。

    如果要使用自己的管道,那么就不得不实现此方法。

    除此之外,还可以实现下面几种方法:

  • open_spider(self, spider)

    This method is called when the spider is opened.

  • close_spider(self, spider)

    This method is called when the spider is closed.

  • from_crawler(cls, crawler)

    If present, this classmethod is called to create a pipeline instance from a Crawler. It must return a new instance of the pipeline. Crawler object provides access to all Scrapy core components like settings and signals; it is a way for pipeline to access them and hook its functionality into Scrapy.

    编写完自己的 Item Pipeline后,我们还需要在 settings.py 中激活才能使用。像这样:

    1
    2
    3
    ITEM_PIPELINES = {
    'myproject.pipelines.JsonWriterPipeline': 800,
    }

    需要注意的是,管道组件以字典的形式配置,并分配一整数值(0 ~ 1000),项目将按升序方式依次执行。


补一篇关于 Scrapy 的笔记算是对很久之前的一个总结吧!

路漫漫其修远兮吾将上下而求索。

I know nothing but my ignorance.

Scrapy 框架中的数据流

尽管文档中这样提到:Scrapy中的数据流由执行引擎控制,如下所示

  1. The Engine gets the initial Requests to crawl from the Spider.
  2. The Engine schedules the Requests in the Scheduler and asks for the next Requests to crawl.
  3. The Scheduler returns the next Requests to the Engine.
  4. The Engine sends the Requests to the Downloader, passing through the Downloader Middlewares (see process_request()).
  5. Once the page finishes downloading the Downloader generates a Response (with that page) and sends it to the Engine, passing through the Downloader Middlewares (see process_response()).
  6. The Engine receives the Response from the Downloader and sends it to the Spider for processing, passing through the Spider Middleware (see process_spider_input()).
  7. The Spider processes the Response and returns scraped items and new Requests (to follow) to the Engine, passing through the Spider Middleware (see process_spider_output()).
  8. The Engine sends processed items to Item Pipelines, then send processed Requests to the Scheduler and asks for possible next Requests to crawl.
  9. The process repeats (from step 1) until there are no more requests from the Scheduler.

但是具体到程序中是如何体现的呢?在项目运行时,控制台中就有输出提示信息。如果要更直观的体现,不妨在每步对应的函数中打印自己设置的提示信息。例如:在自己的项目管道中可以这样做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyPipeline(object):
# 在 open_spider 以及 parse 之后执行
def process_item(self, item, spider):
print("----- process_item -----" )
return item

# 在 from_crawler 之后执行
def open_spider(self, spider):
print("------------ open_spider --------------")

# 在 process_item 之后执行
def close_spider(self, spider):
print("------------ close_spider --------------")

# 最先执行
@classmethod
def from_crawler(cls, crawler):
print("------------ from_crawler --------------")

return cls()

项目运行后,就可以看见他们的输出顺序了:

1
2
3
4
------------ from_crawler --------------
------------ open_spider --------------
------------ process_item --------------
------------ close_spider --------------

了解框架的处理逻辑对我们编写高效代码是很有好处的。

写在前面

随着自己写的博客日益增加,博客列表页的展示也逐渐变得有些力不从心。要浏览所有的博客就不得不疯狂的滑鼠标。冗长的页面带来的体验十分的差劲。这个时候不得不将他们做一下简单分页处理。分页的方式有很多,而便捷的 Django 为我们准备了十分友好的类 Paginator 来帮助我们进行分页。


Paginator 对象

先看看源码中的初始化或者说构造方法:

1
2
3
4
5
6
7
class Paginator:
def __init__(self, object_list, per_page, orphans=0,allow_empty_first_page=True):
self.object_list = object_list
self._check_object_list_is_ordered()
self.per_page = int(per_page)
self.orphans = int(orphans)
self.allow_empty_first_page = allow_empty_first_page

显然,要创建一个 Paginator 对象, 就不得不提供 object_list 和 per_page 对象。

object_list

A list, tuple, QuerySet, or other sliceable object with a count() or len() method. For consistent pagination, QuerySets should be ordered, e.g. with an order_by() clause or with a default ordering on the model.

从这段文档说明中,可以大致了解 object_list 是个这样的对象:列表、元组、QuerySet…同时,文档建议如果是 QuerySet 对象的话,应当对其进行排序,如使用 order_by() 方法,或者采用模型中默认的排序方法。

per_page

The maximum number of items to include on a page, not including orphans (see the orphans optional argument below).

显然,per_page 指的是每页要展示的选项最大个数。例如:每页显示 5 篇文章,那么就是 per_page=5。同时也强调,不包括 orphans。

orphans

Use this when you don’t want to have a last page with very few items. If the last page would normally have a number of items less than or equal to orphans, then those items will be added to the previous page (which becomes the last page) instead of leaving the items on a page by themselves. For example, with 23 items, per_page=10, and orphans=3, there will be two pages; the first page with 10 items and the second (and last) page with 13 items. orphans defaults to zero, which means pages are never combined and the last page may have one item.

orphans 顾名思义就是孤儿的意思。通俗来讲就是,在分页时,发现最后一页可能就只有一两个选项去了,如果觉得最后一页只是很少一部分,不想单独占一页,那么就可以将其添加在前一页中。而 orphans 值恰恰就是这样一个阈值,当小于它时,就可以将剩下那部分加到前一页中。假设我们有 23 个选项,每页展示是个选项,那么最多可以分成 3 页,第三页就只有 3 个选项。如果我们设置 orphans 大于等于 3,那么第一页是 10,第二页是 13 了。


如何使用

文档中给出了一个十分详细的例子供我们参考:

在我们的视图 views.py 中:

1
2
3
4
5
6
7
8
9
from django.core.paginator import Paginator
from django.shortcuts import render

def listing(request):
contact_list = Contacts.objects.all()
paginator = Paginator(contact_list, 25) # Show 25 contacts per page
page = request.GET.get('page')
contacts = paginator.get_page(page)
return render(request, 'list.html', {'contacts': contacts})

在我们的模板 list.html 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{% for contact in contacts %}
{# Each "contact" is a Contact model object. #}
{{ contact.full_name|upper }}<br>
...
{% endfor %}
<div class="pagination">
<span class="step-links">
{% if contacts.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ contacts.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ contacts.number }} of {{ contacts.paginator.num_pages }}.
</span>
{% if contacts.has_next %}
<a href="?page={{ contacts.next_page_number }}">next</a>
<a href="?page={{ contacts.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>

一点点注释:

has_previous() 判断是否有上一页
has_next() 判断是否有上一页
previous_page_number() 返回上一个页码
contacts.number 一个基于 1 的页码
paginator.num_pages() 页码的基于 1 的范围迭代器

获取更多可以参考 官方文档


欢迎参观

学Django时,顺便写了简单的个人博客。前端用的bootstrap框架。笔记都会同步的。