一篇我们介绍了如何解析CSV和JSON数据:如何解析互联网数据:CSV和JSON篇,今天我们将介绍如何解析HTML和XML数据。
今天的介绍能够帮助你轻而易举地从网页中(比如下面的中航电子的2017年一季度交易数据)提取自己想要的数据:
准备
在Python中可以解析html和xml数据的软件包很多,今天我们介绍的是lxml,先安装:
$ pip install lxml
如果不熟悉pip的使用,可以参考另一篇文章:如何管理python软件包。
解析HTML数据
首先,回顾一下HTML的一些基本概念:
标签/tag:比如<html>, <h1>, <head>...一般成对出现,例如开始标签<html>和结束标签</html>
元素/element:开始标签到结束标签整段代码,标签对之间的即为内容(content)
属性/attribute:标签可拥有key=value形式的属性,比如<div class="header">...</div>
简单地理解,HTML网页就是由一组元素构成的一个集合。另外,大多数HTML标签是可以嵌套的,因此元素可以包含一系列子元素。有了这些概念做基础,我们将能够很容易理解软件包lxml的使用。实际上,在lxml里面,每个HTML元素对应一个lxml.html.HtmlElement对象,该对象提供一组标准的方法取访问包含在该元素内部的数据,比如属性、内容和子元素等。
例子
考察下面的链接,它提供中航电子在2017年第一季度的交易数据,我们打算从里面提取一些数据:
>>> url = "http://quotes.money.163.com/trade/lsjysj_600372.html?year=2017&season=1"
先把该网页爬取下来:
>>> import urllib2
>>> rsp = urllib2.urlopen(url).read()
>>> print rsp[0:15]
<!DOCTYPE html>
将字符串rsp转换成HtmlElement对象:
>>> from lxml import html
>>> doc = html.document_fromstring(rsp)
>>> type(doc)
<class 'lxml.html.HtmlElement'>
>>> doc.tag
'html'
所以其实doc就是一个html元素,它包含一些元素,比如head, body, link, div...
比如,如果你想提取该网页里面所有的链接(links):
>>> links = [ link for link in doc.iterlinks() ]
>>> len(links)
106
>>> links[0]
(<Element link at 0x1029179f0>, 'href', 'http://img1.cache.netease.com/f2e/finance/gegu/s.1064000.css', 0)
>>> links[0][2]
'http://img1.cache.netease.com/f2e/finance/gegu/s.1064000.css'
如果你想查看元素直接包含哪些子元素,可以调用getchildren()方法:
>>> doc.getchildren()
[<Element head at 0x10299a0a8>, <Element body at 0x10299a470>]
对嵌套很深的元素,如果熟悉xpath表达式,最直接的办法是调用xpath(...)方法:
>>> [ td.text for td in doc.xpath('/html/body/div[2]/div[4]/table/tr[1]/td')]
['2017-03-31', '19.02', '19.50', '19.02', '19.30', '0.36', '1.90', '102,212', '19,747', '2.53', '0.58']
此外,还可以通过find, findall, find_class, get_element_by_id等方法查找目标元素,比如:
>>> [ td.text for td in doc.findall('./body/div[2]/div[4]/table/tr[1]/td')]
['2017-03-31', '19.02', '19.50', '19.02', '19.30', '0.36', '1.90', '102,212', '19,747', '2.53', '0.58']
如果元素有属性,提取属性值也很方便,比如:
>>> form = doc.forms[0]
>>> form.tag
'form'
>>> form.attrib
{'action': '/trade/lsjysj_600372.html', 'id': 'date'}
>>> form.keys()
['id', 'action']
>>> form.get('action')
'/trade/lsjysj_600372.html'
>>> form.items()
[('id', 'date'), ('action', '/trade/lsjysj_600372.html')]
'>>> form.form_values()
[('year', '2017'), ('season', '1')]
>>> form.method
'GET'
做为一个完整的例子,下面的脚本就是爬取中航电子在2017年第一季度的数据:
输出效果:
(test) $ head -3 600372.csv
日期;开盘价;最高价;最低价;收盘价;涨跌额;涨跌幅(%);成交量(手);成交金额(万元);振幅(%);换手率(%)
2017-03-31;19.02;19.50;19.02;19.30;0.36;1.90;102,212;19,747;2.53;0.58
2017-03-31;19.02;19.50;19.02;19.30;0.36;1.90;102,212;19,747;2.53;0.58
解析xml数据
xml的格式和HTML类似,也是由标签构成的,但是要比HTML文件简单许多,看下面的xml文件片段处理:
>>> xmlstr="""\
... <target name="run" depends="jar">
... <java fork="true" classname="${main-class}">
... <classpath>
... <path refid="classpath"/>
... <path refid="application"/>
... </classpath>
... </java>
... </target>"""
>>> from lxml import etree
第一步是获取根节点:
>>> root = etree.fromstring(xmlstr)
>>> root.tag
'target'
如果要提取节点属性:
>>> root.items()
[('name', 'run'), ('depends', 'jar')]
>>> root.keys()
['name', 'depends'
>>> root.get("name")
'run'
>>> root.values()
['run', 'jar']
可以使用find, xpath等方法去获取和查找子节点:
>>> java = root.find("./java")
>>> java.tag
'java'
>>> java.keys()
['fork', 'classname']
>>> [ path.get("refid") for path in root.xpath("//path")]
['classpath', 'application']
lxml软件的功能很强大,如果有兴趣进一步了解,可以查看官方文档:
http://lxml.de/index.html
今天就写这么,欢迎大家留言、评论和关注。
天碰到要在一个页面获取另外一个页面url传过来的参数,一开始很本能的想到了用 split(“?”)这样一步步的分解出需要的参数。
喜欢的朋友可以测试下,希望对大家有所帮助!
js方法一:正则分析法,指定参数名获取值。
function getQueryString(name){
var reg =new RegExp('(^|&)'+name+'=([^&]*)(&|$)','i');
var r = window.location.search.substr(1).match(reg);
if(r !=null){
return unescape(r[2]);
}
return null;
}
// 这样调用:
// http://orzhtml.github.io?a=1&b=2&c=3
console.log(getQueryString("a"));
console.log(getQueryString("b"));
console.log(getQueryString("c"));
结果截图:
下面举一个例子:
若地址栏URL为:abc.html?id=123&url=http://orzhtml.github.io
那么,但你用上面的方法去调用:alert(getQueryString("url"));
则会弹出一个对话框:内容就是 http://orzhtml.github.io
如果用:alert(getQueryString("id"));那么弹出的内容就是 123 啦;
当然如果你没有传参数的话,比如你的地址是 abc.html 后面没有参数,那强行输出调用结果有的时候会报错:
所以我们要加一个判断 ,判断我们请求的参数是否为空,首先把值赋给一个变量:
var myurl= getQueryString("url");
if(myurl != null && myurl.toString().length>1) {
alert(myurl);
}
js方法二:获取所有参数这样就不会报错了,结果返回始终会是一个对象!
function GetRequest(){
var url = location.search;//获取url中"?"符后的字串
var theRequest ={};
if(url.indexOf("?")!=-1){
var str = url.substr(1);
strs = str.split("&");
for(var i =0; i < strs.length; i ++){
theRequest[strs[i].split("=")[0]]= unescape(strs[i].split("=")[1]);
}
}
return theRequest;
}
// 这样调用
// http://orzhtml.github.io?a=4&b=5&c=6
var Request = {};
Request = GetRequest();
console.log(Request);
console.log(Request['a']);
console.log(Request['b']);
console.log(Request['c']);
结果截图:
本文内容均属个人原创作品,转载此文章须附上出处及原文链接。
加关注,定时推送,互动精彩多,若你有更好的见解,欢迎留言探讨!
作者 1点25 来源:https://www.cnblogs.com/nicerblog/p/11466442.html
D是数据的唯一标识,传统的做法是利用UUID和数据库的自增ID。
在互联网企业中,大部分公司使用的都是Mysql,并且因为需要事务支持,所以通常会使用Innodb存储引擎。
UUID太长以及无序,所以并不适合在Innodb中来作为主键,自增ID比较合适。
但是随着公司的业务发展,数据量将越来越大,需要对数据进行分表,而分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。
这时就需要一个单独的机制来负责生成唯一ID,生成出来的ID也可以叫做分布式ID,或全局ID。下面来分析各个生成分布式ID的机制。
这篇文章并不会分析的特别详细,主要是做一些总结,以后再出一些详细某个方案的文章。
数据库自增ID
第一种方案仍然还是基于数据库的自增ID,需要单独使用一个数据库实例,在这个实例中新建一个单独的表:
表结构如下:
CREATE DATABASE `SEQID`; CREATE TABLE SEQID.SEQUENCE_ID ( id bigint(20) unsigned NOT NULL auto_increment, stub char(10) NOT NULL default '', PRIMARY KEY (id), UNIQUE KEY stub (stub) ) ENGINE=MyISAM;
可以使用下面的语句生成并获取到一个自增ID
begin; replace into SEQUENCE_ID (stub) VALUES ('anyword'); select last_insert_id(); commit;
stub字段在这里并没有什么特殊的意义,只是为了方便的去插入数据,只有能插入数据才能产生自增id。
而对于插入,我们用的是replace,replace会先看是否存在stub指定值一样的数据,如果存在则先delete再insert,如果不存在则直接insert。
这种生成分布式ID的机制,需要一个单独的Mysql实例,虽然可行,但是基于性能与可靠性来考虑的话都不够。
业务系统每次需要一个ID时,都需要请求数据库获取,性能低,并且如果此数据库实例下线了,那么将影响所有的业务系统。
为了解决数据库可靠性问题,我们可以使用第二种分布式ID生成方案。
数据库多主模式
如果我们两个数据库组成一个主从模式集群,正常情况下可以解决数据库可靠性问题,但是如果主库挂掉后,数据没有及时同步到从库,这个时候会出现ID重复的现象。
我们可以使用双主模式集群,也就是两个Mysql实例都能单独的生产自增ID,这样能够提高效率。
但是如果不经过其他改造的话,这两个Mysql实例很可能会生成同样的ID。需要单独给每个Mysql实例配置不同的起始值和自增步长。
第一台Mysql实例配置:
set @@auto_increment_offset = 1; -- 起始值 set @@auto_increment_increment = 2; -- 步长
第二台Mysql实例配置:
set @@auto_increment_offset = 2; -- 起始值 set @@auto_increment_increment = 2; -- 步长
经过上面的配置后,这两个Mysql实例生成的id序列如下:mysql1,起始值为1,步长为2,ID生成的序列为:1,3,5,7,9,...mysql2,起始值为2,步长为2,ID生成的序列为:2,4,6,8,10,...
对于这种生成分布式ID的方案,需要单独新增一个生成分布式ID应用,比如DistributIdService,该应用提供一个接口供业务应用获取ID。
业务应用需要一个ID时,通过rpc的方式请求DistributIdService,DistributIdService随机去上面的两个Mysql实例中去获取ID。
实行这种方案后,就算其中某一台Mysql实例下线了,也不会影响DistributIdService,DistributIdService仍然可以利用另外一台Mysql来生成ID。
但是这种方案的扩展性不太好,如果两台Mysql实例不够用,需要新增Mysql实例来提高性能时,这时就会比较麻烦。
现在如果要新增一个实例mysql3,要怎么操作呢?
第一,mysql1、mysql2的步长肯定都要修改为3,而且只能是人工去修改,这是需要时间的。
第二,因为mysql1和mysql2是不停在自增的,对于mysql3的起始值我们可能要定得大一点,以给充分的时间去修改mysql1,mysql2的步长。
第三,在修改步长的时候很可能会出现重复ID,要解决这个问题,可能需要停机才行。
为了解决上面的问题,以及能够进一步提高DistributIdService的性能,如果使用第三种生成分布式ID机制。
号段模式
我们可以使用号段的方式来获取自增ID,号段可以理解成批量获取。
比如DistributIdService从数据库获取ID时,如果能批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。
比如DistributIdService每次从数据库获取ID时,就获取一个号段,比如(1,1000],这个范围表示了1000个ID。
业务应用在请求DistributIdService提供ID时,DistributIdService只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库。
一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段。
所以,我们需要对数据库表进行改动,如下:
CREATE TABLE id_generator ( id int(10) NOT NULL, current_max_id bigint(20) NOT NULL COMMENT '当前最大id', increment_step int(10) NOT NULL COMMENT '号段的长度', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值),因为自增逻辑被移到DistributIdService中去了,所以数据库不需要这部分逻辑了。
这种方案不再强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启,会丢失一段ID,导致ID空洞。
为了提高DistributIdService的高可用,需要做一个集群,业务在请求DistributIdService集群获取ID时,会随机的选择某一个DistributIdService节点进行获取。
对每一个DistributIdService节点来说,数据库连接的是同一个数据库,那么可能会产生多个DistributIdService节点同时请求数据库获取号段,那么这个时候需要利用乐观锁来进行控制。
比如在数据库表中增加一个version字段,在获取号段时使用如下SQL:
update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version}
因为newMaxId是DistributIdService中根据oldMaxId+步长算出来的,只要上面的update更新成功了就表示号段获取成功了。
为了提供数据库层的高可用,需要对数据库使用多主模式进行部署。
对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长。
比如如果现在是两台Mysql,那么mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7....mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10...
更详细的可以参考滴滴开源的TinyId:
https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D
在TinyId中还增加了一步来提高效率,在上面的实现中,ID自增的逻辑是在DistributIdService中实现的,而实际上可以把自增的逻辑转移到业务应用本地。这样对于业务应用来说只需要获取号段,每次自增时不再需要请求调用DistributIdService了。
雪花算法
上面的三种方法总的来说是基于自增思想的,而接下来就介绍比较著名的雪花算法-snowflake。
我们可以换个角度来对分布式ID进行思考,只要能让负责生成分布式ID的每台机器在每毫秒内生成不一样的ID就行了。
snowflake是twitter开源的分布式ID生成算法,是一种算法,所以它和上面的三种生成分布式ID机制不太一样,它不依赖数据库。
核心思想是:分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的分配如下图:
根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID。
只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。
snowflake算法实现起来并不难,提供一个github上用java实现的:
https://github.com/beyondfengyu/SnowFlake
在大厂里,其实并没有直接使用snowflake,而是进行了改造。
因为snowflake算法中最难实践的就是工作机器id,原始的snowflake算法需要人工去为每台机器去指定一个机器id,并配置在某个地方从而让snowflake从此处获取机器id。
但是在大厂里,机器是很多的,人力成本太大且容易出错,所以大厂对snowflake进行了改造。
百度(uid-generator)
github地址:uid-generator
uid-generator使用的就是snowflake,只是在生产机器id,也叫做workId时有所不同。
uid-generator中的workId是由uid-generator自动生成的。
并且考虑到了应用部署在docker上的情况,在uid-generator中用户可以自己去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配。
说的简单一点就是:应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据,数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId,而数据由host,port组成。
对于uid-generator中的workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位。
需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,同一个应用每重启一次就会消费一个workId。
具体可参考https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
美团(Leaf)
github地址:Leaf
美团的Leaf也是一个分布式ID生成框架。它非常全面,即支持号段模式,也支持snowflake模式。号段模式这里就不介绍了,和上面的分析类似。
Leaf中的snowflake模式和原始snowflake算法的不同点,也主要在workId的生成。
Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
总结
总得来说,上面两种都是自动生成workId,以让系统更加稳定以及减少人工成功。
Redis
这里额外再介绍一下使用Redis来生成分布式ID,其实和利用Mysql自增ID类似,可以利用Redis中的incr命令来实现原子性的自增与返回,比如:
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1 OK 127.0.0.1:6379> incr seq_id // 增加1,并返回 (integer) 2 127.0.0.1:6379> incr seq_id // 增加1,并返回 (integer) 3
使用redis的效率是非常高的,但是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
RDB持久化相当于定时打一个快照进行持久化,如果打完快照后,连续自增了几次,还没来得及做下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。
AOF持久化相当于对每条写命令进行持久化,如果Redis挂掉了,不会出现ID重复的现象,但是会由于incr命令过多,导致重启恢复数据时间过长。
*请认真填写需求信息,我们会在24小时内与您取得联系。