明真破妄章颂
经名:明真破妄章颂。北宋张继先着。一卷。底本出处:《正统道藏》洞神部赞颂类。
虚靖张真君着
道生天地
混沌未分先有道,道无形色亦无情。
自然化育生天地,天道轻清地道宁。
天地生人
道生天地始无名,分判阴阳立五行。
人位其中灵万物,人从天地道生成。
人禀阴阳
三才天地人同炁,人禀先天一炁灵。
一炁具身名日道,感通天地及神明。
一炁生三
一真真外更无真,祖炁通灵具此身。
道一生三生妙用,元精元炁与元神。
人身三宝
元精元炁与元神,三者无形亦有形。
运用得传真可见,光明无极是分明。
一无相关
雷乃先天炁化成,诸天仙圣总同真。
我身一炁相关召,同祖同宗贴骨亲。
以心合神
真心动处是雷机,神合神兮妙更奇。
只此更无差别处,如磁吸铁不相违。
法即是心
此心心外元无法,咒诀符图少合真。
心真将何为妙用,灵光一点运元神。
神可通天
元神直捧一封书,一道寒光射太虚。
径达玉京金阙去,玄恩星火下天衢。
呼天叱吒
叱叱望空将令打,不识元神召使者。
天高星远岂能闻,从便之乎并也者。
金光召雷
金光灼灼照雷城,百万雷兵禀令行。
不用符图并咒诀,旱天能雨雨能晴。
妄想行持
不悟阴阳阖辟机,想存作用总成痴。
雷符烧尽千千道,雨泽何曾一点施。
真念降魔
降魔何处是工夫,中有元神静定居。
慧剑挥时神鬼伏,便教祸害自然除。
心妄鬼欺
妄念纷纷且失真,符图咒诀费精神。
鬼神一见嘻嘻笑,打石抛砖更害人。
祖炁阳晶
先天祖炁至阳晶,炼尽阴魂一性灵。
九转玄功成顷刻,阴阳交媾结胎婴。
辩真阴阳
真阴真阳既不识,水可浴兮火可炙。
灵光一点不分明,受度亡魂有何益。
南昌非心
玄关一窍有阴阳,心肾元非水火乡。
寄语炼师高着眼,莫将心府作南昌。
普度有法
普度工夫又不同,金光灼灼照罗酆。
黍珠一颗空悬处,太极还归无极中。
取炁不同
昧却自家元气主,妄想天罡对空取。
恰似骑牛去觅牛,此等之人何足语。
不识元神
召将先轰令一声,令声惊起我元神。
缘何不见元神面,只为时人昧本真。
印须心印
心印相传付有绿,今人印木不知玄。
祖师心印相同处,颗颗光明彻九天。
灵光一点
书符道妙起工夫,委聚毫端篆作符。
着相想存行呎诀,将来只是墨和朱。
先天字号
号头用处须天篆,元是皇人按笔书。
苍颉后天尘世字,用之总是惑迷愚。
妄想阴阳
阴阳吞啖运符中,心肾盈亏黑与红。
一个乾坤如许大,这些伎俩岂能通。
肾中一点
两肾中间一点明,痴人守此欲通灵。
谁知此处皆阴气,若比阳晶隔万程。
心下元神
人言心下一包空,精气元神聚此中。
何似痴人容易惑,盖缘不识主人翁。
胆非雷府
妄将脏腑作雷霆,怒气膨膨起震惊。
击破琉璃瓶子后,何曾闻得有雷声。
误指雷霆
一阳生处地雷复,外肾应非雷所居。
秽浊圣贤难忏悔,将来必定被雷诛。
不辨真阳
心肝脾肺肾胆府,嘘呵咽嘻呼吹取。
父母后天阴阳气,如何可以祈晴雨。
七事纯阴
涕唾津精气血液,七件皆阴总无益。
身中只有一阴阳,江湖多少无人识。
秽气触真
有言脐下寸三分,作用金光此处存。
岂识此中阴浊气,运成秽浊不堪闻。
似是而非
妄言一窍在眉心,直入三分可许深。
误杀世人真可笑,如将「石俞」石作黄金。
注:「石俞」是一字
错认后天
脐轮后与肾相连,两处空空总后天。
若问先天玄妙处,除非得遇至人传。
雷动无时
雷动有用雷霆煞,又等停星日月时。
如此天机容易测,雨旸何用法来祈。
邪能惑正
几多道眼不曾明,役将祛神辄现形。
只为身中无主宰,阴魂假托姓名灵。
当明真要
一员神将数家法,多是宗师撰造成。
更过几年多是假,行符咒水岂能灵。
作用两全
道法难忘咒与符,必须道妙两相扶。
先天玄妙工夫到,咒诀符图可有无。
道本法体
法行大道合先天,咒诀符图总是玄。
至道杳然无所得,符图咒诀也徒然。
万法归一
道生于一复何疑,可以无为可有为。
万法本来归一处,何分正一与清微。
造化在我
本质虽殊炁不殊,当於亲处下工夫。
人身大抵同天地,造化阴阳总属吾。
兀坐顽空
无心兀兀坐多年,将谓神仙已有绿。
不解龙昤并虎啸,但知空坐也徒然。
阴阳互根
顽石中空藏白玉,纵饶见得光生目。
太阴元受太阳精,初八上弦圆十六。
因法生谤
本为同门共指迷,上根一览悟玄微。
若将此向愚痴说,生谤生瞋总是非。
明真破妄章颂竟
]]>明真破妄章颂
经名:明真破妄章颂。北宋张继先着。一卷。底本出处:《正统道藏》洞神部赞颂类。
虚靖张真君着
道生天地
混沌未分先有道,道无形色亦无情。
自然化育生天地,天道轻清地道宁。
天地生人
道生天地始无名,分判阴阳立五行。
人位其中灵万物,人从天地道生成。
人禀阴阳
三才天地人同炁,人禀先天一炁灵。
一炁具身名日道,感通天地及神明。
]]>
有朋友拜托写段程序,根据系统导出的txt文件和Word模板生成文档。
txt部分内容:1
2
3
4
5
6
7
8
9
10
11
12ersonInfo:
id=3XXXXXXXXXX
accountUid=XXXXXXXXXXXXXXXXXXXXXXX
createDate=2017-01-16 14:44:37.0
name=王XX
idNo=61XXXXXXXXXXXXXXX
Project:
id=1XX
name=XX
projectId=1XXXXXXXXXXXX
createDate=2015-12-14 17:39:50.0
把Word中相关字段替换成txt中的值。
1.pip install docx
装好包后,运行代码提示:1
2
3File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/docx-0.2.4-py3.3.egg/docx.py", line 30, in <module>
from exceptions import PendingDeprecationWarning
ImportError: No module named 'exceptions'
在爆栈找到解决方案:docx包不兼容Python 3,卸载后,改用pip install python-docx
安装即可正常使用
2.生成的新Word文档中,原有的图片丢失。
翻了docx的API文档,它把文件中的每一段作为一个paragraph,每个paragraph分为许多不同的run,同一句话中,如果字体、大小、粗细等不同,也算不同的run。不知道这图片是怎么算的。只要操作过一次文字,图片就会丢失。
替换之后再用代码插入图片。
3.先if再替换,有许多地方没有替换成功。
没想明白怎么回事,去掉if,不管有没有字段,全部替换。
1 | #! python3 |
有朋友拜托写段程序,根据系统导出的txt文件和Word模板生成文档。
txt部分内容:1
2
3
4
5
6
7
8
9
10
11
12ersonInfo:
id=3XXXXXXXXXX
accountUid=XXXXXXXXXXXXXXXXXXXXXXX
createDate=2017-01-16 14:44:37.0
name=王XX
idNo=61XXXXXXXXXXXXXXX
Project:
id=1XX
name=XX
projectId=1XXXXXXXXXXXX
createDate=2015-12-14 17:39:50.0
把Word中相关字段替换成txt中的值。
1.pip install docx
装好包后,运行代码提示:1
2
3File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/docx-0.2.4-py3.3.egg/docx.py", line 30, in <module>
from exceptions import PendingDeprecationWarning
ImportError: No module named 'exceptions'
在爆栈找到解决方案:docx包不兼容Python 3,卸载后,改用pip install python-docx
安装即可正常使用
手机上装了腾讯地图+指南针+GPS,走到哪都不会迷路。驴妈妈上可买到便宜的门票,景点检票处直接刷自己身份证就可以进去。
全程人均花费五百元左右。
苏州火车站有直接到观前街的公交车。但是当时在网上看到附近一座桥景致不错,就定了走过桥到察院场站坐车。其实是座水泥桥,外围包了一层木结构。当天下雨,附近又在破土修路,桥上被来来往往的车沾了泥,近处车辆马达声、远处机器隆隆声不绝于耳,大煞风景。
坐车到观前街已经十一点多,直接去松鹤楼用餐。每个包厢一张圆桌,大约能坐十五人。如想独占一桌需拿号排队,如与他人共享可立即上楼。
吃完去玄妙观后的布丁酒店,放下行李出门往东直走至平江路。这段路上可以慢慢晃过去,南侧有小桥流水,北侧有旧时代的居民楼,楼道七弯八拐,高处放着农具、杂物,与我们浙中乡下农村不同,有独特的新鲜感。
到平江路后往南逛到头,不要一路直走,多注意岔道,走进去别有洞天。有个外面包着铁皮的艺术馆邪气甚重,馆内陈设、气氛相当诡异,最好别进去。我身体弱,回来后因故萎靡了好一阵子。
原本打算去新市桥码头坐夜游船,但出发前得知当晚下暴雨,遂改去七里山塘。需要先坐地铁1号线,中途换乘2号线,买票时直接买山街塘站即可。华灯初上的山塘街特别美。
找了家羊肉店填了肚子,出来后天降暴雨,雨伞根本挡不住,淋成了落汤鸡。
早上沿老路从下榻的布丁酒店往东走到平江路,这次拐往北方,路边买早点吃。路上看到块耦园的指示牌,于是拐到东边进去一游。
出来后往北逛到狮子林,附近解决午餐后继续去拙政园、苏州博物馆、忠王府。苏博西侧是有名的朱鸿兴面馆,大肉面(忘了是不是这名)特别美味。
晚上去新市桥码头坐船。回来后休息了一会,再去逛观前街。
早上在附近找家店吃了油条豆浆,到寒山寺时已经中午。附近找了家店吃完午饭,逛完寒山寺即去火车站赶车回家。
附上规划行程的工具
杭州东站,出发去苏州!
苏州火车站南广场隔岸的古城楼
姑苏名人
出站口接人的小伙子
在松鹤楼解决了午饭
玄妙观
进路边的民居甬道逛逛
猫的天空之城
小巷子里居然有义庄,若不是大门闭,倒想进去瞧瞧
平江路某艺术馆,此处本有妖怪。
评弹博物馆
巧克力店
小学里居然有供奉孔子的大成殿
佛堂,似是僧人静修室
假山很大,可以玩半天,可惜当时下雨,匆匆出来了
不知道这叫什么花
那天的风很温柔
去耦园路上的手写地图
彼岸花
牡丹,花期已过
没怎么拍照
朱鸿兴面馆大肉面,就在苏州博物馆西边,特别好吃。
苏州博物馆达摩像
忠王府龙椅
船游苏州
味道不怎么样,贵得要死。
三味书屋:读经味如稻梁、读史味如肴馔、诸子百家味如醯醢
迅哥的座位,当年那篇课文带动了全班课桌刻字的风潮,老师也阻止不了。
古人的生活相较现在「简单」得多。在这一点上挺羡慕他们,这样的环境可以专心做事,不会因为科技带来的便利而分心。
范仲淹与稽山书院
绍兴府学宫图
绍兴府中学毕业文凭
这里是戏台,名叫鱼乐园
忘了叫什么名,又甜又腻。
百草园
农村至今家家都保留了灶台,水缸已经被自来水取代。
廊上挂满竹牌,正反两面分别写了情侣们的爱情誓愿,也有单身青年祈求缘分。
千古名作,两首《钗头凤》
沈园一角
出沈园后在街头逛了几小时就回去了。
]]>
走到灵隐寺左拐,路上一排小型精舍,卖各种佛具、茶叶、手工艺品。
农村家家都有这种风车,图上右侧缺了把手。谷子倒入上面的漏斗,摇动风车,谷糠从左侧飞出,谷粒从左下的口落下。
踩在水中特别凉快
路过中天竺法镜寺,进去转了一圈。
法镜寺后山即是三生石。前几天看新闻说近来某剧热播导致三生石被游客瞎涂乱刻,甚是可惜。
其实这三生石与男女情侣没有关系。
僧圆泽传
苏轼
洛师惠林寺,故光禄卿李登居第。禄山陷东都,登以居守死之。
子源,少时以贵游子,豪侈善歌闻于时,及登死,悲愤自誓,不仕、不娶、不食肉,居寺中五十余年。
寺有僧圆泽,富而知音,源与之游,甚密,促膝交语竟日,人莫能测。
一日相约游青城峨嵋山,源欲自荆州沂峡,泽欲取长安斜谷路,源不可,曰:「行止固不由人。」遂自荆州路。
舟次南浦,见妇人锦裆负瓮而汲者,泽望而泣:「吾不欲由此者,为是也。」
源惊问之,泽曰:「妇人姓王氏,吾当为之子,孕三岁矣!吾不来,故不得乳。今既见,无可逃者,公当以符咒助我速生。三日浴儿时,愿公临我,以笑为信。后十三年,中秋月夜,杭州天竺寺外,当与公相见。」
源悲悔,而为具沐浴易服,至暮,泽亡而妇乳。三日往视之,儿见源果笑,具以告王氏,出家财,葬泽山下。
遂不果行,反寺中,问其徒,则既有治命矣!
后十三年,自洛适吴,赴其约。至约所,闻葛洪川畔,有牧童,扣牛角而歌之曰:
三生石上旧精魂,赏月吟风莫要论。
惭愧情人远相访,此身虽异性长存。
呼问:「泽公健否?」
答曰:「李公真信士。然俗缘未尽,慎勿相近,惟勤修不堕,乃复相见。」
又歌曰:
身前身后事茫茫,欲话因缘恐断肠。
吴越山川寻己遍,却回烟棹上瞿塘。
遂去不知所之。
后三年,李德裕奏源忠臣子,笃孝。拜谏议大夫,不就。竟死寺中,年八十。
巧的是,几个月后,就在三生石不远,灵隐旁边,我也有一段奇遇。
继续一路南行,走到杭图佛学分馆
有次在书店翻到的,根据我的经验,对于懒得跟你聊天的人,这三条都是白搭。
帮朋友刷单送的
傍晚的西湖
断桥与荷花,附近的亭子里有一群人带了音响,轮流唱歌。
另一边是宝石山
西湖边树上的鸽子
卦摊,听不懂他的方言
黑云压城
小人书!
夜跑归途遇到的流浪猫
楼下的面馆
好吃,但是太贵了。
乡下带来的嫩玉米
新款的3D眼镜,第一次见到
运河一角
城隍阁顶看西湖、延安路、城隍庙
城隍阁中名人画像
河坊街御乐堂
]]>走到灵隐寺左拐,路上一排小型精舍,卖各种佛具、茶叶、手工艺品。
年前看了电影《降临》,讲述了一群学者与造访地球的外星人沟通的故事。女主角在接触、学习外星语言的过程中,渐渐获得预知未来的能力,陆陆续续看到自己的一生。自己将会与现在一位同事步入婚姻,生下女儿,被丈夫抛弃,女儿早夭……
电影的结尾,女主角对未来的老公,一个尚无预知能力的物理学家,问道:「If you could see your whole life, from start to finish. Would you change thing?」
这个问题非常有趣。我们每个人,对自己的未来,都有不同程度的预知。近如手头的工作任务啥时做完,晚饭几点吃,吃什么;远如自己和身边的人亲人将会经历老、病、死……只是不知道这些事情发生的确切时间点罢了。
正因为不知道,未来的每一天都是新鲜、刺激、有趣的,尽情地生命,体验新经历,品尝新食物,玩新游戏,看新书,发现新世界……
也正因为不知道,生命充满了随时会暴发的苦。口渴肚饥是轻微的苦,生病受伤是剧烈的苦。世事无常,欢聚会结束,美食会变质,衣物会褪色破烂,高楼会倒塌,富豪会变穷人,爱人会变心,亲人会衰老死亡,生命每时每刻都在不停地流失,也许下一刻灾难就降临到自己头上。
该以怎样的态度面对这些迟早会发生的意外?该以怎样的态度面对已经遭受的不幸?
去年看过不少抑郁症病人写的内心独白,不少人的想法是:「我没做什么坏事,为什么是我!」能成功走出抑郁症的人,无一例外都抛弃了这种消极的想法。过去已经无法改变,怨天尤人没有任何意义。把做坏事与生病联系出因果关系,更是愚昧无知的表现。
人做任何事,都是先有想法再有行为,有行为才有变化。这想法可能是潜意识,也可能是细微到很难察觉的心识。倘若思想打成了死结,放弃自立自救自强,人就只能随波逐流,看天吃饭。甚至情绪也取决于外部环境,尝到甜高兴,吃到苦悲伤,这与动物、植物、死物有什么分别?
人和它们最关键的区别在于有思想、会思考,能用理智超越欲望和情绪。这种灵性,即是儒家所谓的「命」。子曰:「不知命,无以为君子也;不知礼,无以立也;不知言,无以知人也。」如果没有意识到自己身上相对于禽兽有这么独特的优势,又如何能修身炼己、行义达道,成为一名君子呢?
但许多人放弃了这一优势,每天所思所想所作,无非是如何满足自己的各种感官和口腹之欲,不如意则难过、恼火、埋怨、慎恨。一生的情绪注定起起落落,得不到解脱。这样的心态,面对女主角的抉择,只会痛苦地过会后半生。
Despite knowing the journey and where it leads, I embrace it. And I welcome every moment of it.
女主角的自白瞬间升华了电影主题,深感共鸣。兜兜转转这么多年,直到前段时间才真正明白这既是生活应有的态度,也是修道当秉持的心法。根器愚钝,这么简单的道理,从「会说」到「明白」,花了近八年。也体会到老子「吾言甚易知,甚易行;天下莫能知,莫能行」诚非虚言。
电影中还有句台词,解开了我一个心结:
Trust me, you can be very good at communication, but still end up single.
确实是这样。
事后看影评得知还有原著小说:特德·姜《你一生的故事》,读来别有一番精彩。
]]>年前看了电影《降临》,讲述了一群学者与造访地球的外星人沟通的故事。女主角在接触、学习外星语言的过程中,渐渐获得预知未来的能力,陆陆续续看到自己的一生。自己将会与现在一位同事步入婚姻,生下女儿,被丈夫抛弃,女儿早夭……
电影的结尾,女主角对未来的老公,一个尚无预知能力的物理学家,问道:「If you could see your whole life, from start to finish. Would you change thing?」
]]>金品黄传略
李仲先
清末,东阳士人月旦①,有「八骏」②之目,金品黄先生居其一焉。先生原名国清,品黄乃其庠名,字菊丞(晚年改丞为存),号少陶。同治庚午(1870年)岁正生于邑东之瑞象头。瑞,一依山之小聚落耳,家亦不中赀③,旦夕辛勤,仅堪温饱。其尊翁云美公初令入塾,不过望其略晓文字为他日外出谋生之用耳。而先生聪慧善读,塾师嘉其颖悟,学必有成,劝其父令读勿辍,乃得专心向学。光绪十八年(1892年)掇芹④,廿一年补增,翌年补廪⑤。廿九年应省试,房考官以文优荐其卷于主司,文中「忠君非忠,忠国为忠;忠国非忠,忠民为忠」之语,有干时忌,为主考所摈。
先生自进学后即在东、在金训蒙以自给,积有年所,复以己束修⑥所蓄,就读于金华丽正书院。光绪三十二年,入浙江官立法政学堂学习法律,先生之毕生业法实由此发剏⑦,其秘密加入同盟会亦在斯校时。宣统元年(1909年),以优行贡⑧,廷试二等,以府经历⑨签分江苏。时国事日非,复不乐为吏,未就,仍回法政卒业。民国肇建,初任职于浙江省财政司支应科,兼监狱学校教习;不一年,旋任金华推事,代理监督,时法制草创,新旧扞格,遇事多所掣肘,居半载,辞去。乃于金华地方审判厅管区内行律师职务。法院改制,金华审判厅裁撤,改指杭州。约可二岁,法院复其旧,遂又归金华,直至抗日军兴。其在金华执律师业盖凡廿有余年,任金华律师公会会长亦自创直至金华陷敌时止。
昔之律师,不乏以皂为白,枉法谋利之辈。人或目为讼棍。先生独抑强扶弱,伸义护法,不为势屈,不为利动。理亏而欲涉讼求为诉状者,酬以重金所不为;蒙冤者不取分毫亦为之。尝有武义乡农,田为豪强占夺,涉讼经年不得直。先生阅其县讼案牍,详询颠末,愤其冤屈,允即为撰词上诉。而其人已因讼倾家,在金无所得食,则令居己家,供其衣食几半载,至胜诉而后已。铭感之余,其人请为佣以偿值,先生拒之,并不责其衣食之费,于此可想见先生之为人。律师多因人争论而渔利,先生则好排难解纷,遇有因细故欲讼而干先生者,辄晓之利害,劝令罢讼,间或为召争执双方而调停之,以息事宁人为旨。其尤为时人所重者,厥为调解金、赵两族纠纷事。东阳金姓其蕃而巍山赵姓素为望族,金姓祖坟在东嵊接境处,置有祀田,子孙聚居者夥,而赵姓也多田产在彼。东嵊公路之始建(先修东阳至长乐段,称东长路),路经两姓田,定线中纠葛甚多,纷争弥久,至有欲谋聚众械斗以取胜者。先生在金闻悉,急往东调解。人或泥其行,谓积怨既深,非仓卒可解,且汝亦金姓,避嫌为上。先生为之不顾,至东细行勘察,竭诚秉公,往返两间,奔波劝说,终得弭平事端。人无不称道先生之诚、之能、之惠及桑梓⑩者。
先生平素行事,大率类是。故不数年间,誉日益著,业日益兴,「道德律师」之名,随在口碑。浸至南面执法者亦惮先生之严正,审美其所辩护之案件不敢有所枉纵。先生亦以平冤抑、张正义而自喜,与他人之业律师惧有玷令名而自馁者迥不相侔,是以垂老预营寿域⑾,迳书墓碑曰「律师金品黄之墓」,曾不讳言律师之名。
匪特执业如斯,复颇多急公益、拯贫弱之举。东阳地瘠人稠,外出谋生者众,泥木工匠之在杭垣⑿者无虑万人。执业低微,被目为乡愚,每受欺凌;且终岁胼胝⒀,所入戋戋⒁,一旦失业或罹疾患,即衣食不周,困顿落魄。先生深谙乡人流离之苦,在杭执业时与邑之在杭闻人共创立东阳同乡会,俾能缓急相助,免人鱼肉。其在金,复为首创建东阳会馆于东市街,设置床榻,雇人爨炊⒂,利邑人来金膳宿之需,节客邸旅食之费,阖邑称便。1922年东阳大水,历时且久,田禾凡四度被淹,遂大饥馑,粮价腾贵,饥民嗷嗷。先生急同身受,四处筹措款项,疏通富户粜⒃粮,筏运至东平价出售凡数十万斤,灾民之赖以全活者甚众。先生之乐善利众,亦不独于故乡为然。在金华八咏门外筑停厝⒄所,旌孝门、通远门外分购荒山作义冢地,为羁旅无力归葬与夫家贫无地者停柩、殡葬之所,皆独力为之。复筹组施药局于东岳殿,夏则施药;筹组救济处,冬则施衣米。他如救灾会、桥工局等当地善举,几无不与。1927年北伐战起,孙传芳部驻金华军颇滋骚扰,复忧战乱殃及黎庶,先生乃倡议组织红十字会。及战事逼近,即于舍前立长竿揭红十字旗,收容妇孺,以策安全。先生既以好公益著在人口中,其时兴办实业如电灯、织业公司等人多邀共襄其事。第⒅先生志在兴业而不专以图利,乃发起办林木公司,于金、汤县界内外植树六十余万苗,盖亦今之绿化意也。迨抗日战起,木已拱把⒆,顾尽毁于日寇。
先生善举不一端,然厥功最伟、费力最勤,则在兴办八婺女中。先生雅重女权,民国初年,金华乡人有一子一女者,女既嫁而子死,乡人身殁,女归掌父业,族人共攻之。女愬⒇之先生。先生主男女平等女可继父业为立词讼,终经判决而维女权。此在当时,纵不被视为异端,亦诧为异事。本此一贯之主张,因当时金华中学堂不收女生,虽有教会办之成美女中,取费昂贵,非常人所能企及,遂拟创建女中。兹事体大,故与友好讨论再三,并尝就商诸钱兆鹏(后任中共金华支部书记)等人。始于1924年着手筹备,请三五知己分头集款得千余元,遂在次夏开办。由石镜湖任董事会主任,公推先生为校长。初无校舍,借用火神庙作教室,腾迁其酒坊巷私宅为师生宿舍;无教师,情恳金中教员兼课,不给束修,惟于学期末暨端午、中秋等节,邀教师至家一宴,外籍教师则假其宅以寓家小而已。然权宜非久计,乃自周历府属八县,劝募捐款,各邑明达,素仰先生声誉,复感其诚毅,乐于输将(21);又商请官绅,出邑之公产为校产,八县咸与,故初定校名为旧金华府属八县立女子初级中学。集腋成裘,众擎易举,基金既集,即鸠工庀材(22),兴建校舍于四眼井。惨淡经营,逐年建设,学校渐具规模,基础于是奠立。抗日军兴,为避敌轰炸,迁校琐园,先生随迁。金华沦陷,学校中辍,复风闻敌伪正探寻其所在,拟胁迫其出任维持会长,遂遄归东阳故里。时先生亦垂垂老矣,然犹不甘心于学校之辍办。终经乃婿东阳中学校长卢绶青之助,1943年秋于东中左近之村落(时东中避敌迁县南鄙之石门)马鞍山赁民居重行招生在东阳办学。傍近东中,俾便该校教员前来兼课,先生自主校务。明年9月,再迁张宅。东西颠簸,校务烦冗,兼以高龄,生活清苦,体日以衰,终于1944年之12月卒于校内,享年七十有五。诚亦可谓与八婺同其始而以学校终其生者矣。
先生生当清季,游泮水,食廪饩,贡优行,授经历,人方以为行且扶摇直上矣。乃薄仕宦而不为,复投身革命党,观其早年省试文中「忠君非忠、忠民为忠」诸语,盖素禀民主意识者也,且终其生不渝。晚逢战乱,僻处乡隅,道途梗阴,鲜闻外事,间亦吟咏以寄情志。尝偶有外省间关(23)归者,与道及寇氛日炽,生民涂炭及当道者官贪兵懦等情事,与述「前方吃紧、后方紧吃」诸民谣。先生闻后愤慨不平,吟哦时则以手作势,口道「该杀、该杀」至成诗方已,具见其忧国疾恶之情。其于家事私事则颇随和,与子侄言,辄曰「凡事只求过得去」,以己之不事家产而勖(24)家人不多事聚敛也。自奉则廉,夫人持家则俭,先生优贡之捷鸣锣报到时(25),夫人方在村后林中扒松毛以为薪,此事士子中传为谈资,其勤俭可以想知。第举公益则曾无少吝,其金华宅旁县体育场(今红军巷转角处)之场地即为先生所捐赠,原购入欲建居宅者。以平生好施,遂家鲜余财,舍遗宅一区外,身后亦颇萧条云。
已上(26)先生事略中,太半得诸哲嗣(27)曾任杭县副县长之凤悟先生。殆由秉「子不言父之过」之训,少及乃父之短。傥以为容有溢美之辞而招求全之毁,则非所敢知矣。唯于先生行状,或当无讹。今则凤悟先生亦已作古,噫!
整理者附注:
金品黄传略
李仲先
清末,东阳士人月旦①,有「八骏」②之目,金品黄先生居其一焉。先生原名国清,品黄乃其庠名,字菊丞(晚年改丞为存),号少陶。同治庚午(1870年)岁正生于邑东之瑞象头。瑞,一依山之小聚落耳,家亦不中赀③,旦夕辛勤,仅堪温饱。其尊翁云美公初令入塾,不过望其略晓文字为他日外出谋生之用耳。而先生聪慧善读,塾师嘉其颖悟,学必有成,劝其父令读勿辍,乃得专心向学。光绪十八年(1892年)掇芹④,廿一年补增,翌年补廪⑤。廿九年应省试,房考官以文优荐其卷于主司,文中「忠君非忠,忠国为忠;忠国非忠,忠民为忠」之语,有干时忌,为主考所摈。
]]>云游难,云游难,万里水烟四海宽,说著这般滋味苦,教人怎不鼻头酸。
初别家山辞骨肉,腰下有钱三百足,思量寻思访道难,今夜不知何处宿。
不觉行行三两程,人言此地是漳城,身上衣裳典卖尽,路上何曾见一人。
初到江村宿孤馆,鸟啼花落千林晚,明朝早膳又起行,只有随身一柄伞。
渐渐来来兴化军,风雨潇潇欲送春,惟有一身赤??,囊中尚有三两文。
行得艰辛脚无力,满身瘙痒都生虱,茫茫到此赤条条,思欲归乡归不得。
争奈旬余守肚饥,埋名隐姓有谁知,来到罗源兴福寺,遂乃捐身作仆儿。
初作仆时未半月,复与主僧时作别,火云飞上攴提峰,路上石头如火热。
炎炎畏日正烧空,不堪赤脚走途中,一块肉山流出水,岂曾有扇可摇风。
且喜过除三伏暑,踪迹于今复剑浦,真个彻骨彻髓贫,荒郊一夜梧桐雨。
黄昏四顾泪珠流,无笠无蓑愁不愁,偎傍茅檐待天晓,村翁不许茅檐头。
闻说建宁人好善,特来此地求衣饭,耳边且闻惭愧声,阿谁肯具慈悲眼。
忆著从前富贵时,低头看鼻皱双眉,家家门前空舒手,那有一人怜乞儿。
福建出来到龙虎,上清宫中谒宫主,未相识前求挂搭,知堂嫌我身滥缕。
恰似先来到武夷,黄冠道士叱骂时,些儿馊饭冷熟水,道我孤寒玷辱伊。
江之东西湖南北,浙之左右接西蜀,广闽淮海数万里,千山万水空碌碌。
云游不觉已多年,道友笑我何风颠,旧游经复再去来,大事匆匆莫怨天。
我生果有神仙分,前程有人可师问,于今历练已颟顸,胸中不著一点闷。
记得兵火起淮西,凄凉数里皆横尸,幸而天与残生活,受此饥渴不堪悲。
记得武林天雨雪,衣衫破碎风刮骨,何况身中精气全,犹自冻得皮迸血。
又思古庙风雨时,香炉无火纸钱飞,神号鬼哭天惨惨,露冷云寥猿夜啼。
又思草履卧岩霜,月照苍苔落叶黄,未得些儿真受用,如何禁得不凄凉。
偶然一日天开眼,陈泥丸公知我懒,癸酉中秋野外晴,独坐松阴说长短。
元来家里有真金,前日辛勤枉用心,记得长生留命诀,结茅静坐白云深。
炼金丹,亦容易,或在山中或在市,等闲作些云游歌,恐人不识云游意
又
尝记得,洞庭一夜雨。无蓑无笠处,偎傍茅檐待天明,村翁不许檐头住。
又记得,武林七日雪。衣衫破又裂,不是白玉蟾教他,冻得皮迸血,只是寒彻骨。
又记得,江东夏热时。路上石头如火热,教我何处歇?无扇可摇风,赤脚走不辍。
又记得,青城秋月夜。独自松阴下,步虚一阙罢,口与心说话,寒烟漠漠万籁静,彼时到山方撮乍。
又记得,潇湘些小风吹转,华胥梦衔,山日正红,一声老鸦鸣鸦鸣,过耳寻无踪,这些子,欢喜消息与谁通。
又记得,淮西兵马起,枯骨排数里,欲餐又无粮,欲渴复无水。
又记得,一年到村落,瘟黄正作恶,人来请符水,无处堪摸索,神将也显灵,乱把鬼神捉。
又记得,北邙山下行,古墓秋草生,纸钱雨未干,白杨风萧萧,荒苔月盈盈,一夜鬼神哭不止,赖得度人一卷经。
又记得,通衢展手处,千家说惭愧,万家说调数,倚门眼看鼻,频频道且过,满面看尽笑,喝骂教吾去。
又记得,入堂求挂搭,嫌我太滥缕,直堂与单位,知堂言不合,未得两日间,街头行得匝,复入悲田院,乞儿相混杂。
又记得,几年霜天卧荒草,几夜月明自绝倒,几日淋漓雨,古庙之中独自坐,受尽寒,忍尽饥,未见些子禅,未见些子道。贤哉翠虚翁,一见便怜我,说一句,痛处针便住,教我行持,片晌间,骨毛寒心化结成,一粒红,渠言只此是金丹,万卷经总是闲。道人千万个,岂识真常道。这些无蹊跷,不用暗旗号,也是难,八十老翁咬铁盘;也是易,一下新竹刀又利。说与君,云游今几春,蓬头赤??,那肯教人识。
司马子微初学仙时,以元砾百片置于案前,每读一卷度人经则移瓦一片于案下。每日百刻,课经百卷,如此勤苦,久而行之,位至上清。定箓太霄丹元真人又如葛孝先,初炼丹时,常以念珠持于手中,每日坐丹炉边,常念玉帝全号一万遍,如是勤苦,久而行之,位至玉虚紫灵普化玄静真人。
我辈何人,生于中华,诞于良家,六根既圆,性识聪慧,宜生勤苦之念,早臻太上之阶,乌跃于扶桑,兔飞于广寒,燕归于乌衣,雁度于衡山。羲和驱日月,日月催百年,人生如梦幻,视死如夜眠,几度空搔首,溺志在诗酒。浑不念道业,心猿无所守,吾今划自兹,回首前程路,青春不再来,光阴莫虚度,他日决视人寰,眼卑宇宙,骑白云,步紫极,始自今日,勉之,勉之。
]]>云游难,云游难,万里水烟四海宽,说著这般滋味苦,教人怎不鼻头酸。
初别家山辞骨肉,腰下有钱三百足,思量寻思访道难,今夜不知何处宿。
不觉行行三两程,人言此地是漳城,身上衣裳典卖尽,路上何曾见一人。
初到江村宿孤馆,鸟啼花落千林晚,明朝早膳又起行,只有随身一柄伞。
渐渐来来兴化军,风雨潇潇欲送春,惟有一身赤??,囊中尚有三两文。
行得艰辛脚无力,满身瘙痒都生虱,茫茫到此赤条条,思欲归乡归不得。
争奈旬余守肚饥,埋名隐姓有谁知,来到罗源兴福寺,遂乃捐身作仆儿。
初作仆时未半月,复与主僧时作别,火云飞上攴提峰,路上石头如火热。
炎炎畏日正烧空,不堪赤脚走途中,一块肉山流出水,岂曾有扇可摇风。
且喜过除三伏暑,踪迹于今复剑浦,真个彻骨彻髓贫,荒郊一夜梧桐雨。
黄昏四顾泪珠流,无笠无蓑愁不愁,偎傍茅檐待天晓,村翁不许茅檐头。
闻说建宁人好善,特来此地求衣饭,耳边且闻惭愧声,阿谁肯具慈悲眼。
忆著从前富贵时,低头看鼻皱双眉,家家门前空舒手,那有一人怜乞儿。
福建出来到龙虎,上清宫中谒宫主,未相识前求挂搭,知堂嫌我身滥缕。
恰似先来到武夷,黄冠道士叱骂时,些儿馊饭冷熟水,道我孤寒玷辱伊。
江之东西湖南北,浙之左右接西蜀,广闽淮海数万里,千山万水空碌碌。
云游不觉已多年,道友笑我何风颠,旧游经复再去来,大事匆匆莫怨天。
我生果有神仙分,前程有人可师问,于今历练已颟顸,胸中不著一点闷。
记得兵火起淮西,凄凉数里皆横尸,幸而天与残生活,受此饥渴不堪悲。
记得武林天雨雪,衣衫破碎风刮骨,何况身中精气全,犹自冻得皮迸血。
又思古庙风雨时,香炉无火纸钱飞,神号鬼哭天惨惨,露冷云寥猿夜啼。
又思草履卧岩霜,月照苍苔落叶黄,未得些儿真受用,如何禁得不凄凉。
偶然一日天开眼,陈泥丸公知我懒,癸酉中秋野外晴,独坐松阴说长短。
元来家里有真金,前日辛勤枉用心,记得长生留命诀,结茅静坐白云深。
炼金丹,亦容易,或在山中或在市,等闲作些云游歌,恐人不识云游意
安装Python 2.7后,本来在3.4下能正常使用的脚本无法运行。网上有的方法是把两个版本的主程序分别改名为python2和python3,人眼判断脚本,手输命令行执行脚本。像我这样喜欢双击、拖拽的懒人当然不会满足,找到了更智能的解决方案。
安装 Python 3.3 以上的版本时,Python会在C:\Windows
文件夹下安装启动器py.exe
。双击脚本调用的就是这个程序:
如果系统中同时存在 Python 2 和 Python 3,可用它指定版本来运行代码:
py -2 helloworld.py
py -3 helloworld.py
2和3即是版本。
每次都添加参数太麻烦,直接在Python脚本第一行指定版本:
#! python3
可以双击,也可以命令行运行:
py helloworld.py
如果没有在首行指定版本而用上述命令运行或双击,则默认调用Python 2
使用pip:
py -2 -m pip install requests
py -3 -m pip install requests
-m pip
表示运行 pip 模块
原有的python
和pip
命令仍然有效,默认执行哪一个版本呢?看环境变量中路径的先后次序。
C:\Python34\;C:\Python34\Scripts;C:\Python27\;C:\Python27\Scripts;
如上则调用Python 3
安装Python 2.7后,本来在3.4下能正常使用的脚本无法运行。网上有的方法是把两个版本的主程序分别改名为python2和python3,人眼判断脚本,手输命令行执行脚本。像我这样喜欢双击、拖拽的懒人当然不会满足,找到了更智能的解决方案。
]]>在发上一篇《Python 3 学习笔记》时,每次执行hexo s
或hexo g
,都会报错:
FATAL Something's wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.html
Template render error: tag name expected
at Error.exports.TemplateError (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\lib.js:51:19)
at Object.extend.fail (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\parser.js:64:15)
at Object.extend.parseStatement (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\parser.js:510:18)
.......其余省略
文中有这句话:
在Jinja2中,用
{% ... %}
表示指令
其中{%和%}被当成hexo模板中的标签,解析出错。
将它用如下代码包住:1
2{ }
{ }
修改后的Markdown原文:
1 | 在Jinja2中,用`{ }{ }{ }`表示指令 |
效果:
在Jinja2中,用{% ... %}
表示指令
用```包住的代码块不需要这样特殊处理。
]]>在发上一篇《Python 3 学习笔记》时,每次执行hexo s
或hexo g
,都会报错:
FATAL Something's wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.html
Template render error: tag name expected
at Error.exports.TemplateError (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\lib.js:51:19)
at Object.extend.fail (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\parser.js:64:15)
at Object.extend.parseStatement (E:\hexo\node_modules\hexo\node_modules\nunjucks\src\parser.js:510:18)
.......其余省略
]]>
安装后可在程序菜单中找到IDLE
进入交互帮助模式:help()
退出:quit
1 | import sys |
添加新路径1
sys.path.insert(0, '新路径')
b
前缀的单引号或双引号表示,如b'abc'
,每个字符都只占一个字节Python中,通常用全部大写的变量名表示常量。但是Python根本没有任何机制保证该常量不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法
可以用科学计数法,如1
a = 1.2e-5
Python的整数没有大小限制
Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf(无限大
1 | import fractions |
会自动进行约分
列表,元素可变。
1 | a_list = ['a', 'b', 'mpilgrim', 'z', 'example'] |
元组,元素不可变
引用、切片(会创建新tuple)等方法与list相同1
a_tuple = ("a", "b", "mpilgrim", "z", "example") # 也可以用单引号
如果创建单元素元组,需要在值后加一个逗号
好处:
1 | # 使用元组,下面的括号都可以省略 |
1 | # 创建set |
dict是键值对的无序集合。向dict添加一个键的同时,必须为该键增添一个值
1 | # 创建dict |
值为list时1
2
3
4
5
6
7
8
9
10
11
12SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
# 长度
len(SUFFIXES) # 2
# 检测值
1000 in SUFFIXES
# 查询值
SUFFIXES[1000] # ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
SUFFIXES[1000][3] # 'TB'
None 是 Python 的一个特殊常量。 它是唯一的空值。None 与 False 不同。None 不是 0 。None 不是空字符串。将 None 与任何非 None 的东西进行比较将总是返回 False 。
None 是唯一的空值。它有着自己的数据类型(NoneType)。可将 None 赋值给任何变量,但不能创建其它 NoneType 对象。所有值为 None 变量是相等的。
1 | int(), float(), bool(), str() |
把函数名赋给一个变量,相当于给这个函数起了一个“别名”1
2a = abs
a(-1)
1 | def my_abs(x): |
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
也可以返回多个值:1
2
3
4
5
6
7
8
9
10
11import math
def move(x, y, step, angle = 0): # angel=0是默认参数,必须写在后
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
# 调用
x, y = move(100, 100, 60, math.pi / 6)
# 也可以缺少angle
x, y = move(100, 100, 60)
实际上是返回一个tuple
如果想定义一个什么事也不做的空函数,可以用pass
语句。pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
1
2def nop():
pass
pass
还可以用在其他语句里,比如:1
2
3# 缺少了pass,代码运行就会有语法错误。
if age >= 18:
pass
1 | def enroll(name, gender, age=6, city='Beijing'): |
注意1
2
3def add_end(L=[]): # 默认参数指向[],[]会变改变
L.append('END')
return L
这里的默认参数L
也是一个变量,指向对象[]
,每次调用该参数,如果改变了L
所指向对象的内容,则下次调用时,默认参数的内容就变了。上面函数多次执行add_end()
(不传入参数)结果分别为:1
2
3
4['END']
['END', 'END']
['END', 'END', 'END']
....
定义默认参数要牢记一点:默认参数必须指向不变对象!比如None
1
2
3
4
5def add_end(L=None): # 默认参数指向None,None无法被改变
if L is None:
L = []
L.append('END')
return L
允许传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 参数前加*号即可,在函数内部,参数numbers接收到的是一个tuple
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
# 调用时可以传入任意个参数
calc()
calc(1)
calc(1,3)
# 把list或tuple的元素变成可变参数,也是加*号
nums = [1, 2, 3]
calc(*nums)
允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 两个*号定义
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
# 调用时可以只传入必选参数
person('Michael', 30)
# 也可以传入任意个数的关键字参数
person('Bob', 35, city='Beijing')
person('Adam', 45, gender='M', job='Engineer')
# 可以先组装出一个dict,然后,把该dict转换为关键字参数传进去
# kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)
要限制关键字参数的名字,就可以用命名关键字参数。
命名关键字参数需要一个特殊分隔符,后面的参数被视为命名关键字参数1
2
3
4
5
6
7# 可以有默认值
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
# 调用时必须传入参数名,否则将报错。
# 和位置参数一样不能省略
person('Jack', 24, city='Beijing', job='Engineer')
*
不是参数,而是特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数
可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用,除了可变参数无法和命名关键字参数混合。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数/命名关键字参数和关键字参数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
#调用
f1(1, 2)
# a = 1 b = 2 c = 0 args = () kw = {}
f1(1, 2, c=3)
# a = 1 b = 2 c = 3 args = () kw = {}
f1(1, 2, 3, 'a', 'b')
# a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
f1(1, 2, 3, 'a', 'b', x=99)
# a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
f2(1, 2, d=99, ext=None)
# a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
# 通过一个tuple和dict,也可以调用上述函数
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
# a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)
# a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
所以,对于任意函数,都可以通过类似func(args, *kw)的形式调用它,无论它的参数是如何定义的
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出1
2
3
4
5
6
7
8
9
10
11
12
13
14# 非尾递归
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
# 尾递归:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
用于迭代1
2
3names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)
对list实现下标循环,enumerate
函数可以把一个list变成索引-元素对:1
2for i, value in enumerate(['A', 'B', 'C']):
print(i, value)
1 | sum = 0 |
List Comprehensions1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 生成[1x1, 2x2, 3x3, ..., 10x10]
[x * x for x in range(1, 11)]
# 加上判断,筛选出仅偶数的平方
[x * x for x in range(1, 11) if x % 2 == 0]
# 使用两层循环,生成全排列
[m + n for m in 'ABC' for n in 'XYZ']
# 使用多个变量
d = {'x': 'A', 'y': 'B', 'z': 'C' }
[k + '=' + v for k, v in d.items()]
# 列出当前目录下的所有文件和目录名
import os #
[d for d in os.listdir('.')]
# 把一个list中所有的字符串变成小写:
L = ['Hello', 'World', 'IBM', 'Apple']
[s.lower() for s in L]
Python中,一边循环一边计算的机制,称为生成器generator。
generator保存的是算法,用到时才计算。
列表生成式在生成时已经全部计算好。
把一个列表生成式的[]改成(),就创建了一个generator1
2
3
4
5
6
7g = (x * x for x in range(10))
# 通过next()函数计算出generator的下一个值,没有更多的元素时,抛出StopIteration的错误。
next(g)
# 也可以用于迭代,此时不需要next()
for n in g
如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator
以斐波拉契数列为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26# 函数
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
# 改成生成器
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
# 一个新的例子
def odd():
print('step 1')
yield 1
print('step 2')
yield 3
print('step 3')
yield 5
generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
用for循环调用这种generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中。接上例:1
2
3
4
5
6
7
8g = fib(6)
while True:
try:
x = next(g)
print('g:', x)
except StopIteration as e:
print('Generator return value:', e.value)
break
可以被next()函数调用并不断返回下一个值的对象称为迭代器。
可迭代对象Iterable有两类:
一类是集合数据类型,如list、tuple、dict、set、str等;
一类是generator。
1 | # 判断一个对象是不是可迭代对象,用for in循环迭代 |
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。可以使用iter()函数把它们变成迭代器1
iter('abc')
为什么list、dict、str等数据类型不是Iterator?
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Functional Programming
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言
Higher-order function
变量可以指向函数,函数名就是指向函数的变量。对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数1
2
3
4
5def add(x, y, f):
return f(x) + f(y)
# 调用
add(-5, 6, abs)
Python内建了map()和reduce()函数
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。1
2
3
4
5
6
7def f(x):
return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list
list(r)
# 结果为[1, 4, 9, 16, 25, 36, 49, 64, 81]
reduce函数的参数与map类似。它把一个函数作用在一个可迭代对象Iterable上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。效果即:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
1
2
3
4
5
6# 对一个序列求和,其实可以用sum()函数,这里仅用于说明reduce的用法
from functools import reduce
def add(x, y):
return x + y
reduce(add, [1, 3, 5, 7, 9]) # 结果为25
map与reduce一起使用,把str转换为int:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from functools import reduce
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
return reduce(fn, map(char2num, s))
# 还可以用lambda函数进一步简化成
from functools import reduce
def char2num(s):
return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
Python内建的filter()函数用于过滤序列。
和map()类似,filter()也接收一个函数和一个序列。和map()不同的时,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。1
2
3
4
5
6
7
8
9
10
11# 只保留奇数
def is_odd(n):
return n % 2 == 1
result = filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])
# 把一个序列中的空字符串删掉
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。
eg:欧拉筛法找出素数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 生成器,从3开始的奇数序列
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
# 筛选函数
def _not_divisible(n):
return lambda x: x % n > 0
# 生成器,不断返回下一个素数
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
Iterator是惰性计算的序列,所以我们可以用Python表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。
sorted()函数就可以对list进行排序1
sorted([36, 5, -12, 9, -21])
sorted()函数也是一个高阶函数:1
2
3
4
5# 传入key函数来实现自定义的排序
sorted([36, 5, -12, 9, -21], key=abs)
# 传入reverse=True可以反向排序
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 返回求和函数
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
# 调用
f = lazy_sum(1, 3, 5, 7, 9)
f()
# 每次调用都会返回一个新的函数
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
f1==f2 # False
函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
闭包的注意点1
2
3
4
5
6
7
8
9
10def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count() # 三个函数执行结果都是9
# 原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:1
2
3
4
5
6
7
8
9def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
Python对匿名函数提供了有限支持1
2#计算f(x)=x的平方,直接传入匿名函数:
map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])
关键字lambda表示匿名函数,冒号前面的x表示函数参数
匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,也可以把匿名函数作为返回值返回。
本质上,decorator就是一个返回函数的高阶函数。
eg:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 原函数
def now():
print('2015-3-25')
# 定义一个装饰器decorator
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# 借助Python的@语法,把decorator置于函数的定义处
# 相当于执行了语句:now = log(now)
@log
def now():
print('2015-3-25')
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数1
2
3
4
5
6
7
8
9
10
11
12
13
14# 定义装饰器
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
# 给原函数加上装饰器
# @语句相当于 now = log('execute')(now),两层括号,第一对括号返回装饰器,第二对括号使用装饰器进行包装
@log('execute')
def now():
print('2015-3-25')
仍然存在问题
经过decorator装饰之后的函数,它们的name已经从原来的’now’变成了’wrapper’
一个完整的decorator的写法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import functools
# 不带参数
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# 带参数
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
偏函数Partial function,和数学意义上的偏函数不一样,用于设定参数的默认值(仍然可以传入其他值)。
创建偏函数:new_func = functools.partial(func,*args,**kw)
eg:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 转换二进制,自定义函数的方法
def int2(x, base=2):
return int(x, base)
# 转换二进制,创建偏函数
import functools
int2 = functools.partial(int, base=2)
# 上述代码实际上固定了int()函数的关键字参数base,相当于:
kw = { 'base': 2 }
int('10010', **kw)
# 如果传入单个值,如
max2 = functools.partial(max, 10)
# 实际上会把10作为*args的一部分自动加到左边,也就是:
max2(5, 6, 7)
# 相当于:
args = (10, 5, 6, 7)
max(*args)
Python 3,所有的字符串都是使用Unicode编码的字符序列。不再存在以UTF-8或者CP-1252编码的情况。
以r或R开头(代表raw)的python中的字符串表示(非转义的)原始字符串
以u或U开头的字符串表示unicode字符串
以b或B开头的字符串字节形式表示的字符串bytes
字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串1
'ABCDEFG'[::2] # 'ACEG'
详见字符串和编码
计算机系统通用的字符编码工作方式:
为了避免乱码问题,应当始终坚持使用UTF-8
编码对str
和bytes
进行转换
1 | # 字符转整数编码 |
Python的字符串类型是str
,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。
反之,如果从网络或磁盘上读取了字节流,那么读到的数据就是bytes。1
2
3
4
5
6
7# str转bytes
# python3中,str没有decode方法
'ABC'.encode('ascii') # b'ABC'
'中文'.encode('utf-8') # b'\xe4\xb8\xad\xe6\x96\x87'
# bytes转str
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:1
2#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
1 | # str统计字符个数 |
常见占位符
|占位符|数据类型|
|–|–|
|%d |整数|
|%f|浮点数|
|%s|字符串|
|%x|十六进制整数|
1 | # 格式化整数和浮点数可以指定是否补0和整数与小数的位数 |
文档字符串(docstring)也是字符串。当前的文档字符串占用了多行,所以它使用了相邻的3个引号来标记字符串的起始和终止1
2
3
4
5
6
7
8'''Convert a file size to human-readable form.
Keyword arguments:
size -- file size in bytes
a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
if False, use multiples of 1000
Returns: string
'''
Python 3支持把值格式化(format)成字符串。可以有非常复杂的表达式,最基本的用法是使用单个占位符(placeholder)将一个值插入字符串。1
2
3
4>>> username = 'mark'
>>> password = 'PapayaWhip'
>>> "{0}'s password is {1}".format(username, password)
"mark's password is PapayaWhip"
整型替换字段被当做传给format()方法的参数列表的位置索引。即,{0}会被第一个参数替换(在此例中即username),{1}被第二个参数替换(password)
1 | >>> si_suffixes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] |
{0}代表传递给format()方法的第一个参数,即si_suffixes。注意si_suffixes是一个列表。所以{0[0]}指代si_suffixes的第一个元素,即’KB’。同时,{0[1]}指代该列表的第二个元素,即:’MB’。大括号以外的内容 — 包括1000,等号,还有空格等 — 则按原样输出。语句最后返回字符串为’1000KB = 1MB’。
这个例子说明格式说明符可以通过利用(类似)Python的语法访问到对象的元素或属性。这就叫做复合字段名(compound field names)。以下复合字段名都是“有效的”。
- 使用列表作为参数,并且通过下标索引来访问其元素(跟上一例类似)
- 使用字典作为参数,并且通过键来访问其值
- 使用模块作为参数,并且通过名字来访问其变量及函数
- 使用类的实例作为参数,并且通过名字来访问其方法和属性
以上方法的任意组合
为了使你确信的确如此,下面这个样例就组合使用了上面所有方法:1
2
3
4>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'
下面是描述它如何工作的:
sys
模块保存了当前正在运行的Python实例的信息。由于已经导入了这个模块,因此可以将其作为format()
方法的参数。所以替换域{0}
指代sys
模块。
sys.modules
是一个保存当前Python实例中所有已经导入模块的字典。模块的名字作为字典的键;模块自身则是键所对应的值。所以{0.modules}
指代保存当前己被导入模块的字典。
替换域{0.modules[humansize]}指代humansize模块。
请注意以上两句在语法上轻微的不同。在实际的Python代码中,字典sys.modules的键是字符串类型的;为了引用它们,我们需要在模块名周围放上引号(比如 ‘humansize’)。但是在使用替换域的时候,我们在省略了字典的键名周围的引号(比如 humansize)。在此,我们引用PEP 3101:字符串格式化高级用法,“解析键名的规则非常简单。如果名字以数字开头,则它被当作数字使用,其他情况则被认为是字符串。”
sys.modules[‘humansize’].SUFFIXES是在humansize模块的开头定义的一个字典对象。{0.modules[humansize].SUFFIXES}即指向该字典。
sys.modules[‘humansize’].SUFFIXES[1000]是一个si(国际单位制)后缀列表:[‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’]。所以替换域{0.modules[humansize].SUFFIXES[1000]}指向该列表。
sys.modules[‘humansize’].SUFFIXES[1000][0]即si后缀列表的第一个元素:’KB’。因此,整个替换域{0.modules[humansize].SUFFIXES[1000][0]}最后都被两个字符KB替换。
1 | if size < multiple: |
{0:.1f}中的:.1f则不一定了。第二部分(包括冒号及其后边的部分)即格式说明符(format specifier),它进一步定义了被替换的变量应该如何被格式化。
☞格式说明符的允许你使用各种各种实用的方法来修饰被替换的文本,就像C语言中的printf()函数一样。我们可以添加使用零填充(zero-padding),衬距(space-padding),对齐字符串(align strings),控制10进制数输出精度,甚至将数字转换成16进制数输出。
在替换域中,冒号(:)标示格式说明符的开始。“.1”的意思是四舍五入到保留一们小数点。“f”的意思是定点数(与指数标记法或者其他10进制数表示方法相对应)。因此,如果给定size为698.24,suffix为’GB’,那么格式化后的字符串将是’698.2 GB’,因为698.24被四舍五入到一位小数表示,然后后缀’GB’再被追加到这个串最后。1
2>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'
模块(Module):一个.py文件
包(package):一个目录
相同名字的函数和变量完全可以分别存在不同的模块中,但是尽量不要与内置函数名字冲突。
Python的所有内置函数
每一个包目录下面都会有一个__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py
可以是空文件,也可以有Python代码。无论一个包的哪个部分被导入,
import os
系统在导入模块时,要做以下三件事:
1.为源代码文件中定义的对象创建一个名字空间,通过这个名字空间可以访问到模块中定义的函数及变量。
2.在新创建的名字空间里执行源代码文件.
3.创建一个名为源代码文件的对象,该对象引用模块的名字空间,这样就可以通过这个对象访问模块中的函数及变量
import os as system
模块导入时可以使用 as 关键字来改变模块的引用对象名字。
from socket import gethostname
使用from语句可以将模块中的对象直接导入到当前的名字空间。 from语句不创建一个到模块名字空间的引用对象,而是把被导入模块的一个或多个对象直接放入当前的名字空间。可以使用星号代表模块中除下划线开头的所有对象。不过,如果一个模块如果定义有列表__all__
,则from module import 语句只能导入__all__
列表中存在的对象。
import 语句可以在程序的任何位置使用,你可以在程序中多次导入同一个模块,但模块中的代码仅仅在该模块被首次导入时执行。后面的import语句只是简单的创建一个到模块名字空间的引用而已。sys.modules字典中保存着所有被导入模块的模块名到模块对象的映射。这个字典用来决定是否需要使用import语句来导入一个模块的最新拷贝。
from module import 语句只能用于一个模块的最顶层。特别注意*:由于存在作用域冲突,不允许在函数中使用from 语句。
每个模块都拥有__name__
属性,它是一个内容为模块名字的字符串。最顶层的模块名称是__main__
。命令行或是交互模式下程序都运行在__main__
模块内部。利用__name__
属性,我们可以让同一个程序在不同的场合(单独执行或被导入)具有不同的行为,象下面这样做:1
2
3
4
5
6
7# 检查是单独执行还是被导入
if __name__ == '__main__':
# Yes
statements
else:
# No (可能被作为模块导入)
statements
可以被 import 语句导入的模块共有以下四类:
当查询模块 foo 时,解释器按照 sys.path 列表中目录顺序来查找以下文件(目录也是文件的一种):
1.定义为一个包的目录 foo
2.foo.so, foomodule.so, foomodule.sl,或 foomodule.dll (已编译扩展)
3.foo.pyo (只在使用 -O 或 -OO 选项时)
4.foo.pyc
5.foo.py
对于.py文件,当一个模块第一次被导入时,它就被汇编为字节代码,并将字节码写入一个同名的.pyc文件。后来的导入操作会直接读取.pyc文件而不是.py文件。(除非.py文件的修改日期更新,这种情况会重新生成.pyc文件) 在解释器使用 -O 选项时,扩展名为.pyo的同名文件被使用。pyo文件的内容去掉行号、断言、及其他调试信息的字节码,体积更小,运行速度更快。如果使用-OO选项代替-O,则文档字符串也会在创建.pyo文件时也被忽略。
如果在sys.path提供的所有路径均查找失败,解释器会继续在内建模块中寻找,如果再次失败,则引发 ImportError 异常。
.pyc和.pyo文件的汇编,当且仅当import 语句执行时进行。
当 import 语句搜索文件时,文件名是大小写敏感的。即使在文件系统大小写不敏感的系统上也是如此(Windows等)。
reload(sys)
如果更新了一个已经用import语句导入的模块,内建函数reload()可以重新导入并运行更新后的模块代码。在reload()运行之后的针对模块的操作都会使用新导入代码,不过reload()并不会更新使用旧模块创建的对象,因此有可能出现新旧版本对象共存的情况。
注意 使用C或C++编译的模块不能通过 reload() 函数来重新导入。记住一个原则,除非是在调试和开发过程中,否则不要使用reload()函数。
无论一个包的哪个部分被导入,在文件__init__.py
中的代码都会运行。导入过程遇到的所有 __init__.py
文件都被运行。
from Graphics.Primitive import *
这个语句的原意图是想将Graphics.Primitive包下的所有模块导入到当前的名称空间。然而,由于不同平台间文件名规则不同(比如大小写敏感问题), Python不能正确判定哪些模块要被导入。这个语句只会顺序运行 Graphics 和 Primitive 文件夹下的__init__.py
文件。__init__.py
中定义一个名字all的列表。
这和导入模块不同,类似语句可以正常导入模块下的所有函数。
下面这个语句只会执行Graphics目录下的__init__.py
文件,而不会导入任何模块: import Graphics。不过既然 import Graphics 语句会运行 Graphics 目录下的 __init__.py
文件,只需要在其中把所有模块都import进去就行了。
sys.modules包含了当前所load的所有的modules的dict(其中包含了builtin的modules)
Python模块的标准文件模板:1
2
3
4
5
6#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释 '
__author__ = '作者名'
通过_
前缀定义作用域:
- 正常的函数和变量名是公开的(public),可以被直接引用。
__xxx__
:特殊变量,可以被直接引用,但是有特殊用途。模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名_xxx
和__xxx
:非公开(private),只是一个命名习惯,外部仍然可以访问
Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
安装Python时确保勾选pip
和Add python.exe to Path
。
一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者pypi上搜索。
以安装Python Imaging Library
为例,这是Python下非常强大的处理图像的工具库。
运行命令:pip install Pillow
即可。
简单用法:1
2
3
4
5from PIL import Image
im = Image.open("d:\\backup\\140591\\桌面\\首页图片\\11.jpg")
print(im.format, im.size, im.mode)
im.thumbnail((200, 100)) # 缩小图片
im.save('22.jpg', 'JPEG') # 保存
其他常用的第三方库还有MySQL的驱动:mysql-connector-python
,用于科学计算的NumPy库:numpy
,用于生成文本的模板工具Jinja2
,等等。
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件。默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中。
要添加自己的搜索目录,有两种方法:
- 直接修改sys.path,添加要搜索的目录:
sys.path.append('/Users/michael/my_py_scripts')
。这种方法是在运行时修改,运行结束后失效。- 设置环境变量
PYTHONPATH
。
sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称。
运行python3 hello.py
获得的sys.argv
就是['hello.py']
。
在命令行直接运行一个模块文件时,Python解释器把一个特殊变量__name__
置为`main`,注意带了引号。如果是import一个模块,则此变量值为模块文件名,不带路径或扩展名。
Object Oriented Programming,简称OOP。
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112# 定义
class Student(object):
# 限制该class实例在运行期间能添加的属性,可选。对子类无效
__slots__ = ('name', 'age')
# 此处定义类属性
count = 0
# __init__方法的第一个参数永远是self,表示创建的实例本身
# 此处定义实例属性
def __init__(self, name, score):
self.name = name
self.score = score
# 使此class可以适用于系统的len()函数,len('king'),len(obj)...可选
def __len__(self):
# 自己实现
# 类似Java的toString方法,打印对象时调用。可选。
# 返回用户看到的字符串
def __str__(self):
return 'Student object (name: %s)' % self.name
# 在解释器中直接输变量,会调用此__repr__()方法。可选
# 返回程序开发者看到的字符串,此处是偷懒的写法,使两者一样。
__repr__ = __str__
# 使此类的对象可以被用于for ... in循环。可选
# 返回一个迭代对象,该迭代对象必须实现__next__()方法,迭代结束时raise StopIteration()
def __iter__(self):
# 自定义实现
# 使此类的对象可以像list那样按照下标取出元素,可选。
# 如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,此处未实现
# 与此对应的是__setitem__()方法,把对象视作list或dict来对集合赋值;__delitem__()方法,删除某个元素
def __getitem__(self, n):
if isinstance(n, int): # n是索引
# 取出对象...
if isinstance(n, slice): # n是切片,slice表示切片对象
start = n.start
stop = n.stop
if start is None:
start = 0
# 取出list...注意要对负数、步长参数作处理
# 调用的属性或方法在类中不存在时,会在此处找,可选。
# 默认返回None
def __getattr__(self, attr):
if attr=='sex':
return 'man' # 返回属性
if attr=='hisage': # 仅作示范,不用在意名字
return lambda: 25 # 返回函数
raise AttributeError('自定义错误信息') # 抛出Error,不再返回默认的None
# 直接把实例当成方法用,可选。
# 一般用instance.method()的形式调用方法,但是定义了此方法后,可以用instance(),如:Student('Michael')()。
# 可添加参数
def __call__(self):
print('My name is %s.' % self.name)
# 其他方法也和__init__类似
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
# @property装饰器把一个getter方法变成属性
# 此处@property又创建了另一个装饰器@score.setter
@property
def score(self):
return self._score
# 如果不设置此set方法,则为只读属性
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
#################---使用----#############
# 创建实例
s = Student('Bart Simpson', 59)
# score属性的set与get
s.score = 60 # 实际转化为s.set_score(60)
s.score # 实际转化为s.get_score()
# 绑定属性。对象名改成Class名即可对Class绑定
s.age = 8 # 注意class中并没有定义age变量
# 删除属性
del s.name
# 绑定方法,注意区别。对象名改成Class名即可对Class绑定
# 第一种方法:
def nono():
print('nono')
s.nono = nono
s.nono()
# 第二种方法:
def nono(self):
print('nono')
from types import MethodType
s.nono = MethodType(nono, bart)
s.nono()
# 例外:slots只能限制添加属性,不能限制通过添加方法来添加属性:
# 绑定下面的方法即可绕过slots限制
def set_city(self, city):
self.city=city
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
有了__init__
方法,在创建实例的时候,必须传入与__init__
方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。
重点提一下__getattr__
动态调用的用法。例如很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似http://api.server/user/timeline/list
。如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。利用完全动态的__getattr__
,我们可以写出一个链式调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 定义
class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__
# 调用
Chain().status.user.timeline.list
# 结果为:'/status/user/timeline/list'
__call__()
使得我们对实例进行直接调用就好比对一个函数进行调用一样,这么一来,我们就模糊了对象和函数的界限。能被调用的对象(也就是可以当成一个函数使用)就是一个Callable
对象:1
2
3
4
5
6callable(Student()) # 对于类来说,必须实现__call__()才能当函数用
callable(max) # True
callable([1, 2, 3]) # False
callable(None) # False
callable('str') # False
callable(str) # True
实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。例外:__xxx__
是特殊变量,特殊变量是可以直接访问的,不是private变量。
一个下划线开头的实例变量名_xxx
,外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__xxx
是因为Python解释器对外把__xxx
变量改成了_类名__xxx
,所以,仍然可以通过_类名__xxx
来访问该变量。不同版本的Python解释器可能会把__name改成不同的变量名。
###继承和多态
多态的好处:调用方只管调用,不管细节。这就是著名的开闭原则:对扩展开放,对修改封闭。
举例说明:1
2def run_twice(animal):
animal.run()
对于静态语言(例如Java)来说,上述代码传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。
1 | # 对象类型,使用type()返回对应的Class类型 |
Python中,如果调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法。自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()
方法
要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如:1
2
3
4def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
上述代码从文件流fp中读取图像,首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。
在设计类的继承关系时,通常,主线都是单一继承下来的。但是,如果需要“混入”额外的功能,通过多重继承就可以实现。这种设计通常称之为MixIn。
eg:
Python自带了TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。1
2
3
4
5
6
7
8
9
10
11# 编写一个多进程模式的TCP服务
class MyTCPServer(TCPServer, ForkingMixIn):
pass
# 编写一个多线程模式的UDP服务
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
# 如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
1 | # 定义方法一 |
1 | #### type()查看类或变量的类型 |
str类用来创建字符串对象
int类用来创建整数对象
type类用来创建类对象
Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
除此之外还可以用metaclass动态创建类。
eg,给自定义的MyList添加add方法:1
2
3
4
5
6
7
8
9
10
11
12# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type): # 默认习惯,metaclass的类名总是以Metaclass结尾
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
# 使用ListMetaclass来定制类
class MyList(list, metaclass=ListMetaclass): # 传入关键字参数metaclass
pass
# Python解释器在创建MyList时,找到metaclass关键字就会通过元类的__new__()来创建类。
# __new__()的参数:新类的对象、新类的名字、新类继承的父类集合、新类的属性方法集合
创建类的过程:
__metaclass__
属性模块中添加__metaclass__
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21'''模块中添加__metaclass__'''
# 元类会自动将你通常传给'type'的参数作为自己的参数传入
def upper_attr(future_class_name, future_class_parents, future_class_attr):
'''返回一个类对象,将属性都转为大写形式'''
# 选择所有不以'__'开头的属性
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
# 将它们转为大写形式
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 通过'type'来做类对象的创建
return type(future_class_name, future_class_parents, uppercase_attr)
'''注意这条语句的位置,它属于整个模块'''
__metaclass__ = upper_attr # 这会作用到这个模块中的所有类
class Foo(object):
# 我们也可以只在这里定义__metaclass__,这样就只会作用于这个类中
bar = 'bip'
hasattr(Foo, 'bar') # 输出: False
hasattr(Foo, 'BAR') # 输出:True
类定义中添加__metaclass__
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 沿用上面的代码
# 必须传入type或子类
class UpperAttrMetaClass(type):
# __new__ 是在__init__之前被调用的特殊方法
# __new__是用来创建对象并返回之的方法
# 而__init__只是用来将传入的参数初始化给对象
# 你很少用到__new__,除非你希望能够控制对象的创建
# 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
# 如果你希望的话,你也可以在__init__中做些事情
# 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 复用type.__new__方法
return type.__new__(cls, name, bases, uppercase_attr)
# 最后一句也可以使用super
return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)
metaclass修改类定义的典型的例子:ORM,全称Object Relational Mapping,即对象-关系映射。就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101# 定义一个User类来操作对应的数据库表User
class User(Model):
# 定义类的属性到列的映射:
# 父类Model和属性类型StringField、IntegerField是由ORM框架提供的
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()
# 输出如下:
'''
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
'''
'''-----------上面是此ORM框架的使用,下面是定义-----------'''
# Field类负责保存数据库表的字段名和字段类型
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)
# 在Field的基础上,进一步定义各种类型的Field
class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
# 编写ModelMetaclass
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 先排除对Model类对象的修改,则此元类只会影响User类对象的创建
if name=='Model':
return type.__new__(cls, name, bases, attrs)
# 显示正在修改的类,调试用
print('Found model: %s' % name)
# 用于存储Field属性映射
mappings = dict()
# 遍历类的所有属性,把所有Field属性保存到mappings中
# 字典attrs中保存属性名称和属性值的映射,如id与IntegerField('id')。
for k, v in attrs.items():
if isinstance(v, Field):
# 调试用的输出语句
print('Found mapping: %s ==> %s' % (k, v))
# 存储符合条件的Filed属性
mappings[k] = v
# 从类属性中删除该Field属性
for k in mappings.keys():
attrs.pop(k)
# 保存属性和列的映射关系
attrs['__mappings__'] = mappings
# 把表名保存到__table__中,假设表名和类名一致
attrs['__table__'] = name
return type.__new__(cls, name, bases, attrs)
# 基类Model,可以定义各种操作数据库的方法,比如save(),delete(),find(),update()等等
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
# 变量名
fields = []
# 参数
params = []
# 变量值
args = []
for k, v in self.__mappings__.items():
# 这里的v是指字典中的value,阅读前面的代码可知是个Field的子类的实例
# 该实例一个名为name的属性,创建对象时传入
# 如StringField('username'),此处`username`即是name属性
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
# 'xxx'.join(alist)用xxx为分隔符把列表alist连接成字符串
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
1 | try: |
Python中错误类型都继承自BaseException
。
常见的错误类型和继承关系:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
1 | class FooError(ValueError): |
1 | def foo(s): |
启动解释器时可以用-O
参数来关闭assert:python3 -O err.py
。注意是大写的英文字母O。
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
第一种方法,命令行启动:python3 -m pdb 调试的文件.py
此时输入小写字母l
查看文件内容。n
单步执行代码。p 变量名
查看变量q
结束调试。
第二种方法1
2
3
4
5import pdb
# 在需要暂停的地方插入此代码
# 程序运行到这里会暂停并进入pdb调试环境
pdb.set_trace()
仍然用命令行运行文件python3 调试的文件.py
,调试完c
继续运行
PyCharm
Eclipse加上pydev插件
以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
开发一个Dict类,mydict.py:1
2
3
4
5
6
7
8
9
10
11
12
13class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
单元测试模块,包含测试类,mydict_test.py:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51import unittest
from mydict import Dict
# 测试类继承unittest.TestCase
class TestDict(unittest.TestCase):
# 调用测试方法前执行,可用于打开资源如数据库
def setUp(self):
pass
# 调用测试方法后执行
def tearDown(self):
pass
# 以test开头的方法就是测试方法,测试时会被自动执行
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
# 用于运行单元测试
if __name__ == '__main__': # 直接运行此模块时,判断条件成立
unittest.main() # 注意是包名和方法
# 如果不加上述代码,则需要用如下命令运行:
# python3 -m unittest mydict_test
# mydict_test是此文件名
# 推荐用此方法
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用…表示中间一大段烦人的输出。
还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39# mydict.py
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__=='__main__':
import doctest
doctest.testmod()
运行模块python3 mydict.py
,没有任何输出说明doctest运行正确
同步和异步IO的区别就在于是否等待IO执行的结果。等待=同步IO。
本章只介绍同步IO。异步IO复杂度太高暂略过。
Python内置了读写文件的函数,用法和C是兼容的。
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
1 | # 打开文件,r表示read,w为write,a为append,b为binary |
捕捉异常:1
2
3
4
5
6
7
8
9
10try:
f = open('/path/to/file', 'r')
print(f.read())
finally:
if f:
f.close()
# 等效的简单写法,不必调用close()
with open('/path/to/file', 'r') as f:
print(f.read())
有个read()方法的对象,在Python中统称为file-like Object
StringIO:在内存中读写str1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24from io import StringIO
f = StringIO()
# 写入
f.write('hello')
f.write(' ')
f.write('world!')
# 获得写入后的str
f.getvalue() # hello world!
####----------------------------------
from io import StringIO
# 用一个str初始化StringIO
f = StringIO('Hello!\nHi!\nGoodbye!')
# 读取
while True:
s = f.readline()
if s == '':
break
print(s.strip())
BytesIO:在内存中读写bytes1
2
3
4
5
6
7
8
9from io import BytesIO
f = BytesIO()
f.write('中文'.encode('utf-8')) # 写入UTF-8编码的bytes
f.getvalue()
# 用一个bytes初始化BytesIO
f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
# 读取
f.read()
os模块,代表 操作系统(operating system),包含非常多的函数用于获取(和修改)本地目录、文件进程、环境变量等的信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68import os
# 操作系统的类型
os.name
'''
posix ====> Linux、Unix或Mac OS X
nt ====> Windows系统
'''
# 环境变量
os.environ
'''
environ({'PSMODULEPATH': 'C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\', 'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files'.....
'''
# 获取某个环境变量的值
os.environ.get('PATH')
os.environ.get('x', 'default')
s = os.getenv('PATH')
#################################################
# -------------------目录操作-------------------#
#################################################
# 获取当前工作目录 get current working directory
os.getcwd()
# 改变当前目录 change directory, 可以用相对路径
os.chdir('/Users/pilgrim/diveintopython3/examples')
# 查看当前工作目录的绝对路径:
os.path.abspath('.')
# 拼接路径,此方法可以正确处理不同操作系统的路径分隔符
# 合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作
os.path.join('F:\\PythonWorkspace', 'NewDir')
# expanduser函数用来将包含~符号(表示当前用户Home目录)的路径扩展为完整的路径
os.path.expanduser("~/.pythonrc")
# eg:
os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')
# 创建新目录
os.mkdir('F:\\PythonWorkspace\\NewDir')
# 删除目录:
os.rmdir('F:\\PythonWorkspace\\NewDir')
# 拆分路径,此方法可以正确处理不同操作系统的路径分隔符
os.path.split('F:\\PythonWorkspace\\file.txt') # ('F:\\PythonWorkspace', 'file.txt')
# 获取文件扩展名
os.path.splitext('F:\\PythonWorkspace\\file.txt') # ('F:\\PythonWorkspace\\file', '.txt')
# 重命名文件:
os.rename('test.txt', 'test.py')
# 删掉文件:
os.remove('test.py')
# os模块中不存在复制文件的函数,原因是复制文件并非由操作系统提供的系统调用。可以使用shutil模块的copyfile()函数
# 列出当前工作目录下的所有目录
# listdir()只返回文件名,不包含完整路径
# 不在当前目录下时,isdir必须加上路径,如果只传入文件夹名字,它会在当前工作目录下搜索是否存在此文件夹
[x for x in os.listdir('.') if os.path.isdir(x)]
# 列出所有的.py文件
[x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']
元信息: 创建时间,最后修改时间,文件大小等等1
2
3
4
5
6
7
8
9
10
11import os
import time
# os.stat() 函数返回一个包含多种文件元信息的对象
metadata = os.stat('feed.xml')
# 最后修改时间,从纪元(1970年1月1号的第一秒钟)到现在的秒数
metadata.st_mtime
# time.localtime() 函数将从纪元到现在的秒数转换成包含年、月、日、小时、分钟、秒的结构体。
time.localtime(metadata.st_mtime)
glob 模块是Python标准库中的另一个工具,它可以通过编程的方法获得一个目录的内容,并且它使用熟悉的命令行下的通配符。
1 | import glob |
1 | import pickle |
JSON标准规定JSON编码是UTF-8
JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
---|---|
{} | dict |
[] | list |
“string” | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import json
d = dict(name='Bob', age=20, score=88)
# 序列化
temp = json.dumps(d)
# 和pickle类似,也可以直接把对象序列化后写入一个file-like Object
f = open('dump.txt', 'wb')
json.dump(d, f)
f.close()
# 反序列化
json.loads(temp)
# 也可以直接从一个file-like Object中直接反序列化出对象
f = open('dump.txt', 'rb')
d = json.load(f)
f.close()
除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数:
https://docs.python.org/3/library/json.html#json.dumps
可选参数default用于把任意一个对象变成一个可序列为JSON的对象。
Json序列化类实例的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import json
# 类
class Student(object):
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
s = Student('Bob', 20, 88)
# 用于序列化Student类的方法
def student2dict(std):
return {
'name': std.name,
'age': std.age,
'score': std.score
}
# 序列化
json_str = json.dumps(s, default=student2dict)
# 用于反序列化Student类的方法
def dict2student(d):
return Student(d['name'], d['age'], d['score'])
# 反序列化
json.loads(json_str, object_hook=dict2student)
# 通用的序列化方法
# 通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slots__的class。
json.dumps(s, default=lambda obj: obj.__dict__)
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
*nix系统的创建多进程方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
'''
运行结果如下:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
'''
Windows中可以用跨平台的multiprocessing
多进程模块
1 | from multiprocessing import Process |
批量创建子进程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('运行任务 %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('任务 %s 运行了 %0.2f 秒。' % (name, (end - start)))
if __name__=='__main__':
print('父进程 %s' % os.getpid())
# 创建4个子进程,编号从0开始
p = Pool(4)
# 执行5个任务
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('等待所有子进程运行结束')
# 调用join()之前必须先调用close()
# close()之后就不能继续添加新的Process了
p.close()
# 等待所有子进程执行完毕
p.join()
print('所有进程运行完毕')
'''
用Sublime Text和Python解释器执行都没有得到正确的结果
命令行执行结果如下:
父进程 2372
等待所有子进程运行结束
运行任务 0 (5268)...
运行任务 1 (5544)...
运行任务 2 (6008)...
运行任务 3 (3732)...
任务 2 运行了 0.19 秒。
运行任务 4 (6008)...
任务 0 运行了 0.38 秒。
任务 1 运行了 1.31 秒。
任务 3 运行了 1.84 秒。
任务 4 运行了 5.75 秒。
所有进程运行完毕
'''
# 由于线程池中只有4个线程,第5个任务(即任务4)必须等其他任务执行结束,才能被空闲的进程执行
相对父进程来说,子进程是一个外部进程。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
eg:Python代码中运行命令,这和命令行直接运行的效果是一样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import subprocess
# 仅运行命令
# 这一条命令能直接在SublimeText控制台输出结果,不知道为什么
# ping
r = subprocess.call(['ping', 'www.python.org'])
# 调用 cmd 执行后面的命令,输出path环境变量
r = subprocess.call(['cmd', '/c', 'echo', '%path%'])
print('Exit code:', r)
# 向子进程输入数据
# 相当于运行命令:
# adb shell
# adb help
# exit
p = subprocess.Popen(['adb shell'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'adb help\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
1 | from multiprocessing import Process, Queue |
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:
_thread
低级模块threading
高级模块,对_thread进行了封装。启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import time, threading
# 新线程执行的代码:
def loop():
# current_thread()返回当前线程的实例
print('thread %s is running...' % threading.current_thread().name)
# ... 做一些事
print('thread %s ended.' % threading.current_thread().name)
# name参数指定子线程的名字
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
# 输出CPU核心数量
print(multiprocessing.cpu_count())
主线程实例的名字叫MainThread,子线程的名字在创建时指定。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
1 | # 创建锁 |
Python解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
1 | import threading |
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
多任务,通常用Master-Worker模式,Master负责分配任务,Worker负责执行任务。
多进程模式最大的优点就是稳定性高,缺点是创建进程的代价大(特别是Windows下),操作系统能同时运行的进程数也是有限的。
多线程模式通常比多进程快一点,但是也快不到哪去,而且任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
在Windows下,多线程的效率比多进程要高。
操作系统在切换进程或者线程时,需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),再把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
服务进程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import random, time, queue
from multiprocessing import freeze_support
from multiprocessing.managers import BaseManager
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
def return_task_queue():
global task_queue
return task_queue
def return_result_queue():
global result_queue
return result_queue
def test():
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
# QueueManager.register('get_task_queue', callable=lambda: task_queue)
# QueueManager.register('get_result_queue', callable=lambda: result_queue)
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
if __name__ == '__main__':
freeze_support()
test()
注意,在分布式多进程环境下,添加任务到Queue必须通过manager.get_task_queue()获得的Queue接口添加。
任务进程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import time, sys, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass
# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect() # 服务端用的是start()方法
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
try:
n = task.get(timeout=1)
print('run task %d * %d...' % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except Queue.Empty:
print('task queue is empty.')
# 处理结束:
print('worker exit.')
正则表达式知识见笔者的另一篇笔记:https://www.zybuluo.com/king/note/43674
正则匹配默认是贪婪匹配。
Python提供re模块,包含所有正则表达式的功能。
1 | import re |
datetime是Python处理日期和时间的标准库。
日期和时间部分的格式见:https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46# datetime是模块,datetime模块还包含一个datetime类
from datetime import datetime
# 获取当前日期和时间
now = datetime.now() # 015-10-18 16:59:01.015529
# 用指定日期时间创建datetime
dt = datetime(2015, 4, 19, 12, 20)
# 1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp,时间戳
# 全球各地的计算机在任意时刻的timestamp都是完全相同的
# datetime转换为timestamp
dt.timestamp() # 1429417200.0, 小数位表示毫秒数
# timestamp转换为datetime,本地时间
datetime.fromtimestamp(t)
# timestamp转换为datetime,UTC标准时区
datetime.utcfromtimestamp(t)
# str转换为datetime,转换后无时区信息
cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')
# datetime转换为str
dt.strftime('%a, %b %d %H:%M'))
# datetime加减,需要导入timedelta类
from datetime import datetime, timedelta
dt + timedelta(days=2, hours=12)
# 本地时间转换为UTC时间
# 一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区
from datetime import datetime, timedelta, timezone
# 创建时区UTC+8:00
tz_utc_8 = timezone(timedelta(hours=8))
now = datetime.now()
# 强制设置为UTC+8:00
dt = now.replace(tzinfo=tz_utc_8)
print(dt) # 2015-10-18 17:13:21.891555+08:00
# 时区转换
# 拿到UTC时间,并强制设置时区为UTC+0:00:
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
# astimezone()将转换时区为北京时间:
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
# astimezone()将转换时区为东京时间:
tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
# astimezone()将bj_dt转换时区为东京时间:
tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。1
2
3
4
5
6
7
8
9
10
11from collections import namedtuple
# 定义一个点坐标
# namedtuple('名称', [属性list]):
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# 引用元素
p.x
# 定义一个圆
Circle = namedtuple('Circle', ['x', 'y', 'r'])
list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。
1 | from collections import deque |
使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict
1 | from collections import defaultdict |
使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。
如果要保持Key的顺序,可以用OrderedDict:
1 | from collections import OrderedDict |
OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from collections import OrderedDict
class LastUpdatedOrderedDict(OrderedDict):
def __init__(self, capacity):
super(LastUpdatedOrderedDict, self).__init__()
self._capacity = capacity
def __setitem__(self, key, value):
containsKey = 1 if key in self else 0
if len(self) - containsKey >= self._capacity:
last = self.popitem(last=False)
print('remove:', last)
if containsKey:
del self[key]
print('set:', (key, value))
else:
print('add:', (key, value))
OrderedDict.__setitem__(self, key, value)
Counter是一个简单的计数器
Counter实际上也是dict的一个子类1
2
3
4
5
6
7
8
9from collections import Counter
# 统计字符出现的个数
c = Counter()
for ch in 'programming':
c[ch] = c[ch] + 1
print(c)
# Counter({'g': 2, 'm': 2, 'r': 2, 'n': 1, 'i': 1, 'a': 1, 'p': 1, 'o': 1})
Base64是一种用64个字符来表示任意二进制数据的方法。
Base64的原理很简单,首先,准备一个包含64个字符的数组:['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit,可以表示0 ~ 2^6-1,一共64个int,对应上方的数组。查表获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据。如果要编码的二进制数据不是3的倍数,Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。
1 | import base64 |
struct模块用于解决bytes和其他二进制数据类型的转换。
struct模块定义的数据类型可以参考Python官方文档:https://docs.python.org/3/library/struct.html#format-characters1
2
3
4
5
6
7
8
9import struct
# 把任意数据类型变成bytes
# pack的第一个参数是处理指令,>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。
# 后面的参数个数要和处理指令一致
struct.pack('>I', 10240099) # b'\x00\x9c@c'
# 把bytes变成相应的数据类型
# 根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数
struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80') # (4042322160, 32896)
Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。
首先找一个bmp文件,没有的话用“画图”画一个。读入前30个字节来分析:s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'
BMP格式采用小端方式存储数据,文件头的结构按顺序如下:
两个字节:’BM’表示Windows位图,’BA’表示OS/2位图;
一个4字节整数:表示位图大小;
一个4字节整数:保留位,始终为0;
一个4字节整数:实际图像的偏移量;
一个4字节整数:Header的字节数;
一个4字节整数:图像宽度;
一个4字节整数:图像高度;
一个2字节整数:始终为1;
一个2字节整数:颜色数。
组合起来用unpack读取:struct.unpack('<ccIIIIIIHH', s)
结果为:(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)
Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。
摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。目的是为了发现原始数据是否被人篡改过。
摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,并非不可能出现,但是非常非常困难。
1 | import hashlib |
摘要算法应用如:存储用户口令的摘要。
为防止简单密码被黑客用MD5反推,对原始口令加一个复杂字符串再计算MD5,俗称“加盐”。经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。
要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
1 | import itertools |
操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
当SAX解析器读到一个节点时:<a href="/">python</a>
会产生3个事件:
start_element事件,在读取<a href="/">
时;
char_data事件,在读取python
时;
end_element事件,在读取</a>
时。
1 | from xml.parsers.expat import ParserCreate # 必须实现这三个方法 class DefaultSaxHandler(object): def start_element(self, name, attrs): print('sax:start_element: %s, attrs: %s' % (name, str(attrs))) def end_element(self, name): print('sax:end_element: %s' % name) def char_data(self, text): print('sax:char_data: %s' % text) xml = r'''<?xml version="1.0"?> <ol> <li><a href="/python">Python</a></li> <li><a href="/ruby">Ruby</a></li> </ol> ''' handler = DefaultSaxHandler() parser = ParserCreate() parser.StartElementHandler = handler.start_element parser.EndElementHandler = handler.end_element parser.CharacterDataHandler = handler.char_data parser.Parse(xml) |
需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面再合并。
生成XML:最简单也是最有效的生成XML的方法是拼接字符串。复杂的XML呢?建议你不要用XML,改成JSON。
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33from html.parser import HTMLParser
from html.entities import name2codepoint
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
print('<%s>' % tag)
def handle_endtag(self, tag):
print('</%s>' % tag)
def handle_startendtag(self, tag, attrs):
print('<%s/>' % tag)
def handle_data(self, data):
print(data)
def handle_comment(self, data):
print('<!--', data, '-->')
def handle_entityref(self, name):
print('&%s;' % name)
def handle_charref(self, name):
print('&#%s;' % name)
parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
<p>Some <a href=\"#\">html</a> HTML tutorial...<br>END</p>
</body></html>''')
urllib的request模块可以发送一个GET请求到指定的页面,然后返回HTTP的响应:1
2
3
4
5
6
7
8from urllib import request
with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
data = f.read()
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', data.decode('utf-8'))
如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:1
2
3
4
5
6
7
8
9from urllib import request
req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))
如果要以POST发送一个请求,只需要把参数data以bytes形式传入。
我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from urllib import request, parse
print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
('username', email),
('password', passwd),
('entry', 'mweibo'),
('client_id', ''),
('savestate', '1'),
('ec', ''),
('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])
req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')
with request.urlopen(req, data=login_data.encode('utf-8')) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))
如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:1
2
3
4
5
6proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass
PIL:Python Imaging Library, 用于进行图像处理
由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55from PIL import Image
# ------------------缩小图像--------------------
# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('test.jpg')
# 获得图像尺寸:
w, h = im.size
# 缩放到50%:
im.thumbnail((w//2, h//2))
# 把缩放后的图像用jpeg格式保存:
im.save('thumbnail.jpg', 'jpeg')
# -----------------模糊效果----------------------------------
# 应用模糊滤镜:
im2 = im.filter(ImageFilter.BLUR)
# ------------------生成字母验证码图片--------------------
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
# 随机字母:
def rndChar():
return chr(random.randint(65, 90))
# 随机颜色1:
def rndColor():
return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))
# 随机颜色2:
def rndColor2():
return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))
# 240 x 60:
width = 60 * 4
height = 60
image = Image.new('RGB', (width, height), (255, 255, 255))
# 创建Font对象:
font = ImageFont.truetype('Arial.ttf', 36)
# 创建Draw对象:
draw = ImageDraw.Draw(image)
# 填充每个像素:
for x in range(width):
for y in range(height):
draw.point((x, y), fill=rndColor())
# 输出文字:
for t in range(4):
draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())
# 模糊:
image = image.filter(ImageFilter.BLUR)
image.save('code.jpg', 'jpeg')
virtualenv就是用来为一个应用创建一套“隔离”的Python运行环境。需要用pip安装。
命令virtualenv就可以创建一个独立的Python运行环境,我们还加上了参数–no-site-packages,这样,已经安装到系统Python环境中的所有第三方包都不会复制过来。
此处教程写的是Mac,故笔记略过
相关第三方库: Tk、wxWidgets、Qt、GTK
Python自带的tkinter封装了访问Tk的接口。
Tk支持多个操作系统,使用Tcl语言开发。Tk会调用操作系统的GUI接口。
1 | from tkinter import * |
互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。
IP协议负责把数据从一台计算机通过网络发送到另一台计算。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。IPv6地址实际上是一个128位整数。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示打开了一个网络链接,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
访问新浪:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# 导入socket库:
import socket
# 创建一个socket:
# AF_INET:IPv4 AF_INET6:IPv6
# SOCK_STREAM:面向流的TCP协议
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接。注意参数是tuple
s.connect(('www.sina.com.cn', 80))
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
# 接收数据:
buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
# 关闭连接:
s.close()
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open(r'F:\sina.html', 'wb') as f:
f.write(html)
标准端口
Web:80
SMTP:25
FTP:21
端口号小于1024的是Internet标准服务的端口
端口号大于1024的,可以任意使用。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
服务器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 监听端口。小于1024的端口号必须要有管理员权限才能绑定
s.bind(('127.0.0.1', 9999))
# 调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:
s.listen(5)
print('Waiting for connection...')
# 服务器程序通过一个永久循环来接受来自客户端的连接
while True:
# 接受一个新连接:
sock, addr = s.accept()
# 创建新线程来处理TCP连接:
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)
客户端程序:1
2
3
4
5
6
7
8
9
10
11s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()
同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
TCP建立可靠连接
UDP不需要建立连接,不保证到达
服务器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import socket
# SOCK_DGRAM指定了这个Socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))
# 相比TCP,不需要调用listen()方法,直接接收来自任何客户端的数据
# 注意这里省掉了多线程
while True:
# 接收数据:
# recvfrom()方法返回数据和客户端的地址与端口
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)
客户端:1
2
3
4
5
6
7s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
s.close()
SQLite是一种嵌入式数据库,它的数据库就是一个文件。C语言编写,体积很小。不能承受高并发访问。
操作关系数据库:
1 | # 导入SQLite驱动: |
查询上述数据库:1
2
3
4
5
6
7
8
9
10
11conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 执行查询语句:
cursor.execute('select * from user where id=?', '1')
# 获得查询结果集:
values = cursor.fetchall() # [('1', 'Michael')]
cursor.close()
conn.close()
打开后一定记得关闭。建议用try catch finally
使用Cursor对象执行insert,update,delete语句时,执行结果由rowcount返回影响的行数。
使用Cursor对象执行select语句时,通过featchall()可以拿到结果集。结果集是一个list,每个元素都是一个tuple,对应一行记录。
MySQL内部有多种数据库引擎,最常用的引擎是支持数据库事务的InnoDB。
安装MySQL:http://dev.mysql.com/downloads/mysql/5.6.html
安装时请选择UTF-8编码。
安装MySQL驱动:pip install mysql-connector-python --allow-external mysql-connector-python
数据库表是一个二维表,包含多行多列。一个list表示多行,list的每一个元素是tuple,表示一行记录。
Python的DB-API返回的数据结构这样表示:1
2
3
4
5[
('1', 'Michael'),
('2', 'Bob'),
('3', 'Adam')
]
用tuple表示一行很难看出表的结构。如果把一个tuple用class实例来表示,就可以更容易地看出表的结构来:1
2
3
4
5
6
7
8
9
10class User(object):
def __init__(self, id, name):
self.id = id
self.name = name
[
User('1', 'Michael'),
User('2', 'Bob'),
User('3', 'Adam')
]
这就是传说中的ORM技术:Object-Relational Mapping,把关系数据库的表结构映射到对象上。
ORM框架用于做这个转换。Python中,最有名的ORM框架是SQLAlchemy
响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误
HTTP GET请求的格式:1
2
3
4GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
每个Header一行一个,换行符是\r\n。
HTTP POST请求的格式:1
2
3
4
5
6POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。
HTTP响应的格式:1
2
3
4
5
6200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding时,Body数据是被压缩的
详细了解HTTP协议《HTTP权威指南》。
HTML定义了一套语法规则,告诉浏览器如何把一个丰富多彩的页面显示出来。
CSS是Cascading Style Sheets(层叠样式表)的简称,CSS用来控制HTML里的所有元素如何展现。
给标题元素<h1>
加一个样式,变成48号字体,灰色,带阴影:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
JavaScript是为了让HTML具有交互性而作为脚本语言添加的,JavaScript既可以内嵌到HTML中,也可以从外部链接到HTML中。
当用户点击标题时把标题变成红色,就必须通过JavaScript来实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
<script>
function change() {
document.getElementsByTagName('h1')[0].style.color = '#ff0000';
}
</script>
</head>
<body>
<h1 onclick="change()">Hello, world!</h1>
</body>
</html>
一个Web应用的本质就是:
WSGI:Web Server Gateway Interface,它只要求Web开发者实现一个函数,就可以响应HTTP请求。1
2
3
4
5
6
7
8
9# environ:一个包含所有HTTP请求信息的dict对象;
# start_response:一个发送HTTP响应的函数。
def application(environ, start_response):
# 发送HTTP响应的Header,只能发送一次
# 参数一是HTTP响应码
# 参数二是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示
start_response('200 OK', [('Content-Type', 'text/html')])
# 发送HTTP响应的Body
return [b'<h1>Hello, web!</h1>']
application()函数由WSGI服务器来调用
Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。1
2
3
4
5
6
7
8
9
10
11
12
13# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]
# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
# 开始监听HTTP请求:
httpd.serve_forever()
启动成功后,打开浏览器,输入http://localhost:8000/,就可以看到结果
一个Web App,就是写一个WSGI的处理函数,针对每个HTTP请求进行响应。
在WSGI接口之上能进一步抽象,让我们专注于用一个函数处理一个URL,至于URL到函数的映射,就交给Web框架来做。
写一个app.py,处理3个URL,分别是:
Flask通过Python的装饰器在内部自动地把URL和函数给关联起来。
1 | from flask import Flask |
除了Flask,常见的Python Web框架还有:
使用模板,我们需要预先准备一个HTML文档,这个HTML文档不是普通的HTML,而是嵌入了一些变量和指令,然后,根据我们传入的数据,替换后,得到最终的HTML,发送给用户
MVC
Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('home.html')
@app.route('/signin', methods=['GET'])
def signin_form():
return render_template('form.html')
@app.route('/signin', methods=['POST'])
def signin():
username = request.form['username']
password = request.form['password']
if username=='admin' and password=='password':
return render_template('signin-ok.html', username=username)
return render_template('form.html', message='Bad username or password', username=username)
if __name__ == '__main__':
app.run()
编写jinja2模板:
home.html
用来显示首页的模板:1
2
3
4
5
6
7
8<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
</html>
form.html
用来显示登录表单的模板:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<html>
<head>
<title>Please Sign In</title>
</head>
<body>
{% if message %}
<p style="color:red">{{ message }}</p>
{% endif %}
<form action="/signin" method="post">
<legend>Please sign in:</legend>
<p><input name="username" placeholder="Username" value="{{ username }}"></p>
<p><input name="password" placeholder="Password" type="password"></p>
<p><button type="submit">Sign In</button></p>
</form>
</body>
</html>
signin-ok.html
登录成功的模板:1
2
3
4
5
6
7
8<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
</html>
一定要把模板放到正确的templates目录下,templates和app.py在同级目录下
Jinja2模板中,我们用表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用{% ... %}
表示指令。
比如循环输出页码:1
2
3{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}
除了Jinja2,常见的模板还有:
{% ... %}
和的模板。同步IO:等待IO操作完成,才能继续进行下一步操作。
异步IO:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:1
2
3
4loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)
协程,又称微线程,纤程。英文名Coroutine。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
和多线程比,最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程
Python对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。
协程实现的生产者-消费者模型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 一个generator
def consumer():
r = ''
while True:
# 当produce调用send语句时,这里的yield仅用来接收参数交赋值给n, consumer不会产生中断
# 当comsumer循环一圈后再执行到这里,此时produce还没有调用send,comsumer会中断执行
n = yield r # 拿到消息n,下面处理后再通过yield传回结果
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None) # 启动生成器,不会调用yield。参数不传None会报错,
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 切换到consumer执行。拿到结果后继续
print('[PRODUCER] Consumer return: %s' % r)
c.close() # 关闭conumer
c = consumer()
produce(c)
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
1 | import asyncio |
把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
用Task封装两个coroutine1
2
3
4
5
6
7
8
9
10
11
12
13import threading
import asyncio
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出:1
2
3
4
5Hello world! (<_MainThread(MainThread, started 6132)>)
Hello world! (<_MainThread(MainThread, started 6132)>)
(----此处中断一秒----)
Hello again! (<_MainThread(MainThread, started 6132)>)
Hello again! (<_MainThread(MainThread, started 6132)>)
由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。
如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。
用asyncio的异步网络连接来获取sina、sohu和163的网站首页:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
# 这一句只是创建asyncio协程,yield from connect 才是执行协程
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
可见3个连接由一个线程通过coroutine并发完成。
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:
原代码:1
2
3
4
5@asyncio.coroutine
def hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")
用新语法重新编写如下:1
2
3
4async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")
把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。
编写一个HTTP服务器,分别处理以下URL:
/
- 首页返回b'<h1>Index</h1>'
;
/hello/{name}
- 根据URL参数返回文本hello, %s!
。
1 | import asyncio |
下载安装后,打开软件即可使用,无须设置。每人有800M的免费流量,用完限速,也可购买。
直接把我CSDN博客上发过的图片扒过来:
不用的时候退出软件即可。
获取最新Hosts:racaljk/hosts
直接全选、复制、粘贴到文本文件,保存并重命名为hosts
,无后缀名。然后把该文件剪切到C:\WINDOWS\system32\drivers\etc
文件夹下。
也可以使用该项目作者提供的工具自动更新hosts:Host Tools
用于测试翻墙效果
]]>ssh:connect to host github.com port 22:bad file number
将端口修改为 443
1.打开~\.ssh\config
文件(~为你的系统当前用户文件夹),如果没有就新建一个,内容为
Host github.com
Hostname ssh.github.com
Port 443
2.关闭并重启终端。再次push即可看到成功提示
]]>ssh:connect to host github.com port 22:bad
道教的六天说
王宗昱
“六天”这个概念在早期道教的经典里是经常见到的,道教对它的批判也是显而易见的。然而,中国学术界对这个概念及其在道教史上的演化一直没有作出深入的讨论[1]。“六天”这个本出于儒家的概念何以在道教中这么重要,反而鲜见于儒家经典,道教对它的批判传达给了我们什么信息?
一、六天的来历
在正史和儒家的经典里,我目前见到的最早的言及“六天”一词的材料是《礼记》孔颖达疏和《旧唐书》[2]。《礼记·郊特牲》孔疏中说:
先儒说郊,其义有二。案《圣证论》以天体无二,郊即圜丘,圜丘即郊。郑氏以为天有六天,丘郊各异。今具载郑义兼以王氏难。郑氏谓天有六天,天为至极之尊,其体只应是一。而郑氏以为六者,指其尊极清虚之体其实是一,论其五行生育之功则别有五。以五配一,故为六天。[3]
《旧唐书·礼仪志》中所录贞观二年礼部尚书许敬宗的奏文中有云:
二年七月,礼部尚书许敬宗与礼官等又奏议:据祠令及新礼,并用郑玄六天之议,圆丘祀昊天上帝,南郊祭太微感帝,明堂祭太微五帝。谨按郑玄此义,唯据纬书,所说六天,皆谓星象,而昊天上帝,不属穹苍。故注《月令》及《周官》,皆谓圆丘所祭昊天上帝为北辰耀魄宝。考其所说,舛谬特深。按《周易》云:日月丽于天,百谷草木丽于地。又云:在天成象,在地成形。足明辰象非天,草木非地。《毛诗传》云:元气昊大,则称昊天。此则苍天为体,不入星辰之例。且天地各一,是曰两仪。天尚无二,焉得有六。[4]
由上面的材料可知,六天指的是以郑玄为代表的儒家对官方祭礼的解释。并且,上文所言六天均未明言该辞出于郑玄,止是王肃等人对郑玄主张所用的一种代称。虽然现存《孝经》、《礼记》和《周官》等书的郑玄注文中均未有“六天”字样,但循其文义并质之史书,可知“六天”所蕴涵的内容确然为郑玄之主张,信乎不诬。
古代皇帝对天神即“禘”的祭祀由来已久。上天是帝王的祖先,也是该氏族的祖先。郑玄注《礼记·大传》时解释了“禘”之义:“大祭其先祖所由生,谓郊祀天也[5]。”这之中当然糅合了该氏族或当时官方的历史观。这个天后来被称为皇天上帝或昊天上帝。五帝同样也是帝王的祖先,应该是这些历史观逐渐积淀的产物。徐旭生认为,直到战国前期,五帝的名称尚未出现,因为在《左传》、《国语》、《论语》、《墨子》《孟子》、中均不见此名称[6]。五帝一辞出现后其具体所指在相当长一段时间内亦无定论。这种指称传说中五个帝王的“五帝”在后代被人叫做“五人帝”。随着五行说的发展,出现了五方帝的观念。五方帝是否为配合五人帝而出现?尚未见证据。据《史记·封禅书》记载:“二年,东击项籍而还入关,问:故秦时上帝祠何帝也?对曰:四帝,有白、青、黄、赤帝之祠。高祖曰:吾闻天有五帝,而有四,何也?莫知其说。于是高祖曰:吾知之矣,乃待我而具五也。乃立黑帝祠,命曰北畤。有司进祠,上不亲往。悉招故秦祝官,复置太祝、太宰,如其故仪礼。[7]”这条材料既说明秦时的五方帝崇拜未能完备,也说明汉高祖所言之五方帝或许未与五人帝相对应。五方帝之出现自有渊源。即使秦人之祭白帝也并非出于五方观念。徐旭生之分析可资参考[8]。然而,刘邦以秦人祀四帝为怪而齐之以五帝当然是受了五方观念的支配。
中国古代皇家的祭祀制度是以秦汉儒生的规整为基础的。后世儒家便以此为据设定并“监督”皇家祭祀。但是,秦汉时的实际施行却不甚严整。以司马迁《封禅书》视之,可见一斑。他追述了舜以来的祭礼,并概括周礼:“周公既相成王,郊祀后稷以配天,宗祀文王于明堂以配上帝。自禹兴而修社祀,后稷稼穑,古有稷祠,郊社所从来尚矣。[9]”马端临曾就汉代皇家祭祀之经权史实以及司马迁之用意有一番评论:“西汉之所谓郊祀,盖袭秦之制而杂以方士之说。曰泰一,曰五帝,丛杂而祀之,皆谓之郊天。太史公作《封禅书》,所序者秦汉间不经之祠,而必以舜类上帝,三代郊祀之礼先之。[10]”。司马迁作为史家或者并非意在为后世建立典范,而后世以典范视之。由《封禅书》可知,汉武帝时开始有了对太一的祭祀。这个太一就是天神,而五方神则是它的辅佐[11]。汉武帝建的太一坛下环设五帝坛,“各如其方[12]”。“六天”的结构此时已经形成。
这六个祭祀对象都是被看作天神的。后来,它们才被加上纬书里的那些名称。所以马端临又说:“至班孟坚则直名其书曰《郊祀志》。盖汉世以三代之所以郊祀者祀泰一、五帝,于是以天为有六,以祀六帝为郊,自迁、固以来议论相袭而然矣。康成注二礼,凡祀天处必指以为所祀者为某帝。其所谓天者非一帝,故其所谓配天者亦非一祖,于是释帝郊祖宗以为或祀一帝,或祀五帝,各配以一祖。其病盖在于取谶纬之书解经,以秦汉之事为三代之事。然六天之祀,汉人崇之;六天之说,迁、固志之;则其谬亦非始于康成也。”。郑玄注《礼记·大传》时说:“王者之先祖皆感太微五帝之精以生。苍则灵威仰,赤则赤熛怒,黄则含枢纽,白则白招拒,黑则汁光纪。皆用正岁之正月郊祭之。盖特尊焉。《孝经》曰:郊祀后稷以配天,配灵威仰也;宗祀文王于明堂以配上帝,泛配五帝也。”[13]。皮锡瑞在《孝经郑注疏》中勾勒了秦汉时感生帝观念的概貌,可知感生帝虽与五方帝有关,却亦各有其义[14]。感天而生人的观念在关于姜嫄的传说中已然可见。五行说出现后,所感者则为五行之精,各个朝代帝王的祖先便是感于五行之德,而五帝的观念本来用于氏族祖先起源的解释,由此而有感生帝。郑玄用来解释上帝的涵义也是顺理成章的事。他很明了儒家祭天的真意所。他在注《孝经》“严父莫大于配天”时说:“尊严其父,莫大于配天。生事敬爱,死为神主也。[15]”因此,他把作为感生帝的五方帝与五人帝相配。在他对《周礼·小宗伯》的注文中就有:“五帝:苍曰灵威仰,太昊食焉;赤曰赤熛怒,炎帝食焉;黄曰含枢纽,黄帝食焉;白曰白招拒,少昊食焉;黑曰汁光纪,颛顼食焉。”[16]
我们可以看出,所谓儒家的六天说是郑玄根据以纬书为代表的对天神的说法就官方祭礼作的一种解释。这个解释试图对此前官方的历史观的积累作一个编排,兼容并包,各有位序。他的解释是秦王焚书以后儒生事业的总结。王肃治经学专攻郑玄。“六天”本是他讽刺郑玄的用语。他以国戚之尊使皇家罢废了五天帝的祭祀,但是郑玄的六天说在儒生中仍然颇有势力,使得此后几百年间皇家祭祀在郑学与王学之间依违不定。六天说也多为此期间儒生用来指称郑玄所主祭礼。然而自玄学之后汉学萧条千余年,程朱更以义理之学释天,不言六天,以致六天之说湮没不闻。至乾嘉之时汉学复兴,逐渐有人重理旧业,儒生方才知晓古代六天说理的真相。杭世骏《续礼记集说》汇辑了汉学宋学各家对六天的评说,可资参考。
二、道教对“六天”的批判
由前面的介绍我们可以明白了:所谓六天指的是至少自周代以来的血食牺牲的祭祀制度,而这个制度恰恰是中国官方政治制度乃至意识形态的基础。儒家的一整套礼教就是在这个基础上发展起来的。祭祀的对象则反映了至汉代逐渐形成的官方的历史观。这个历史观也就是古史辨派所指的层累地造成的中国古代史的主干。
道教所说的“六天”的第一个涵义从儒家本义来,但是道教通过对它的批判又给它加入了新的涵义。在早期道教经典里最能代表这种批判的是成书于刘宋时期的《三天内解经》上卷。它在批判六天的同时表达了道教自己的历史观和宗教仪式特征。
《三天内解经》的主要线索是叙述道教的历史观即老君传道设教的历史:老子出于空洞虚无之中,降生之后,布散玄元始三气,造设天地人物,并在伏羲女娲之时传授了无为大道、佛道和清约大道。此后,老君虽然千变万化重现人间,但是在汉代末年以前他一直是和世俗的官方政治相协调的。这个俗世的政治就叫做“六天”。《三天内解经》说,当老君传授三道时,“六天治兴,三道教行。老子帝帝出为国师。伏羲时号为郁华子,祝融时号为广寿子,……,一日九变或二十四变,千变万化,随世沉浮,不可胜载。……至周幽王时,老子知周祚当衰,被发佯狂,辞周而去。[17]”由此可见,老子与六天的分歧产生在周代。经文中指摘的“下古僭薄,妖恶转兴,酌祭巫鬼,真伪不分”正是指周以来的历史。老子辞周而去以后所传的《道德经》、《老子中经》以及《太平经》等都是与六天之治相对立的。但是,老君并没有因此立即与六天之治决裂。他试图使六天回复正道,派于吉、李微、王方平、东方朔等人,“助六天检正邪气”。因为屡教无效,所以终于“以汉安元年壬午岁五月一日,老君于蜀郡渠亭山石室中与道士张道陵将诣昆仑大治新出太上。太上谓世人不畏真正而畏邪鬼,因自号为新出老君,即拜张为太玄都正一平气三天之师,付张正一盟威之道,新出老君之制,罢废六天三道时事,平正三天,洗除浮华,纳朴归真,承受太上真经制科律”。《三天内解经》叙述的历史试图对道教的产生给一个逻辑的解释。老君所以要和六天决裂是因为六天世界中的“人鬼交错”并在汉代发展到极端。经云:“至汉世群邪滋盛,六天气勃,三道交错,疠气纵横,医巫滋彰。皆弃真从伪,弦歌鼓舞,烹杀六畜,酌祭邪鬼。”这末尾两句正是道教批判六天的关键所在。是否用血食牺牲祭祀死人是道教与“六天”的根本分歧。《三天内解经》说,老君传授正一盟威之道的同时确立了“三天正法”,它的内容是:“民不妄淫祀他鬼神,使神不饮食,师不受钱。不得淫盗治病疗疾,不得饮酒食肉。民人唯听五腊吉日祠家亲祖父母,二月八月祭社灶,自非三天正法”。这个教义被道教称为清约。《陆先生道门科略》里明确说:“盟威法师不受钱,神不饮食,谓之清约”。在《太上正一盟威法箓》和《陆先生道门科略》中都明确地讲到这类祭祀方面的规定[18]。在灵宝经系统中也可以看出这种教义。《太上洞玄灵宝智慧罪根上品大戒经》列出的“上品十戒”的第七条即为“不得杀生祠祀六天鬼神”(又见《本行宿缘经》、《太上洞玄灵宝三元玉京玄都大献经》等多种道经)。在《三天内解经》对道教的历史观作了一个较为系统的阐述之后,我们就可以看出道教有关祭祀的规定在它整个教义体系中的地位之重要。并且我们也可以通过它对六天的批判看出这样一种教义同以牺牲玉帛钟鼓为形式的周代以来的儒家礼乐制度是针锋相对的[19]。
实际上,在葛洪的《抱朴子内篇》中已经透露了道教对牺牲祭祀的反对态度。在《明本》篇中揭陈儒道之间的诸般区别时,葛洪曾经明确说道:“儒者祭祀以求福,道者履正以禳邪”[20]。道教要禳除的邪似乎也应包括儒家的祭祀对象,因为他还曾经提到“血食之鬼”也属于“邪气”之列[21]。他在《道意》和《勤求》两篇中对祭祀的害处有所论列,认为“长生之道,不在祭祀事鬼神也”,“夫长生制在大药耳,非祠祷之所得也。[22]”他对秦汉帝王祭祀以求仙的批评很可以说明秦皇汉武的做法并不属于道教的范畴。但是,葛洪的这些讨论并不系统,尤其是他将道教的禁止祭祀和朝廷曾经有的禁淫祀放在一起讨论,更使他的观点不甚明确[23]。由于学术界以往对儒道两家关于祭祀的立场没有做出系统的研究,所以葛洪的这些讨论未足以引起人们的注意。现在我们可以看出葛洪在《道意》篇中关于祭祀的长篇论述是有道教的教义做背景的,虽然这个教义也许是道教教团产生之后才明确的。对照《三天内解经》和《陆先生道门科略》,我们也许会明了《道意》篇所论虽然有些属于葛洪个人的发挥,但他的讨论中表达的主张与其他道教经典是一致的。他对李家道的批评不但给我们提供了《道门科略》之外的新史料,并且他还批评李家道“未纯为清省”[24]。清省之“清”和清约之“清”应属同义。更有甚者,他说到的克制“血食恶神”的道术也和我们后面要谈到的道教对儒家祭祀对象的批判态度有关系[25]。在《对俗》篇中,葛洪曾论及修道不会危及儒家的祭祀制度,但由前引材料可知葛洪的真正立场,而《对俗》篇中的设问内容恰恰说明道徒是不事祭祀的[26]。
道教的反对血食牺牲实际是反对祭祀死人(祖先另当别论)。道教崇拜的神明是气化而生的,并非胎生肉人。《三天内解经》云:“道源本起,出於无先,溟涬鸿濛,无有所因,虚生自然,变化生成。道德丈人者,生於元气之先,是道中之尊,故为道德丈人也。因此而有太清玄元无上三天无极大道太上老君太上丈人天帝君九老仙都君九气丈人等百千万重道气千二百官君太清玉陛下。今世人上章书太清,正谓此诸天真也。”气化而生,故不受血食。儒家的祭祀对象都是已经死去的生人,即使是那些传说中的人物也都被视为生人,想象他们死后仍然有生时的生活,故以血食牺牲祀之。大道清虚,岂有斯事?《三内天内解经》明确地指出这些作为道教徒崇拜对象的“诸天真”的气化性质,并且没有把东方朔这些历史上修道成“真”的“生人”作为道教的神明,这是符合道教的传统的。后来的《太上老君虚无自然本起经》也说:“道无有,故无祭礼,故不杀生。”我们从《三天内解经》中可以看出这种神仙体系的结构是有其宗教教义作基础的。我现在还不知道这种反对祭祀死人的教义在早期道教中究竟有多大程度的普遍性,也不知道《列仙传》中那些人物在各地民间受祀仪式的具体情况,但是,道教的神明乃至人的生命的气化性质在南北方的道教经典里是屡见不鲜的,并且我们也可以在先秦道家的经典里找到渊源,从道教静室中不设偶像的惯例也可知不祭祀生人的宗教习惯在中国古代是有着长期的传统的。如果我们在这方面进一步追讨,可能会看到先秦道家的虚无自然的义理也许本来就是从民间的某种宗教教义中发展出来的哲学。由上可见,儒道两家的历史观反映了宗教制度的对立。我当然不能说儒道两家在不同时期构造的历史观只是为其宗教目的服务的,但以《三天内解经》代表的道教的历史观是有这样的用意的。
陈国符先生在《南北朝天师道考长编》的《宣化》一节中首先就引证了《三天内解经》、《陆先生道门科略》以及其他大量材料用以标明道教的仪式上的特征。这说明陈先生很看重这些材料在道教教义中的重要地位。只是这些材料中除了《三天内解经》和《陆先生道门科略》外,只有取自《老子想尔注》的一条是正面指出道教严禁“祭餟祷祠”,其余大多是批判饮食鬼神的材料。不过,由陈先生汇集的材料我们也可以看出道教的灵宝、上清诸派均反对血食牺牲。需要指出的是,陈先生把这种批判看作是“巫觋旧法,六天之治”与“三天正法”的对立。我认为,虽然《三天内解经》在批判六天时也提到了“医巫滋彰”,但它的六天概念还主要是指向儒家的。
除了陈先生举出的那些材料,我们还可以在其他重要的道经中看到对六天的批判。《大道家令戒》中有一段与《三天内解经》颇为相似的历史叙述:“至于黄帝以来,民多机巧,服牛乘马,货赂为官。稍稍欲薄尽于五帝。夏商周三代,转见世利;秦始五霸,更相克害,有贼死者,万亿不可胜数,皆由不信其道。道乃世世为帝王师,而王者不能尊奉,至倾移颠殒之患,临危济厄,万无一存。道重人命,以周之末世始出,奉道于琅琊,以授于吉。······五霸世衰,赤汉承天,道佐代乱,出黄石之书,以授张良。道亦形变,谁能识真?汉世既定,末世纵横,民人趋利,强弱忿争。道伤民命,一去难还。故使天授气治民,曰新出老君。”《女青鬼律》所列的鬼名也把六天之鬼派在首位,名叫“高天万丈鬼”。其文云:“高天万丈鬼,百鬼中皇姓,系天六方鬼之主。”此外还有“五方直符鬼”,即东方青帝、南方赤帝、西方白帝、北方黑帝、中央黄帝,虽然它们的具体名称并非叫做“灵威仰”之类。与五方直符鬼相对照,可知“高天万丈鬼”或指昊天上帝,称为皇姓显然因为它们为皇家的祭祀对象。《女青鬼律》中所列与六天之治有关的鬼为周代以来皇家的祭祀对象,如五岳鬼、九江鬼、三河鬼、四渎鬼(二卷),皇天上帝土玉鬼(六卷),“血食鬼”则反映了道教反对用牺牲祭祀神的宗教立场。六天的祭祀对象被道教列进了鬼界,不能作为崇拜的对象。在《女青鬼律》第五卷末列出的禁忌戒条中有“不祠祭故气,不指鬼呼神”的规定。这些“故气”是已死的“无头坏军死将”,是已死的生人。说它们是故气是指它们是离开了人身的气。故气的另一个含义就是指那些被皇家祭祀的诸神。“太上”已经罢废了“六天”,即使六天之气曾经与道家相谐,但既已被“罢废”,自然被称为“故气”。在《太上正一咒鬼经》中也能看出道教的这种立场。经中有“但捕血祀诸奸神”的诗句。经中又说到道民欲求家庭平安、百事顺遂,应请神及“将军”来家中,要对付的诸种精怪首先就是“故气血食之鬼”。因此,六天世界祭祀的天神、帝王连同他们的功臣将相都属于为非作歹的鬼怪。《道要灵祇神鬼品经》引《洞渊经》说:“道言自伏羲以来,败军死将,举众形残,刀兵枉死,万万为群。或有灵尸,骨骨分张,身首他处,不得集聚。或有身无头,有足无首,有口无目,千千万亿,游托自守,依山倚水,与云中李子敖浮游云间来往海滨,东西南北,乘风驾雀,捉人鸡犬,妄令作妖,妖魅横生,求其血食。”《太上正一咒鬼经》的主旨也是批判末劫时期的社会现实,斥之为“野道”。这些野道有:“东方野道青龙蛟,南方野道炎火耀,西方野道白虎啸,北方野道玄武尾掉,中央野道黄帝。[27]”可以肯定,在天师道那里,官方祭祀的太微五帝是受到批判的。《女青鬼律》和《太上正一咒鬼经》都是天师道的经典。因此,“六天”在道教中的涵义也是由天师道确定的。“六天”在道教里既是官方政治的代称,也是鬼域的代称。
由于天师道对六天鬼神作了否定的批判,于是许多道经都把六天看作最凶恶的敌人。道教徒上章杀鬼,传播教义,都要把“灭六天”作为重要的任务。道教造作了“除六天玉文三天正法”,即现存于道藏中的《太上三天正法经》[28]。该经内容表明:所谓“三天正法”就是要除灭六天。“禀承三天制,驱洗六天凶”。虽然这里述说的三天正法比较空洞,不像陆修静那样重视“清约”,但它在文中也透露了所谓六天即指夏商周三代以来的政治。上清系统中还有《洞真太上神虎隐文》、《洞真太上金篇虎符真文经》和《洞真太上说智慧消魔真经》,也是颇有代表性的讨伐六天的道经。《智慧消魔经》和“神虎”诸经实为一组。《智慧消魔经》开篇即言此经中所说“可以逐邪起疾,驱精除害,散六天之鬼气,制万妖之侵者”。它宣说“六天灵鬼以病乱真”,而这组道经的功能就是“逐截六天之气”。“说之以智慧,又复记之以消魔。智慧可以驱神,消魔可以灭邪。复授之以神虎金符(神虎太符,金虎真符),使威馘六天。”在以上这些道经中,六天都是鬼域的代称。六天也逐渐有了其他名号,如“六天大鬼”(《金篇虎符经》、《神虎隐文》)、“六天大邪王”(《太上洞渊神咒经》)、“六天大魔王”(《太上太霄琅书经》、《三十九章经》等)。
随着道教教义的发展,出现了道教的劫运说。这种思想与道教对六天的批判是紧密相联的。在道教看来,自伏羲以后是六天之治适逢其运,而六天之治本也是禀承太上旨意而宣化的。太上屡遣真人下世“使助六天检正邪气”,也说明那时六天之治劫数未尽。新出老君颁行三天正法正一盟威之道的同时罢废了六天之治,即是宣布了它的劫数已尽,别开新纪元,修持成真者即为种民。于是,六天又成为末劫的代名词,对六天的批判也就成了道教宣说本教顺应时运、开劫度人的有机组成部分。老君复出宣化新约,则六天之治自然已至穷途末运,这是顺理成章的事。天师道的经典已经屡见末劫观念,并逐渐对六天的末劫性质加以刻画。这种末劫说的一个特征是把本来被天师道斥为鬼蜮的那些鬼怪说成是太上故意放之人间以示惩戒。《正一天师告赵升口诀》中说道:“上言今世微薄,运劫欲尽,人民凶逆,相习来久,······故放天灾于九州之内,白骨千里,虽好者百不遗一。”《太上正一咒鬼经》的某些内容颇似《女青鬼律》,也主张咒鬼名字即能除病。《女青鬼律》中也有末劫观念,宣说“除辟大厄却灾患”,“太平度世为种民”(第五卷)。但上面两段对末劫的解说却不见于《女青鬼律》。当然,对六天之鬼的解释不仅一种。《太上三天正法经》中说:“太上大道君给以鬼兵,使于三代之中驱除恶人。”《太上三天正法经》也是一部以末劫说批判六天的代表性经典,但其中却没有“放灾天下”的论述。然而,这种放灾天下的末劫说显然是很有影响的道教六天说,以至产生了《太上洞渊神咒经》这部极尽夸张之能事的道经。《太上洞渊神咒经》说:“自伏羲三千年,大水流溢,人民半死,······至周秦之灭,人民顿无。及汉魏末时,人民流移,其死亦半……”(第一卷)。在该经作者眼里,六天治下自始就是一个恐怖的世界。在该经中,那些作祟为害的也有六天鬼神:“山林社祀,世间庙主,坏军死将,脱籍之鬼,来助邪王,病痛世人。”然而,也许这样机械地去寻求《太上洞渊神咒经》的六天痕迹已经不甚相宜了,因为它的产生既有道教长期以来的观念的支配,也有西晋末年的社会背景的影响。并且,在该经中甚至并不烦言“六天”,反而在“六天大邪王”之外更立了“三天大邪王”、“九天大邪王”的名目。相比之下,倒是《太上三天正法经》较为明确地表达出“六天”一辞的末劫之义。该经叙述历史与《三天内解经》等书同其轨辙,而附之以末劫说,点出六天之治晚期的末劫性质及“除六天玉文三天正法”之“佐时除凶”的历史必然性。无论六天自始即为一魔鬼世界,抑或六天帝王们承受老君之旨后“转自伪辞”,致使太上放灾天下,末劫毕竟已经成为“六天”一辞的应有之义。在一些道经中言及六天之末劫性质时甚至毋庸赘言,其义已明,如“六天崩颓,三道正行”(《三十九章经》),“三天运明,六天运终”(《洞真太上八道命籍经》)。
由以上介绍已然可见:六天这个本为标识儒家祭祀制度的词汇在道教批判官方政治的过程中逐渐加入了新的定义,也日益成为道教内表述自己教义的一个词。儒家的六天一词进入道教之后,一变而为鬼域,再变而为末劫,三变而为地狱,道教的六天说成了自己教义的一个组成部分。在道教的地狱说中,我们会看到道教对六天鬼神的彻底改造并融进自身的过程,也能进一步看出道教对六天本义的记忆是那般地深刻。
三、酆都六天宫及其他
酆都六天宫的记载见于多种道经,记述较为详细的有《真诰》、《登真隐诀》、《太上洞玄灵宝三元品戒功德轻重经》、《道迹仙灵记》、《洞真上清开天三图七星移度经》、《太上洞渊北帝天蓬护命消灾神咒妙经》等书[29]。其间或有相为源流者,亦有异文互出者。笔者不擅道经年代考据,此处以《真诰》为本作叙述。这不仅因为《真诰》的叙述在诸书中最为详细完整,并且由它可以使我们进一步看出道教对六天的记忆。
《真诰》第十五卷《阐幽微第一》中说:“罗酆山在北方癸地。山高二千六百里,周回三万里。其山下有洞天,在山之周回一万五千里。其上其下并有鬼神宫室。山上有六宫洞,中有六宫,辄周回千里,是为六天鬼神之宫也。上为外宫洞,中为内宫,制度等耳。第一宫名为纣绝阴天宫,以次东行,第二宫名为泰煞谅事宗天宫,第三宫名为明晨耐犯武城天宫,第四宫名为恬昭罪气天宫,第五宫名为宗灵七非天宫,第六宫名为敢司连宛屡天宫。凡六天宫是为鬼神六天之治也。洞中六天宫亦同名相像如一也。世人有知酆都六天宫门名,则百鬼不敢为害。”
“酆”这个字自古至今可以说只有两个含义,一是指周朝的都城,二是指道教的罗酆山。当然,它还作过人的姓氏,而这个姓氏也是因为文王的后代封为酆侯故其后代姓酆。先秦至汉代的史籍中言及的“酆”君指周都,即使是偶见于纬书及《山海经》中的几条有关姜尚和后稷的材料亦可明其文中之酆当指周王立都之地[30]。周立都于丰水之滨,汉字中才有了“酆”字。“罗酆”一辞大约始见于葛洪《抱朴子》一书的《对俗》篇[31]。“酆”字成了鬼域的代称也始见于此。为何在酆字上加“罗”字,尚不得其解。“酆”与同样被道教视为鬼域的泰山在起源上并无关联,因为它这个名字就说明它有自己的原由。陶弘景在前引文字的注解中也说:“至于地狱,所在尽有,不尽一处,泰山河海亦各有焉。”至于在《阐幽微》之外是否还有什么材料指出“罗酆”在地理上有其确实的方所,我尚未见。它也和明代才由官方正式命名的的四川酆都县并无关联,俞樾曾予以辨明[32]。
我们知道了酆这个字的本义,就可以明了为什么在六天宫里有那样一些人物。这六宫各有主人,依次为;北太帝君炎帝,北斗君周武王,东明公夏启,西明公周文王、南明公邵公、北明公吴季子。当然,在道经中也出现过对六宫的其他解释,但是依我看,存于《阐幽微》的这个解释最切于“酆都”之名,虽然它也许并不是最早出现的解释。酆本为周朝都城,而其六宫宫主中有四位是姬家人[33]。当然,酆都成为道教指称地狱的习语以后,它也不再仅仅为姬氏独享。既然作为六天之治的代表,自有六天鬼神居其间。陶弘景以后问世的《太上元始天尊说北帝伏魔神咒经》中更变本加厉地描画酆六天宫的内景,说到六天鬼神身份时有云:“或为三皇五帝王古帝王形象,垂旒带佩”。
粗看起来,酆都六天宫中都是些六天鬼神。但是,这样一个结构却是逐渐发展起来的,综合了道教史上不同的信仰成分和中国古代的各种传说。并且,见于《阐幽微》的记载是否全部反映了关于酆都信仰的内容尚属疑问。要想梳理出它的轨迹恐怕是非常困难的事情,因为酆都作为地狱的代称大约在刘宋以前就已经为道教中习用。酆都、北酆、罗酆之类的词语道经中所在多有,而真正可以用来描述其形成过程之详情的则甚为寥寥[34]。本文搜罗的材料亦仅用以说明酆都六天宫之由来,不在描述其形成的历史。探赜索隐,有俟乎来者。
酆都作为六天之治的祭祀对象的总称自是名副其实的事,但酆都并不自始即有六天宫的结构,六宫在开始也并不叫做六天宫。酆都六天宫是酆都、六天、六宫这些原本分离的观念逐渐合成的。在许多道经里,北酆、罗酆、酆都、酆山往往并不和六宫连称。酆都与六宫结合则可能是因了道教对三官地狱的构想。《太上洞玄灵宝三元品戒功德轻重经》中讲述了天地水三官宫府,并且也不止有六宫。经中言及的宫府名号与《阐幽微》相关的有:明晨武城宫、耐犯明晨宫府、纣绝阴天府、七非恬照府、谅事府、宗灵阴天宫、连宛泉曲府、泰煞九幽府、罪气咸池府。该经中也了两处宫室分别名为北酆宫和北酆都宫(又名罗酆宫)。北酆宫中元二品右宫,下辖左府连宛泉曲府、右府泰煞九幽府、中府罪气咸池府。它们合成“北酆三宫”。罗酆宫则为下元三品右宫。该经的宫府是以三官总其纲,所谓三元(上元、中元、下元)即为天地水三官。由此可知,罗酆并非自始即有《阐幽微》中的六个宫名,并形成了《北帝酆都六宫度死法》。同样是早期灵宝经的《本行宿缘经》中虽无这些宫室名称,却可以看出它表述的酆都也是一种宫室。其经云:“不信之人,则得罪于三官地狱,三界六天。北帝大魔王之下治,在大北海,天地之北方,罗酆之山。高二千六百里,周旋三万里,内外皆七宝宫室,亦盛都也。天下鬼神之所宗处。今以此天宫名号相示。人能咒诵之者,恶鬼不敢近。”此经仍以酆都与三官相连,谓之为地狱之属。这里说到了六天。该经还说:“六天为三界大魔王,领六天鬼神之事。”由此我们可以知道酆都六天宫所以称为“六天”宫室是因为其中有六天治下的鬼神。当然,道教也把所有鬼神纳入其中。由上面这段引文,我们可以看到它与《阐幽微》的一个重要的相似之处,即六天鬼神和咒鬼的道术。在《七星移度经》中也明确讲到了酆都的六天鬼神和咒鬼术,实为《阐幽微》之先声。六天鬼神与咒鬼术本是天师道的观念。因此可以说,由酆都宫过渡到六天宫,其间肯定经过了三官地狱的模塑,六天宫室的结构则反映了对早期道教六天鬼神的记忆。发掘了“酆都六天宫”中隐含的这一信息,我们就可以解读《阐幽微》中那些文字和陶弘景的注释。
《阐幽微》说六天宫是“六天鬼神之宫”,似乎并不难理解。我们还要解读其中的鬼官。《三天内解经》中叙述六天之治的历史时说到前汉帝业所以中断,“皆由不信真正,无有辅翼,故群妖乱作,没于鬼官。”“没于鬼官”就是指到了鬼界做主持,也称地下主。《阐幽微》中罗列的这许多姓名正是鬼官。这些人物无一是修持道教的人,除了六天的帝王就是有功于官方的文臣武将。陶弘景在“文王为西明公领北帝师”条下注云:“此父子并称圣德而不免鬼官,虽为杀戮之过,亦当是不学仙道故也。”不学仙道即《三天内解经》说的“不信真正”。这句话明确指出了六天鬼神和道教神明在宗教归属上的对立。从他对周文王父子三人居鬼官的评论中也可以反推出道教的立场。陶弘景认为:“太公执饱秉钺,威罚最深,乃载出《列仙》;邵恩流甘棠,翻为魁职,亦复难了。[35]”按陶弘景的逻辑,姜太公杀戮过多,应列为鬼官,因为不学仙道和杀戮过多是列名鬼官的衡量标准。至于后面列入的那些鬼官则应是道教对“败军死将”的一贯立场。他们有功于六天之治,所以死后仍为六天之鬼。他们生前为那个以祭祀生人为特征的宗教服务,死后也被这个制度视为神明,甚至死后还在为这个制度服务。《阐幽微》指出有“西明郎十六人”,“主天下房庙血食之鬼”。以上这些都说明六朝时代的酆都传说继承并发挥了道教对六天之治的批判,是一以贯之的。虽然酆都六天宫的学说给六天鬼神的功能加入了新的解释,但六天鬼神的本义仍然存在。
道教构造的酆都六天宫实际是道教地狱说的进一步精制化。道教的地狱说是对中国古代关于人死后地下世界传说的发挥,其功能与古代的信仰已经有所不同。在某些道经中,三官也是地狱。我现在还不清楚道教为什么要在三官之外构造了一个酆都六天宫。在《太上洞玄灵宝赤书玉诀妙经》上卷中虽也偶然言及三官,却主要是用北酆表称地狱。道教的地狱说是一个庞大的系统,地狱的名目很多,也并非全都出于对六天的批判。酆都或许完全是道教自己构造的。其他与六天有关的地狱名目是泰山、蒿里、五岳四渎。这三者本都是官方祭祀之所或祭祀的对象,道教将其纳入自己的地狱系统是出于对六天之治的批判立场。
泰山地狱的传说由来已久,并且由中国古代文学可知其流传甚广。虽然古今中外都曾有著名学者作过探讨,但是这个论题仍然还有许多方面值得进一步发掘,其中一个重要的方面就是它与道教的关系。我所见到的前贤论著并非未曾就此方面作过讨论[36],却未能于两者之关系给出一个根本性的解释,至多不过搜罗道经以叙述泰山信仰在道教中的传播和发展,且多为后世材料,偏于纷繁,益增迷离。我们当然不能说泰山治鬼、泰山地狱的观念是道教的发明,我目前也没有根据说这种观念是齐鲁本地文化对源于西方的周朝文化的反动,但是我们可以推测这种观念是民间对官方祭祀所持的反动立场,并且这个立场更由后来的道教得以彰显。朝野观念的不同就在于:官方认为在泰山祭祀的是神明,而民间认为他们祭祀是些死人,进而把泰山视为死人魂灵的聚集地。后代士人悚然于官方的神道设教,不敢作分外之想,以至有俞樾谓泰山治鬼为不经[37]。实际上这种观念在某些早期士人的作品中已见其影响。陆机的《泰山吟》有“幽涂延万鬼,神房集百灵”的诗句。神房中之“百灵”实为所祭祀的过世帝王。东汉的镇墓文中也有这样的文字:“生人属西长安,死人属东泰山”[38]。参合史书中的记载,我可以推断:泰山地狱的观念就是针对官方祭祀所构造的一种传说,不能视为朝野意识形态在泰山的巧合。道教至少是延续了民间的这种观念并加以刻划,凸显了泰山地狱的六天本色。前引《女青鬼律》第一卷中说到的作为皇家神明的“高天万丈鬼”就是位于泰山的鬼。该书说此鬼“住在太山东南角道水中,诸死人所归”。《道要灵祇神鬼品经》引《老子天地鬼神目录》说:“太山岳丞,姓君名后”。我现在能找到的材料只有这两条,但是足以表达道教眼中泰山地狱的真义。
蒿里俗称“死人里”,和泰山一样,是死人鬼魂聚集的所在。历史上曾为聚讼的“蒿里”“高里”之异同给我们留下了也许永远都解不开的谜。《史记》和《汉书》都记载:汉武帝于太初元年至泰山“亲禅高里,祠后土”[39]。这个高里是山名。我们现在已无确凿的证据说明这个高里就是蒿里,但我们有证据说这两个名字至少在晋代已被等而视之了。陆机《泰山吟》诗云:“泰山一何高,迢迢造天庭。峻极高已远,层云郁冥冥。梁父亦有馆,蒿里亦有亭。幽涂延万鬼,神房集百灵。长吟泰山侧,慷慨激楚声。[40]”颜师古对陆机的说法很不满意。他注《汉书》高里句时说:“此高字自作高下之高,而死人里谓之蒿里,或呼下里也,字则为蓬蒿之蒿。或者既见泰山神灵之府,蒿里又在其旁,即误以为高里为蒿里,混同一事。文学之士共有此谬。陆士衡尚不免,况其余乎?今流俗书本,此高字有作蒿字者,妄加增耳。”颜师古的注虽然表达了他作为缙绅之士的立场,但也透露了一个信息。我们由此知道即使作为官方认可的史书其传本也将蒿里等同于高里。并且,若无他的批评,后人委实不敢遽然将陆机诗中的蒿里等同于高里,因为即使是三百年前的大学者顾炎武也是根据他的注解去理解陆机《泰山吟》,并以此诗为等同二名之始,虽然他也取颜师古的立场且进一步为之辩诬。他甚至还发出慨叹:“自陆机《泰山吟》始以梁父蒿里并列,而后之言鬼者因之。遂令古今帝王降禅之坛,一变而为阎王鬼伯之祠矣。[41]”我们委实应该感谢颜师古和顾炎武。他们从反面证实了在民间确乎存在着巨大的一种力量,它对高里禅地表达了与官方截然相反的态度。我尚未见道教对蒿里地狱的解释,蒿里在道经中出现的次数很少。道教或许是把泰山、蒿里视为异名同实,未必有祭天祭地之区分。五岳四渎作为地狱屡见于道经,但我也尚未发现有什么材料对于它被称为地狱的原因有所论述。然而五岳四渎是官方祭祀的对象,千百年来未曾废弃。因此,道教把它称为地狱明显表达了对官方礼仪的批判态度。
我在前节末尾将道教的六天概念划分为三方面的涵义,这只是一种主观的假说,试图揭发出道教“六天”一辞的蕴涵。鬼域之义是揭明道教自标身份之后将与其相异的宗教均列为恶鬼[42],其中以六天鬼神为首恶。道教所说的地狱中也有鬼魂并且以六天鬼神为主,但道教的地狱自有其功用。地狱一辞应始于汉末。虽然我们现在拿不出证据说明在汉译佛经之前即有地狱一辞,但显然在这之前已有类似的观念。《说苑》中有“唯丧与狱坐于地”的话[43],狱在先秦本指讼案,而汉时又指囚人之所。道教的地狱则兼有这两方面的意思:死人所归和刑法之所。由此而言,地狱之鬼是丧生之鬼而非言其兴妖作怪之性质。虽然道经中并未自觉对这两类鬼神做出区分,而我们重构道教教义则不能不有此界划。六天之治将三皇五帝、败军死将的鬼魂奉为神明,则六天世界宜乎称为地狱世界。既成地狱,也同样有其考校并拘禁罪人之功能。这或许就是六天与三官并称为地狱的原因。也正是由于这种功能,使本来处于受批判地位的六天鬼神被道教容纳进自己的神灵系统。
诚然,六天作为地狱,它的作用当然首先是负面的。在早期上清经和灵宝经中都有许多关于如何使道教徒得免六天鬼气、除去死籍的论述(如《上清天关三图》和《太上洞玄灵宝赤书玉诀妙经》、《元始五老赤书玉篇真文天书经》等)。同时,若道教徒违背道戒,惩罚的办法也是把他放入地狱。这些地狱除了三官之外,还有酆都、泰山、五岳四渎等。于是,六天地狱也逐渐成为道教考校世人或道教徒的地方。同样是《元始五老赤书玉篇真文天书经》,它的上卷和中卷是明显以六天为敌的,要“制北酆,正鬼气”;但在下卷却请出了“北酆都伯”等六天鬼神为道教行使考校职能。该书说:“敕太乙使者下与北酆都伯使者周行天地,司察人神功过深浅,列言上宫。有修斋立德,即勒录仙籍鬼神,随功进秩。人鬼有犯,移还鬼府。”又说:“北斗惨然,俱下与五帝、五岳四渎、江淮河济水帝、九部刺奸、三官考召地上神祇,周行天下,司察善恶功过轻重,列言上天。”在下卷末尾论及道教斋日典礼时更说道:“诸地上五岳神仙真人、四海水帝、北酆、三官、三界地祇,一切神灵,莫不束带肃然,持斋敬道,重法以崇天真也。”下卷与上卷及中卷的论述不能简单视之为自相矛盾,因为此三卷很可能并非形成于一时一人之手,而下卷之内容似与更早的《老子中经》有关。六天地狱或六天鬼神有了考校功能并列入道教的神灵系统虽有逻辑上的顺理成章,但道教教义的形成过程并非如此简单。因为,在《老子中经》里,太一北辰的功能就是考校世人功过,却并未明言它就是官方祭祀的太一神。是否在古代官方与民间对太一神有不同的认识?是否道教后来把司功过的太一和官方的祭祀对象合而为一,成为后来的“北帝”?北酆伯是否就是太一、北帝?我只是有这些怀疑。如果我们通过研究找到了一个肯定的答案,那么就可能会圆满解释六天鬼神为何及如何被道教吸收的。
毕竟,由前引道经已经可以看出道教的确吸收了六天的鬼神。还可以找到一些材料。古灵宝经《太上洞玄灵宝本行宿缘经》中说:“仙人有阶级,真人有品次矣。六天为三界大魔王,领六天鬼神之事。魔王承奉太上众真天尊上人也。道士功成,魔王即保举焉。”《道要灵祇神鬼品经》引《太上太霄琅书经》说:“东华青童曰,魔有数种,天魔、地魔、鬼魔。今言六天大魔王者,是天地之大魔王。其宿世有功德,故得为此魔王。与帝释比德,共执事于天地人。上属太上玉京,为天帝之下官耳。其余无所不制。夫人学道,先经小魔试;道成时,大魔皆临大试,过,便保举上登玉京台。”不但六天的鬼神被吸收进道教的神灵系统,而且,六天地狱的宫室也成为道教自己的了。在这部道经的末尾讲到酆都等地狱“悉为诸仙人之下宫”。正因为六天鬼神和他们所在的六天地狱变成了道教教义的一部分,道教才那么认真地想象宫室的处所、内部构造及其人物的名衔等级。《真诰》第十五卷和十六卷保存下来的据称是承自杨许之流的酆都地狱说可以看作是道教吸收六天的最终结果,只是它偏重于描述内部结构,于义理不甚着意,所以后人很难评价它在道教史上的地位。由这部书也可以看出六天鬼神已经和道教自身三官有同样的功能,其意旨与前引诸道经一致。在第十六卷中有这样的话:“西王母见我苦行,酆都北帝愍我道心,告敕司命,传檄三官,摄取形骸,还魂复真。使我颐胎,位为灵神。”于是,在《阐幽微》中的鬼官就有了双重身分。首先他们是帝王将相的死魂,有鬼气,是道教徒要设法克制的。这是六天鬼神的本来性质。其次,他们受命于道教考校世人功过,同时他们自己也可以由这种新的意义上的“替天行道”能进阶仙位。第十五卷即明确讲到四明公(夏启、文王、邵公、季札)“后并当升仙阶”。六天宫也不再只是与道教相对立的外在的地狱了。第十五卷中的咒语说:“吾太上弟子,下统六天。六天之宫,是我所部。不但所部,乃太上之所主。吾知六天之宫名,故得长生。敢有犯者,太上当斩汝形。”这段咒语的主旨当然是防止六天鬼气的侵害,使六天鬼气不能干“犯”。但是在这里,六天宫明确是“太上所主”,是受道教乃至道徒驱使的。陶弘景正是继承了道教的这一发展,所以在《真灵位业图》中把酆都的鬼神编入第七级,使它们成为道教神灵系统的组成部分。《真灵位业图》使道教原本为自然气化的神明中也掺入了胎生肉人的灵魂,这是否为一矛盾以及道教如何加以调整以保持自己教义的一贯性尚是一个有待研究的问题。
本文篇幅已嫌过长,而所论尚未透彻,也实非我今日学力所能克就。读者可以由文中看出,围绕六天还有许多分支论题值得深入探讨。本文不过寻章摘句,远非系统研究,即所论及材料亦有尚未得其牙后慧者。此外,六天一辞以后又成为道教三界之一,本文不复论及。网疏安能无漏?故本文之取材是否得当、理论是否妥帖、推断是否合理,乃至行文转折是否顺遂,均有待同人批评。目前我本人能够肯定的只有道教中六天所具涵义,以及由此涵义之揭发而显其民间性的真实义蕴。学界长期以来所指称的道教的民间性尚是一个较为模糊的表述,似乎同义于“民俗”。我在本文中曾多次提到道教的反官方性,但我并不想以此界定其民间性。反官方性只可以用来指称道教的某些时期的某些方面。若为研究道教及其思想渊源计,应当注意它的与官方宗教的相异性。它的反官方性由其相异性而来,是民间文化自我确认的一种表现形式。我们长期以来未能对这方面给以较多的注意。一是未注意它的宗教性,其次便是没有注意到它的相异性。对于它的历史观,我们轻率地视之为虚诞而不予理会。三皇五帝的崇拜、祖先的血食牺牲,千百年来习非成是,蔚为典章,虽今日仍为国家假其威以“来远人”。因此,儒家的宗教体系在今日仍居于统治地位,同时道教和其他民间文化因素千百年来也一直作为与它相异的信仰存在。上古以来的保存于先秦道家和汉末以来的道教中的种种传统也正需要我们作一番“习非成是”的工夫,不再让它们仅仅作为寓言和文学作品,而是完整地构建它的历史观、劫运说和礼仪,把它们与儒教、佛教作一种比较宗教研究。因为道家和儒家在同一时期都在致力于构造历史,用宗教的历史传说代替真实的历史。以顾颉刚先生为带代表的古史辨派在剥尽画皮的同时并未否定三皇五帝的宗教史价值[44]。然而辨伪的流风被后人错误地贯彻到了宗教史尤其是道教史研究中。平实而论,这些虚构的荒诞历史在宗教史中却是虚而不诞,恰恰以其虚而成其宗教史实。古史辨派在辨伪的的同时也在历史上第一次处理了宗教史的材料,这是他们留给后人的一笔遗产。笔者对六天的讨论即意在尝试处理这些“虚诞”的史料,而检阅先贤旧章,更觉有此必要。
虽已连缀成文,笔者尚不免惴然于心,因为这样一个视角并非我自己体贴出来。许多西方道教学者在研究道教的民间性时其着眼点和我们有很大的不同。他们十分看重道教和官方意识形态的对抗性,因为他们这样做有一个前提:中国社会自很早以来就分裂为两部分。这个立场使他们对道教的性质及其在中国文化史中的地位得出了与我们完全不同的结论。由于地域和语言的隔绝,中国学者要理解他们的立场大约还要经历很长一段时间。然而,我也感到他们的确注意到了一些我们惯常不去注意的材料及其涵义。也正因为如此,我在来到欧洲的短短几个月里不自觉地受到了这种立场的影响,虽然我对他们的立场和方法还有怀疑,并且认为他们的某些看法有过激之嫌,同时我也仍然持有从国内先贤继承下来的见解。我现在提交给会议的这篇文章就是我在这方面的一个尝试之作,或许落于“邯郸学步”之诮,亦未可知。果真能有一知半解得许于与会同仁,那么首先应该感谢荷兰莱顿大学教授、真诚挚爱中国文化的施舟人先生。他受我的老师汤一介先生的嘱托,为我在这里的研修提供了无私的帮助。这篇文章据以出发的立场乃至《三天内解经》等材料的涵义都是他指点给我的。至于在剪取材料并作疏解方面如有错误,则由我本人负责。
1996年5月于荷兰莱顿大学汉学院
注释:
[1] 日本学者小林正美先生在《六朝道教史研究》(东京创文社1990年出版)一书中对道教的六天观念做了讨论。他的引书为我提供了一些资料线索。他从天师道的发展以及南北道教之区别入手讨论了六天观念的起源,但嫌于简略。笔者尚无能力对他的研究作出批评。本文的讨论试从儒家六天之义入手,而重点则在于从整体上探讨六天概念在道教中的涵义。
[2] 华夏出版社出版的《道教大辞典》六天条引《孝经》云“宗祀文王于明堂,以配六天上帝”。查孔安国和郑注《孝经》均无六天二字。唐以前史书多引此句,亦无六天二字。不知《道教大辞典》所据何本。
[3] 见中华书局《十三经注疏》,1980年出版,第1444页。
[4] 《旧唐书》,中华书局标点本第823页。
[5] 见中华书局《十三经注疏》第1506页。
[6] 见徐旭生《中国古史的传说时代》(增订本),科学出版社1960年出版,第19页。
[7] 见《史记》中华书局标点本第1378页。杨宽则认为:“五方帝五色之祠,最迟当春秋时已有。”见《古史辨》第七册上编,第250页。开明书店1941年6月初版。杨先生亦援引此条反推秦始皇比汉高祖更应知天有五帝。我以为这个推论未妥。
[8] 见《中国古史的传说时代》第207页。
[9] 《史记》第1357页。
[10] 见《文献通考·郊祀考》,商务印书馆1936年出版,第611页。
[11] 《史记》第1386页和1401页。
[12] 《史记》第1394页。
[13] 见中华书局《十三经注疏》,第1506页。
[14] 见《四部备要》线装本,皮锡瑞《孝经郑注疏》卷下,第4页。
[15] 见前书卷下第1页。
[16] 见中华书局《十三经注疏》,第766页。
[17] 本文引用道经均依据涵芬楼影印本。除个别随文注明卷数外,不复注明册数及页数。
[18] 这三部经典关于祭祀的规定有差异。《太上正一盟威法箓》中说:“常以五腊吉日于堂上祀家亲九祖,二月八月同日祀社灶。其余不得私祀他鬼神。”《陆先生道门科略》中说:“唯天子祭天,三公祭五岳,诸侯祭山川,民人五腊吉日祠先人,二月八月祭社灶,自此以外,不得有所祭。”这两种经典都允许二八月祭社灶。《三天内解经》与《陆先生道门科略》年代相同,故《三天内解经》或有脱文。陆修静仍然把六天祭祀对象列入,应当看作是他对朝廷的让步。
[19] 大渊忍尔在讨论《洞渊神咒经》时也注意到这个问题。他用“非血食主义”标识道教的主张。见其所著《初期道教史的研究》,日本冈山印刷株式会社昭和39年出版,第525页。
[20] 见王明先生《抱朴子内篇校释》,中华书局1980年版,第170页。
[21] 见前书第76页。
[22] 分别见前书第68页和第233页。
[23] 见前书第157页。
[24] 见前书第158页。其文云:“又诸妖道百余种,皆煞生血食,独有李家道无为为小差。然虽不屠宰,每供福食,无有限剂,市买所具,务于丰泰。精鲜之物不得不买,或数十人厨,费亦多矣,复未纯为清省也,亦皆宜在禁绝之列。”
[25] 见前书第287页。
[26] 见前书第45页。设问为:“审其神仙可以学致,翻然凌霄,背俗弃世,烝尝之礼,莫之修奉。先鬼有知,其不饿乎!”
[27] “五天帝”在道教各派中的性质尚需分别审度,不能一律看待。例如在《洞玄灵宝五老摄召北酆鬼魔赤书玉诀》中言及有五方鬼魔,分别叫“青帝鬼魔”、“赤帝鬼魔”、“黄帝鬼魔”、“黑帝鬼魔”,都是被驱逐的对象,但是所驱之神文也分别称为“某帝”神文。此书实出自《元始五老赤书玉篇真文天书经》上卷,该卷所列“元始五老灵宝官号”之“字”则为太微五帝名号。虽然该书也有批判六天文字,但其“六天”应是统称,并未如《女青鬼律》等书指实。
[28] 我的理由是:大渊忍尔与石井昌子所编辑的《道教经典目录索引》中所列《除六天玉文三天正法》大多出于《太上三天正法经》。最重要的证据是《云笈七签》第八卷。该卷介绍的上清经中有《除六天玉文三天正法》而无《太上三天正法经》,显然是以《除六天玉文三天正法》作为该经之代称。
[29] 萧登福在《汉魏六朝佛道两教之天堂地狱说》(台湾学生书局1988年出版)一书中列有《道教诸经论所言地狱异说表》,介绍了部分酆都六宫的不同说法。
[30] 《尚书帝命验》中有两条材料,均言赤雀衔丹书入于酆。其一曰:“季秋之月甲子,有赤雀衔丹书入于酆,止于昌户口,拜稽首至于蟠溪之水。尚钓涯,王下趣拜曰:公望七年,乃今见光景如斯。答曰:望钓得玉璜,刻曰:姬受命,左旌,遂置车左。王躬执驱,号曰师尚父。”《海内西经》中有云:“流黄酆氏之国,中方三万里,有涂四方。中有山,在后稷葬西。”
[31] 见《抱朴子内篇校释》第46页。
[32] 俞樾《茶香室丛钞》卷16引范成大《吴船录》云:“忠州酆都县,去县三里有平都山,碑牒所传前汉王方平、后汉阴长生皆在此得道仙去。”俞樾按语中说:“酆都县平都山为道书七十二福地之一,宜为神仙窟宅,而世乃传为鬼伯所居,殊不可解。读《吴船录》,乃知因阴君传讹。盖相沿既久,不知为幽冥之主者,此俗说所由来也。至北极治鬼之所,别有用心,其地与此酆都不相涉也。”
[33] 陶弘景在《阐幽微》注文中特别提到文王、武王、邵公父子三人,却未着意于季札亦姬姓后人,或因季札之传说另有其源。见王明《抱朴子·对俗》篇注136和陶弘景《阐幽微》中“项梁”注。
[34] 萧登福将六朝道经中对罗酆六天宫的论述分为四个系统,以四部经典为代表:《真诰》、《伏魔经》、《天关三图上清经》、《三元品戒经》。他的讨论也是横向考察,未能纵向追讨源流。
[35] “饱”、“魁”两分别应作“旄”、“鬼”。据《丛书集成》所收《学津讨源》本。
[36] 这我里所指的主要是顾炎武《山东考古录》、《日知录》卷30“泰山治鬼”条,赵翼《陔余丛考》卷35、俞樾《茶香室丛钞》卷16、法国学者沙畹(Chavannes)的《泰山》、日本学者泽田瑞穗的《地狱辨》,吴荣曾《镇墓文中所见到的东汉道巫关系》(《文物》1981年第三期)、余英时《中国古代死后世界观的演变》(载北京大学出版社1983年之《燕园论学集》)。笔者于此无意菲薄前贤,沙畹和泽田瑞穗的著作对文献及泰山实地的材料广为搜罗,笔者受益良多。余英时、吴荣曾的论文试图揭发泰山信仰与上古以来思想史的联系,以求一贯之解释,与笔者之立场亦无根本区别。但是,他们在泰山与道教关系的见解上未突破顾炎武《山东考古录》。
[37] 见《茶香室丛钞》卷15“渿河桥”条。其文云:“余谓后世言神言鬼,皆托之泰山。虽虚诞之说,而未始无理。盖因天事天,因地事地,此封禅之所起也。——神道属天王者,既封泰山以报天,则泰山有神道矣。鬼道有地王者,既禅泰山下小山,如云云,亭亭,梁父、高里诸山有鬼道矣。······但谓高里诸山治鬼则可,谓泰山治鬼则不可。《三国志·管辂传》曰‘但恐至泰山治鬼,不得治生人’,则当时已失其义矣。”俞樾有见于官方祭祀之义丧于后世,而未见民间对官方祭祀之义有别裁焉。
[38] 此文字原出于《古器物识小录》。罗振玉《贞松堂集古遗文》中有一镇墓文:“生属长安,死属太山,生死异处,不得相防(妨)。”此两条转引自吴荣曾论文。吴先生评论说:东汉都城在洛阳不在长安,所以这种说法应自西汉传下来。我认为,这两条出土文字的巧合正说明了它未必指墓主之身世,而是当时流行的写法。这个生死异处的人物是否指泰山所祭祀的帝王呢?
[39] 见《史记》第481页,《汉书》第199页,中华书局本。
[40] 见宋人郭茂倩所编《乐府诗集》卷41。中华书局1979年版,第二册,第605页。
[41] 见顾炎武《山东考古录》“辨高里山”条。此处文字引自吴青坛《说铃》。顾炎武还说:“蒿里之名,见于古挽歌,不言其地”,似乎以为蒿里当初并不是某一具体方所。书中又有“辨渿河”条,称渿河“在高里山之左,有桥跨之,曰渿河桥。世传人死魂不得还,而曰奈何。”由此可知,高里山确乎被世人目为“死人里”。《说铃》还收入清人孔贞宣的《泰山纪胜》,其中“社首蒿里”条说明了皇家社首祭礼之义,证明社首与蒿里相连。说到蒿里西北洞中曾于明代发现宋真宗所投玉简事时又说:“荐绅先生所不道,为其矫诬而不可训也”。由此可见,蒿里传说是与官方的宗教体系截然相对的,而正统文人的讳莫如深也是后人无法探明蒿里来源的根本原因。
[42] 施舟人教授屡对我言及道教的这种做法颇类似基督教建教之初的做法。
[43] 见《说苑》第十七《杂言》。
[44] 见顾颉刚《三皇考》,《古史辨》第七册中编,第51页,开明书店1941年初版。
(原载《北京大学百年国学文粹》,北京大学出版社1998年出版。)
]]>道教的六天说
王宗昱
“六天”这个概念在早期道教的经典里是经常见到的,道教对它的批判也是显而易见的。然而,中国学术界对这个概念及其在道教史上的演化一直没有作出深入的讨论[1]。“六天”这个本出于儒家的概念何以在道教中这么重要,反而鲜见于儒家经典,道教对它的批判传达给了我们什么信息?
一、六天的来历
在正史和儒家的经典里,我目前见到的最早的言及“六天”一词的材料是《礼记》孔颖达疏和《旧唐书》[2]。《礼记·郊特牲》孔疏中说:
先儒说郊,其义有二。案《圣证论》以天体无二,郊即圜丘,圜丘即郊。郑氏以为天有六天,丘郊各异。今具载郑义兼以王氏难。郑氏谓天有六天,天为至极之尊,其体只应是一。而郑氏以为六者,指其尊极清虚之体其实是一,论其五行生育之功则别有五。以五配一,故为六天。[3]
《旧唐书·礼仪志》中所录贞观二年礼部尚书许敬宗的奏文中有云:
二年七月,礼部尚书许敬宗与礼官等又奏议:据祠令及新礼,并用郑玄六天之议,圆丘祀昊天上帝,南郊祭太微感帝,明堂祭太微五帝。谨按郑玄此义,唯据纬书,所说六天,皆谓星象,而昊天上帝,不属穹苍。故注《月令》及《周官》,皆谓圆丘所祭昊天上帝为北辰耀魄宝。考其所说,舛谬特深。按《周易》云:日月丽于天,百谷草木丽于地。又云:在天成象,在地成形。足明辰象非天,草木非地。《毛诗传》云:元气昊大,则称昊天。此则苍天为体,不入星辰之例。且天地各一,是曰两仪。天尚无二,焉得有六。[4]
由上面的材料可知,六天指的是以郑玄为代表的儒家对官方祭礼的解释。并且,上文所言六天均未明言该辞出于郑玄,止是王肃等人对郑玄主张所用的一种代称。虽然现存《孝经》、《礼记》和《周官》等书的郑玄注文中均未有“六天”字样,但循其文义并质之史书,可知“六天”所蕴涵的内容确然为郑玄之主张,信乎不诬。
]]>http://www.heibanke.com/lesson/crawler_ex00/
每页都会出现新数字,这一关考的就是获取网页和解析内容,非常简单:
1 | import requests |
输出:
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/19016
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/13579
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/43396
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/39642
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/96911
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/30965
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/67917
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/22213
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/72586
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/48151
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/53639
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/10963
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/65392
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/36133
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/72324
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/57633
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/91251
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/87016
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/77055
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/30366
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/83679
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/31388
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/99446
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/69428
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/34798
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/16780
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/36499
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/21070
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/96749
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/71822
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/48739
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/62816
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/80182
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/68171
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/45458
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/56056
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/87450
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/52695
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/36675
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/25997
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/73222
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/93891
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/29052
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/72996
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/73999
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/23814
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/98084
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/51103
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/39603
正在读取网址 http://www.heibanke.com/lesson/crawler_ex00/34316
结束!
访问最后一个网址,提示:恭喜你,你找到了答案.继续你的爬虫之旅吧!
OK,进入下一关。
http://www.heibanke.com/lesson/crawler_ex01/
1 | import requests |
输出:
密码是 26
回到登录网址,输入密码过关!
http://www.heibanke.com/lesson/crawler_ex02/
这一关需要登录,随便注册一个账号,在爬虫中要用。另外要注意Post中不能少了csrfmiddlewaretoken。上一关中虽然有这一项,但是不加也可以。不过这一关里必须加上这个参数。
header必须加上Cookie。
以上信息是把每一步获取的网页源码控制台打印,然后分析出来的。
1 | import requests |
输出:
正在猜密码: 1
正在猜密码: 2
正在猜密码: 3
正在猜密码: 4
正在猜密码: 5
正在猜密码: 6
正在猜密码: 7
正在猜密码: 8
正在猜密码: 9
正在猜密码: 10
正在猜密码: 11
正在猜密码: 12
正在猜密码: 13
正在猜密码: 14
正在猜密码: 15
正在猜密码: 16
正在猜密码: 17
正在猜密码: 18
正在猜密码: 19
密码是 19
手动输密码过关!
这一关的逻辑设置得很奇怪,找作者问了也没看明白。不做了。
]]>http://www.heibanke.com/lesson/crawler_ex00/
每页都会出现新数字,这一关考的就是获取网页和解析内容,非常简单:
1 | import requests |
近来了解了李鸿章生平,读到这首绝命诗,感慨良多。
附上纽约时报1896年对李鸿章的采访。原文
以下是中译文,出自《帝国的回忆》。
]]>离开在欧洲考察访问的最后一站英国后,大清帝国前直隶总督兼北洋大臣李鸿章乘“圣-路易斯”号邮轮于当地时间1896年8月28日抵达美国纽约,开始对美国进行访问。李鸿章在美国受到了美国总统克利夫兰的接见,并和美国一些要员及群众见面,受到了“史无前例的礼遇”(《纽约时报》)。9月2日上午9时许,李鸿章在纽约华尔道夫饭店接受了记者的采访。
美国记者:尊敬的阁下,您已经谈了我们很多事情,您能否告诉我们,什么是您认为我们做得不好的事的呢?
李鸿章:我不想批评美国,我对美国政府给予我的接待毫无怨言,这些都是我所期待的。只是一件事让我吃惊或失望,那就是你们国家有形形色色的政党存在,而我只对其中一部分有所了解。其他政党会不会使国家出现混乱呢?你们的报纸能不能靠国家利益将各个政党联合起来呢?美国记者:那么阁下,您在这个国家的所见所闻中什么最使您最感兴趣呢?
李鸿章:我对我在美国见到的一切都很喜欢,所有事情都让我高兴。最使我感到惊讶的是20层或更高一些的摩天大楼,我在中国和欧洲从没见过这种高楼。这些楼看起来建得很牢固,能抗任何狂风吧?但中国不能建这么高的楼房,因台风会很快把它吹倒,而且高层建筑若没有你们这样好的电梯配套也很不方便。美国记者:阁下,您赞成贵国的普通百姓都接受教育吗?
李鸿章:我们的习惯是送所有男孩上学。(翻译插话:在清国,男孩,才是真正的孩子。)我们有很好的学校,但只得付得起学费的富家子弟才能上学,穷人家的孩子没有机会上学。但是,我们现在还没有你们这么多的学校和学堂,我们计划将来在国内建立更多的学校。美国记者:阁下,您赞成妇女接受教育吗?
李鸿章(停顿一会儿):在我们清国,女孩在家中请女教师提供教育,所有有经济能力的家庭都会雇请女家庭教师。我们现在还没有女子就读的公立学校,也没有更高一级的教育机构。这是由于我们的风俗习惯与你们(包括欧洲和美国)不同,也许我们应该学习你们的教育制度,并将最适合我们国情的那种引入国内,这确是我们所需要的。记者:总督阁下,您期待对现存的排华法案进行任何修改吗?
李鸿章:我知道,你们又将进行选举了,新政府必然会在施政上有些变化。因此,我不敢在修改法案前发表任何要求废除《格利法》的言论,我只是期望美国新闻界能助清国移民一臂之力。我知道报纸在这个国家有很大的影响力,希望整个报界都能帮助清国侨民,呼吁废除排华法案,或至少对《格利法》进行较大修改。美国记者:阁下,您能说明选择经加拿大而非美国西部回国路线的理由吗?是不是您的同胞在我国西部一些地区没有受到善待?
李鸿章:我有两个原因不愿经过美国西部各州。
第一,当我在清国北方港口城市担任高官时,听到了很多加州清国侨民的抱怨。这些抱怨表明,清国人在那里未能获得美国宪法赋予他们的权利,他们请求我帮助他们使他们的美国移民身份得到完全承认,并享受作为美国移民所应享有的权利。而你们的《格利法》不但不给予他们与其他国家移民同等的权利,还拒绝保障他们合法的权益,因此我不希望经过以这种方式对待我同胞的地方,也不打算接受当地华人代表递交的要求保证他们在西部各州权益的请愿信。
第二,当我还是一名优秀的水手时,就知道必须学会自己照顾自己。我比别人年纪要大好多岁,从温哥华回国的航程要比从旧金山出发更短些。我现在才知道,清国“皇后号”船体宽阔舒适,在太平洋的所有港口都难以找到如此之好的远洋客船。
排华法案是世界上最不公平的法案。所有的政治经济学家都承认,竞争促使全世界的市场迸发活力,而竞争既适用于商品也适用于劳动力。我们知道,《格利法》是由于受到爱尔兰裔移民欲独霸加州劳工市场的影响,因为清国人是他们很强的竞争对手,所以他们想排除华人。如果我们清国也抵制你们的产品,拒绝购买美国商品,取消你们的产品销往清国的特许权,试问你们将作何感想呢?不要把我当成清国什么高官,而要当成一名国际主义者,不要把我当成达官贵人,而要当作清国或世界其他国家一名普通公民。请让我问问,你们把廉价的华人劳工逐出美国究竟能获得什么呢?廉价劳工意味着更便宜的商品,顾客以低廉价格就能买到高质量的商品。
你们不是很为你们作为美国人自豪吗?你们的国家代表着世界上最高的现代文明,你们因你们的民主和自由而自豪,但你们的排华法案对华人来说是自由的吗?这不是自由!因为你们禁止使用廉价劳工生产的产品,不让他们在农场干活。你们专利局的统计数据表明,你们是世界上最有创造力的人,你们发明的东西比任何其他国家的总和都多。在这方面,你们走在了欧洲的前面。因为你们不限制你们在制造业方面的发展,搞农业的人不限于搞农业,他们还将农业、商业和工业结合了起来。你们不象英国,他们只是世界的作坊。你们致力于一切进步和发展的事业。在工艺技术和产品质量方面,你们也领先于欧洲国家。但不幸的是,你们还竞争不过欧洲,因为你们的产品比他们的贵。这都是因为你们的劳动力太贵,以致生产的产品因价格太高而不能成功地与欧洲国家竞争。劳动力太贵,是因为你们排除华工。这是你们的失误。如果让劳动力自由竞争,你们就能够获得廉价的劳力。华人比爱尔兰人和美国其他劳动阶级都更勤俭,所以其他族裔的劳工仇视华人。
我相信美国报界能帮助华人一臂之力,取消排华法案。美国记者:美国资本在清国投资有什么出路吗?
李鸿章:只有将货币、劳动力和土地都有机地结合起来,才会产生财富。清国政府非常高兴地欢迎任何资本到我国投资。我的好朋友格兰特将军曾对我说,你们必须要求欧美资本进入清国以建立现代化的工业企业,帮助清国人民开发利用本国丰富的自然资源。但这些企业的管理权应掌握在清国政府手中。我们欢迎你们来华投资,资金和技工由你们提供。但是,对于铁路、电讯等事物,要由我们自己控制。我们必须保护国家主权,不允许任何人危及我们的神圣权力。我将牢记格兰特将军的遗训,所有资本,无论是美国的还是欧洲的,都可以自由来华投资。美国记者:阁下,您赞成将美国的或欧洲的报纸介绍到贵国吗?
李鸿章:中国办有报纸,但遗憾的是中国的编辑们不愿将真相告诉读者,他们不像你们的报纸讲真话,只讲真话。中国的编辑们在讲真话的时候十分吝啬,他们只讲部分的真实,而且他们的报纸也没有你们报纸这么大的发行量。由于不能诚实地说明真相,我们的报纸就失去了新闻本身的高贵价值,也就未能成为广泛传播文明的方式了。
近来了解了李鸿章生平,读到这首绝命诗,感慨良多。
]]>
这是玉泉校区西南角,以前从没来过。从学校正门口下车,一路走到这里大约花了二十多分钟,已经放弃在这里租房了,出行太不方便。不过景色可人,起兴浏览一番。
上一张照片往右拐,不知道何时建了一个休闲山庄。可惜最近没有闲下来的心思,手头还有许多事情要做。
上面几张都是青芝坞,玉泉校区南门外的住宿+吃喝圣地,毗邻植物园,东南不远处就是西湖,风景绝佳之处。
周四晚去朝晖看房路过这里,年轻人在摆地摊。
周六借了表妹的VPN,进学校论坛逛了一圈,看到这个帖子,现在的年轻人们真有干劲。群里信息太少,最后我在豆瓣上找到了房子。
收拾东西,终于要搬离这个住了三年的地方,是结束也是新的开始。三年来走了很多弯路。现在要戴上紧箍咒,重新做回孙悟空。
某同学婚礼上拿回来的,以为就是一个小狗形的装饰品,摆了一个月后告诉我这是毛巾。
母亲给的保湿霜,但我没这种习惯,看到就不想用。
下雨没带伞,困在医院里,心情很低落。旁边的老太太说看不清楚药品的标签,接连拿出几个药盒问我。回答了几句,心情也好了一点,果然助人为快乐之本,但是我总是把事情搞砸,就连最好的朋友也全都闹过矛盾。也许不再交新朋友才适合我。
这些书我都只看了开头,没有细读。以前是没心境,但这段时间发生了这么多事,我想终于可以安下心来做事了。虽然现在还不平静,但我感觉也快了。最近做什么事都没劲,这样的状态也挺好,可以让我慢慢死心踏地看道书和练功。这次搬的新住处,看房子的时候通道阴阴的,我反而特别喜欢,有种住在阴暗角落的感觉,颇符合现在的心情。以前我的新浪博客名叫「长夜未央」,因为习惯躲在黑暗里,不想走到台面上去和人打交道。接触的人多,是非也多,过错往往在我身上,说话做事不动脑子。
古龙笔下有个「大老板朱停」,心宽体胖,做任何事情都会先停下来想一想,想得多了,就没什么事情是非做不可的。我也应该停一停,就停在黑夜中吧。
积累了四年的一毛硬币,打算抽空去银行换。
房东给我的时候,我以为是个挂件。原来这是门禁卡,便于携带。
窗外是京杭大运河。杨坚杨广父子兴洛阳、开运河、三征高丽,敝在当代功在千秋,背了所有骂名,好处全被李唐接收。
新家第一天晚上。平时吃得不多,这些可以撑两个星期了。
]]>兰溪的姑婆生日。
乡下种田收益越来越低,村民们纷纷外出打工,江西佬们闻讯而来,承包农田种起了西瓜、樟树。又在田里搭了棚子过日子。
住处附近的公园,景色很美,可惜近来思绪纷乱,无心欣赏。
年后来杭州,路遇街亭。
忘了当时什么心情。最近一两年的心情都不美丽。
把心念系在水上,久而久之心也会平静下来。与持咒、数息、观照摄心一样,都依靠专注使自己达到内心的平静。用志不分,乃凝于神。
小美女最近快三周岁了
去年国庆期间贾爷来杭州,约了一波大学同学网吧五连坐。
西湖文化广场。
当天穿得太多,走到半山腰就热得不行,于是下山走了。姑姑和姑父两人继续上山游玩。
东阳方言叫它「泡泡糖」,不知道普通话叫什么。
金氏宗祠。曾经也是宋徽宗敕封的「江南第一家」。后因郡内大水,子孙散居浙江各地。
去年拍过上游的衢江,这次拍了下游。
冷
还有持枪巡逻的阿sir们,没敢拍。
终于见到了大名鼎鼎的安利!
回玉泉校区游荡。还爬了老和山,下山后接到同学电话,原来是上山前见到我的踪影,现在来电确认。
城隍阁。
过年在家烧火灶
剪了短发,自拍一张。
姐姐家楼下电梯口
吴山庙会。板蓝根还能这么吃?
老同学领证结婚,一块吃了一顿饭
亲戚家拜年
]]>宋之前一直有专门的官员执掌占卜事宜,战争这样的国家大事当然会算。春秋时代的《吴子兵法》中有八不卜与六不占,特意提出这十四个要点,正好说明出征前占卜是一种普遍行为:
吴子曰:「凡料敌有不卜而与之战者八。一曰:”疾风大寒,早兴寐迁,刊木济水,不惮艰难。二曰:盛夏炎热,晏兴无间,行驱饥渴,务于取远。三曰:师既淹久,粮食无有,百姓怨怒,妖祥数起,上不能止。四曰:军资既竭,薪刍既寡,天多阴雨,欲掠无所。五曰:徒众不多,水地不利,人马疾疫,四邻不至。六曰:道远日暮,士众劳惧,倦而未食,解甲而息。七曰:将薄吏轻,士卒不固,三军数惊,师徒无助。八曰:陈而未定,舍而未毕,行坂涉险,半隐半出。诸如此者,击之无疑。」
「有不占而避之者六。一曰:土地广大,人民富众。二曰:上爱其下。惠施流布。三曰:赏信刑察,发必得时。四曰:陈功居列,任贤使能。五曰:师徒之众,兵甲之精。六曰:四邻之助,大国之援。凡此不如敌人,避之勿疑。所谓见可而进,知难而退也。」
这种明显打不过和明显能赢的战争没必要占卜。《左传》中也有相关的战例:
楚屈瑕将盟贰、轸。郧人军于蒲骚,将与随、绞、州、蓼伐楚师。莫敖患之。斗廉曰:「郧人军其郊,必不诫,且日虞四邑之至也。君次于郊郢,以御四邑。我以锐师宵加于郧,郧有虞心而恃其城,莫有斗志。若败郧师,四邑必离。」莫敖曰:「盍请济师于王?」对曰:「师克在和,不在众。商、周之不敌,君之所闻也。成军以出,又何济焉?」莫敖曰:「卜之?」对曰:「卜以决疑,不疑何卜?」遂败郧师于蒲骚,卒盟而还。
从上面的分析和战例中,我们可以看到,占卜只不过是一种辅助的手段,是一样工具。好比人的外衣服。夏天快到了,谁还会穿棉大衣呢?每样工具都有特定的用途。战争中占卜的用途不是预知胜负,而是用于动员军队、振奋士气。
唐太宗李世民非常熟悉算命那一套,他四岁的时候,有个术士来他家里见到他,赞叹说「龙凤之姿,天日之表,年将二十,必能济世安民」。李渊大喜,给他取名「世民」。李世民十八岁的时候鼓动李渊起兵,李渊对他说,将来如果事成,你来当太子!还给李世民的长子取名「承乾」,给人一种钦定的感觉!
后来李世民率唐军南征北讨,在虎牢关以少胜多,一举击破郑王王世充与夏王窦建德的联军。从此有余力与李唐争雄的军阀都被消灭殆尽。李世民也因军功被封为史无前例的「天策上将」,为百官之首,可以自行组建领导班子「天策府」,俨然一个小朝廷。可是李渊却迟迟不肯立李世民为太子,李世民与李建成李元吉的矛盾日益激化,终于决定发动玄武门之变先下手为强,事先找人占吉凶。张公谨劝谏说:
「凡卜筮者,将以决嫌疑,定犹豫,今既事在不疑,何卜之有?纵卜之不吉,势不可已,愿大王思之。」
就算占卜结果不吉,我们难道就坐以待毙?如今箭在弦上不得不发,占卜又有何用!
李世民登基后,有次与卫国公李靖谈论兵法
太宗曰:「阴阳术数,废之可乎?」
靖曰:「不可。兵者,诡道也。托之以阴阳术数,则使贪使愚,兹不可废也。」
这一段就说得非常明白了,出征前占卜就是拿来忽悠那些不懂军事的人啊,他们拿衣服、图森破,给他们分析军事形势是对牛弹琴,不如直接告诉他们「老天爷已经研究决定了,这场战争我们能赢」更有效更直接。
当然,占卜结果不会事事吉利。周武王伐纣,路上大风吹折了军旗,雷声吓死了武王马车的御者。周公说今年逆太岁,占卜都是凶兆,还是回到我们的西方国家去吧。姜太公把他怒斥了一番,说商纣无道,杀害贤良,任用谗臣,失了天命人心,如今我们顺应历史的进程,征战必胜,你那些朽骨枯草懂什么?于是下令烧掉龟壳蓍草,继续前行。后来的事情我们都知道了,周朝延续了八百年,充分说明武王的自我奋斗、姜太公的决定权,都是很重要的!
清初噶尔丹叛乱,康熙命李文贞占卜,得复卦,上爻发动,爻辞「凶,有灾眚。用行师终有大败,以其国君凶,至于十年不克征」,脸都白了。康熙大笑说:
今噶尔丹背天犯顺,自蹈危机,兆乃应彼,非应我也。
于是亲自率军三次征讨噶尔丹,果然连获大捷。
如果你没有得到主帅的授意,自己私下起占卜成败还到处说,抓起来砍了。这是军令,不要拿自己的小命开玩笑。
今人研习卜筮,往往容易陷入命定与改命的怪圈,在这种小细节上纠结,得不偿失。古人的思想觉悟比你们不知道高到哪里去了。
]]>右上角加号上的1表示浏览器当前页面有一个RSS源,点加号就可以订阅它。第一行的「岁月如歌」是我的博客,右侧的1表示更新了一篇新日志。
目前该插件有一个BUG,Android目录下的某个博客的所有文章,每次打开浏览器都被识别为新文章。没研究过是怎么回事。
]]>当然会影响啦。只是影响力大小不一,比如唐太宗李世民的「世民」二字就是算命先生钦点的:龙凤之姿,天日之表,其年将二十,必能济世安民矣。算命先生走后,李渊怕走漏风声,派人追杀。没想到这家伙跑得比西方记者还快,到处都找不着他,只能作罢。
不过,后来李世民受封天策上将,乃至夺权称帝,都是靠自我的奋斗,顺应了历史的进程,这些就不关算命先生什么事了。
李世民曾与卫国公李靖有一段对话:
太宗曰:阴阳术数,废之可乎?
靖曰:不可。兵者,诡道也。托之以阴阳术数,则使贪使愚,兹不可废也。
上文中的阴阳术数就是现在俗称的算命,对统治者来说,这只是忽悠愚民的利器。还记得陈胜吴广是怎么起义的吗?把「陈胜王」三个字写在白绸上塞进鱼肚子,伪装成是天意。
更早的西汉末年,曾发生过一件极为讽刺的事。当时有算命先生算出一句谶言:刘秀当为天子。
可是当时有两个刘秀(也许有其他平民也叫这名字,不过登上历史舞台的就这两位),一个是汉中山靖王刘胜之后,由于汉武帝施行推恩令,到刘秀这一代已经失去了爵位,成了地道的农民,天天下地干农活。当他听说这个预言时,开玩笑说:何用知非仆邪?你们怎么知道不是我?
另一个刘秀是当朝国师、经学大儒刘歆(xin),在数学上也有贡献,算出圆周率是3.15471。他为避汉哀帝刘欣的讳改名为刘秀。听到谶言后打算诛杀王莽,事败被杀。
真是成也谶言,败也谶言。
统治者用算命建立威望、聚拢人心,易学家用算命精研易理,算命先生用算命发财,普通人用算命寻找心理寄托。普通人被算命坑的例子就更多了,数不胜数。真正能从中受益的,一百个里有一个,已经很不错了。知乎上有个问题:有哪些东西是 1949 年后才有的,却常被我们当做传统文化?其实在算命方面也有许多内容,是现代人创造,谎称传统。各位想算命的还是慎重。
]]> (一)
在很多人心目中,武侠小说非但不是文学,甚至也不能算是小说,对一个写武侠小说的人说来,这实在是件很悲哀的事情,幸好还有一点事实是任何人都不能否认的──一样东西如果能存在就一定有它存在的价值。
武侠小说不但存在,而且已存在了很久!
关于武侠小说的源起,一向有很多种不同的说法:“从太史公的《游侠列传》开始,中国就有了武侠小说。”这当然是其中最堂皇的一种,可惜接受这种说法的人并不多。
因为武侠小说是传奇的,如果一定要将它和太史公那种严肃的传记文学,相提并论,就未免有点自欺欺人了。
在唐人的小说笔记中,才有些故事和武侠小说比较接近。
《唐人说荟》卷五,张鷟的《耳目记》中,就有段故事是非常“武侠”的。
“隋末,深州诸葛昂,性豪侠,渤海高瓒闻而造之,为设鸡肫而已,瓒小其用,明日大设,屈昂数十人,烹猪羊等长八尺,薄饼阔丈余,裹馅粗如庭柱,盘作酒碗行巡,自作金刚舞以送之。昂至后日,屈瓒所屈客数百人,大设,车行酒,马行炙,挫碓斩脍,硙砾蒜齑,唱夜又歌狮子舞。瓒明日,复烹一双子十余岁,呈其头颅手足,座客皆喉而吐之。
昂后日报设,先令美妾行酒,妾无故笑,昂叱下,头臾蒸此妾坐银盘,仍饰以脂粉,衣以锦绣,遂擘腿肉以啖,瓒诸人皆掩目,昂于奶房间撮肥肉食之,尽饱而止。瓒羞之,夜遁而去。”
这段故事描写诸葛昂和高瓒的豪野残酷,已令人不可思议,这种描写的手法,也已经很接近现代武侠小说中比较残酷的描写。
但这故事却是片断的,它的形式和小说还是有段很大的距离。当时民间的小说、传奇、平话、银字儿中,也有很多故事是非常“武侠”的,譬如说,盗盒的红线、昆仑奴、妙手空空儿、虬髯客,这些人物就几乎已经是现在武侠小说中人物的典型。
武侠小说中最主要的武器是剑,关于剑术的描写,从唐时已比现代武侠小说中描写得更神奇。
红线,大李将军,公孙大娘……这些人的剑术,都已被渲染得接近神话,杜甫的《观公孙大娘弟子舞剑器行》,其中对公孙大娘和她弟子李十二娘剑术的描写当然更生动而传神。
号称“草圣“的唐代大书法家,也曾自言:“始吾闻公主与担夫争路,而得笔法之意,后见公孙氏舞剑器,而得其神。”
“剑器”虽然不是剑,但其中的精髓却无疑是和剑术一脉相连的,由此可见,武侠小说中关于剑术和武功的描写,并非无根据。
这些古老的传说和记载,点点滴滴,都是武侠小说的起源,再经过民间的平话、弹词、和说书的改变,才渐渐演变成现在的这种型式。
(二)
《彭公案》、《施公案》、《七侠五义》、《小五义》和《三侠剑》就都是根据“说书”而写成的,已可算是我们这一代所能接触到的,最早的一批武侠小说。
可是这种小说中的英雄,大都不是可以令人热血沸腾的真正英雄,因为在清末那种社会环境里,根本就不鼓励人们做英雄,老成持重的君子,才是一般人认为应该受到表扬的。
这至少证明了武侠小说的一点价值……从一本武侠小说中,也可以看到作者当时的时代背景。
现代的武侠小说呢?
我有很多朋友都是智慧很高,很有文学修养的人,他们往往会对我道:“我从来没有看过武侠小说,几时送一套你认为最得意的给我,让我看看武侠小说里写的究竟是什么。”
我笑笑。
我只能笑笑,因为我懂得他们的意思。
他们认为武侠小说并不值得看,现在所以要看,只不过因为我是他们的朋友,而且有一种好奇。
他们认为武侠小说的读者,绝不会是他们那阶层的人,绝不会是思想新颖的高级知识分子。
他们嘴里虽说要看,其实心里早已否认了武侠小说的价值。
而他根本就没有看过武侠小说,根本就不知道武侠小说写的是什么。
我不怪他,并非因为武侠小说的确给了人一种根深蒂固的观念,使人认为就算不看也能知道它的内容。
因为武侠小说的确已落入了一些固定的形式。
──一个有志气”,“天赋异禀”的少年,如何去辛苦学武,学成后如何去扬眉吐气,出人头地。
这段经历中当然包括了无数次神话般的巧合与奇遇,当然也包括了一段仇恨,一段爱情,最后是报仇雪恨,有情人成了眷属。
──一个正直的侠客,如何运用他的智慧和武功,破了江湖中一个规模庞大的恶势力。
这位侠客不但“少年英俊,文武双全”,而且运气特别好,有时甚至能以“易容术”化妆成各式各样的人,连这些人的至亲好友,父母妻子都辨不出他的真伪。
这种写法并不坏,其中的人物有英雄侠士、风尘异人、节妇烈女,也有枭雄恶霸、荡妇淫娃、奸险小人,其中的情节一定很曲折离奇,紧张刺激,而且很香艳。
只可惜这种型式已写得太多了些,已成了俗套,成了公式,而且通常都写得太荒唐无稽,太鲜血淋淋,却忘了只有“人性”才是小说中不可缺少的。
人性并不仅是愤怒、仇恨、悲哀、恐惧,其中包括了爱与友情,慷慨与侠义,幽默与同情。
我们为什么要特别着重其中丑恶的一面?
(三)
我们这一代的武侠小说,如果真是由平江不肖生的《江湖奇侠传》开始,至还珠楼主的《蜀山剑侠传》到达巅峰,至王度卢的《铁骑银瓶》和朱贞木的《七杀碑》为一变,至金庸的《射鵰英雄传》又一变,到现在又有十几年了,现在无疑又已到了应该变的时候!
要求变,就得求新,就得突破那些陈旧的固定形式,尝试去吸收。
《战争与和平》写的是一个大时代中的动乱,和人性中善与恶的冲突,《人鼠之间》写的却是人性的骄傲和卑贱,《国际机场》写的是一个人如何在极度危险中重新认清自我,《小妇人》写的是青春与欢乐,《老人与海》写的是勇气的价值,和生命的可贵。
这些伟大的作家们,用他们敏锐的观察力,丰富的想象力,和一种悲天悯人的同情心,有力的刻划出人性,表达出他们的主题,使读者在悲欢感动之余,还能对这世上的人与事,看得更深,更远些。
这样的故事,这样的写法,武侠小说也同样可以用,为什么偏偏没有人用过?
谁规定武侠小说一定要怎么样,才能算“正宗”!
武狭小说也和别的小说一样,要能吸引人,能振奋人心,激起人心的共鸣,就是成功的!
有很多人都认为当今小说最蓬勃兴旺的地方,不在欧美,而在日本。
因为日本小说不但能保持它自己的悠久传统和独有趣味,还能吸收。
它吸收了中国的古典文学,也吸引了很多种西方思想。
日本作者能将外来文学作品的精华融会贯通,创造出一种新的民族风格的文学。武侠小说的作者为什么不能?
武侠小说既然也有自己悠久的传统和独特的趣味,若能再尽量吸收其它文学作品的精华,岂非也同样能创造出一种新的风格,独立的风格,让武侠小说也能在文学的领域中占一席地,让别人不能否认它的价值,让不看武侠小说的人也来看武侠小说!
这就是我们最大的愿望。
现在我们的力量也许还不够,但我们至少应该向这条路上走去,摆脱一切束缚往这条路上走去。
现在我们才起步虽已迟了些,却还是不太迟!
(一)
作为一个作家,总是会觉得自己像一条茧中的蛹,总是想要求一种突破,可是这种突破是需要煎熬的,有时候经过了很长久很长久的煎熬之后,还是不能化为蝴蝶,化作蚕,更不要希望练成丝了。
所以有许多作家困死在茧中,所以他们常常酗酒、吸毒、逃避、自暴自弃,甚至会把一根“雷明顿”的散弹猎枪含在喉咙里,用一根本来握笔的手指扳开枪擎口扣下扳机,把他自己和他的绝望同时毁灭。
创作是一件多么艰苦的事,除了他们自己之外恐怕很少有人能明白。
可是一个作家只要活着就一定要创作,否则他就会消失。
无声无息的消失就不如轰轰烈烈的毁灭了。
所以每一个作家都希望自己能够有一种新的突破,新的创作。对他们来说,这种意境简直已经接近“禅”与“道”。
在这段过程中,他们所受到的挫折辱骂与讪笑,甚至不会比唐三藏在求经的路途中所受的挫折和苦难少。
宗教、艺术、文学,在某一方面来讲是殊途同归的。在他们求新求变的过程中,总是免不了会有一些痛苦的煎熬。
(二)
作为一个已经写了二十五年武侠小说,已经写了两千余万字,而且已经被改编为两百多部武侠电影的作者来说,想求新求变,想创作突破,这种欲望也许已经比一个沉水的溺者,想看到一根浮木的希望更强烈。
只可惜这种希望往往是空的。
所以溺者死,作者亡,也是一件很平常的事,他们不死不亡的几率通常都不超过千分之一。
《风铃中的刀声》绝不是一条及时赶来的救援船,更不会是一块陆地。我最多只不过希望它是一根浮木而已,最多只不过希望它能带给我一点点生命上的绿意。
(三)
有一夜,在酒后,和倪匡兄,闲聊之中我忽然想起来这个名字。聊起来,故事也就来了,那时候谁也不知道这个故事是个什么样的故事,只不过有点故事的影子而已。有一天,酒后醉,醉后醒。这个故事的影子居然成了一点形。
然后在床上,在浴中,在车里,在樽边,在我还可以思想的时候,这个故事就好像一只蛹忽然化作了蝴蝶。
蝴蝶也有很多种,有的美,有的丑,有的平凡,有的珍贵。
这只蝴蝶会是一只什么样的蝴蝶?
谁知道?
(四)
有一夜,有很多朋友在我家里喝酒,其中有编者、有作家、有导演、有明星、有名士、有美人,甚至还有江湖豪客、武术名家。
我提议玩一种游戏,一种很不好玩的游戏。
我提议由一个人说一个名词,然后每个人都要在很短的时间里说出他们认为和那个名词有关的另外三个名词。
譬如说:一个人说出来的名词是“花生”。
另外一个人联想到三个名词就是“杰美·卡特”、“青春痘”、“红标米酒”。
那一天我提出来的是:“风铃”。
大家立刻联想到的有:
秋天、风、小孩的手、装饰、钉子、等待、音乐匣、悠闲、屋檐下、离别、幻想、门、问题、伴侣、寂寞、思情、警惕、忧郁、回忆、怀念……
在这些回答中有很多是会很容易就会和风铃联想到一起的,有一些回答却会使别人觉得很奇突,譬如说钉子。“你怎么会把钉子和风铃联想到一起?”我问那个做出这个回答的人。
这一次他的回答更绝:“没有钉子风铃怎么能挂得住?”小孩的手呢?小孩的手又和风铃有什么关系?
回答的人说:“你有没有看见过一个小孩在看到风铃时不用手去玩一玩的?”
“你呢?”他们问我:“你对于风铃的联想是什么?”
“我和你们有点不同。”我说:“大概是因为我是一个写小说的,而小说所写的总是人,所以我对每一件事情每一样东西联想到的都是人。”
“这次你联想到的是一些什么人?”
“浪子、远人、过客、离夫。”我忽然又说:“这次我甚至会联想到马蹄声。”
“马蹄声?风铃怎么会让你联想到马蹄声?”
我给他们的是三行在新诗中流传极广的名句:
我答答的马蹄,
是个美丽的错误,
我不是归人,是个过客。
(五)
一个寂寞的少妇独坐在风铃下,等待着她所思念的远人归来,她的心情多么凄凉多么寂寞。
在这种情况下,每一种声音都会带给她无穷的幻想和希望,让她觉得远人已归。
等到她的希望和幻想破灭时,虽然会觉得哀伤痛苦,但是那一阵短短的希望毕竟还是美丽的。
所以诗人才会说:“是个美丽的错误”。
如果等到希望都没有的时候,那才是真正的悲哀。
在这一篇“风铃中的刀声”中,一开始我写的就是这么样的一个故事。
这个故事里当然也有刀。
(六)
一刀挥出,刀锋破空,震动了风铃。凄厉的刀声衬得风铃声更优雅美丽,这种声音最容易撩起人们的相思。
相思中的人果然回来了,可是他的归来却又让所有的希望全部碎灭。
这是个多么残酷的故事,不幸的是真实有时比故事残酷。
于是思念就变成了仇恨,感怀就变成了怨毒。
于是血就要开始流了。
“为什么武侠小说里总是少不了要有流血的故事?”有人问我。
“不是武侠小说里少不了要有流血,而是人世间永远都避免不了这样的事。”我说:“在这个世界上每一个角落里,随时随刻都可能有这一类的事发生。”
“这种事难道就永远不能停止?”
“当然可以阻止。”我说:“只不过要付出很大的代价而已。”
我又补充:“这种代价虽然每个人都可以付出,但却很少有人愿意付出。”
“为什么?”
“因为要付出这种代价就要牺牲。”
“牺牲什么?”
“牺牲自己。”我说:“抑制自己的愤怒,容忍别人的过失,忘记别人对自己的伤害,培养自己对别人的爱心。在某些方面来说,都可以算是一种自我牺牲。”
“我明白了。”问我话的朋友说:“这个世界上的血腥和暴力一直很难被阻止,就因为大多数人都不愿意去管这种事。”
他的神情严肃而沉痛:“因为要牺牲任何事都很容易,要牺牲自己却是非常困难。”
“是的。”
我也用一种同样严肃而沉痛的表情看着我的朋友,用一种仿佛风铃的声音对他说:
“可是如果你认为这个世界上已经没有愿意牺牲自己的人,那你就完全错了。”
我的朋友笑了,大笑!
我也笑。
(七)
我笑,是因为我开心,我开心是因为我的朋友都知道,武侠小说里写的并不是血腥与暴力,而是容忍、爱心与牺牲。
我也相信这一类的故事也同样可以激动人心。
现代的社会越来越复杂,越来越现实。
现代人随时随地都会遭受到各式各样的约束。
可是以前不同。
“过去的日子都是好日子”,这句话我并不赞成。
可是过去的确有过好日子。
在现代的西方,你就算明知一个人是杀人犯,明知他杀了你的兄弟妻子,假如没有确实的证据,你也只有眼看着他逍遥法外。
因为你若想“以牙还牙,以血还血”,你去杀了他,那么你也变成一个杀人犯。
“报复”并不是种很好的法子,只不过那至少总比让恶人逍遥法外的好。
在以前某一种时代里,是不会有这种事的。
那是种很痛快的时代,快意恩仇,敢爱敢恨,善有善报,恶有恶报。
用不着老天替你报,你自己就可以报复。
我写的就是那种时代。
我写的就是那种时代中的江湖人。
在那种时代中,江湖中有各式各样的人。
有大侠,也有大盗;有镖客,也有刺客;有义士,也有隐士;有神偷,也有神捕;有侠女,也有妓女;有市井匹夫,也有世家子弟。
他们的生活通常都是多彩多姿的,充满了冒险和刺激。
有很多人对他们憎恶厌恨,也有很多人羡慕他们。
因为他们通常都衣着光鲜,出手豪阔,大碗喝酒,大块吃肉。
只可惜这只不过是他们快乐的一面──
他们还有另一面。
痛苦的一面。
神捕捉了神偷,设宴庆功,大吃大喝,喝得半死为止。
大盗捞了一票,分一点给穷人,自己去花天酒地,把钱花光为止。
大侠有名有势,不管走到哪里去,都会受到别人的尊敬和欢迎。
世家子弟们从小锦衣玉食,要什么有什么。
这种生活确实是值得羡慕的,可是你有没有看见过他们的另一面?
他们也有他们的寂寞和痛苦。
夜深人静,从大醉中醒来,忽然发现躺在自己旁边的是个自己连名字都不知道的人。
这种滋味你有没有尝试过?
在欢呼和喝彩声中,一个人回到家里,面对着漆黑的窗户,只希望快点天亮。
这种心情你有没有想到过?
今宵花天酒地,狂欢极乐,却连自己明日会在什么地方都不知道。
甚至连今宵酒醉在何地都不知道。
杨柳飞舞,晓风残月,这种意境虽然美,却又美得多么凄凉,多么让人心碎?
这种欢乐,你愿不愿意享受?
假如你要什么就有什么,这人生中还有什么是值得你去追求的?
这种空虚有谁知道?
我知道。
因为我也是个江湖人,也是个没有根的浪子,如果有人说我这是在慢性自杀,自寻死路,那只因为他不知道──
不知道我手里早已有了杯毒酒。
当然是最好的毒酒。
武侠小说中写的本就是江湖人,可是我现在想写的却有点不同。
我想写一系列的故事,每篇故事都以一个典型的代表人物为中心。
我想写他们的快乐,也要写他们的痛苦。
我想让他们来做一面镜子,让大家都可以从这面镜子中看出自己应该怎么做。
无论如何,他们总是可爱的人。
因为他们敢爱敢恨,敢哭敢笑,因为他们讲义气、有原则。
人生毕竟也是可爱的。
人活着,就应该懂得怎么去享受生命,怎么去追寻快乐。
一个人脸上若是脏了,是不是要去照照镜子才知道怎样去擦掉?
我只希望这面镜子也能做到这一点,能够帮助人擦掉生命中的污垢。
我真的希望每个人的人生都能变得很快乐。
一九七四、十、二七、黎明。
在很多人心目中,武侠小说非但不是文学,甚至也不能算是小说。对一个写武侠小说的人说来,这实在是件很悲哀的事。幸好还有一点事实是任何人都不能否认的—一一样东西如果能存在,就一定有它存在的价值。
武侠小说不但存在,而且已存在了很久!
关于武侠小说的起源,一向有很多种不同的说法:“从太史公的游侠列传开始,中国就有了武侠小说。”这当然是其中最堂皇的一种,可惜接受这种说法的人并不多。
因为武侠小说是传奇的,如果一定要将它和太史公那种严肃的传记文学相提并论,就未免有点自欺欺人了。
在唐人的小说笔记中,才有些故事和武侠小说比较接近。
《唐人说荟》卷五,张鷟的《耳目记》中,就有段故事是非常“武侠”的。
“隋末,深州诸葛昂,性豪侠,渤海高瓒闻而造之,为设鸡肫而已,瓒小其用,明日大设,屈昂数十人,烹猪羊等长八尺,薄饼阔丈余,里裹粗如庭柱,盘作酒碗行巡,自作金刚舞以送之。
“昂至后日,高瓒所屈客数百人,大设,车行酒,马行炙,挫椎斩脍,硙轹蒜齑,唱夜叉歌狮子舞。
“瓒明日,复烹一双子十余岁,呈其头颅手足,座客皆喉而吐之。
“昂后日报设,先令美妾行酒,妾无故笑,昂叱下,须臾蒸此妾坐银盘,仍饰以脂粉,衣以锦绣,遂擘腿肉以啖,瓒诸人皆掩目,昂于奶房间撮肥肉食之,尽饱而止。
“瓒羞之,夜遁而去。”
这段故事描写诸葛昂和高瓒的豪野残酷,已令人不可思议。这种描写的手法,也已经很接近武侠小说中比较残酷的描写。
但这故事却是片段的,它的形式和小说还是有段很大的距离。
当时民间的小说、传奇、评话、银字儿中,也有很多故事是非常“武侠”的,譬如说,盗盒的红线、昆仑奴、妙手空空儿、虬髯客,这些人物就几乎已是现代武侠小说中人物的典型。
武侠小说中最主要的武器是剑,关于剑术的描写,从唐时就已比现代武侠小说中描写得更神奇。
红线、大李将军、公孙大娘……这些人的剑术,都已被渲染得接近神话杜甫的《观公孙大娘弟子舞剑器行》,其中对公孙大娘和她弟子李十二娘剑术的描写,当然更生动而传神!
号称“草圣”的唐代大书法家也曾自言:“始吾闻公主与担夫争路,而笔法之意,后见公孙氏舞剑器,直得其神。”
“剑器”虽然不是剑,但其中的精髓却无疑是和剑术一脉相通的。由此可见,武侠小说中关于剑术和武功的描写,并非全无根据。
这些古老的传说和记载,点点滴滴,都是武侠小说的起源,再经过民间评话、弹词和说书的改变,才渐渐演变成现在的这种形式。
(二)
《彭公案》、《施公案》、《七侠五义》、《小五义》、就是根据“说书”而写成的,已可算是我们这一代所能接触到的,最早的一种武侠小说。
可是这种小说中的英雄,大都不是可以令人热血沸腾的真正英雄,因为在清末那种社会环境里,根本就不鼓励人们做英雄,老成持重的君子,才是一般人认为应该受到表扬的。
这至少证明了武侠小说的一点价值——从一本武侠小说中,也可以看到作者当时的时代背景。
现代的武侠小说呢?
(三)
现代的武侠小说,若由平江不肖生的《江湖奇侠传》开始算起,大致可以分成三个时代。
写《蜀山剑侠传》的还珠楼主,是第一个时代的领袖。写《七杀碑》的朱贞木,写《铁骑银瓶》的王度庐可以算是第二个时代的代表。
到了金庸写《射雕》,将武侠小说带进了另一个局面。
这个时代,无疑是武侠小说最盛行的时代,写武侠小说的人,最多时曾经有三百个。
就因为武侠小说已经写得太多,读者们也看得太多,所以有很多读者看了一部书的前两本,就已经可以预测到结局。
最妙的是,越是奇诡的故事,读者越能猜到结局。
因为同样“奇诡”的故事已被写过无数次了。易容、毒药、诈死,最善良的女人就是“女魔头”——这些圈套都已很难令读者上钩。
所以情节的诡奇变化,已不能再算是武侠小说中最大的吸引力。
但人性中的冲突却是永远有吸引力的。
武侠小说中已不该再写神,写魔头,已应该开始写人,活生生的人,有血有肉的人!
武侠小说中的主角应该有人的优点,也应该有人的缺点,更应该有人的惑情。
写《包法利夫人》的大文豪福楼拜尔曾经夸下句海口,他说:“十九世纪后将再无小说。”
因为他认为所有的故事情节,所有的情感变化,都已被十九世纪的那些伟大的作家写尽了。
可是他错了。
他忽略了一点!
纵然是同样的故事情节,你若从不同的角度去看,写出来的小说就是完全不同的。
人类的观念和看法,本就在永不停地改变!随着时代改变!
武侠小说写的虽然是古代的事,也未尝不可注入作者自己新的观念。
因为小说本就是虚构的!
写小说不是写历史传记。写小说最大的目的,就是要吸引读者,感动读者。
武侠小说的情节若已无法改变,为什么不能改变一下,写人类的情感,人性的冲突,由情感的冲突中,制造高潮和动作。
应该怎样来写动作,的确也是武侠小说的一大难题。
我总认为“动作”并不一定就是“打”!
小说中的动作和电影画面的动作,可以给人一种生猛的刺激,但小说中瞄写的动作就是没有电影画面中这种鲜明刺激的力量。
小说中动作的描写,应该是简单的,短而有力的,虎虎有生气的,不落俗套的。
小说中动作的描写,应该先制造冲突,情感的冲突,事件的冲突,尽力将各种冲突堆构成一个高潮。
然后你再制造气氛,紧张的气氛,肃杀的气氛。
用气氛来烘托动作的刺激。
武侠小说毕竟不是国术指导。
武侠小说也不是教你如何去打人杀人的!
血和暴力,虽然永远有它的吸引力,但是太多的血和暴力,就会令人反胃了。
(四)
最近我的胃很不好,心情也不佳,所以除了维持《七种武器》和《陆小凤》两个连续性的故事外,已很久没有开新稿。
近月在报刊上连载的《历劫江湖》和《金剑残骨令》,都是我十五年前的旧书。我并不反对把“旧书新登”,因为温故而知新,至少可以让读者看到一个作家写作路线的改变!
《天涯·明月·刀》,是我最新的一篇稿子。我自己也不知道它是不是能给读者一点“新”的感受,我只知道我是在尽力朝这个方向走!
每在写一篇新稿之前,我总喜欢写一点自己对武狭小说的看法和感想,零零碎碎已写了很多。抛砖引玉,我希望读者们也能写一点自己的感想,让武侠小说能再往前走一步。
走一大步。
古 龙
一九七四、四、十七、夜、夜深。
有一天我在台湾电视公司看排戏,排戏的大都是我的朋友,我的朋友们大都是很优秀的演员。
其中有一位不但是个优秀的演最,也是个优秀的剧作者,优秀的导演,曾经执导过一部出色而不落俗套的影片,在很多影展中获得彩声。
这么样一个人,当然很有智慧,很有文学修养,他忽然对我说:“我从来没有看过武侠小说,几时送一套你认为最得意的给我,让我看看武侠小说里写的究竟是些什么。”
我笑笑。
我只能笑笑,因为我懂得他的意思。
他认为武侠小说并不值得看,现在所以要看,只不过因为我是他的朋友,而且有一点好奇。
他认为武侠小说的读者绝不会是他那一阶层的人,绝不会是思想新颖的高级知识分子。
他嘴里说要看看,其实心里却早已否定了武侠小说的价值。
而他根本就没有看过武侠小说,根本就不知道武侠小说写的究竟是什么。
我不怪他也,并非因为他是我的朋友,所以才不怪他,而是因为武侠小说的确给予别人一种根深蒂固的观念,使人认为就算不看也能知道它的内容。
有这种观念的并不止他一个,很多人都对我说过同样的话。说话时的态度和心理也几乎完全相同。
因为武侠小说的确已落入了固定的形式。
武侠小说的形式大致可以分为几种:
一个有志气,而“天赋异禀”的少年,如何去辛苦学武,学成后如何扬眉吐气,出入头地。
这段历程中当然包括了无教次神话般的巧合与奇遇,当然,也包括了一段仇恨,一段爱情,最后是报仇雪恨,有情人终成了眷属。
一个正直的侠客,如何运用他的智慧和武功,破了江湖中一个为非作歹,规模庞大的恶势力,这位侠客不但“少年英俊,文武双全”,而且运气特别好,有时他甚至能以“易容术”化妆成各式各样的人,连这些人的至亲好友,父母妻子都辨不出真伪。
这种写法并不坏,其中的人物包括了英雄侠士,风尘异人,节妇烈女,也包括枭雄恶霸,歹徒小人,荡妇淫娃。
所以这种故事一定曲折离奇,紧张刺激,而且还很香艳。
这种形式并不坏,只可惜写得太多了些,已成了俗套,成了公式,假如有人将故事写得更奇秘些,就会被认为是“新”,故事的变化多些,就会被认为是在“变”,其实却根本没有突破这种形式。
“新”与“变”并不是这意思。
“红与黑”写的是一个少年如何引诱别人妻子的心理过程。“国际机场”写的是一个人如何在极度危险中如何重新认清自我,“小妇人”写的是青春与欢乐,“老人与海”写的是勇气和价值,以及生命的可贵,“人鼠之间”写的是人性的骄傲和卑贱……
这些伟大的作家们,因他们敏锐的观察力和丰富的想像力,有力的刻画出人性,表达了他们的主题,使读者在为他们书中的人物悲欢感动之余,还能对这世上的人与事,看得更深些,更远些。
他们表现的方式往往令人拍案叫绝。
这么样的故事,这么样的写法,武侠小说也一样可以用,为什么偏偏没有人写过?
谁规定武侠小说一定要怎么样写,才能算正宗的武侠小说?
武侠小说也和别的小说一样,只要你能吸引读者,使读者被你的人物故事所感动,你就算成功。
有一天我遇见了一个我很喜欢的女孩子,她读的书并不多,但却不笨。
当她知道我是个“作家”时,她眼睛里立刻发出了光,立刻问我:“你写的是什么小说?”
我说谎,却从不愿在我喜欢的人面前说谎,因为世上绝没有一个人的记忆力能好得始终能记得自己的谎言,我若喜欢她,就难免要时常和她相处,若时常相处,谎言就一定会被拆穿。
所以我说:“我写的是武侠小说。”
她听了之后,眼睛里那种兴奋而关怀的光辉立刻消失。
我甚至不敢去看她,因为我早已猜出了她会有什么样的表情。
过了很久,她才带着几分歉意告诉我:“我从不看武侠小说。”
直到我跟她很熟之后,我才敢问她:“为什么不看?”
她的回答使我很意外。
她说:“我看不懂。”
武侠小说本是通俗的,为什么会使人觉得看不懂?
我想了很久,才想通。
她看不懂的是武侠小说中那种“自成一格”的对话,那种繁复艰涩的招式名称,也看不懂那种四个字一句,很有“古风”的描写字句。
她奇怪,武侠小说为什么不能将文字写得简单明了些?为什么不将对话写得比较生活化些,比较有人情味?
我只能解释:“因为我们写的是古时的事,古代的人物。”
她立刻追问:“你怎么知道古时的人说话是什么样子的?你听过他们说话吗?”
我怔住,我不能回答!
她又说:“你们难道以为像平剧和古代小说中那种对话,就是古代人说话的方式?就算真的是,你们也不必那么样写呀,因为你们写小说的最大目的,就是要人看,别人若看不懂,就不看,别人不看,你们写什么?”
她说话的技巧并不高明,却很直接。
她说的道理也许并不完全对,但至少有点道理。
写小说,当然是给别人看的,看的人越多越好。
武侠小说当然有人看,但武侠小说的读者,几乎也和武侠小说本身一样,范围太窄,不看武侠小说的人,比看的人多得多。
我们若要争取更多的读者,就要想法子要不看武侠小说的人也来看武侠小说,想法子要他们对武侠小说的观念改变。
所以我们就要新,就要变!
要新,要变,就要尝试,就要吸收。
有很多人都认为当今小说最蓬勃兴旺的地方,不在欧美,而在日本。
因为日本的小说不但能保持它自己的悠久传统,还能吸收。
它吸收了中国的古典文学,也吸收了很多种西方思想。
日本作者先能将外来文学作品的精华融化贯通,创造出一种新的民族风格的文学,武侠小说的作者为什么不能。
有人说:“从太史公的游侠列传开始,中国就有了武侠小说。”
武侠小说既然也有自己悠久的传统,若能再尽量吸收其他文学作品的精华,总有一天,我们也能将武侠小说创造出一种新的风格,独立的风格,让武侠小说也能在文学的领域中占一席之地,让别人不能否认它的价值。
让不看武侠小说的人也来看武侠小说!
这就是我们最大的愿望。
现在我们的力量虽然还不够,但我们至少应该向这条路上去走,挣脱一切束缚往这条路上去走。
现在我们才起步虽已迟了些,却还不太迟!
一九六九.十二.二十一
刀不仅是一种武器,而且在俗传的十八般武器中排名第一。
可是在某一方面来说,刀是比不上剑的,它没有剑那种高雅神秘浪漫的气质,也没有剑的尊贵。
剑有时候是一种华丽的装饰,有时候是一种身分和地位的象征。
在某一种时候,剑甚至是一种权力和威严的象征。
刀不是。
剑是优雅的,是属于贵族的,刀却是普遍化的,平民化的。
有关剑的联想,往往是在宫廷里,在深山里,在白云间。
刀却是和人类的生活息息相关的。
人出世以后,从剪断他脐带的剪刀开始,就和刀脱不开关系,切菜、裁衣、剪布、理发、修须、整甲、分肉、剖鱼、切面、示警、扬威、正法,这些事没有一件可以少得了刀。
人类的生活里,不能没有刀,好像人类的生活里,不能没有米和水一样。
奇怪的是,在人们的心目中,刀还比剑更残酷更惨烈更凶悍更野蛮更刚猛。
刀有很多种,有单刀、双刀、朴刀、戒刀、锯齿刀、砍山刀、鬼头刀、雁翎刀、五凤朝阳刀、鱼鳞紫金刀。
飞刀无疑也是刀的一种,虽然在正史中很少有记载,却更增加了它的神秘性与传奇性。
正于“扁钻”是不是属于刀的一种呢?那就无法可考了。
李寻欢这个人物是虚构的,李寻欢的“小李飞刀”当然也是。
大家都认为这个世界上根本不可能有李寻欢这样的人物,也不可能有“小李飞刀”这样的武器。
因为这个人物太侠义正气,屈己从人,这种武器太玄奇神妙,已经脱离了现实。
因为大家所谓的“现实”,是活在现代这个世界中的人们,而不是李寻欢那个时代。
所以李寻欢和他的小李飞刀是不是虚构的并不重要,重要的是这个人物是否他能活在他的读者们的心里,是否能激起大家的共鸣,是不是能让大家和他共悲喜同欢笑。
本来谁也不知道李寻欢和他的飞刀究竟是什么样子的,可是经过电影的处理后,却使得他们更形象化,也更大众化了。
从某一种角度看大众化就是俗,就是从俗,就是远离文学和艺术。
可是我总认为在现在这么样一种社会形态中,大众化一点也没有什么不好。
那至少比一个人躲在象牙塔里独自哭泣的好。
有关李寻欢和他的飞刀的故事是一部小说,“飞刀·又见飞刀”这部小说,当然也和李寻欢的故事有密不可分的关系。
可是他们之间有很多完全不相同的地方。
──虽然这两个故事同样是李寻欢两代间的恩怨情仇,却是完全独立的。
──小李飞刀的故事虽然已经被很多次搬上银幕和萤光幕,但他的故事,却已经被写成小说很久了,“飞刀”的故事现在已经拍摄成电影了,小说却刚刚开始写。
这种例子就好像萧十一郎一样,先有电影才有小说。
这种情况可以避免很多不必要的枝节,使得故事更精简,变化更多。
因为电影是一种整体的作业,不知道要消耗多少人的心血,也不知道要消耗多少物力和财力。
所以写电影小说的时候,和写一般小说的心情是绝不相同的。
幸好写这两种小说还有一点相同的地方,总希望能让读者激起一点欢欣鼓舞之心,敌忾同仇之气。
我想这也许就是我写小说的最大目的之一。
──当然并不是全部目的。
还有一点我必须声明。
现在我腕伤犹未愈,还不能不停的写很多字,所以我只能由我口述,请人代笔。
这种写稿的方式,是我以前一直不愿意做的。
因为这样写稿常常会忽略很多文字上和故事上的细节,对于人性的刻划和感伤,也绝不会有自己用笔去写出来的那种体会。
最少绝不会有那种细致婉转的伤感,那么深的感触。
当然在文字上也会有一点欠缺的;因为中国文字的精巧,几乎就像是中国文人的伤感那么细腻。
幸好我也不必向各位抱歉,因为像这么样写出来的小说情节一定是比较流畅紧凑的,一定不会生涩苦闷冗长的毛病。
而生涩苦闷冗长一向是常常出现在我小说中的毛病。
古 龙
于病后,
非关病酒。不在酒后。
一九七七年二月十日夜
又是个新的尝试,因为武侠小说实在已经到了应该变的时候。
在很多人心目中,武侠小说非但不是文学,不是文艺,甚至也不能算是小说。正如蚯蚓,虽然也会动,却很少人将它当做动物。
造成这种看法的固然是因为某些人的偏见,但我们自己也不能完全推卸责任。
武侠小说有时的确写得太荒唐太无稽,太鲜血淋漓,却忘了只有“人性”才是每本小说中都不可缺少的。
人性并不仅是愤怒、仇恨、悲哀、恐惧,其中也包括了爱与友情、慷慨与侠义、幽默与同情的,我们为什么要去特别强调其中丑恶的一面呢?
还有,我们这一代的武侠小说约莫由平江不肖生的《江湖奇侠传》开始,至王度庐的《铁骑银瓶》和朱贞木的《七杀碑》为一变,至金庸的《射雕英雄传》又一变,到现在已又有十几年了。
这十几年中,出版的武侠小说已算不出有几千几百种,有的故事简直成了老套,成为公式,老资格的读者只要一看开头,就可以猜到结局。
所以武侠小说作者若想提高自己的地位,就得变;若想提高读者的兴趣,也得变。
有人说,应该从“武”,变到“侠”,若将这句话说得更明白些,也就是说武侠小说应该多写些光明,少写些黑暗;多写些人性,少写些血。
也有人说,这么样一变,武侠小说根本就变了质,就不是“正宗”的武侠小说了,有的读者根本就不愿意接受,不能接受。
这两种说法也许都不错,所以我们只有尝试,不短地尝试。我们虽然不敢奢望别人将我们的武侠小说看成文学,至少总希望别人能将它看成“小说”,也和别的小说有同样的地位,同样能振奋人心,同样能激起人心的共鸣。
每一小节几乎都是个独立的故事,即使分开来看,也不会减少它的趣味──如果它还有一点趣味,这尝试就不能算失败。
“白玉老虎”这故事,写的是一个人内心的冲突,情感与理智的冲突,情感与责任的冲突,情感与仇恨的冲突。
我总认为,故事情节的变化有穷尽时,只有情感的冲突才永远能激动人心。
这故事中主要写的是赵无忌这个人。
现在赵无忌内心的冲突已经被打成了一个结,死结。
所以这故事也应该告一段落。
(一)
在很多人心目中,武侠小说非但不是文学,甚至也不能算是小说,对一个写武侠小说的人说来,这实在是件很悲哀的事情,幸好还有一点事实是任何人都不能否认的──一样东西如果能存在就一定有它存在的价值。
武侠小说不但存在,而且已存在了很久!
关于武侠小说的源起,一向有很多种不同的说法:“从太史公的《游侠列传》开始,中国就有了武侠小说。”这当然是其中最堂皇的一种,可惜接受这种说法的人并不多。
因为武侠小说是传奇的,如果一定要将它和太史公那种严肃的传记文学,相提并论,就未免有点自欺欺人了。
在唐人的小说笔记中,才有些故事和武侠小说比较接近。
《唐人说荟》卷五,张鷟的《耳目记》中,就有段故事是非常“武侠”的。
“隋末,深州诸葛昂,性豪侠,渤海高瓒闻而造之,为设鸡肫而已,瓒小其用,明日大设,屈昂数十人,烹猪羊等长八尺,薄饼阔丈余,裹馅粗如庭柱,盘作酒碗行巡,自作金刚舞以送之。昂至后日,屈瓒所屈客数百人,大设,车行酒,马行炙,挫碓斩脍,硙砾蒜齑,唱夜又歌狮子舞。瓒明日,复烹一双子十余岁,呈其头颅手足,座客皆喉而吐之。
]]>
猪爷懒得开车,和老婆两个人坐着鸡爷的车去金华,我和老蔡乘高铁过去。鸡猪二爷出发晚,开车又慢。我们到金华时,他们车刚到萧山。毛驴开车来火车站接我们,没带家里的钥匙,父母又出门喝喜酒,只好带我们去市区找了家肯德基坐下聊天吃点东西。
直接在饭店与鸡爷等人碰头。终于见到猪爷的老婆,传说中的「默默」。大家吃吃聊聊到八点多。猪爷向我和老蔡传授相亲秘诀:如果你没有车,女方就不太想出去。如果你有辆好车,女方一看,车还不错,可以发展发展。默默在一边一个劲地附和,表示女生确实是这样的心态。猪爷的经历太奇葩,一律「日后再说」,我们两个老实人没法照搬。
送两位妹子回宾馆休息后,我们五个步行来到附近的网吧开黑五连坐。第一次玩Dota2就不知不觉超神。
第二天老蔡一大早起来去当伴郎,我们其他人中午一块找了家店吃午饭,又去逛了附近的燕尾洲公园。
到婚礼现场,毛驴正在绑丝带。毛驴的母亲也在,非常热情,开了两瓶啤酒和我们干了几杯。
上楼去看了新娘。和毛驴是高中同学,大一时经常来学校找他,还记得有次我打游戏很开心,边出门边大声说话,把刚上楼的她吓一跳。这次的中式红妆也非常漂亮。
等得无聊,和鸡猪爷夫妇一行五人又去网吧玩了两局。三连坐有点虚,一输一赢。网吧有个活动拍三张照片转发朋友圈就送饮料,照办后要了一杯菠萝橙汁。交差后就删了。
入场时想塞红包给毛驴,结果说本科同学的红包一律不收。我们六人坐了半桌,坐等婚礼开始。
毛驴即兴发表爱情宣言。
婚礼搞活动时被抽到,上台拿了几个玩具,全送给默默了。
结束后六人挤鸡爷的车来到金华火车站,放下我和老蔡坐高铁回杭。奇葩的火车站,高铁站居然在隔壁,没有直接通道,坐了电瓶车绕路三公里才来到这里。老板说刚载两个黑人过去。
再放一些近期拍的照片
东阳冬至必吃的食物,麻糍。方言叫「油麻袋」。
同德医院看病中
地铁施工的巨大吊臂
城站西侧的流浪者
与王DY见面当天,到早了,先在附近西湖逛了一番
与王DY见面后雅兴不减,独自撑伞夜游西湖,从一公园绕道断桥,一路走到浙大附中坐车回家。这是宝石山。
见面光顾着说话,吃得不多,横穿西湖后肚子饿,在肯德基吃点东西。
小区外的卖唱歌手,不是我喜欢的风格。
214,这天是我姐的生日。
2015年的第一场雪
只是一部历史评论集。
猪爷懒得开车,和老婆两个人坐着鸡爷的车去金华,我和老蔡乘高铁过去。鸡猪二爷出发晚,开车又慢。我们到金华时,他们车刚到萧山。毛驴开车来火车站接我们,没带家里的钥匙,父母又出门喝喜酒,只好带我们去市区找了家肯德基坐下聊天吃点东西。
以「如何看待《万万没想到西游篇》大电影在知乎上被众多大神唾弃的现象? - 张兆杰的回答」为例。
使用Chrome抓包发现,点击「等人赞同」链接时,浏览器向如下网址发送GET请求:
https://www.zhihu.com/answer/26236952/voters_profile
26236952是此答案对应的aid:
返回的JSON数据中有这么一项:
"next": "/answer/26236952/voters_profile?total=808&offset=10&follows=NYRY--TZ3-l5H4gXp5RoqQVv"
下拉滚动条,浏览器会继续发送GET请求:
https://www.zhihu.com/answer/26236952/voters_profile?total=808&offset=10&follows=NYRY--TZ3-l5H4gXp5RoqQVv
正好是next指向的网址。
每次GET请求返回的JSON数据都包含了10个点赞用户的信息,单个用户的信息如下:
<div class="zm-profile-card clearfix no-hovercard">
<div class="zg-right">
<button data-follow="m:button" data-id="6b500d547283f3cc186eafff2a8033b4" class="zg-btn zg-btn-follow zm-rich-follow-btn small nth-0">关注她</button>
</div>
<a title="薛定谔的喵"
data-tip="p$t$fang-ting-20-32"
class="zm-item-link-avatar"
target="_blank"
href="/people/fang-ting-20-32">
<img src="https://pic1.zhimg.com/52ae34056536f4088c7667890f14aaa8_m.jpg" class="zm-item-img-avatar">
</a>
<div class="body">
<div class="author ellipsis">
<a data-tip="p$t$fang-ting-20-32" href="https://www.zhihu.com/people/fang-ting-20-32" target="_blank" class="zg-link" title="薛定谔的喵">薛定谔的喵</a>
<span class="bio hidden-phone">世界那么大,我来看看!</span>
</div>
<ul class="status">
<li><span>13 赞同</span></li>
<li><span>6 感谢</span></li>
<li class="hidden-phone"><a href="/people/fang-ting-20-32/asks" target="_blank">0 提问</a></li>
<li class="hidden-phone"><a href="/people/fang-ting-20-32/answers" target="_blank">19 回答</a></li>
</ul>
</div>
</div>
由于0回答则必然0赞同0感谢,只须提取提问和回答数。
如果是匿名用户:
<div class="zm-profile-card clearfix no-hovercard">
<span class="zm-item-link-avatar">
<img title="匿名用户" class="zm-item-img-avatar" src="https://pic2.zhimg.com/aadd7b895_m.jpg"/>
</span>
<div class="body">
匿名用户
</div>
</div>
1. 答案对应的aid需要自己在网页源码中找
2. 如果需要获取完整的抓取结果,将下面save这一行取消注释
# 想看完整结果可以保存到csv文件,用Excel查看
# save(infos, aid)
3. 首次使用时需要运行 ZhiHuClient().login(username, password) 登录一次,以后不需要
4. 控制台输出如下:
总赞数:822,四零用户数:182,比例:0.22141119221411193
四零用户是指:0赞同、0感谢、0提问、0回答。有些知友万年潜水,从来不提问和回答,他们的数据也会是四个0。所以四零用户不等于水军号。而且现在养水军号一般会养上一段时间伪装成正常号再使用,很难判断。
改进空间:后来测了一下,发送GET请求的网址可以精简:
https://www.zhihu.com/answer/26236952/voters_profile?offset=10
只需改变offset值就能获取不同的信息,可以改成多线程爬取。
1 | #!/usr/bin/env python |
以「如何看待《万万没想到西游篇》大电影在知乎上被众多大神唾弃的现象? - 张兆杰的回答」为例。
使用Chrome抓包发现,点击「等人赞同」链接时,浏览器向如下网址发送GET请求:
https://www.zhihu.com/answer/26236952/voters_profile
26236952是此答案对应的aid:
书接上文:《纷争世事无穷尽》,昨晚写了简单的脚本,先在本机运行几天,排除各类Bug后再挂到SAE。
由于网址总数不多(一万七千左右),没用Bloom过滤器,直接使用txt保存+set查重复。SAE上貌似不能直接创建文件,得改成使用数据库。另外SAE只支持Python 2.7,还得改一些语法。
忽略已存在的提问,仅扫描并举报新出现的提问。为免爬得太快被封,仅使用单线程。
脚本中一共有三个类:
ZhiHuClient().login(username, password)
方法登录并生成Cookies,人肉识别验证码。以后就可以直接用Cookies登录了。scanning()
方法。firstScanning()
方法抓取已存在的问题网址。完成初始化后,每次启动脚本只需一行代码:
Scanner().scanning()
输出如下:
==================================================
检测到cookie文件,直接使用cookie登录
已登陆账号: ........
已读取 checkedURLs 16931 个
开始扫描
已举报: 壬申、癸卯、戊申、乙卯,男,求先生看看事业……? 2015-12-16 12:03:07
已举报: 请帮忙看看这个八字,地支三合火,水为用神,又要走伤官大运 有点怕? 2015-12-16 12:22:47
已举报: 请各位高手随缘看一下婚姻和事业,以后发展怎么样? 2015-12-16 12:39:16
已举报: 能不能解释我做过的一些能预示未来的梦? 2015-12-16 12:46:29
已举报: 有没有算命的骗子在街上误拉住一位易学大师算命,反倒被大师算得跪地求解的事件? 2015-12-16 12:53:41
已举报: 老婆要生了,求个宝宝的名字,姓于,多谢? 2015-12-16 13:39:11
已举报: 麻烦大师帮忙看看手相? 2015-12-16 13:49:33
已举报: 求大神帮忙想个老人居住的地方的名字,复古,自然,朴实,有活力的? 2015-12-16 13:55:46
已举报: 从命理学角度看,如梵高这般死后才被世人承认而获得成就的人生可以被推断出来吗? 2015-12-16 14:10:14
已举报: 求算子嗣? 2015-12-16 14:21:35
每天不定时会集中收到私信通知处理结果,在https://www.zhihu.com/community/report可以看到本账号举报结果汇总:
脚本跑了一天半,举报了120个左右的新问题,关闭率在95%以上。从图上最后一列可以发现,虽然脚本一律以「针对具体病情的求药问药」为理由举报所有问题,但是管理员接到举报,自己也会判断属于哪一类。所以不再需要在脚本中加入判断逻辑。
Scanner
类的canReport()
方法用于检测一个问题是否符合举报条件;getReason()
方法判断举报原因。这两个规则还没开始写,目前一律以「针对具体病情的求药问药」为理由举报所有问题。1 | #!/usr/bin/env python |
书接上文:《纷争世事无穷尽》,昨晚写了简单的脚本,先在本机运行几天,排除各类Bug后再挂到SAE。
由于网址总数不多(一万七千左右),没用Bloom过滤器,直接使用txt保存+set查重复。SAE上貌似不能直接创建文件,得改成使用数据库。另外SAE只支持Python 2.7,还得改一些语法。
]]>setLatestEventInfo()
方法。在网上搜索一番,整理如下:
API docs
// 常用字段
contentIntent 设置PendingIntent对象,点击时发送该Intent
defaults 添加默认效果
flags 设置flag位,例如FLAG_NO_CLEAR等
icon 设置图标
sound 设置声音
tickerText 显示在状态栏中的文字
when 发送此通知的时间戳
// 效果常量
DEFAULT_ALL 使用所有默认值,比如声音,震动,闪屏等等
DEFAULT_LIGHTS 使用默认闪光提示
DEFAULT_SOUNDS 使用默认提示声音
DEFAULT_VIBRATE 使用默认手机震动
// 以上的效果常量可以叠加:
notification.defaults = DEFAULT_SOUND|DEFAULT_VIBRATE;
notification.defaults |= DEFAULT_LIGHTS;
// 最好在真机上测试,震动效果模拟器上没有。
// 加入手机震动,一定要在AndroidManifest.xml中加入权限:
<uses-permission android:name="android.permission.VIBRATE" />
// Flag
FLAG_AUTO_CANCEL 该通知能被状态栏的清除按钮给清除掉
FLAG_NO_CLEAR 该通知能被状态栏的清除按钮给清除掉
FLAG_ONGOING_EVENT 通知放置在正在运行
FLAG_INSISTENT 是否一直进行,比如音乐一直播放,知道用户响应
// NotificationManager的常用方法
public void cancelAll() // 移除所有通知(只是针对当前Context下的Notification)
public void cancel(int id) // 移除标记为id的通知 (只是针对当前Context下的所有Notification)
public void notify(String tag ,int id, Notification notification) // 将通知加入状态栏,标签为tag,标记为id
public void notify(int id, Notification notification) // 将通知加入状态栏,标记为id
// Notification的常用方法
// 通知提示音
Uri soundUri = Uri.fromFile(new File("/system/media/audio/ringtones/Basic_tone.ogg"));
notification.sound = soundUri;
// 通知震动,第0、2、4..表示静止时间,1、3、5...表示震动时间
long[] vibrates = {0, 1000, 1000, 1000};
notification.vibrate = vibrates;
// LED灯
notification.ledARGB = Color.GREEN;
notification.ledOnMS = 1000;
notification.ledOffMS = 1000;
notification.flags = Notification.FLAG_SHOW_LIGHTS;
1 | NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); |
1 | NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); |
1 | //api 16 |
Activity不可见时在状态栏显示通知1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
clearNotification();
}
@Override
protected void onStop() {
showNotification();
super.onStop();
}
@Override
protected void onStart() {
clearNotification();
super.onStart();
}
/**
* 在状态栏显示通知
*/
private void showNotification(){
// 创建一个NotificationManager的引用
NotificationManager notificationManager = (NotificationManager)
this.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
// 定义Notification的各种属性
Notification notification =new Notification(R.drawable.icon,
"督导系统", System.currentTimeMillis());
//FLAG_AUTO_CANCEL 该通知能被状态栏的清除按钮给清除掉
//FLAG_NO_CLEAR 该通知不能被状态栏的清除按钮给清除掉
//FLAG_ONGOING_EVENT 通知放置在正在运行
//FLAG_INSISTENT 是否一直进行,比如音乐一直播放,知道用户响应
notification.flags |= Notification.FLAG_ONGOING_EVENT; // 将此通知放到通知栏的"Ongoing"即"正在运行"组中
notification.flags |= Notification.FLAG_NO_CLEAR; // 表明在点击了通知栏中的"清除通知"后,此通知不清除,经常与FLAG_ONGOING_EVENT一起使用
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
//DEFAULT_ALL 使用所有默认值,比如声音,震动,闪屏等等
//DEFAULT_LIGHTS 使用默认闪光提示
//DEFAULT_SOUNDS 使用默认提示声音
//DEFAULT_VIBRATE 使用默认手机震动,需加上<uses-permission android:name="android.permission.VIBRATE" />权限
notification.defaults = Notification.DEFAULT_LIGHTS;
//叠加效果常量
//notification.defaults=Notification.DEFAULT_LIGHTS|Notification.DEFAULT_SOUND;
notification.ledARGB = Color.BLUE;
notification.ledOnMS =5000; //闪光时间,毫秒
// 设置通知的事件消息
CharSequence contentTitle ="督导系统标题"; // 通知栏标题
CharSequence contentText ="督导系统内容"; // 通知栏内容
Intent notificationIntent =new Intent(MainActivity.this, MainActivity.class); // 点击该通知后要跳转的Activity
PendingIntent contentItent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, contentTitle, contentText, contentItent);
// 把Notification传递给NotificationManager
notificationManager.notify(0, notification);
}
//删除通知
private void clearNotification(){
// 启动后删除之前我们定义的通知
NotificationManager notificationManager = (NotificationManager) this
.getSystemService(NOTIFICATION_SERVICE);
notificationManager.cancel(0);
}
}
setLatestEventInfo()
方法。在网上搜索一番,整理如下:]]>
Visual Basic for Applications(VBA)是微软开发出来在其桌面应用程序中执行通用的自动化(OLE)任务的编程语言。主要能用来扩展Windows的应用程式功能,特别是Microsoft Office软件,如Word、Excel、Access、Powerpoint。在以上程序中按下Alt+F11即可打开VBA专用的IDE,无需额外安装。
如果不明白一个操作如何用代码实现,可以使用录制宏的功能,手动操作一次,查看宏代码并精简——录制得到的代码包括了大量可以删除的默认设置。
IDE中将光标定位到某个函数,按下F1可看到该函数的全中文帮助,非常方便。
1 | For Each docOpened In Documents |
1 | Sub 目录下doc转txt() |
1 | Dim httpRequest As Object |
1 | Public Function Escape(ByVal strText As String) As String |
1 | Dim i As Integer, j As Integer, k As Integer 'i用于遍历,j用于计数须合并的行数,k用于填充颜色 |
1 | Dim sr |
1 | Sub 存成html() |
1 | Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean) |
1 | iFileName = Application.GetOpenFilename("Excel文件 (*.xlsx;*.xls), *.xlsx;*.xls") |
1 | With wbf.Sort |
1 | Sub HzWb() |
1 | '新建一张表用于存放待保存的数据 |
1 | Sub 替换昨今去() |
1 | Sub 新闻排版() |
Visual Basic for Applications(VBA)是微软开发出来在其桌面应用程序中执行通用的自动化(OLE)任务的编程语言。主要能用来扩展Windows的应用程式功能,特别是Microsoft Office软件,如Word、Excel、Access、Powerpoint。在以上程序中按下Alt+F11即可打开VBA专用的IDE,无需额外安装。
如果不明白一个操作如何用代码实现,可以使用录制宏的功能,手动操作一次,查看宏代码并精简——录制得到的代码包括了大量可以删除的默认设置。
IDE中将光标定位到某个函数,按下F1可看到该函数的全中文帮助,非常方便。
]]>如题。/data
的文件权限是drwxrwx--x
。
在cmd中运行adb命令
adb shell su -c "chmod 777 /data"
子目录需要使用同样的命令修改权限才可操作。
Linux/Unix 的档案调用权限分为三级 : 档案拥有者、群组、其他。利用 chmod 可以藉以控制档案如何被他人所调用。
chmod [-cfvR] [--help] [--version] mode file...
参数说明:
-c : 若该档案权限确实已经更改,才显示其更改动作
-f : 若该档案权限无法被更改也不要显示错误讯息
-v : 显示权限变更的详细资料
-R : 对目前目录下的所有档案与子目录进行相同的权限变更(即以递回的方式逐个变更)
--help : 显示辅助说明
--version : 显示版本
mode: 权限设定字串,格式如下 : [ugoa...][[+-=][rwxX]...][,...],其中
u 表示该档案的拥有者user
g 表示与该档案的拥有者属于同一个群体者group
o 表示其他以外的人other
a 表示这三者皆是all
+ 表示增加权限、- 表示取消权限、= 表示唯一设定权限。
r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当该档案是个子目录或者该档案已经被设定过为可执行。
-----------------
也可以用数字来表示权限如 chmod 777 file,三个数字分别表示user、group、other权限
r=4,w=2,x=1。7表示rwx,依次类推
chmod a+r file1 file2
chmod ug+w,o-w file
chmod u+x file
chmod -R a+r *
chmod a=rwx file
chmod 777 file # 与上等效
chmod ug=rwx,o=x file
chmod 771 file # 与上等效
chmod 4755 filename
如题。/data
的文件权限是drwxrwx--x
。
在cmd中运行adb命令
adb shell su -c "chmod 777 /data"
子目录需要使用同样的命令修改权限才可操作。
]]>财爻戌土持世,休囚又逢日建辰土来冲,为自己经济状况不佳。子孙伏而不出,后继乏力。明年寅月冲开四爻官鬼申金,生旺子孙午火,跳槽加薪之象。正好符合我自己的计划。
妻财两现,以初爻丑土为用,丑土月耗日扶又入空亡,兼之变卦六冲,这次相亲没戏。
应爻亥水临月,对方的条件好很多。飞神亥水生合伏神寅木,临勾陈,表示和一位朋友非常合得来,暗生情愫。但是亥水入日墓,动弹不得,女方不会主动。三爻官鬼酉金动化回头克,克尽无用,不生应爻,这位朋友对她没有情意。
六爻中并不需要参考爻辞,这里只有一爻独发,不妨看看。有时与事暗合,爻辞参与占断,会有神来之笔。
九三。井渫不食。为我心恻。可用汲。王明。并受其福。
三爻的位置是中部偏下,井水里的杂质已经沉淀到了底部,此段井水可以饮用,但是没有人来取食,只因五爻的阳爻所阻。上卦为坎,坎为忧,故称心恻。不过五爻的阻碍并不会持续太久,清除之后,三爻位置处的井水就可以顺利打上来饮用了。五爻为尊位,对应到人事,就是最尊贵的「王」。「王明」即疏通五爻,上下皆受其福。
光看这爻辞,症结都在九五爻,只要九五一通,皆大欢喜。难道关键在我身上?怎么和六爻的分析结果截然相反。暂且记录下来,且看结果如何。
12月16日更新
13日乙未年戊子月癸亥日晚见面喝茶聊了两小时,16日丙寅日己亥时再约看电影时对方表示「我觉得我们做朋友挺好」。与六爻占断结果相符,至于对方的私事,就不得而知了。这一例也验证了六爻不需看爻辞。
bat是Windows下的批处理文件,每一行都是一条DOS命令。
@echo off
echo 无后缀名: %~n1
echo 有后缀名: %~nx1
echo 绝对路径: %1
echo 短路径名的绝对路径: %~s1
echo 驱动器和路径: %~dp1
echo 驱动器: %~d1
echo 路径: %~p1
echo 文件属性: %~a1
echo 日期/时间: %~t1
echo 文件大小: %~z1
pause
把上述命令存入txt文件,将后缀名改为.bat。随便找个文件拖到.bat文件的图标上:
初学Java时用命令行编译和运行,每次都要cd入目录,手输javac和java命令,非常不便。于是偷懒写了个.bat文件:
@echo off
javac %~nx1
java %~n1
pause
每次写完一拖就搞定!
]]>bat是Windows下的批处理文件,每一行都是一条DOS命令。
]]>电脑与手机用其他方式连在同一个局域网中也可以使用下面的方法。
1.在手机上安装终端类App,开启Root权限,打开App,输入以下命令:
su
setprop service.adb.tcp.port 5555
stop adbd
start adbd
其中,su表示获取root权限
2.查看手机的Wifi连接,获取手机IP地址。
3.电脑上打开CMD,输入如下命令:
adb connect 手机IP地址
命令行输入adb devices或者Eclipse的DDMS都可以看到已连接的设备,可以直接进行真机调试
每次连电脑都要在手机上输一大串终端命令实在太繁琐,干脆写了个APP代劳。必须给该APP开启Root权限。连上Wifi后打开APP点击按钮:
Github:loveNight/AndroidTerminal
Eclipse + ADT插件写的。导入工程后直接用USB调试装到自己手机上使用。
手机上运行终端命令的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* CMD 命令行执行工具
*/
public class CmdUtils {
public static final String COMMAND_SU = "su"; // 表示获取root权限(APP必须已root)
public static final String COMMAND_LINE_END = "\n";
public static final String COMMAND_EXIT = "exit\n";
/**
* Android手机用Wifi连上电脑ADB调试
* 须在手机终端输入如下命令
* 此终端必须已经Root
*/
public static final String[] wifiConnectToComputer = {
"setprop service.adb.tcp.port 5555",
"stop adbd",
"start adbd"
};
public static Result execute(String[] commands) {
//----------------- 待写:检查此手机是否已经Root-------------
Runtime runtime = Runtime.getRuntime();
Process process = null;
DataOutputStream output = null; // 用于向终端进程输入命令
BufferedReader successResult = null;
BufferedReader errorResult = null;
StringBuilder successMsg = null;
StringBuilder errorMsg = null;
try {
process = runtime.exec(COMMAND_SU);
output = new DataOutputStream(process.getOutputStream());
for (String command : commands) {
if (command == null) {
continue;
}
output.write(command.getBytes());
output.writeBytes(COMMAND_LINE_END); // 输完一行命令要按回车
output.flush();
}
output.writeBytes(COMMAND_EXIT);
output.flush();
process.waitFor(); // 当前线程等待,直到process线程执行结束
successMsg = new StringBuilder();
errorMsg = new StringBuilder();
successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s;
while ( (s = successResult.readLine()) != null) {
successMsg.append(s).append("\n");
}
while ( (s = errorResult.readLine()) != null) {
errorMsg.append(s).append("\n");
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally { // 回收资源
try {
if (output != null) {
output.close();
}
if (successResult != null) {
successResult.close();
}
if (errorResult != null) {
errorResult.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (process != null) {
process.destroy();
}
}
return new Result(successMsg == null ? null : successMsg.toString()
, errorMsg == null ? null : errorMsg.toString());
}
public static class Result {
public String successMsg;
public String errorMsg;
public Result(String successMsg, String errorMsg) {
super();
this.successMsg = successMsg;
this.errorMsg = errorMsg;
}
}
}
获取手机IP地址需要Wifi权限:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
代码如下:
1 | import java.net.InetAddress; |
电脑与手机用其他方式连在同一个局域网中也可以使用下面的方法。
]]>-用户
或-User
后缀的菜单项,其对应的配置文件都保存在Packages\User
文件夹下,将它们上传到Github,便于同步。
快捷键完整版见后文,这里只列个人习惯。
四种 Goto :
Ctrl + P 文件定位
Ctrl + ; 词语定位 #
Ctrl + R 函数定位 @
Ctrl + G 行号定位 :
括号前后移动光标:Ctrl + M
以单词为单位前后移动光标:Ctrl + Left/Right
Ctrl+→ 向右单位性地移动光标,快速移动光标。
重新打开刚刚关闭的标签页:Ctrl + Shift + T
当前行与上/下一行交换位置:Shift + Ctrl + Up/Down
向光标前插入一行:Shift + Ctrl + Enter
向光标后插入一行:Ctrl + Enter
复制光标所在整行,插入到下一行:Ctrl + Shift + D
合并选中的多行代码为一行:Ctrl + J
快速折叠文件内所有函数:Ctrl + K + 1 (数字表示级别)
代码格式化: Ctrl + Shift + P,输入reindent lines。(一般输几个字母就出现了命令,直接回车)
先装插件管理器(Package Control):https://packagecontrol.io/installation
安装插件:Ctrl+Shift+P,输入install,等待几秒后出现插件列表,输插件名再回车就能自动安装。下文插件名加链接的可以点进去学习使用方法。建议安装插件后都进设置菜单配置一下。
删除插件:Ctrl+Shift+P,输入remove package,选择插件再回车。
Packages\FileHeader\template
中的 body 对应文件模板,header 对应文件头Setting
,左侧选Account
,复制Api Key。Sublime中安装此插件会用到。以后就可以登录网站查看自己的code时间统计图。Packages\ConvertToUTF8
下找到对应操作系统的Default.sublime-keymap
文件,把快捷键改掉以Eclipse中常用的syso为例,把它照搬到Sublime Text中:
单击菜单中的工具
,新代码段...
,删掉默认内容,粘贴下方代码并保存到Packages\User\Java Snippets
文件夹,文件名为Print.sublime-snippet
。写Java代码时只须输入syso
再按回车就能快速插入System.out.println()
了。
后缀必须是.sublime-snippet
,文件名可以自由命名,目录结构也可以随意更改,只要在Packages\User\
下就行。每个文件中只能写包含一个<snippet>
结点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<snippet>
<!-- 快速补全的内容 -->
<!-- 1表示第一个输入点,1后加冒号和字符表示默认值 -->
<!-- 如System.out.println(${1:"Hello World"}),可输入多行代码 -->
<content><![CDATA[System.out.println(${1});]]></content>
<!-- 触发字符 -->
<!-- Eclipse中可以用syso快速插入Java的输出语句 -->
<tabTrigger>syso</tabTrigger>
<!-- 指定的语法才会触发,可选 -->
<!-- 此处的语法指的是Ctrl+Shift+P,Set Syntax中设置的语法 -->
<!-- 必须把完整的语言名写在后缀中 -->
<scope>source.java</scope>
<!-- 触发时的提示 -->
<description>System.out.println()</description>
</snippet>
Sublime Text 3 为每种语法(Syntax)都设置了Snippet。
查看方法:把Sublime Text 3 根目录下Packages
文件夹中任意文件复制出来,改后缀为.rar
,解压缩。后缀为.sublime-snippet
的就是了。可以用Sublime打开。
首选项
,设置-用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58{
// 开启选中范围内搜索
"auto_find_in_selection": true,
// 侧边栏文件夹显示加粗,区别于文件
"bold_folder_labels": true,
"color_scheme": "Packages/Color Scheme - Default/Solarized (Dark).tmTheme",
// 使用 unix 风格的换行符
"default_line_ending": "unix",
// 覆盖操作系统设置的DPI,否则标签上的中文显示为方块
"dpi_scale": 1.0,
// true则禁用Emmet的tab键功能
// "disable_tab_abbreviations": true,
// 右侧代码预览时给所在区域加上边框
"draw_minimap_border": true,
// 保证在文件保存时,在结尾插入一个换行符。
// 这样 git 提交时不会生产额外的 diff
"ensure_newline_at_eof_on_save": true,
// 默认显示行号右侧的代码段闭合展开三角号
"fade_fold_buttons": false,
"font_face": "Microsoft YaHei Mono",
"font_size": 13,
// 当前行高亮
"highlight_line": true,
// 高亮未保存文件
"highlight_modified_tabs": true,
"ignored_packages":
[
"Vintage"
],
// 窗口失焦立即保存文件
"save_on_focus_lost": true,
// 自动移除行尾多余空格
"trim_trailing_white_space_on_save": true,
"trim_automatic_white_space": true,
// 关闭自动更新
"update_check": false,
// 自动换行
"word_wrap": "true",
// Tab转空格
"translate_tabs_to_spaces": true
}
首选项
,插件设置
,Anaconda
,Settings - User
1 | { |
首选项
,按键绑定-用户
:1
2
3
4
5
6
7
8
9
10
11
12
13
14[
// 代码提示
{ "keys": ["alt+space"], "command": "auto_complete" },
{ "keys": ["alt+space"], "command": "replace_completion_with_auto_complete", "context":
[
{ "key": "last_command", "operator": "equal", "operand": "insert_best_completion" },
{ "key": "auto_complete_visible", "operator": "equal", "operand": false },
{ "key": "setting.tab_completion", "operator": "equal", "operand": true }
]
},
// 跳转到函数定义
{ "keys": ["ctrl+alt+d"], "command": "goto_definition" },
]
Sublime Text 3默认将文件存为UTF-8编码,如果要手动输命令编译Sublime Text 3 写的Java源文件,必须加-encoding UTF-8
参数
打开Sublime Text 3,依次点击工具
、编译系统
、新编译系统
,粘贴下面的代码并保存为My-----Java.sublime-build
。文件名加这么多斜杠是为了能在菜单中一眼找出来。
然后按Ctrl+Shift+P,输入My---
,选择Build With:My-----Java
并回车,即可将刚才的文件设置为当前编译系统。Ctrl+B编译,Ctrl+Shift+B运行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46{
// 命令和参数,未指定路径则在PATH环境变量中找
"cmd": ["javac","-encoding","UTF-8","-d",".","$file"],
// 可选。获取cmd的错误输出
"file_regex": "^(...*?):([0-9]*):?([0-9]*)",
// 可选。`工具`菜单中`编译`为`自动`时生效
"selector": "source.java",
// 可选。输出"cmd"的编码。必须是合法的Python编码,缺省为"UTF-8"
"encoding":"GBK",
// variants 可选。用来替代主构建系统的备选。如果构建系统的"selector"与激活的文件匹配,变量"name"则会出现在 Command Palette 中。
"variants":
[
{
// 仅在"variants"中是合法的 (详见 variants)。用来标识系统中不同的构建系统。如果"name"是"Run" ,则会显示在Tools | Build System 菜单下,并且可以使用Ctrl + Shift + B调用
"name": "Run",
// 可选。如果该选项为"true" ,"cmd"则可以通过shell运行。
"shell": true,
"cmd" : ["start","cmd","/c", "java ${file_base_name} &echo. & pause"],
// /c是执行完命令后关闭cmd窗口,
// /k是执行完命令后不关闭cmd窗口。
// echo. 相当于输入一个回车
// pause命令使cmd窗口按任意键后才关闭
// 可选。在运行首行的"cmd"前会切换到该目录。运行结束后会切换到原来的目录。
"working_dir": "${file_path}",
"encoding":"GBK"
}
]
// 还有:
// line_regex 可选。当"cmd"的错误输出中,file_regex与该行不匹配,如果line_regex存在,并且确实与当前行匹配, 则遍历整个缓冲区,直到与file regex匹配的行出现,并用这两个匹配决定最终要跳转的文件或行。
// target 可选。运行的Sublime Text命令,缺省为"exec" (Packages/Default/exec.py)。该命令从 .build-system中获取配置数据。用来替代缺省的构建系统命令。注意,如果你希望替代构建系统的缺省命令,请在.sublime-build 文件中专门设置。
// env 可选。在环境变量被传递给"cmd"前,将他们封装成词典。
// path 可选。该选项可以在调用"cmd"前替换当前进程的PATH 。原来的 PATH 将在运行后恢复。使用这个选项可以在不修改系统设置的前提下将目录添加到 PATH 中。
}
在JDK的bin目录下新建runJava.bat
文件:
1 | @echo off |
用上面的方法新建并设置为当前编译系统,按Ctrl+B即可编译+运行,这种方法的缺点是无法在控制台输入,如果程序需要输入内容,则直接报错1
2
3
4
5
6{
"shell_cmd": "runJava.bat \"$file\"",
"file_regex": "^(...*?):([0-9]*):?([0-9]*)",
"selector": "source.java",
"encoding": "GBK"
}
用法同上,仅粘贴代码:1
2
3
4
5
6
7
8
9{
"shell_cmd": "python -u \"$file\"",
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"selector": "source.python",
// 默认是utf-8,控制台无法输出中文。
// Window Code Page 936,指的是GBK编码
"encoding":"cp936"
}
- Ctrl+D 选中光标所占的文本,继续操作则会选中下一个相同的文本。
- Alt+F3 选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑。举个栗子:快速选中并更改所有相同的变量名、函数名等。
- Ctrl+L 选中整行,继续操作则继续选择下一行,效果和 Shift+↓ 效果一样。
- Ctrl+Shift+L 先选中多行,再按下快捷键,会在每行行尾插入光标,即可同时编辑这些行。
- Ctrl+Shift+M 选择括号内的内容(继续选择父括号)。举个栗子:快速选中删除函数中的代码,重写函数体代码或重写括号内里的内容。
- Ctrl+M 光标移动至括号内结束或开始的位置。
- Ctrl+Enter 在下一行插入新行。举个栗子:即使光标不在行尾,也能快速向下插入一行。
- Ctrl+Shift+Enter 在上一行插入新行。举个栗子:即使光标不在行首,也能快速向上插入一行。
- Ctrl+Shift+[ 选中代码,按下快捷键,折叠代码。
- Ctrl+Shift+] 选中代码,按下快捷键,展开代码。
- Ctrl+K+0 展开所有折叠代码。
- Ctrl+← 向左单位性地移动光标,快速移动光标。
- Ctrl+→ 向右单位性地移动光标,快速移动光标。
- shift+↑ 向上选中多行。
- shift+↓ 向下选中多行。
- Shift+← 向左选中文本。
- Shift+→ 向右选中文本。
- Ctrl+Shift+← 向左单位性地选中文本。
- Ctrl+Shift+→ 向右单位性地选中文本。
- Ctrl+Shift+↑ 将光标所在行和上一行代码互换(将光标所在行插入到上一行之前)。
- Ctrl+Shift+↓ 将光标所在行和下一行代码互换(将光标所在行插入到下一行之后)。
- Ctrl+Alt+↑ 向上添加多行光标,可同时编辑多行。
- Ctrl+Alt+↓ 向下添加多行光标,可同时编辑多行。
- Ctrl+J 合并选中的多行代码为一行。举个栗子:将多行格式的CSS属性合并为一行。
- Ctrl+Shift+D 复制光标所在整行,插入到下一行。
- Tab 向右缩进。只对光标后(或者选中的)的代码有效
- Shift+Tab 向左缩进。
- Ctrl+[ 向左缩进。对整行有效
- Ctrl+] 向右缩进。对整行有效
- Ctrl+K+K 从光标处开始删除代码至行尾。按住Ctrl,按两次K。
- Ctrl+Shift+K 删除整行。
- Ctrl+/ 注释单行。
- Ctrl+Shift+/ 注释多行。
- Ctrl+K+U 转换大写。
- Ctrl+K+L 转换小写。
- Ctrl+Z 撤销。
- Ctrl+Y 恢复撤销。
- Ctrl+U 软撤销,感觉和 Gtrl+Z 一样。
- Ctrl+F2 设置书签,F2切换书签
- Ctrl+T 左右字母互换。
- F6 单词检测拼写
- Ctrl+F 打开底部搜索框,查找关键字。
- Ctrl+shift+F 在文件夹内查找,与普通编辑器不同的地方是sublime允许添加多个文件夹进行查找,略高端,未研究。
- Ctrl+P 打开搜索框。举个栗子:1、输入当前项目中的文件名,快速搜索文件,2、输入@和关键字,查找文件中函数名,3、输入:和数字,跳转到文件中该行代码,4、输入#和关键字,查找变量名。
- Ctrl+G 打开搜索框,自动带:,输入数字跳转到该行代码。举个栗子:在页面代码比较长的文件中快速定位。
- Ctrl+R 打开搜索框,自动带@,输入关键字,查找文件中的函数名。举个栗子:在函数较多的页面快速查找某个函数。
- Ctrl+: 打开搜索框,自动带#,输入关键字,查找文件中的变量名、属性名等。
- Esc 退出光标多行选择,退出搜索框,命令框等。
- Ctrl+Shift+P 打开命令框。场景栗子:打开命名框,输入关键字,调用sublime text或插件的功能,例如使用package安装插件。
- Ctrl+Tab 按文件浏览过的顺序,切换当前窗口的标签页。
- Ctrl+PageDown 向左切换当前窗口的标签页。
- Ctrl+PageUp 向右切换当前窗口的标签页。
- Alt+Shift+1 窗口分屏,恢复默认1屏(非小键盘的数字)
- Alt+Shift+2 左右分屏-2列
- Alt+Shift+3 左右分屏-3列
- Alt+Shift+4 左右分屏-4列
- Alt+Shift+5 等分4屏
- Alt+Shift+8 垂直分屏-2屏
- Alt+Shift+9 垂直分屏-3屏
- Ctrl+K+B 开启/关闭侧边栏。
- F11 全屏模式
- Shift+F11 免打扰模式
多重选择功能允许在页面中同时存在多个光标,让很多本来需要正则表达式、高级搜索和替换才能完成的任务也变得游刃有余了。
激活多重选择的方法有两几种:
]]>
- 按住 Ctrl 然后在页面中希望中现光标的位置点击。
- 选择数行文本,然后按下 Shift + Ctrl + L。
- 通过反复按下 Ctrl + D 即可将全文中与光标当前所在位置的词相同的词逐一加入选择,而直接按下 Alt+F3即可一次性选择所有相同的词。
- 按下鼠标中键来进行垂直方向的纵列选择,也可以进入多重编辑状态。
-用户
或-User
后缀的菜单项,其对应的配置文件都保存在Packages\User
文件夹下,将它们上传到Github,便于同步。
年龄一大,难免要操心终生大事——操心的不是我,而是各路亲戚。我只想着如何避免这些事,老婆孩子是一条枷锁,使自己的下半生不得自由。可惜事与愿违,越来越大的压力,使我很难再坚持这样的做法。母亲身体不好,每次谈及这方面的事,总是说自己这几年还可以帮忙带带小孩,再晚就带不动了。听到心里总感觉无限心酸。
但我自己又很明确自己的心意,可能由于荷尔蒙作怪,以前羡慕过别人成双成对,也曾经狂热地喜欢过女神。不过从来没有想过成家后会怎么样——也许是因为女神压根没令我看到这方面的希望,我才没想。只有生病躺在床上一动不动的时候,才会觉得有个人在一边照顾也挺好。这样的想法挺自私的。把付出看作是对自己的限制,不想承受,却又偶尔希望接受他人的照顾。
迫于压力而结婚,既对不起我自己,也对不起女方。女生总对婚姻有无限向往,而我并不是值得托付终生居家过日子的人,也不想往那个方向作出改变。事情陷入了死结,究竟要怎么办呢?等吧。
这两年是同龄人结婚的高发期,像我这种朋友挺少的人,光是这一年也参加了三次婚宴,一次订婚。朋友圈里各种晒婚礼、晒大肚子、晒小孩,仍然没有什么成家的想法。朋友圈里的照片看上去光鲜又精彩,但是和一些朋友多聊了聊,婚姻又没有TA们想象得那么好。
正在码字的时候,又收到毛驴的微信,12月20日在金华结婚。打算到时候坐鸡爷或猪爷的车一块过去,挺热闹的。
晚上和同学吃饭,聊到自己不想结婚,同学说,「你还没遇到能令你想结婚的人」。这是青春期情窦初开时就听过的话,那时有一种盲目的信仰,就如鸡汤里说的那样,总有一天你会遇上一个人,惊艳了时光,温柔了岁月,让你觉得所有的等待都是值得的。然而作为一名理科生,看看男女比例,就知道总得有一大波人打光棍儿。
学了术数之后,遇到最多的是各路痴男怨女,不管卦主年龄几何,都喜欢问目前遇到的谁谁是不是真命天子/女。其实这方面并没有正缘、真命之类的说法,人与人之间的因缘不同,缘深纠葛多,缘浅轻易散,来来去去就是这么一回事,没有永恒长久的伴侣,最多也只能持续数十年。更多人没有走到生命的尽头,感情已经产生了无法弥补的裂痕,有的干脆离婚,有的貌合神离,依旧维持着僵硬的关系。印象中最惨的一个人,年近四十仍然单身,还问自己能不能遇到真爱。可怜的人啊,爱只是大脑中编织的美梦,易碎也易醒。
那时候对爱情有什么样的幻想,现在已经记不起来了。可能就像这只火锅,什么玩意都往里加,倾尽最美好的想象。有部韩国电影《建筑学概论》有一幕类似的场景,女主角喝辣汤时哭着说,「我感觉目前的生活就跟辣汤一样,也不知里面放了什么,只是辣而已」。结合电影前期的铺垫,这句台词令人非常触动。
我的生活也非常简单,只是淡而已。今年加了另一种味道:相亲。过年时村后面有户老太太——我完全没印象——跑来和我妈说,她外孙女年初一要来村里上坟,让我到时候过去见见。
乡下什么都好,只是没有茶楼、咖啡厅之类聊天的地方,相亲习俗都是男方直接去女方家里。这我非常难以接受,在城市生活久了,习惯男女双方感情稳定下来才见家长。姐姐也是定下来要结婚,才把姐夫带回家来。在那之前我们全都以为她还单身。城里的这种习惯,有一个原因是男女双方往往有一方是或者都是外地人,去对方家里不方便。然而乡下那种小地方,骑个电瓶车,五分钟就到了。要是去镇上找家店吃个饭反而不如直接去对方家更方便。
在我的坚持反对下,年初这一「劫」总算躲了过去。不过我妈又在盘算着这次过年让我再去见见。那老太太一再夸口外孙女非常漂亮。要是真漂亮,这么大还没嫁出去,不是眼光高就是性格不好相处。像我这样不想结婚的还只是极少数吧,目前还没见过第二个。
拜年的时候,二阿姨突然说,她朋友的朋友的女儿在附近村子里,让我也去见见。我急中生智想了个借口说自己最近一直感冒咳嗽停不下来,去了对方家里一直嗽形象不好。于是拿来女方的手机号让我自己联系。
女方话不多,大部分时间都是我在问,顺便介绍下自己的情况。平时也找不到什么共同话题。对方并不讳言自己的懒——懒得读书,懒得学习。每天没话找话聊也蛮辛苦,后来约出来见了一次,就没下文了。奇葩的是刚坐下来就说自己离不开手机,一直低头玩,有一搭没一搭了聊了大半小时,对方手机没电,就走了。我自己一个人继续吃自助餐到再也塞不下为止。
半年后我再点开她朋友圈,发现一条状态都看不到,看来已经把我删了。
后来小阿姨介绍了个丽水的妹子,加好友聊了两次,又是查户口式一问一答的聊天,联系了两次后就再没下文了。
村里小伙伴订婚,酒席摆在湖心塘的城北大酒店。这几年回来得太少,都没注意到它什么时候开建。同桌的老太太们见了我都问这是谁。看来我这几年真是太瘦了,得加把劲增个肥。小伙伴的未婚妻是他高中同学,富家女。女方的父亲说对男方没什么要求,只要自己女儿喜欢就够了。但是订婚必须做做样子,男方送过去68万当面收下,私下里会还回去,就只是图个面子。小伙伴东借西借,又找银行工作的亲戚贷了一部分,总算凑齐了钱。
小伙伴的母亲也蛮热心的,早几年就一直嚷嚷着要给我介绍和小伙伴未婚妻同村的一个妹子。据说也在杭州工作,是动车的乘务员。在家挺勤快,为人朴实,不追求化妆打扮,老实人,适合居家过日子。说这番话的是小伙伴的外婆,和这位传说中的妹子是邻居。外婆老是感慨谁要是娶了这个妹子,就是捡了块宝。
都快把妹子夸到天上去了,我很不以为然。貌似我也被夸得形象非常高大,以至于妹子的母亲问小伙伴外婆:男方这么优秀,会不会看上我女儿。优秀吗?真要是属于婚恋市场上的优秀,那就不会打二十几年老光棍了。
以前我用「还早,同学们都没结婚」推了几次。不过今年这些挡箭牌们一个个都结婚,每次我往外地跑,我妈都知道。这下推不掉,只好拿了号码加了微信。一如既往一问一答,发消息倒是必回,只是经常过一两小时甚至第二天才回复。生活习惯、兴趣爱好之类问完之后就没共同语言,又开始没话找话之旅。对方也不咸不淡地回着。
小伙伴的外婆闲不住,看妹子回了家就跑过去,问男的(也就是我)有没有给她打电话——老人不懂微信、短信,只知道打电话。又和小伙伴的母亲(下面改称介绍人吧)商量要不要安排我们在这次订婚酒席上见个面……还好这馊主意没人同意,否则真尴尬得要死。
从介绍人那边传过来的消息,妹子每次回家,家人也盘问相亲的事。妹子都说还在联系。我估计她肯一次次回我这些无聊的短信,也是迫于家人的压力吧,只需隔几天回复一两条消息,就可以在家人盘问的时候淡定地说出这四个字「还在联系」,得一时清静,躲过各种唠叨。
我也是这么想和这么做的。这种奇葩的默契真让人无奈。
好景不长,很快热心的介绍人就给我扔了个重磅炸弹。她两头打探消息,发现我和妹子「还在联系」快两个月了,估摸着有戏,打算过年的时候让我上门去见见。我……这让我怎么拒绝。太忙没空?就五分钟的路,过去吃顿晚饭的时间总有吧。没感觉?这不还在联系吗怎么会没感觉。
思前想后,避免上门相亲的唯一方法就是我直接在杭州和妹子见个面,然后被拒绝。从妹子各种冷淡的反应、从不主动发消息可以看得出来对我没什么想法。两人的兴趣爱好相差太大,一直聊不起来。每次我辛辛苦苦想了个话题,三两句就聊不下去。
约了两次,妹子都恰好没空。要命的是都会阐述要做什么事,然后补上一句:下次吧。搞得我也分不清楚这是真没空还是假没空。管它呢继续约,就当应付家人也好。
前阵子姑姑问想找什么样的对象,我想了半天居然答不上来。外貌?我自己长得也不咋的,没资格提要求。学历?只能说明考试能力。性格?聊得来最重要。家世?自己家境也一般,不挑别人了。
姑姑让我打一段自我介绍,我写了两句,也想不到别的能说啥。她拿过手机又摁了半天,发了出去。然后给了我个号码,说是她们学校的小学教师,89年,天津人,在下沙读书,毕业后就留在了杭州。在手机上聊了两次,就互不联系了。介绍人倒是蛮热心,经常两边问怎么了,还跟我姑姑说女方抱怨我不主动,姑父就把我教育了一番。我想想不如见个面表现差劲一点把人吓退算了,结果对方回我周末没空。后来就再没联系过。
顺手放些近期拍的照片
姑父的诊所里第一次见到这种折叠果盘。把玩的时候收到条蒋太公的微信,说在杭州等飞机。出了诊所匆匆赶了过去。
六年多没见,一块吃了顿饭,聊了聊近况,又带他在聊近找菜市场买某种羊肉,可惜跑了许多家都没找到。
有天在公交车上看到个妹子不错,正好这位置正对着她,偷偷拍了一张。
外甥女非要玩我外套上的扣子,解开又扣上,玩得不亦乐乎。
一早出门,满地落叶。
玉泉校区对面街上的二胡,听了一会,放了几个硬币上去。
近来转战SegmentFault,问答氛围比知乎好多了,唯一差点的是人气。选了这张照片作为头像,网名叫许仙
去办了张市民卡,顺便在附近的印象城玩了玩。远处是钱塘江,对面是滨江。鸡爷的房子就买在那一块儿,一百多平,四百万。
冬天回家路过都会进来喝一杯暖暖身子。
六石居然也有不知道高到哪里去的华莱士,真是Exciting!
六石路边有家药酒店的老板在用开水烫笼子里的老鼠,可怜的小家伙。
六石的综合广场,老人们在晒粮食,边上还有好几个孕妇在晒太阳,非常惬意。
六石居然办了美食街。
挖掘机要从娃娃抓起。
初中母校,亮明身份仍然不肯放我进去。
去和老蔡吃了顿饭聊了聊,路上已经忘了这教学楼编号是多少。教四?教二?
市民中心的广场,视野开阔。
村里有个大叔也叫这个名字,和我同一辈。
高中母校,匆匆而过。
杭州的公交车,乘车不忘玩耍。一边的母亲照顾两个孩子,蛮辛苦。
路过买了碗炒粉干,城管过来收摊。小贩说再炒一份就走。城管老爷一怒,抢走了煤气罐。
金华参加金思美的婚礼,晚上在火车站对面找了家七天连锁酒店住下,177元。
与地面平等的电梯,或者说是一条传送带。
雨夜
晚上剪了头发,自拍一张。
年龄一大,难免要操心终生大事——操心的不是我,而是各路亲戚。我只想着如何避免这些事,老婆孩子是一条枷锁,使自己的下半生不得自由。可惜事与愿违,越来越大的压力,使我很难再坚持这样的做法。母亲身体不好,每次谈及这方面的事,总是说自己这几年还可以帮忙带带小孩,再晚就带不动了。听到心里总感觉无限心酸。
]]>前者可能是为了保护自家的微博APP,新浪微博APP的烂是出了名的,什么功能都想做,都想往里塞。以前经常用它刷微博,但是当它的无用功能越来越多之后,渐渐放弃了,顺便连微博也很少上——微博上到处都是转发了N遍的老段子,走了人人网衰落的老路。题外话略去不谈,下面放上目前的研究成果。
研究微博API纯是玩票性质,并没有刚性需求,所以一旦发现它限制多、意义不大,果断放手。在写这些代码过程中,最大的体会是在写单条微博类、用户类时,把JSON转化成类对象的方法,放在类中。以前爬豆瓣相册时,先把JSON数据挨个读取出来,再把每一项通过类的__init__
方法传进去,属性一多就容易写错,代码也不美观。现在折腾来折腾去,总算找到一种优雅的实现方法。半路出家,一些计算机专业的常识,都只能零零星星的学习摸索。加油!
身份验证有两种方式,一是App Key
,需要先模拟登录微博才可使用。二是Access Token
,不需要登录。
前者登录后可在微博API「我的应用」中找到。后者在API测试工具中,登录后可看到自己的Access Token
。
以下代码中默认使用Access Token
方式,如果想使用App Key
,需要手动调用login()
方法并传入用户名和密码。
1 | #!/usr/bin/env python |
前者可能是为了保护自家的微博APP,新浪微博APP的烂是出了名的,什么功能都想做,都想往里塞。以前经常用它刷微博,但是当它的无用功能越来越多之后,渐渐放弃了,顺便连微博也很少上——微博上到处都是转发了N遍的老段子,走了人人网衰落的老路。题外话略去不谈,下面放上目前的研究成果。
]]>网上搜到一份教程,《python使用rsa加密算法模块模拟新浪微博登录》,非常复杂。对照着做的过程中,自己又找出一个简单的登录方法。两种方法都登录成功,记录如下:
本文涉及的Python版本:3.4
登录页:http://login.sina.com.cn/signup/signin.php?entry=sso
抓包可找到登录网址:
https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15/)&_=1448285708709
最后一项是时间戳,经测试,可以直接删除。所以最终登录网址是:
https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)
POST提交的表单中,用户名需要base64编码。
完整登录代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import requests
import json
import base64
def login(username, password):
username = base64.b64encode(username.encode('utf-8')).decode('utf-8')
postData = {
"entry": "sso",
"gateway": "1",
"from": "null",
"savestate": "30",
"useticket": "0",
"pagerefer": "",
"vsnf": "1",
"su": username,
"service": "sso",
"sp": password,
"sr": "1440*900",
"encoding": "UTF-8",
"cdult": "3",
"domain": "sina.com.cn",
"prelt": "0",
"returntype": "TEXT",
}
loginURL = r'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)'
session = requests.Session()
res = session.post(loginURL, data = postData)
jsonStr = res.content.decode('gbk')
info = json.loads(jsonStr)
if info["retcode"] == "0":
print("登录成功")
# 把cookies添加到headers中,必须写这一步,否则后面调用API失败
cookies = session.cookies.get_dict()
cookies = [key + "=" + value for key, value in cookies.items()]
cookies = "; ".join(cookies)
session.headers["cookie"] = cookies
else:
print("登录失败,原因: %s" % info["reason"])
return session
if __name__ == '__main__':
session = login('你的用户名', '你的密码')
返回的session保存了登录状态,尽情做你做想做的事吧,配合微博API效果更佳。
本节依赖的第三方库除了上面用过的requests
,还有用于加密的rsa
,需要另行安装。
加密方式的破解找到一段方法,我没学过javascript,就没细看,直接照搬后面的加密方法。
获得以及查看新浪微博登录js文件:
查看新浪通行证url(http://login.sina.com.cn/signup/signin.php)的源代码,其中可以找到该js的地址http://login.sina.com.cn/js/sso/ssologin.js,不过打开后里面的内容是加密过的,可以在网上找个在线解密站点解密,查看最终用户名和密码的加密方式。
未登录状态返回:
sinaSSOController.preloginCallBack({
"retcode": 0,
"servertime": 1448278360,
"pcid": "gz-8139fa256b150b7a42e09dbdc0b4ed2224eb",
"nonce": "RDY5SN",
"pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245A87AC253062882729293E5506350508E7F9AA3BB77F4333231490F915F6D63C55FE2F08A49B353F444AD3993CACC02DB784ABBB8E42A9B1BBFFFB38BE18D78E87A0E41B9B8F73A928EE0CCEE1F6739884B9777E4FE9E88A1BBE495927AC4A799B3181D6442443",
"rsakv": "1330428213",
"exectime": 9
})
取出括号里的JSON字符串提取信息:1
2
3
4
5
6
7
8
9
10def getLoginInfo():
preLoginURL = r'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&rsakt=mod&client=ssologin.js(v1.4.18)'
html = requests.get(preLoginURL).text
jsonStr = re.findall(r'\((\{.*?\})\)', html)[0]
data = json.loads(jsonStr)
servertime = data["servertime"]
nonce = data["nonce"]
pubkey = data["pubkey"]
rsakv = data["rsakv"]
return servertime, nonce, pubkey, rsakv
1 | def getSu(username): |
1 | def login(su, sp, servertime, nonce, rsakv): |
1 | import requests |
网上搜到一份教程,《python使用rsa加密算法模块模拟新浪微博登录》,非常复杂。对照着做的过程中,自己又找出一个简单的登录方法。两种方法都登录成功,记录如下:
]]>东莞市第三届广告大赛自由创作类金奖。
比如我要传这三个文件:
选中三个文件,拖曳到脚本图标上。上传成功后弹出结果:
![11.jpg](http://7xo8t2.com1.z0.glb.clouddn.com/img/11.jpg)
![22.jpg](http://7xo8t2.com1.z0.glb.clouddn.com/img/22.jpg)
[Python标准库中文版.pdf](http://7xo8t2.com1.z0.glb.clouddn.com/file/Python%E6%A0%87%E5%87%86%E5%BA%93%E4%B8%AD%E6%96%87%E7%89%88.pdf)
生成了图片和文件链接对应的Markdown写法,可以直接把它们复制出来粘贴到文件中。
必须先安装七牛SDK Python版:
pip install qiniu
1 | #!/usr/bin/env python |
hexo s
本地预览能正常显示,但是hexo d
部署到服务器上,首页却只有最新的一篇文章。昨天还好好的,又没有安装新的软件,不知道是怎么回事。换成Jacman主题一切正常,但是NexT的Mist Scheme或默认Scheme都存在这种情况,推测是主题的BUG。
已经有人向作者反映了问题,我自己摸索出来的解决办法:首页文件部署问题 #482
这个问题发生在hexo clean
后第一次hexo g
,此时生成的index.html只显示最近的一篇文章。再运行一次hexo g
就能生成正常的index.html了。
期待作者早日解决此BUG。
补充:发完这篇文章hexo g
的时候,竟然又正常了。
hexo s
本地预览能正常显示,但是hexo d
部署到服务器上,首页却只有最新的一篇文章。os.system
os.open
os.spawn
os.popen
popen2.
commands.
1 | # 运行由args指定的命令,结束后返回return code |
1 | # 调用系统中cmd命令,显示命令执行的结果: |
1 | # test.py |
1 | import subprocess |
或者
1 | import subprocess |
]]>os.system
os.open
os.spawn
os.popen
popen2.
commands.
pip install numpy
出现如下错误:
Python的部分库在安装时要临时编译,找不到编译器就报错。类似的库还有lxml等。根据提示装了Visual C++ 10.0 仍然报错。微软的网站上只找到 Python 2.7 用的VC编译器(我是Python 3.4)。经过一番搜索终于解决。
以numpy为例,步骤如下:
pip install wheel
在Python Extension Packages for Windows下载对应的文件。
英文提示是说vanilla版测试得较少,而且与本页的其他二进制包不兼容,那我们选择mkl版。cp应该是指CPython,后面的数字是Python版本。我下载了numpy‑1.9.3+mkl‑cp34‑none‑win32.whl
版本,注意不要改文件名,否则后续步骤会报错。
命令行定位到.whl文件所在目录,安装命令为:
pip install 带后缀的完整文件名
这么长的文件名,手输容易出错,可以先在命令行输入pip install
,然后右键单击CMD窗口标题栏,选编辑
-> 粘贴
。如图:
以前一直用pip安装第三方库,现在出现个wheel,搞不清楚它们之间的关联。还有网上搜东西时经常看到的easy install是什么玩意?
找到一篇文章,可以理清这些工具之间的脉络:Python 包管理工具解惑
]]>pip install numpy
出现如下错误:
Python的部分库在安装时要临时编译,找不到编译器就报错。类似的库还有lxml等。根据提示装了Visual C++ 10.0 仍然报错。微软的网站上只找到 Python 2.7 用的VC编译器(我是Python 3.4)。经过一番搜索终于解决。
]]>查看网页源码,发现同一张图片有四种网址:
"thumbURL": "http://img1.imgtn.bdimg.com/it/u=757023778,2840825931&fm=21&gp=0.jpg",
"middleURL": "http://img1.imgtn.bdimg.com/it/u=757023778,2840825931&fm=21&gp=0.jpg",
"hoverURL": "http://img1.imgtn.bdimg.com/it/u=757023778,2840825931&fm=23&gp=0.jpg",
"objURL": "http://imgsrc.baidu.com/forum/w=580/sign=b3bcc2f88a5494ee87220f111df4e0e1/78fed309b3de9c82913abac86a81800a18d84344.jpg",
经测试,前三种都有反爬虫措施,用浏览器可以打开,但是刷新一次就403 Forbidden。用爬虫获取不到图片。
第四种objURL是指图片的源网址,获取该网址会出现三种情况:
代码如下:
1 | import requests |
Exciting!
但是上面的代码还有不足,当我们在网页中下拉时,百度会继续加载更多图片。需要再完善一下代码。
打开Chrome,按F12,切换到Network标签,然后将网页向下拉。这时浏览器地址栏的网址并没有改变,而网页中的图片却一张张增加,说明网页在后台与服务器交互数据。警察蜀黍,就是这家伙:
xhr全称XMLHttpRequest,详细介绍见百度:XMLHTTPRequest_百度百科
点开看看:
这么长一串网址,没有头绪。下拉网页,再抓一个xhr,对比一下它们的Request URL,使用在线文字对比工具:文本比较
URL末尾有三处变化,最后一项看上去是时间戳,经过测试,直接把它删了也没事。
那么只需要研究pn
和gsm
值。继续下拉,到底的时候点加载更多图片
,多抓几个对比一下URL的末尾部分:
pn=120&rn=60&gsm=78
pn=180&rn=60&gsm=b4
pn=240&rn=60&gsm=f0
pn=300&rn=60&gsm=12c
pn=360&rn=60&gsm=168
pn
是一个60为步长的等差数列。gsm
看上去是16进制,转换成十进制,发现它就是pn
值,试了也可以删掉。
经测试,rn
是步长值,最大只能取60,填入大于60的数,仍然以60为步长。如果删了rn
,则步长值变为30。pn
是图片编号,从0开始。
现在已经删了时间戳和gsm
两项了,能不能让网址再短一点?继续观察,注意到:
&se=&tab=&width=&height=
这几项没指定值,肯定没用,把没值的都删了。
再看看这两项:
queryWord=%E9%95%BF%E8%80%85%E8%9B%A4
word=%E9%95%BF%E8%80%85%E8%9B%A4
这就是我们本次搜索的关键词。网址中的中文会被编码成UTF-8,每个中文3个字节,每个字节前加上%号。编码和解码方法如下:
那么,我们可以写出指定关键词需要请求的所有网址:1
2
3
4
5
6
7
8import itertools
import urllib
def buildUrls(word):
word = urllib.parse.quote(word)
url = r"http://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&fp=result&queryWord={word}&cl=2&lm=-1&ie=utf-8&oe=utf-8&st=-1&ic=0&word={word}&face=0&istype=2nc=1&pn={pn}&rn=60"
urls = (url.format(word=word, pn=x) for x in itertools.count(start=0, step=60))
return urls
上面的代码中,itertools.count(start=0, step=60)表示一个从0开始,60为步长值的无限等差数列。
把这个数列的数字分别填入url作为pn值,就得到一个无限容量的url生成器,注意生成器必须用圆括号,如果写成中括号就成了列表,程序会在这一步无限执行下去。
下面开始解析每次获取的数据。我们点开看看,返回的是一串json数据。
纳尼,这个objURL怎么不是HTTP开头的。试了几种方法都没成功,Google一下,找到这个:百度图片url解码
OK,既然明白了原理,我们写一个Python版的解密实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: loveNight
# @Date: 2015-11-14 21:11:11
"""解码百度图片搜索json中传递的url
抓包可以获取加载更多图片时,服务器向网址传输的json。
其中originURL是特殊的字符串
解码前:
ippr_z2C$qAzdH3FAzdH3Ffl_z&e3Bftgwt42_z&e3BvgAzdH3F4omlaAzdH3Faa8W3ZyEpymRmx3Y1p7bb&mla
解码后:
http://s9.sinaimg.cn/mw690/001WjZyEty6R6xjYdtu88&690
使用下面两张映射表进行解码。
"""
str_table = {
'_z2C$q': ':',
'_z&e3B': '.',
'AzdH3F': '/'
}
char_table = {
'w': 'a',
'k': 'b',
'v': 'c',
'1': 'd',
'j': 'e',
'u': 'f',
'2': 'g',
'i': 'h',
't': 'i',
'3': 'j',
'h': 'k',
's': 'l',
'4': 'm',
'g': 'n',
'5': 'o',
'r': 'p',
'q': 'q',
'6': 'r',
'f': 's',
'p': 't',
'7': 'u',
'e': 'v',
'o': 'w',
'8': '1',
'd': '2',
'n': '3',
'9': '4',
'c': '5',
'm': '6',
'0': '7',
'b': '8',
'l': '9',
'a': '0'
}
# str 的translate方法需要用单个字符的十进制unicode编码作为key
# value 中的数字会被当成十进制unicode编码转换成字符
# 也可以直接用字符串作为value
char_table = {ord(key): ord(value) for key, value in char_table.items()}
def decode(url):
# 先替换字符串
for key, value in str_table.items():
url = url.replace(key, value)
# 再替换剩下的字符
return url.translate(char_table)
if __name__ == '__main__':
url = r"ippr_z2C$qAzdH3FAzdH3Ffl_z&e3Bftgwt42_z&e3BvgAzdH3F4omlaAzdH3Faa8W3ZyEpymRmx3Y1p7bb&mla"
print(decode(url))
测试成功!
再从JSON字符串中找出所有的originURL:1
2re_url = re.compile(r'"objURL":"(.*?)"')
imgs = re_url.findall(html)
格式化JSON推荐使用Chrome的JSON handle插件
整理一下流程:
整合一下上面的代码,可以写出单线程的下载脚本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: loveNight
import json
import itertools
import urllib
import requests
import os
import re
import sys
str_table = {
'_z2C$q': ':',
'_z&e3B': '.',
'AzdH3F': '/'
}
char_table = {
'w': 'a',
'k': 'b',
'v': 'c',
'1': 'd',
'j': 'e',
'u': 'f',
'2': 'g',
'i': 'h',
't': 'i',
'3': 'j',
'h': 'k',
's': 'l',
'4': 'm',
'g': 'n',
'5': 'o',
'r': 'p',
'q': 'q',
'6': 'r',
'f': 's',
'p': 't',
'7': 'u',
'e': 'v',
'o': 'w',
'8': '1',
'd': '2',
'n': '3',
'9': '4',
'c': '5',
'm': '6',
'0': '7',
'b': '8',
'l': '9',
'a': '0'
}
# str 的translate方法需要用单个字符的十进制unicode编码作为key
# value 中的数字会被当成十进制unicode编码转换成字符
# 也可以直接用字符串作为value
char_table = {ord(key): ord(value) for key, value in char_table.items()}
# 解码图片URL
def decode(url):
# 先替换字符串
for key, value in str_table.items():
url = url.replace(key, value)
# 再替换剩下的字符
return url.translate(char_table)
# 生成网址列表
def buildUrls(word):
word = urllib.parse.quote(word)
url = r"http://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&fp=result&queryWord={word}&cl=2&lm=-1&ie=utf-8&oe=utf-8&st=-1&ic=0&word={word}&face=0&istype=2nc=1&pn={pn}&rn=60"
urls = (url.format(word=word, pn=x) for x in itertools.count(start=0, step=60))
return urls
# 解析JSON获取图片URL
re_url = re.compile(r'"objURL":"(.*?)"')
def resolveImgUrl(html):
imgUrls = [decode(x) for x in re_url.findall(html)]
return imgUrls
def downImg(imgUrl, dirpath, imgName):
filename = os.path.join(dirpath, imgName)
try:
res = requests.get(imgUrl, timeout=15)
if str(res.status_code)[0] == "4":
print(str(res.status_code), ":" , imgUrl)
return False
except Exception as e:
print("抛出异常:", imgUrl)
print(e)
return False
with open(filename, "wb") as f:
f.write(res.content)
return True
def mkDir(dirName):
dirpath = os.path.join(sys.path[0], dirName)
if not os.path.exists(dirpath):
os.mkdir(dirpath)
return dirpath
if __name__ == '__main__':
print("欢迎使用百度图片下载脚本!\n目前仅支持单个关键词。")
print("下载结果保存在脚本目录下的results文件夹中。")
print("=" * 50)
word = input("请输入你要下载的图片关键词:\n")
dirpath = mkDir("results")
urls = buildUrls(word)
index = 0
for url in urls:
print("正在请求:", url)
html = requests.get(url, timeout=10).content.decode('utf-8')
imgUrls = resolveImgUrl(html)
if len(imgUrls) == 0: # 没有图片则结束
break
for url in imgUrls:
if downImg(url, dirpath, str(index) + ".jpg"):
index += 1
print("已下载 %s 张" % index)
执行脚本:
查看同目录下的results文件夹,又看到了亲切的「他」。
上面的代码仍然有改进空间:
多线程一直没学好,想不到更优雅的写法,大家将就看一下吧,欢迎提出改进建议。
百度图片下载脚本之多线程版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: loveNight
# @Date: 2015-10-28 19:59:24
# @Last Modified by: loveNight
# @Last Modified time: 2015-11-15 19:24:57
import urllib
import requests
import os
import re
import sys
import time
import threading
from datetime import datetime as dt
from multiprocessing.dummy import Pool
from multiprocessing import Queue
class BaiduImgDownloader(object):
"""百度图片下载工具,目前只支持单个关键词"""
# 解码网址用的映射表
str_table = {
'_z2C$q': ':',
'_z&e3B': '.',
'AzdH3F': '/'
}
char_table = {
'w': 'a',
'k': 'b',
'v': 'c',
'1': 'd',
'j': 'e',
'u': 'f',
'2': 'g',
'i': 'h',
't': 'i',
'3': 'j',
'h': 'k',
's': 'l',
'4': 'm',
'g': 'n',
'5': 'o',
'r': 'p',
'q': 'q',
'6': 'r',
'f': 's',
'p': 't',
'7': 'u',
'e': 'v',
'o': 'w',
'8': '1',
'd': '2',
'n': '3',
'9': '4',
'c': '5',
'm': '6',
'0': '7',
'b': '8',
'l': '9',
'a': '0'
}
re_objURL = re.compile(r'"objURL":"(.*?)".*?"type":"(.*?)"')
re_downNum = re.compile(r"已下载\s(\d+)\s张图片")
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Accept-Encoding": "gzip, deflate, sdch",
}
def __init__(self, word, dirpath=None, processNum=30):
if " " in word:
raise AttributeError("本脚本仅支持单个关键字")
self.word = word
self.char_table = {ord(key): ord(value)
for key, value in BaiduImgDownloader.char_table.items()}
if not dirpath:
dirpath = os.path.join(sys.path[0], 'results')
self.dirpath = dirpath
self.jsonUrlFile = os.path.join(sys.path[0], 'jsonUrl.txt')
self.logFile = os.path.join(sys.path[0], 'logInfo.txt')
self.errorFile = os.path.join(sys.path[0], 'errorUrl.txt')
if os.path.exists(self.errorFile):
os.remove(self.errorFile)
if not os.path.exists(self.dirpath):
os.mkdir(self.dirpath)
self.pool = Pool(30)
self.session = requests.Session()
self.session.headers = BaiduImgDownloader.headers
self.queue = Queue()
self.messageQueue = Queue()
self.index = 0 # 图片起始编号,牵涉到计数,不要更改
self.promptNum = 10 # 下载几张图片提示一次
self.lock = threading.Lock()
self.delay = 1.5 # 网络请求太频繁会被封
self.QUIT = "QUIT" # Queue中表示任务结束
self.printPrefix = "**" # 用于指定在控制台输出
def start(self):
# 控制台输出线程
t = threading.Thread(target=self.__log)
t.setDaemon(True)
t.start()
self.messageQueue.put(self.printPrefix + "脚本开始执行")
start_time = dt.now()
urls = self.__buildUrls()
self.messageQueue.put(self.printPrefix + "已获取 %s 个Json请求网址" % len(urls))
# 解析出所有图片网址,该方法会阻塞直到任务完成
self.pool.map(self.__resolveImgUrl, urls)
while self.queue.qsize():
imgs = self.queue.get()
self.pool.map_async(self.__downImg, imgs)
self.pool.close()
self.pool.join()
self.messageQueue.put(self.printPrefix + "下载完成!已下载 %s 张图片,总用时 %s" %
(self.index, dt.now() - start_time))
self.messageQueue.put(self.printPrefix + "请到 %s 查看结果!" % self.dirpath)
self.messageQueue.put(self.printPrefix + "错误信息保存在 %s" % self.errorFile)
self.messageQueue.put(self.QUIT)
def __log(self):
"""控制台输出,加锁以免被多线程打乱"""
with open(self.logFile, "w", encoding = "utf-8") as f:
while True:
message = self.messageQueue.get()
if message == self.QUIT:
break
message = str(dt.now()) + " " + message
if self.printPrefix in message:
print(message)
elif "已下载" in message:
# 下载N张图片提示一次
downNum = self.re_downNum.findall(message)
if downNum and int(downNum[0]) % self.promptNum == 0:
print(message)
f.write(message + '\n')
f.flush()
def __getIndex(self):
"""获取文件编号"""
self.lock.acquire()
try:
return self.index
finally:
self.index += 1
self.lock.release()
def decode(self, url):
"""解码图片URL
解码前:
ippr_z2C$qAzdH3FAzdH3Ffl_z&e3Bftgwt42_z&e3BvgAzdH3F4omlaAzdH3Faa8W3ZyEpymRmx3Y1p7bb&mla
解码后:
http://s9.sinaimg.cn/mw690/001WjZyEty6R6xjYdtu88&690
"""
# 先替换字符串
for key, value in self.str_table.items():
url = url.replace(key, value)
# 再替换剩下的字符
return url.translate(self.char_table)
def __buildUrls(self):
"""json请求网址生成器"""
word = urllib.parse.quote(self.word)
url = r"http://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&fp=result&queryWord={word}&cl=2&lm=-1&ie=utf-8&oe=utf-8&st=-1&ic=0&word={word}&face=0&istype=2nc=1&pn={pn}&rn=60"
time.sleep(self.delay)
html = self.session.get(url.format(word=word, pn=0), timeout = 15).content.decode('utf-8')
results = re.findall(r'"displayNum":(\d+),', html)
maxNum = int(results[0]) if results else 0
urls = [url.format(word=word, pn=x)
for x in range(0, maxNum + 1, 60)]
with open(self.jsonUrlFile, "w", encoding="utf-8") as f:
for url in urls:
f.write(url + "\n")
return urls
def __resolveImgUrl(self, url):
"""从指定网页中解析出图片URL"""
time.sleep(self.delay)
html = self.session.get(url, timeout = 15).content.decode('utf-8')
datas = self.re_objURL.findall(html)
imgs = [Image(self.decode(x[0]), x[1]) for x in datas]
self.messageQueue.put(self.printPrefix + "已解析出 %s 个图片网址" % len(imgs))
self.queue.put(imgs)
def __downImg(self, img):
"""下载单张图片,传入的是Image对象"""
imgUrl = img.url
# self.messageQueue.put("线程 %s 正在下载 %s " %
# (threading.current_thread().name, imgUrl))
try:
time.sleep(self.delay)
res = self.session.get(imgUrl, timeout = 15)
message = None
if str(res.status_code)[0] == "4":
message = "\n%s: %s" % (res.status_code, imgUrl)
elif "text/html" in res.headers["Content-Type"]:
message = "\n无法打开图片: %s" % imgUrl
except Exception as e:
message = "\n抛出异常: %s\n%s" % (imgUrl, str(e))
finally:
if message:
self.messageQueue.put(message)
self.__saveError(message)
return
index = self.__getIndex()
# index从0开始
self.messageQueue.put("已下载 %s 张图片:%s" % (index + 1, imgUrl))
filename = os.path.join(self.dirpath, str(index) + "." + img.type)
with open(filename, "wb") as f:
f.write(res.content)
def __saveError(self, message):
self.lock.acquire()
try:
with open(self.errorFile, "a", encoding="utf-8") as f:
f.write(message)
finally:
self.lock.release()
class Image(object):
"""图片类,保存图片信息"""
def __init__(self, url, type):
super(Image, self).__init__()
self.url = url
self.type = type
if __name__ == '__main__':
print("欢迎使用百度图片下载脚本!\n目前仅支持单个关键词。")
print("下载结果保存在脚本目录下的results文件夹中。")
print("=" * 50)
word = input("请输入你要下载的图片关键词:\n")
down = BaiduImgDownloader(word)
down.start()
执行脚本:
欢迎使用百度图片下载脚本!
目前仅支持单个关键词。
下载结果保存在脚本目录下的results文件夹中。
==================================================
请输入你要下载的图片关键词:
长者蛤
2015-11-15 19:25:11.726878 **脚本开始执行
2015-11-15 19:25:16.292022 **已获取 20 个Json请求网址
2015-11-15 19:25:17.885767 **已解析出 30 个图片网址
2015-11-15 19:25:17.917020 **已解析出 60 个图片网址
.....
.....中间省略
.....
2015-11-15 19:33:31.726739 已下载 980 张图片:http://bbs.nantaihu.com/bbs/UpImages2008/2010/8/3/U97946_201083171218512-2.jpg
2015-11-15 19:33:32.695518 已下载 990 张图片:http://pf.u.51img1.com/f9/7f/huangbaoling0_180.gif?v=20120224161006
2015-11-15 19:33:45.473957 已下载 1000 张图片:http://library.taiwanschoolnet.org/cyberfair2003/C0312970394/narrative.files/image018.jpg
2015-11-15 19:33:50.749544 **下载完成!已下载 1000 张图片,总用时 0:08:39.022666
2015-11-15 19:33:50.765169 **请到 F:\PythonWorkspace\Learn\results 查看结果!
2015-11-15 19:33:50.858880 **错误信息保存在 F:\PythonWorkspace\Learn\errorUrl.txt
合租室友太多,平时网速很慢,经常连百度都打不开,要刷新多次。下午测试的时候可以2分钟下完,现在居然要近9分钟。
errorUrl.txt
的内容
无法打开图片: http://s3.t.itc.cn/mblog/pic/20136_20_14/f_b3a1be1796819229739.png
403: http://www.longhoo.net/gb/longhoo/culture/view/images/00024483.jpg
无法打开图片: http://bhyjk.cn/images/IMG44zj10.jpg
404: http://ebook.indaa.com.cn/upload/baike/images/2960001-2970000/2968900/adee30ddd41190838c10291d.jpg
抛出异常: http://www.jzxzj.com/images/xs.jpg
HTTPConnectionPool(host='www.jzxzj.com', port=80): Max retries exceeded with url: /images/xs.jpg (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x0851FBB0>: Failed to establish a new connection: [Errno 11002] getaddrinfo failed',))
抛出异常: http://static.acfun.mm111.net/h/image/20147/846b7553851b6edb2305c6b87f4aed71.jpg
HTTPConnectionPool(host='static.acfun.mm111.net', port=80): Max retries exceeded with url: /h/image/20147/846b7553851b6edb2305c6b87f4aed71.jpg (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x0888B710>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed',))
抛出异常: http://static.acfun.mm111.net/h/image/2015-0-4/23ebeaf5-5130-4072-b90e-48ce4d122ac8.jpg
HTTPConnectionPool(host='static.acfun.mm111.net', port=80): Max retries exceeded with url: /h/image/2015-0-4/23ebeaf5-5130-4072-b90e-48ce4d122ac8.jpg (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x0888F1F0>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed',))
404: http://www.zipooo.com/tupian/1/%CE%EF%C1%F7%BF%EC%B5%DD.gif
无法打开图片: http://www.zhiyin.cn/hsbk/fakeimg/7939883.jpeg
无法打开图片: http://www.zhiyin.cn/hsbk/fakeimg/21753396.jpeg
.....
logInfo.txt
内容:
2015-11-15 19:25:11.726878 **脚本开始执行
2015-11-15 19:25:16.292022 **已获取 20 个Json请求网址
2015-11-15 19:25:17.885767 **已解析出 30 个图片网址
2015-11-15 19:25:17.917020 **已解析出 60 个图片网址
2015-11-15 19:25:17.948262 **已解析出 60 个图片网址
2015-11-15 19:25:17.963895 **已解析出 60 个图片网址
2015-11-15 19:25:17.995146 **已解析出 60 个图片网址
2015-11-15 19:25:18.010769 **已解析出 60 个图片网址
2015-11-15 19:25:18.057638 **已解析出 60 个图片网址
2015-11-15 19:25:18.057638 **已解析出 60 个图片网址
2015-11-15 19:25:18.073264 **已解析出 60 个图片网址
2015-11-15 19:25:18.073264 **已解析出 60 个图片网址
2015-11-15 19:25:18.088890 **已解析出 60 个图片网址
2015-11-15 19:25:18.088890 **已解析出 60 个图片网址
2015-11-15 19:25:18.104516 **已解析出 60 个图片网址
2015-11-15 19:25:18.104516 **已解析出 60 个图片网址
2015-11-15 19:25:18.104516 **已解析出 60 个图片网址
2015-11-15 19:25:18.104516 **已解析出 60 个图片网址
2015-11-15 19:25:18.245137 **已解析出 60 个图片网址
2015-11-15 19:25:18.260764 **已解析出 60 个图片网址
2015-11-15 19:25:18.260764 **已解析出 60 个图片网址
2015-11-15 19:25:18.479555 **已解析出 60 个图片网址
2015-11-15 19:25:27.532965 已下载 1 张图片:http://imgsrc.baidu.com/baike/abpic/item/43e6c7335b379350ad4b5f92.jpg
2015-11-15 19:25:29.084964
无法打开图片: http://s3.t.itc.cn/mblog/pic/20136_20_14/f_b3a1be1796819229739.png
2015-11-15 19:25:29.522473 已下载 2 张图片:http://p4.qhimg.com/dr/200_200_/t0196ad8cefbc7a2c7c.jpg
2015-11-15 19:25:29.822650 已下载 3 张图片:http://library.taiwanschoolnet.org/cyberfair2003/C0312970394/narrative.files/image016.jpg
2015-11-15 19:25:29.938687 已下载 4 张图片:http://blog.ylib.com/public/Utilities/ImageReader.aspx?/0/6/06c67eaf-09db-40a8-a117-c577a6d05d08.jpg
....
仍然可以改进的地方:
查看网页源码,发现同一张图片有四种网址:
]]>写了段代码测试:
1 | #!/usr/bin/env python |
运行结果:
Json解析一万次 14.212283402610288 秒
Re解析一万次 9.272848993345992 秒
看来还是正则表达式更快。不了解这两者的实现,暂无法作更进一步的分析。
]]>写了段代码测试:
]]>运行的时候出现RecursionError
:
Traceback (most recent call last):
File "C:\Python 3.5\lib\multiprocessing\queues.py", line 241, in _feed
obj = ForkingPickler.dumps(obj)
File "C:\Python 3.5\lib\multiprocessing\reduction.py", line 50, in dumps
cls(buf, protocol).dump(obj)
RecursionError: maximum recursion depth exceeded
递归深度超过限制?但是我代码中没用递归啊。翻了一下queues.py
的源码,原来multiprocessing.Queue
在首次调用put()
方法时会启动一个名为QueueFeederThread
的调度线程,用于分配数据。该线程传输数据前会先调用ForkingPickler.dumps(obj)
将数据序列化成bytes
,这个过程中抛出RecursionError
。后续调用我翻了半小时也没看懂,暂时放一边吧。
Google一番得知Python有默认的递归深度限制
>>> import sys
>>> sys.getrecursionlimit()
1000
把它设置得大一点,比如一百万:
sys.setrecursionlimit(1000000)
再次执行脚本,果然不再报错。
附代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: loveNight
import os
import sys
import csv
import time
import requests
import threading
from multiprocessing import Queue
from datetime import datetime, timedelta
from bs4 import BeautifulSoup as BS
from multiprocessing.dummy import Pool
sys.setrecursionlimit(1000000) # 递归深度,默认只有900
os.chdir(sys.path[0])
url_pattern = r"http://www.nea.gov.sg/anti-pollution-radiation-protection/air-pollution-control/psi/historical-psi-readings/year/{0}/month/{1}/day/{2}"
# 表头
table_header = ["Year", "Month", "Day", "Time", "North",
"East", "West", "Central", "Overall Singapore"]
headers = {
"Accept-Encoding": "gzip,deflate,sdch",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0",
"Host": "www.nea.gov.sg",
}
session = requests.Session()
session.headers = headers
delay = 0 # 网络请求之间的间隔
QUIT = "Quit"
queue = Queue()
# 要下载的日期
dt = datetime(2014, 4, 1)
dt_now = datetime.now()
todo = []
while dt <= dt_now:
todo.append(dt)
dt += timedelta(days=1)
# 打开网页
def getPage(url):
if delay:
time.sleep(delay)
return session.get(url).text
# 写入文件
def save(filename):
start = time.time()
with open(filename, "w", newline="") as output:
writer = csv.writer(output)
writer.writerow(table_header)
while True:
lines = queue.get()
if isinstance(lines, str) and lines == QUIT:
break
else:
print("拿到数据,正在写入", datetime.now())
writer.writerows(lines)
print("写入完成!用时 %s " % (time.time() - start))
# 解析指定日期的页面
def resolvePage(dt):
year = dt.year
month = dt.month
day = dt.day
html = getPage(url_pattern.format(year, month, day))
soup = BS(html, "lxml") # 需要安装第三方库lxml,也可以使用自带的html.parser
table = soup.find(name="table", class_="text_psinormal")
if table:
trs = table.find_all("tr")
trs = trs[2:] # 去除表头
lines = []
for tr in trs:
datas = [year, month, day] + [x for x in tr.strings if x != "\n"]
lines.append(datas)
queue.put(lines) # 传入整张表
# 开始下载
filename = "data.csv"
t = threading.Thread(target=save, args=(filename,))
t.daemon = True
t.start()
pool = Pool(30)
pool.map(resolvePage, todo)
pool.close()
pool.join()
queue.put(QUIT)
下载用时127秒。
]]>运行的时候出现RecursionError
:
Traceback (most recent call last):
File "C:\Python 3.5\lib\multiprocessing\queues.py", line 241, in _feed
obj = ForkingPickler.dumps(obj)
File "C:\Python 3.5\lib\multiprocessing\reduction.py", line 50, in dumps
cls(buf, protocol).dump(obj)
RecursionError: maximum recursion depth exceeded
]]>
最近发现知乎的登录界面改了,抓包看了看,果然登录方式也变了,会根据用户名字符串发送不同字段的POST。中午研究了一会,写了个登录知乎的工具类,可以自动处理Cookie,维持登录状态。目前仅实现登录和打开网页的功能。
requests
BeautifulSoup
lxml
这个不装没关系,把__login()
方法中的lxml
改成html.parser
即可。1 | client = ZhiHuClient() |
第一次使用时,需要调用login()方法登录一次,生成cookie文件:1
client.login("你的用户名", "你的密码")
此时会下载并打开验证码图片:
==================================================
已打开验证码图片,请识别!
关闭图片后,会提示你输入验证码:
==================================================
已打开验证码图片,请识别!
请输入验证码:
如果验证码输入错误,会提示:
==================================================
登录失败
错误信息 ---> 请提交正确的验证码 :(
==================================================
已打开验证码图片,请识别!
如果验证码输入正确,提示:
==================================================
登录成功
==================================================
已在同目录下生成cookie文件: cookie
下次登录时会自动载入此cookie文件,不再需要调用login
1 | res = client.open(url) |
返回一个Response
对象,其方法与属性请参照requests
库
1 | #!/usr/bin/env python |
最近发现知乎的登录界面改了,抓包看了看,果然登录方式也变了,会根据用户名字符串发送不同字段的POST。中午研究了一会,写了个登录知乎的工具类,可以自动处理Cookie,维持登录状态。目前仅实现登录和打开网页的功能。
requests
BeautifulSoup
lxml
这个不装没关系,把__login()
方法中的lxml
改成html.parser
即可。GitHub Pages 本用于介绍托管在 GitHub 的项目,也可以用来搭建博客,有300M免费空间。
hexo是一个基于Node.js的静态博客程序,可以方便的生成静态网页托管在github和Heroku上。作者是来自台湾的tommy351。
优势:
生成静态页面快
支持 Markdown
兼容于 Windows, Mac & Linux
部署方便。日常使用仅需五个命令。
高扩展性、自订性,文件少、小,易理解
使用hexo博客必须配置SSH。
打开git bash,输入cd ~/.ssh
,如果果提示:No such file or directory 说明未配置SSH。
ssh-keygen -t rsa -C "你的邮件地址"
,注意命令中的大小写不要搞混。按提示指定保存文件夹,不设置密码。Settings
—> SSH keys
—> Add SSH
key。把公钥粘贴到key中,填好title并点击 Add key。ssh -T git@github.com
,选yes,等待片刻可看到成功提示。git remote set-url origin SSH对应的url
,配置完后可用git remote -v
查看结果这样git push
或hexo d
时不再需要输入账号密码。
注,以下命令行需要在Git终端中执行(右键单击 -> Git bash
)。
npm install -g hexo
,可用hexo -v
查看版本。这里我用的是3.1.1。也可以指定版本:npm install hexo@3.1.1 -g
hexo init
。hexo 会在目标文件夹建立网站所需要的所有文件。npm install
你的Github名.github.io
,比如我是loveNight.github.io
_config.yml
文件,末尾添加如下信息。deploy:
type: git
repository: 上一步的Github仓库地址,项目主页点SSH再复制URL
branch: master
然后执行命令:
hexo generate # 生成静态页面,可以简化为hexo g
hexo deploy # 部署到Github,可以简化为hexo d
浏览器访问loveNight.github.io
就能看到自己的Blog了,一般延迟十分钟左右才能看到效果。一开始看到404页面不要惊慌,耐心等等。
手打党请注意,配置文件的冒号后必须有一个空格。
如果报错
Deployer not found:git
运行命令
npm install hexo-deployer-git --save
1 | hexo generate |
1 | hexo server |
浏览器输入localhost:4000
就可以看到效果。当你修改了文章或配置文件时,保存文件再刷新浏览器就能看到修改后的效果,非常方便。
hexo new post "title" # 生成新文章:\source\_posts\title.md,可省略post
hexo new page "title"
post、page等可以改成其他layout,可用layout在scaffolds目录下查看。在同目录下创建文件来添加自己的layout,也可以编辑现有的layout,比如post的layout默认是\scaffolds\post.md
。
打开新建的文章\source\_posts\postName.md
:
title: HelloWorld! # 文章页面上的显示名称,可以任意修改,不会出现在URL中
date: 2015-11-09 15:56:26 # 文章生成时间,一般不改
categories: # 文章分类目录,参数可省略
- 随笔
- 瞬间
tags: # 文章标签,参数可省略
- hexo
- blog # 个数不限,单个可直接跟在tags后面
---
这里开始使用markdown格式输入你的正文。
多级分类语法格式:(标签也可以用类似的写法)
# 第一种
categories:
- 一级分类
- 二级分类
- etc...
# 第二种:
categories: [一级分类, 二级分类]
首页文章预览添加图片:
photos:
- http://xxx.com/photo1.jpg
- http://xxx.com/photo2.jpg
正文中可以使用<!--more-->
设置文章摘要 如下:
以上显示在摘要中
<!--more-->
以下是余下全文
more 以上内容即是文章摘要,如果设置了主页只显示摘要,则more以下内容点击 Read More
链接打开全文才显示。
hexo现在支持更加简单的命令格式了,比如:
hexo g == hexo generate # 生成
hexo d == hexo deploy # 部署 # 可与hexo g合并为 hexo d -g
hexo s == hexo server # 本地预览
hexo n == hexo new # 写文章
博客中的图片文件可以直接放在source文件夹下,部署时上传到Github仓库中。但是Github项目容量有限,而且主机在国外,访问速度较慢,把图片放在国内的图床上是个更好的选择。我用的是七牛云存储
免费用户实名审核之后,可以获取10GB永久免费存储空间、每月10GB下载流量、每月10万次Put请求、每月100万次Get请求,做图床绰绰有余。
注册账号,新建空间,我的新空间名是blog
,专门用来放置博客上引用的资源。
进入空间后点击「内容管理」,再点击「上传」:
七牛空间没有文件夹的概念,但是允许为文件添加带斜杠/
的前缀,用来给资源分类。这里我设置前缀为img/Hexo 3.1.1 静态博客搭建指南/
。上传了一张图片:
在右侧可以找到外链,复制地址:
Markdown 插入图片的语法为:
![](图片网址)
上传图片 -> 获取外链 -> 写入Markdown,就这么简单!
由于七牛防盗链的白名单无法添加localhost
,暂时不设置防盗链,否则hexo s
调试的时候,看不到图片。
以上操作每插入一张图片就要做一次,相当繁琐,于是写了个脚本简化,详见我的这篇文章《拖曳文件上传到七牛的Python脚本》
注意:文件中配置项的冒号后面必须加空格,否则报错
下面有些选项要配置后文的插件才有效,文件中已注明。
\_config.yml
文件。1 | # Hexo Configuration |
默认主题太丑,换成NexT主题。
git clone https://github.com/iissnan/hexo-theme-next.git themes/next
。_config.yml
配置文件中的theme
属性,将其设置为next
。themes/next
目录下执行git pull
。(暂时不需要)\themes\next\_config.yml
修改主题配置。我的_config.yml
文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242# ---------------------------------------------------------------
# Site Information Settings
# ---------------------------------------------------------------
# Place your favicon.ico to /source directory.
favicon: /favicon.ico
# Set default keywords (Use a comma to separate)
keywords: "loveNight, Android, Python, 神锋"
# Set rss to false to disable feed link.
# Leave rss as empty to use site's feed link.
# Set rss to specific value if you have burned your feed already.
rss:
# Specify the date when the site was setup
#since: 2015
# ---------------------------------------------------------------
# Menu Settings
# ---------------------------------------------------------------
# When running hexo in a subdirectory (e.g. domain.tld/blog)
# Remove leading slashes ( "/archives" -> "archives" )
menu:
home: /
archives: /archives
categories: /categories
tags: /tags
Android: /Android
Python: /Python
API: /API
Notes: /Notes
Links: /Links
About: /About
#commonweal: /404.html
# Enable/Disable menu icons.
# Icon Mapping:
# Map a menu item to a specific FontAwesome icon name.
# Key is the name of menu item and value is the name of FontAwsome icon.
# When an question mask icon presenting up means that the item has no mapping icon.
menu_icons:
enable: true
# Icon Mapping.
home: home
about: user
categories: th
tags: tags
archives: archive
commonweal: heartbeat
# ---------------------------------------------------------------
# Scheme Settings
# ---------------------------------------------------------------
# Schemes
scheme: Mist
# ---------------------------------------------------------------
# Sidebar Settings
# ---------------------------------------------------------------
# Sidebar, available value:
# - post expand on posts automatically. Default.
# - always expand for all pages automatically
# - hide expand only when click on the sidebar toggle icon.
# - remove Totally remove sidebar including sidebar toggle icon.
sidebar: always
# sidebar: post
#sidebar: hide
#sidebar: remove
# Social links
# social:
#GitHub:
#Others:
# Social Icons
social_icons:
enable: true
# Icon Mappings
GitHub: github
Twitter: twitter
Weibo: weibo
# Sidebar Avatar
#avatar:
# Social links
# social:
#GitHub:
#Others:
# TOC in the Sidebar
toc:
enable: true
# Automatically add list number to toc.
number: true
# Creative Commons 4.0 International License.
# http://creativecommons.org/
# Available: by | by-nc | by-nc-nd | by-nc-sa | by-nd | by-sa | zero
#creative_commons: by-nc-sa
#creative_commons:
# ---------------------------------------------------------------
# Misc Theme Settings
# ---------------------------------------------------------------
# Custom Logo.
# !!Only available for Default Scheme currently.
# Options:
# enabled: [true/false] - Replace with specific image
# image: url-of-image - Images's url
custom_logo:
enabled: false
image:
# Code Highlight theme
# Available value:
# normal | night | night eighties | night blue | night bright
# https://github.com/chriskempson/tomorrow-theme
highlight_theme: night blue
# `阅读全文` 按钮跳转之后是否自动滚动页面到设置 `<!-- more -->` 的地方。
scroll_to_more: true
# 是否为侧边栏文章的目录自动添加索引,默认开启。设置为 `false` 关闭。
toc_list_number: true
# Automatically Excerpt
auto_excerpt:
enable: false
length: 150
# Use Lato font
use_font_lato: true
# ---------------------------------------------------------------
# Third Party Services Settings
# ---------------------------------------------------------------
# MathJax Support # 数学公式,要使用就设置为True
mathjax:
# Swiftype Search API Key
swiftype_key:
# Baidu Analytics ID
#baidu_analytics:
# 多说热评文章 true 或者 false
# duoshuo_hotartical: true
# Disqus
#disqus_shortname:
# Share
jiathis: true
# Share
# duoshuo_share: true
# Google Webmaster tools verification setting
# See: https://www.google.com/webmasters/
#google_site_verification:
# Google Analytics
#google_analytics:
# Make duoshuo show UA
# user_id must NOT be null when admin_enable is true!
# you can visit http://dev.duoshuo.com get duoshuo user id.
duoshuo_info:
ua_enable: true
admin_enable: false
user_id: 0
#admin_nickname: ROOT
# Facebook SDK Support.
# https://github.com/iissnan/hexo-theme-next/pull/410
facebook_sdk:
enable: false
app_id: #<app_id>
fb_admin: #<user_id>
like_button: #true
webmaster: #true
#! ---------------------------------------------------------------
#! DO NOT EDIT THE FOLLOWING SETTINGS
#! UNLESS YOU KNOW WHAT YOU ARE DOING
#! ---------------------------------------------------------------
# Motion
use_motion: true
# Fancybox
fancybox: true
# Static files
vendors: vendors
css: css
js: js
images: images
# Theme version
version: 0.4.5.2
先按照NexT 使用文档设置一下,其中的内容下面不再赘述。
谷歌与百度的站点地图,前者适用于其他搜索引擎,用来手动提交以增加收录
安装:
npm install hexo-generator-sitemap@1 --save
npm install hexo-generator-baidu-sitemap@0.1.1 --save
_config.yml
添加代码:
baidusitemap:
path: baidusitemap.xml
谷歌的sitemap.xml
不需要写到配置文件中,自动生效。
在主页后面加/baidusitemap.xml
可以看到baidusitemap(谷歌同理),将该网址它提交给百度搜索:百度站长平台,贴吧账号无法在这里使用。
不过由于Github禁止了百度爬虫,百度无法抓取其中的URL:
试了各种解决方案都没有成功,除非把博客托管到其他平台上。
用如下命令添加新page
hexo new page "Android"
hexo new page "Python"
hexo new page "API"
然后在主题配置文件\themes\next\_config.yml
添加:
menu:
home: /
categories: /categories
archives: /archives
tags: /tags
Android: /Android # 加在这里
Python: /Python
API: /API
打开\themes\next\languages\zh-Hans.yml
,这是简体中文的配置文件,如果你的博客用的是其他语言,请打开对应的文件。
menu:
home: 首页
archives: 归档
categories: 分类
tags: 标签
about: 关于
search: 搜索
commonweal: 公益404
android: Android
python: Python
api: API
注意这里第一列必须全为小写,否则效果是这样的:
设置完后效果如下:
配置文件里写着:
# Sidebar, available value:
# - post expand on posts automatically. Default.
# - always expand for all pages automatically
# - hide expand only when click on the sidebar toggle icon.
# - remove Totally remove sidebar including sidebar toggle icon.
但是当设置成always
时,侧栏在文章页却不会自动弹出。在别的页面都能正常。
在\themes\next\layout/_scripts/pages/post-details.swig
文件里看到这段代码1
2
3
4
5
6
7
8
9// Expand sidebar on post detail page by default, when post has a toc.
motionMiddleWares.sidebar = function () {
var $tocContent = $('.post-toc-content');
if (CONFIG.sidebar === 'post') {
if ($tocContent.length > 0 && $tocContent.html().trim().length > 0) {
displaySidebar();
}
}
};
虽然没学过JS,但是看注释和 if 语句,都是仅在sidebar参数为post
时才会自动展开,坑爹呢。在if
里加上always
判断,即可,修改后如下:1
2
3
4
5
6
7
8
9// Expand sidebar on post detail page by default, when post has a toc.
motionMiddleWares.sidebar = function () {
var $tocContent = $('.post-toc-content');
if (CONFIG.sidebar === 'post' || CONFIG.sidebar === 'always') {
if ($tocContent.length > 0 && $tocContent.html().trim().length > 0) {
displaySidebar();
}
}
};
改完后顺手向作者反映了问题:设置sidebar: always,侧栏在文章页反而不展开
上一步中只有主页和文章页才会弹侧栏,自己添加的Page却不会。打开\themes\next\layout\page.swig
,找到底部的1
2
3{% block sidebar %}
{{ sidebar_template.render(false) }}
{% endblock %}
把false
改成true
。这时不管有没有目录,都会弹出侧栏。需要让它根据目录的度来决定是否弹出,继续修改,结果如下:1
2
3
4
5
6
7{% block sidebar %}
{{ sidebar_template.render(true) }}
{% endblock %}
{% block script_extra %}
{% include '_scripts/pages/post-details.swig' %}
{% endblock %}
直接使用与文章页(post-details)相同的侧栏设置。
现在一般都用宽屏显示器,博客页面两侧留白太多,调整一下宽度。
打开\themes\next\source\css\_common\components\post\post-expand.styl
文件,找到
@media (max-width: 767px)
改为
@media (max-width: 1080px)
打开\themes\next\source\css\ _variables\base.styl
文件,找到
$main-desktop = 960px
$main-desktop-large = 1200px
$content-desktop = 700px
修改$main-desktop
和$content-desktop
的数值:
$main-desktop = 1080px
$main-desktop-large = 1200px
$content-desktop = 810px
Next.Mist主题的文章宽度至此改完了。如果你用的是Next.Pisces,还需要继续修改。
打开\themes\next\source\css\_schemes\Pisces\_layout.styl
文件,将第4行的width
改为1080px
,修改后如下:
.header {
position: relative;
margin: 0 auto;
width: 1080px;
打开\themes\next\source\css\ _variables\base.styl
文件,将$font-size-base
改成16px:
$font-size-base = 16px
取色可以用QQ,以廖雪峰的官方网站为例,我想取其中代码块的颜色。打开QQ按Ctrl+Alt+A
开始截图,将鼠标移到文字上,按住Ctrl
键即可看到该颜色的16进制代码为:
文字毕竟太细,鼠标很难准确移到笔画上,也可以改用Chrome的审查元素取色。按F12,先点箭头,再点代码块:
style选项卡中就出现了字体颜色和背景色:
继续修改上一步中的\themes\next\source\css\ _variables\base.styl
文件,找到文件开头的colors for use across theme
,加入自定义颜色:
// Colors
// colors for use across theme.
// --------------------------------------------------
$whitesmoke = #f5f5f5
$gainsboro = #eee
$gray-lighter = #ddd
$grey-light = #ccc
$grey = #bbb
$grey-dark = #999
$grey-dim = #666
$black-light = #555
$black-dim = #333
$black-deep = #222
$red = #ff2a2a
$blue-bright = #87daff
$blue = #0684bd
$blue-deep = #262a30
$orange = #fc6423
// 下面是我自定义的颜色
$my-link-blue = #0593d3 //链接颜色
$my-link-hover-blue = #0477ab //鼠标悬停后颜色
$my-code-foreground = #dd0055 // 用``围出的代码块字体颜色
$my-code-background = #eee // 用``围出的代码块背景颜色
改掉这几行:
// Global link color.
$link-color = $my-link-blue
$link-hover-color = $my-link-hover-blue
$link-decoration-color = $gray-lighter
$link-decoration-hover-color = $my-link-hover-blue
效果如图:
修改$code-background
和$code-foreground
的值:
// Code & Code Blocks
// 用``围出的代码块
// --------------------------------------------------
$code-font-family = $font-family-monospace
$code-font-size = 15px
$code-background = $my-code-background
$code-foreground = $my-code-foreground
$code-border-radius = 4px
效果如图:
使用中发现网站打开速度巨慢,一开始以为是Github的原因,但是本地调试时也要一分钟才能打开。使用Chrome抓包发现:
57.04 秒!
在\themes\next
目录执行命令
grep 'css?family' -r ./
可以找到罪魁祸首就在\themes\next\layout\_partials\head.swig
文件中:1
2
3
4
5
6
7{% if theme.use_font_lato %}
{% if config.language === 'zh-Hans' %}
<link href='//fonts.lug.ustc.edu.cn/css?family=Lato:300,400,700,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
{% else %}
<link href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
{% endif %}
{% endif %}
作者的原意是想让语言设置为简体的用户都连接到中国科技大学的免费字体库,其他用户链接到Google,没想到这个链接挂了。使用Ping检测工具可以看到fonts.googleapis.com
被解析到了北京的服务器,速度相当快,所以我们直接使用Google的字体库就可以了:1
2
3{% if theme.use_font_lato %}
<link href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
{% endif %}
先准备一些代码。站长统计,注册并获取统计代码:
|
<script type="text/javascript">
var cnzz_protocol = (("https:" == document.location.protocol) ? " https://" : " http://");
document.write(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' type='text/javascript'%3E%3C/script%3E"));
</script>
出于保护隐私的考虑,我编辑掉了部分关键代码,直接复制上面的无法使用。
百度和Google网站地图,上面已经安装了,这是插入到底栏的代码:
| <span><a href="/sitemap.xml">Google网站地图</a></span>
| <span><a href="/baidusitemap.xml">百度网站地图<
不蒜子统计代码:
| 本页点击 <span id="busuanzi_value_page_pv"></span> 次
| 本站总点击 <span id="busuanzi_value_site_pv"></span> 次
| 您是第 <span id="busuanzi_value_site_uv"></span> 位访客
<script async src="https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js">
</script>
百度自动推送代码,在页面被访问时,页面URL将立即被推送给百度,可以增加百度收录:
<script>
(function(){
var bp = document.createElement('script');
bp.src = '//push.zhanzhang.baidu.com/push.js';
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>
下面把这些代码全加入模板中,打开\themes\next\layout\_partials\footer.swig
,修改后的完整文件内容为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49<div class="copyright" >
{% set current = date(Date.now(), "YYYY") %}
© {% if theme.since and theme.since != current %} {{ theme.since }} - {% endif %}
<span itemprop="copyrightYear">{{ current }}</span>
<span class="with-love">
<i class="icon-next-heart fa fa-heart"></i>
</span>
<span class="author" itemprop="copyrightHolder">{{ config.author }}
|
<script type="text/javascript">
var cnzz_protocol = (("https:" == document.location.protocol) ? " https://" : " http://");
document.write(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' type='text/javascript'%3E%3C/script%3E"));
</script>
| <span><a href="/sitemap.xml">Google网站地图</a></span>
| <span><a href="/baidusitemap.xml">百度网站地图</a></span>
</span>
</div>
<div class="powered-by">
{{ __('footer.powered', '<a class="theme-link" href="http://hexo.io">Hexo</a>') }}
</div>
<div class="theme-info">
{{ __('footer.theme') }} -
<a class="theme-link" href="https://github.com/iissnan/hexo-theme-next">
NexT{% if theme.scheme %}.{{ theme.scheme }}{% endif %}
</a>
</div>
| 本站总点击 <span id="busuanzi_value_site_pv"></span> 次
| 您是第 <span id="busuanzi_value_site_uv"></span> 位访客
<script async src="https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js">
</script>
<script>
(function(){
var bp = document.createElement('script');
bp.src = '//push.zhanzhang.baidu.com/push.js';
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>
{% block footer %}{% endblock %}
注意把上面的xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
换成你自己的站长统计代码
设置之后的底栏效果:
上一节中有一段JS代码:
<script async src="https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js">
</script>
现在要添加的阅读量统计也依赖这段代码,上面已经将它添加到页面中,这里可以直接调用它。
打开/themes/next/layout/_macro/post.swig
,找到标签<div class="post-meta"></div>
,在该标签内部合适的位置添加:1
2
3{% if not is_index %}
<span id="busuanzi_container_page_pv"> | 阅读量 <span id="busuanzi_value_page_pv"></span> 次</span>
{% endif %}
我放在这里:
效果:
使用Chrome抓包可以得到多说评论核心脚本embed.js的远程文件地址:
http://static.duoshuo.com/embed.js
在浏览器中打开,右键另存为,放到\theme\next\source\js\
文件夹中。
再打开\themes\next\layout\_scripts\comments\duoshuo.swig
。搜索//static.duoshuo.com/embed.js
,把它改成/js/embed.js
。
如法炮制,把加载慢的.js都本地化。目前Github的响应速度非常快。
其他处理的js有:
\themes\next\layout\_partials\searchswiftype.swig
中的
//s.swiftypecdn.com/install/v2/st.js
1.博客中找一条自己的留言,获取多说id:
2.修改embed.js
,也可以直接下载我的embed.js。
打开上一步本地化的embed.js
,在最顶部添加如下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136//管理员判断开始
function sskadmin(e) {
var ssk = '';
if (e.user_id == 14198272) {
if (checkMobile()) {
ssk = '<span class="ua"><span class="sskadmin">博主</span></span><br><br>';
} else {
ssk = '<span class="ua"><span class="sskadmin">博主</span></span>';
}
} else {
if (checkMobile()) {
ssk = '<br><br>';
}
}
return ssk;
}
//管理员判断结束
//移动客户端判断开始
function checkMobile() {
var isiPad = navigator.userAgent.match(/iPad/i) != null;
if (isiPad) {
return false;
}
var isMobile = navigator.userAgent.match(/iphone|android|phone|mobile|wap|netfront|x11|java|opera mobi|opera mini|ucweb|windows ce|symbian|symbianos|series|webos|sony|blackberry|dopod|nokia|samsung|palmsource|xda|pieplus|meizu|midp|cldc|motorola|foma|docomo|up.browser|up.link|blazer|helio|hosin|huawei|novarra|coolpad|webos|techfaith|palmsource|alcatel|amoi|ktouch|nexian|ericsson|philips|sagem|wellcom|bunjalloo|maui|smartphone|iemobile|spice|bird|zte-|longcos|pantech|gionee|portalmmm|jig browser|hiptop|benq|haier|^lct|320x320|240x320|176x220/i) != null;
if (isMobile) {
return true;
}
return false;
}
//移动客户端判断结束
//显UA开始
function ua(e) {
var r = new Array;
var outputer = '';
if (r = e.match(/FireFox\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_firefox"><i class="fa fa-firefox"></i> FireFox'
} else if (r = e.match(/Maxthon([\d]*)\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_maxthon"><i class="fa fa-globe"></i> Maxthon'
} else if (r = e.match(/BIDUBrowser([\d]*)\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_ucweb"><i class="fa fa-globe"></i> 百度浏览器'
} else if (r = e.match(/UBrowser([\d]*)\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_ucweb"><i class="fa fa-globe"></i> UCBrowser'
} else if (r = e.match(/UCBrowser([\d]*)\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_ucweb"><i class="fa fa-globe"></i> UCBrowser'
} else if (r = e.match(/MetaSr/ig)) {
outputer = '<span class="ua_sogou"><i class="fa fa-globe"></i> 搜狗浏览器'
} else if (r = e.match(/2345Explorer/ig)) {
outputer = '<span class="ua_2345explorer"><i class="fa fa-globe"></i> 2345王牌浏览器'
} else if (r = e.match(/2345chrome/ig)) {
outputer = '<span class="ua_2345chrome"><i class="fa fa-globe"></i> 2345加速浏览器'
} else if (r = e.match(/LBBROWSER/ig)) {
outputer = '<span class="ua_lbbrowser"><i class="fa fa-globe"></i> 猎豹安全浏览器'
} else if (r = e.match(/MicroMessenger\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_qq"><i class="fa fa-weixin"></i> 微信'
/*.split('/')[0]*/
} else if (r = e.match(/QQBrowser\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_qq"><i class="fa fa-qq"></i> QQ浏览器'
/*.split('/')[0]*/
} else if (r = e.match(/QQ\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_qq"><i class="fa fa-qq"></i> QQ浏览器'
/*.split('/')[0]*/
} else if (r = e.match(/MiuiBrowser\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_mi"><i class="fa fa-globe"></i> Miui浏览器'
/*.split('/')[0]*/
} else if (r = e.match(/Chrome([\d]*)\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_chrome"><i class="fa fa-chrome"></i> Chrome'
/*.split('.')[0]*/
} else if (r = e.match(/safari\/([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_apple"><i class="fa fa-safari"></i> Safari'
} else if (r = e.match(/Opera[\s|\/]([^\s]+)/ig)) {
var r1 = r[0].split("/");
outputer = '<span class="ua_opera"><i class="fa fa-opera"></i> Opera'
} else if (r = e.match(/Trident\/7.0/gi)) {
outputer = '<span class="ua_ie"><i class="fa fa-internet-explorer"></i> IE 11'
} else if (r = e.match(/MSIE\s([^\s|;]+)/gi)) {
outputer = '<span class="ua_ie"><i class="fa fa-internet-explorer"></i> IE' + ' ' + r[0]
/*.replace('MSIE', '').split('.')[0]*/
} else {
outputer = '<span class="ua_other"><i class="fa fa-globe"></i> 其它浏览器'
}
if (checkMobile()) {
Mobile = '<br><br>';
} else {
Mobile = '';
}
return outputer + "</span>" + Mobile;
}
function os(e) {
var os = '';
if (e.match(/win/ig)) {
if (e.match(/nt 5.1/ig)) {
os = '<span class="os_xp"><i class="fa fa-windows"></i> Windows XP'
} else if (e.match(/nt 6.1/ig)) {
os = '<span class="os_7"><i class="fa fa-windows"></i> Windows 7'
} else if (e.match(/nt 6.2/ig)) {
os = '<span class="os_8"><i class="fa fa-windows"></i> Windows 8'
} else if (e.match(/nt 6.3/ig)) {
os = '<span class="os_8_1"><i class="fa fa-windows"></i> Windows 8.1'
} else if (e.match(/nt 10.0/ig)) {
os = '<span class="os_8_1"><i class="fa fa-windows"></i> Windows 10'
} else if (e.match(/nt 6.0/ig)) {
os = '<span class="os_vista"><i class="fa fa-windows"></i> Windows Vista'
} else if (e.match(/nt 5/ig)) {
os = '<span class="os_2000"><i class="fa fa-windows"></i> Windows 2000'
} else {
os = '<span class="os_windows"><i class="fa fa-windows"></i> Windows'
}
} else if (e.match(/android/ig)) {
os = '<span class="os_android"><i class="fa fa-android"></i> Android'
} else if (e.match(/ubuntu/ig)) {
os = '<span class="os_ubuntu"><i class="fa fa-desktop"></i> Ubuntu'
} else if (e.match(/linux/ig)) {
os = '<span class="os_linux"><i class="fa fa-linux"></i> Linux'
} else if (e.match(/mac/ig)) {
os = '<span class="os_mac"><i class="fa fa-apple"></i> Mac OS X'
} else if (e.match(/unix/ig)) {
os = '<span class="os_unix"><i class="fa fa-desktop"></i> Unix'
} else if (e.match(/symbian/ig)) {
os = '<span class="os_nokia"><i class="fa fa-mobile"></i> Nokia SymbianOS'
} else {
os = '<span class="os_other"><i class="fa fa-desktop"></i> 其它操作系统'
}
return os + "</span>";
}
//显UA结束
然后搜索
data-qqt-account="' + (r.qqt_account || "") + '">' + u(r.name) + "</span>"),
在后面添加
t += sskadmin(s.author) + "<span class=\"ua\">" + ua(s.agent) + "</span><span class=\"ua\">" + os(s.agent) + "</span>",
3.添加对应CSS。打开\themes\next\source\css\main.styl
,在文件开头插入代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147/*博主标记 CSS开始*/
.sskadmin {
background-color: #00a67c!important;
border-color: #01B171!important;
border-radius: 4px;
padding: 0 5px!important;
opacity: 1;
}
/*博主标记 CSS结束*/
/*多说UA开始*/
span.ua{
margin: 0 1px!important;
color:#FFFFFF!important;
/*text-transform: Capitalize!important;
float: right!important;
line-height: 18px!important;*/
}
.ua_other.os_other{
background-color: #ccc!important;
color: #fff;
border: 1px solid #BBB!important;
border-radius: 4px;
}
.ua_ie{
background-color: #428bca!important;
border-color: #357ebd!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_firefox{
background-color: #f0ad4e!important;
border-color: #eea236!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_maxthon{
background-color: #7373B9!important;
border-color: #7373B9!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_ucweb{
background-color: #FF740F!important;
border-color: #d43f3a!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_sogou{
background-color: #78ACE9!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_2345explorer{
background-color: #2478B8!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_2345chrome{
background-color: #F9D024!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_mi{
background-color: #FF4A00!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_lbbrowser{
background-color: #FC9D2E!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_chrome{
background-color: #EE6252!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_qq{
background-color: #3D88A8!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_apple{
background-color: #E95620!important;
border-color: #4cae4c!important;
border-radius: 4px;
padding: 0 5px!important;
}
.ua_opera{
background-color: #d9534f!important;
border-color: #d43f3a!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_vista,.os_2000,.os_windows,.os_xp,.os_7,.os_8,.os_8_1 {
background-color: #39b3d7!important;
border-color: #46b8da!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_android {
background-color: #98C13D!important;
border-color: #01B171!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_ubuntu{
background-color: #DD4814!important;
border-color: #01B171!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_linux {
background-color: #3A3A3A!important;
border-color: #1F1F1F!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_mac{
background-color: #666666!important;
border-color: #1F1F1F!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_unix{
background-color: #006600!important;
border-color: #1F1F1F!important;
border-radius: 4px;
padding: 0 5px!important;
}
.os_nokia{
background-color: #014485!important;
border-color: #1F1F1F!important;
border-radius: 4px;
padding: 0 5px!important;
}
/*多说UA结束*/
效果:
效果和CSS代码见多说自定义CSS 让你的多说评论动感起来
然后打开多说后台,按下图操作,把代码粘贴进去
大功告成。
我还添加了网上搜集的其他代码,详见注释:
1 | /*头像样式 圆形,鼠标移上会旋转*/ |
更改首页标题格式为「关键词-网站名称 - 网站描述」。打开\themes\next\layout\index.swig
文件,找到这行代码:
1 | {% block title %} {{ config.title }} {% endblock %} |
把它改成:1
2
3{% block title %}
{{ theme.keywords }} - {{ config.title }} - {{ theme.description }}
{% endblock %}
Github上博客的仓库主页空荡荡的,没有README。如果把README.md放入source文件夹,hexo g
生成时会被解析成html文件,放到public文件夹,生成时又会自动删除。
解决方法很简单,在source目录下新建文件README.mdown
,在里面写README即可。hexo g
会把它复制到public文件夹,且不会被解析成html
需要在公司和家里电脑上写博客,打包拷来拷去太麻烦。我使用Coding.net的免费私有仓库来同步hexo文件夹。
1.删除根目录和\theme\next\
下的.git
文件夹。
2.修改根目录下的.gitignore
文件为:
/.deploy_git
/public
其实第一行留不留都一样,它是hexo默认的git配置文件夹,里面也有一个.git
,使/.deploy_git
里的文件无法被提交。public
是每次hexo g
新生成的静态博客文件,不需要同步。
如果没接触过Git,可以看这本书学习一个:《Pro Git》
在git bash里面,输入
git config --global core.longpaths true
部署时会出现如下警告
warning: LF will be replaced by CRLF
LF为换行符,CR为回车符。Windows结束一行用CRLF,Mac和Linux用LF。Git默认在你提交时自动地把行结束符CRLF转换成LF,而在签出代码时把LF转换成CRLF。
要关闭警告,执行如下命令:
git config --global core.autocrlf false
上述命令中:
false表示取消自动转换功能。适合纯Windows
true表示提交代码时把CRLF转换成LF,签出时LF转换成CRLF。适合多平台协作
input表示提交时把CRLF转换成LF,签出时不转换。适合纯Linux或Mac
补充:
Git还提供了一个换行符检查功能core.safecrlf
,可以在提交时检查文件是否混用了不同风格的换行符。可以用同样的命令更改设置。
选项如下:
false - 不做任何检查
warn - 在提交时检查并警告
true - 在提交时检查,如果发现混用则拒绝提交
建议使用最严格的 true 选项
\node_modules\hexo-deployer-git\lib\deployer.js
文件末尾找到这一句:1
Site updated: {{ now('YYYY-MM-DD HH:mm:ss') }}.
改得个性化一点:
在Github对应项目中可以看到效果:
GitHub Pages 本用于介绍托管在 GitHub 的项目,也可以用来搭建博客,有300M免费空间。
hexo是一个基于Node.js的静态博客程序,可以方便的生成静态网页托管在github和Heroku上。作者是来自台湾的tommy351。
优势:
]]>生成静态页面快
支持 Markdown
兼容于 Windows, Mac & Linux
部署方便。日常使用仅需五个命令。
高扩展性、自订性,文件少、小,易理解
稍后把此次博客搭建心得整理好发出来。
以前用新浪的博客,可惜不支持代码块和Markdown,主题近十年没怎么变化,三天两头收到无聊的纸条、转发消息。后来转战CSDN,主题和Markdown非常丑,右上角定期出现广告,没法取消。有次还无故封了博客,找管理员询问得知是误操作。试了博客园,后台有无用的新闻、文章、日志分类,无法取消,默认的代码块不显示行数,Markdown比CSDN还丑。
博客还没配置完,先发这篇文章试试效果。
Python:
1 | print("Hello World") |
Java:1
2
3
4
5public class Hello {
public static void main(String[] args){
System.out.println("Hello World");
}
}
结束
]]>稍后把此次博客搭建心得整理好发出来。
以前用新浪的博客,可惜不支持代码块和Markdown,主题近十年没怎么变化,三天两头收到无聊的纸条、转发消息。后来转战CSDN,主题和Markdown非常丑,右上角定期出现广告,没法取消。有次还无故封了博客,找管理员询问得知是误操作。试了博客园,后台有无用的新闻、文章、日志分类,无法取消,默认的代码块不显示行数,Markdown比CSDN还丑。
]]>