c#操作XML(读XML,写XML,更新,删除节点,与dataset结合等)
2024-09-05 20:55:57
供稿:网友
我用的是一种很笨的方法,但可以帮助初学者了解访问xml节点的过程。
已知有一个xml文件(bookstore.xml)如下:
corets, eva
5.95
1、插入节点
往节点中插入一个节点:
xmldocument xmldoc=new xmldocument();
xmldoc.load("bookstore.xml");
xmlnode root=xmldoc.selectsinglenode("bookstore");//查找
xmlelement xe1=xmldoc.createelement("book");//创建一个节点
xe1.setattribute("genre","李赞红");//设置该节点genre属性
xe1.setattribute("isbn","2-3631-4");//设置该节点isbn属性
xmlelement xesub1=xmldoc.createelement("title");
xesub1.innertext="cs从入门到精通";//设置文本节点
xe1.appendchild(xesub1);//添加到节点中
xmlelement xesub2=xmldoc.createelement("author");
xesub2.innertext="候捷";
xe1.appendchild(xesub2);
xmlelement xesub3=xmldoc.createelement("price");
xesub3.innertext="58.3";
xe1.appendchild(xesub3);
root.appendchild(xe1);//添加到节点中
xmldoc.save("bookstore.xml");
结果为:
corets, eva
5.95
候捷
58.3
2、修改节点:
将genre属性值为“李赞红“的节点的genre值改为“update李赞红”,将该节点的子节点的文本修改为“亚胜”。
xmlnodelist nodelist=xmldoc.selectsinglenode("bookstore").childnodes;//获取bookstore节点的所有子节点
foreach(xmlnode xn in nodelist)//遍历所有子节点
{
xmlelement xe=(xmlelement)xn;//将子节点类型转换为xmlelement类型
if(xe.getattribute("genre")=="李赞红")//如果genre属性值为“李赞红”
{
xe.setattribute("genre","update李赞红");//则修改该属性为“update李赞红”
xmlnodelist nls=xe.childnodes;//继续获取xe子节点的所有子节点
foreach(xmlnode xn1 in nls)//遍历
{
xmlelement xe2=(xmlelement)xn1;//转换类型
if(xe2.name=="author")//如果找到
{
xe2.innertext="亚胜";//则修改
break;//找到退出来就可以了
}
}
break;
}
}
xmldoc.save("bookstore.xml");//保存。
最后结果为:
corets, eva
5.95
亚胜
58.3
3、删除节点
节点的genre属性,删除 节点。
xmlnodelist xnl=xmldoc.selectsinglenode("bookstore").childnodes;
foreach(xmlnode xn in xnl)
{
xmlelement xe=(xmlelement)xn;
if(xe.getattribute("genre")=="fantasy")
{
xe.removeattribute("genre");//删除genre属性
}
else if(xe.getattribute("genre")=="update李赞红")
{
xe.removeall();//删除该节点的全部内容
}
}
xmldoc.save("bookstore.xml");
最后结果为:
corets, eva
5.95
4、显示所有数据。
xmlnode xn=xmldoc.selectsinglenode("bookstore");
xmlnodelist xnl=xn.childnodes;
foreach(xmlnode xnf in xnl)
{
xmlelement xe=(xmlelement)xnf;
console.writeline(xe.getattribute("genre"));//显示属性值
console.writeline(xe.getattribute("isbn"));
xmlnodelist xnf1=xe.childnodes;
foreach(xmlnode xn2 in xnf1)
{
console.writeline(xn2.innertext);//显示子节点点文本
}
}
loading...
2005-10-3
一个通过dataset操作xml的类(源代码)
using system;
using system.data;
using system.xml;
using system.windows.forms;
//***************************************
// 作者: ∮明天去要饭
// qicq: 305725744
// .net群: 6370988
// http://blog.csdn.net/kgdiwss
//***************************************
namespace ystrp.common
{
///
/// operatexmlbydataset 的摘要说明。
///
public class operatexmlbydataset
{
public operatexmlbydataset()
{
//
// todo: 在此处添加构造函数逻辑
//
}
#region getdatasetbyxml
///
/// 读取xml直接返回dataset
///
/// xml文件相对路径
///
public static dataset getdatasetbyxml(string strxmlpath)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
if(ds.tables.count > 0)
{
return ds;
}
return null;
}
catch(exception ex)
{
system.windows.forms.messagebox.show(ex.tostring());
return null;
}
}
#endregion
#region getdataviewbyxml
///
/// 读取xml返回一个经排序或筛选后的dataview
///
///
/// 筛选条件,如:"name = 'kgdiwss'"
/// 排序条件,如:"id desc"
///
public static dataview getdataviewbyxml(string strxmlpath,string strwhere,string strsort)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
dataview dv = new dataview(ds.tables[0]);
if(strsort != null)
{
dv.sort = strsort;
}
if(strwhere != null)
{
dv.rowfilter = strwhere;
}
return dv;
}
catch(exception)
{
return null;
}
}
#endregion
#region writexmlbydataset
///
/// 向xml文件插入一行数据
///
/// xml文件相对路径
/// 要插入行的列名数组,如:string[] columns = {"name","ismarried"};
/// 要插入行每列的值数组,如:string[] columnvalue={"明天去要饭","false"};
/// 成功返回true,否则返回false
public static bool writexmlbydataset(string strxmlpath,string[] columns,string[] columnvalue)
{
try
{
//根据传入的xml路径得到.xsd的路径,两个文件放在同一个目录下
string strxsdpath = strxmlpath.substring(0,strxmlpath.indexof(".")) + ".xsd";
dataset ds = new dataset();
//读xml架构,关系到列的数据类型
ds.readxmlschema(getxmlfullpath(strxsdpath));
ds.readxml(getxmlfullpath(strxmlpath));
datatable dt = ds.tables[0];
//在原来的表格基础上创建新行
datarow newrow = dt.newrow();
//循环给一行中的各个列赋值
for(int i=0; i< columns.length; i++)
{
newrow[columns[i]] = columnvalue[i];
}
dt.rows.add(newrow);
dt.acceptchanges();
ds.acceptchanges();
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
#region updatexmlrow
///
/// 更行符合条件的一条xml记录
///
/// xml文件路径
/// 列名数组
/// 列值数组
/// 条件列名
/// 条件列值
///
public static bool updatexmlrow(string strxmlpath,string[] columns,string[] columnvalue,string strwherecolumnname,string strwherecolumnvalue)
{
try
{
string strxsdpath = strxmlpath.substring(0,strxmlpath.indexof(".")) + ".xsd";
dataset ds = new dataset();
//读xml架构,关系到列的数据类型
ds.readxmlschema(getxmlfullpath(strxsdpath));
ds.readxml(getxmlfullpath(strxmlpath));
//先判断行数
if(ds.tables[0].rows.count > 0)
{
for(int i=0; i< ds.tables[0].rows.count; i++)
{
//如果当前记录为符合where条件的记录
if(ds.tables[0].rows[i][strwherecolumnname].tostring().trim().equals(strwherecolumnvalue))
{
//循环给找到行的各列赋新值
for(int j=0; j < columns.length; j++)
{
ds.tables[0].rows[i][columns[j]] = columnvalue[j];
}
//更新dataset
ds.acceptchanges();
//重新写入xml文件
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
}
}
return false;
}
catch(exception)
{
return false;
}
}
#endregion
#region deletexmlrowbyindex
///
/// 通过删除dataset中ideleterow这一行,然后重写xml以实现删除指定行
///
///
/// 要删除的行在dataset中的index值
public static bool deletexmlrowbyindex(string strxmlpath,int ideleterow)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
if(ds.tables[0].rows.count > 0)
{
//删除符号条件的行
ds.tables[0].rows[ideleterow].delete();
}
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
#region deletexmlrows
///
/// 删除strcolumn列中值为columnvalue的行
///
/// xml相对路径
/// 列名
/// strcolumn列中值为columnvalue的行均会被删除
///
public static bool deletexmlrows(string strxmlpath,string strcolumn,string[] columnvalue)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
//先判断行数
if(ds.tables[0].rows.count > 0)
{
//判断行多还是删除的值多,多的for循环放在里面
if(columnvalue.length > ds.tables[0].rows.count)
{
for(int i=0; i < ds.tables[0].rows.count; i++)
{
for(int j=0; j < columnvalue.length; j++)
{
if(ds.tables[0].rows[i][strcolumn].tostring().trim().equals(columnvalue[j]))
{
ds.tables[0].rows[i].delete();
}
}
}
}
else
{
for(int j=0; j < columnvalue.length; j++)
{
for(int i=0; i < ds.tables[0].rows.count; i++)
{
if(ds.tables[0].rows[i][strcolumn].tostring().trim().equals(columnvalue[j]))
{
ds.tables[0].rows[i].delete();
}
}
}
}
ds.writexml(getxmlfullpath(strxmlpath));
}
return true;
}
catch(exception)
{
return false;
}
}
#endregion
#region deletexmlallrows
///
/// 删除所有行
///
/// xml路径
///
public static bool deletexmlallrows(string strxmlpath)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
//如果记录条数大于0
if(ds.tables[0].rows.count > 0)
{
//移除所有记录
ds.tables[0].rows.clear();
}
//重新写入,这时xml文件中就只剩根节点了
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
#region getxmlfullpath
///
/// 返回完整路径
///
/// xml的路径
///
public static string getxmlfullpath(string strpath)
{
if(strpath.indexof(":") > 0)
{
return strpath;
}
else
{
return application.startuppath + strpath;
}
}
#endregion
}
}
loading...
2005-10-3
一个通过dataset操作xml的类
这段时间写的项目每次都要用到xml保存一些配置,而每次操作xml都觉得挺麻烦,没有数据库那么顺手。后来发现用dataset操作xml很方便,而且灵活性比较好,于是写了一个操作xml的类,用来应付一般的xml操作(源码下载附件)。
1 基本思路
其实用dataset操作xml,归根到底就是对dataset里的表格,行,列等进行操作,然后用dataset里的东西重新写到xml中,从而实现编辑xml的目的。如果再配合上.xsd文件的话,那效果更佳。
2 程序详解
(1) xml文件内容
本类操作的xml和生成的xml格式是一样的,如下:
http://tempuri.org/xml_xmldb.xsd">
2 asp.net程序员
2
开发b/s结构程序
asp.net c#等
建国路xxx
2008-8-31
false
4
c#程序员
2
开发b/s结构程序
asp.net c#等
建国路xxx
2008-8-31
false
然后点击xml文件右下角的“数据”,即可看到熟悉的表格形式,在表格的任意位置上单击右键选择“创建架构”,将会生成一个.xsd文件,该文件用来定义xml各列的类型。其内容如下(点击查看代码2附件):
http://tempuri.org/xml_xmldb.xsd" xmlns:mstns="http://tempuri.org/xml_xmldb.xsd"xmlns="http://tempuri.org/xml_xmldb.xsd" xmlns:xs="http://www.w3.org/2001/xmlschema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"attributeformdefault="qualified" elementformdefault="qualified"> msdata:locale="zh-cn" msdata:enforceconstaints="false">?msdata:autoincrement="true" msdata:autoincrementstep="1"msdata:autoincrementseed="1" />
注意:如果想像数据库一样有一个自动增长的id字段,则可以这样操作:
首先在xml中添加一个元素,这样生成.xsd的时候,就会有一个id段,在.xsd中选中id这一列,在右边的属性中,将“autoincrementseed”和“autoincrementstep”分别设置为1,这样id就会从1开始以步长为1自动增长。
以上代码如果看不懂并不要紧,因为我们可以通过dataset来生成这种格式的内容。接下来将开始操作xml。
(2) 处理xml文件路径
这里主要是对传入的xml路径进行处理,如果传入的是相对路径,则返回完整路径,如果传入的是完整路径,则不做处理直接返回。方法如下:
#region getxmlfullpath
///
/// 返回完整路径
///
/// xml的路径
///
public static string getxmlfullpath(string strpath)
{
//如果路径中含有:符号,则认定为传入的是完整路径
if(strpath.indexof(":") > 0)
{
return strpath;
}
else
{
//返回完整路径
return system.web.httpcontext.current.server.mappath(strpath);
}
}
#endregion
(3) 读取记录
读取xml的数据到dataset中的方法为:
#region getdatasetbyxml
///
/// 读取xml直接返回dataset
///
/// xml文件相对路径
///
public static dataset getdatasetbyxml(string strxmlpath)
{
try
{
dataset ds = new dataset();
//读取xml到dataset
ds.readxml(getxmlfullpath(strxmlpath));
if(ds.tables.count > 0)
{
return ds;
}
return null;
}
catch(exception)
{
return null;
}
}
#endregion
以上方法将得到一个dataset,里面保存的是全部xml记录的信息,而且没有经过任何处理。但很多时候我们需要的只是一些满足条件的记录,这时需要用以下方法得到:
#region getdataviewbyxml
/// 〈summary〉
/// 读取xml返回一个经排序或筛选后的dataview
/// 〈/summary〉
/// 〈param name="strxmlpath"〉〈/param〉
/// 〈param name="strwhere"〉筛选条件,如:"name = 'kgdiwss'"〈/param〉
/// 〈param name="strsort"〉排序条件,如:"id desc"〈/param〉
/// 〈returns〉〈/returns〉
public static dataview getdataviewbyxml(string strxmlpath,string strwhere,string strsort)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
//创建dataview来完成排序或筛选操作
dataview dv = new dataview(ds.tables[0]);
if(strsort != null)
{
//对dataview中的记录进行排序
dv.sort = strsort;
}
if(strwhere != null)
{
//对dataview中的记录进行筛选,找到我们想要的记录
dv.rowfilter = strwhere;
}
return dv;
}
catch(exception)
{
return null;
}
}
#endregion
(4) 插入记录
到现在为止我们已经可以随意读取xml中的记录,接下来来实现写入xml的操作,方法如下:
#region writexmlbydataset
/// 〈summary〉
/// 向xml文件插入一行数据
/// 〈/summary〉
/// 〈param name="strxmlpath"〉xml文件相对路径〈/param〉
/// 〈param name="columns"〉要插入行的列名数组,如:string[] columns = {"name","ismarried"};〈/param〉
/// 〈param name="columnvalue"〉要插入行每列的值数组,如:string[] columnvalue={"kgdiwss","false"};〈/param〉
/// 〈returns〉成功返回true,否则返回false〈/returns〉
public static bool writexmlbydataset(string strxmlpath,string[] columns,string[] columnvalue)
{
try
{
//根据传入的xml路径得到.xsd的路径,两个文件放在同一个目录下string strxsdpath = strxmlpath.substring(0,strxmlpath.indexof(".")) + ".xsd";
dataset ds = new dataset();
//读xml架构,关系到列的数据类型
ds.readxmlschema(getxmlfullpath(strxsdpath));
ds.readxml(getxmlfullpath(strxmlpath));
datatable dt = ds.tables[0];
//在原来的表格基础上创建新行
datarow newrow = dt.newrow();
//循环给 一行中的各个列赋值
for(int i=0; i〈 columns.length; i++)
{
newrow[columns[i]] = columnvalue[i];
}
dt.rows.add(newrow);
dt.acceptchanges();
ds.acceptchanges();
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
可能有的朋友不知道怎么用这个方法插入数据,在后面我将用实例介绍。
(5) 修改记录
修改记录的方法要传入的参数相对较多,因为修改记录需要先定位到具体哪一条记录,再修改指定列的值,以下为修改xml的方法:
#region updatexmlrow
/// 〈summary〉
/// 更行符合条件的一条xml记录
/// 〈/summary〉
/// 〈param name="strxmlpath"〉xml文件路径〈/param〉
/// 〈param name="columns"〉列名数组〈/param〉
/// 〈param name="columnvalue"〉列值数组〈/param〉
/// 〈param name="strwherecolumnname"〉条件列名〈/param〉
/// 〈param name="strwherecolumnvalue"〉条件列值〈/param〉
/// 〈returns〉〈/returns〉
public static bool updatexmlrow(string strxmlpath,string[] columns,string[] columnvalue,string strwherecolumnname,string strwherecolumnvalue)
{
try
{
//同上一方法
string strxsdpath = strxmlpath.substring(0,strxmlpath.indexof(".")) + ".xsd";
dataset ds = new dataset();
//读xml架构,关系到列的数据类型
ds.readxmlschema(getxmlfullpath(strxsdpath));
ds.readxml(getxmlfullpath(strxmlpath));
//先判断行数
if(ds.tables[0].rows.count 〉 0)
{
for(int i=0; i〈 ds.tables[0].rows.count; i++)
{
//如果当前记录为符合where条件的记录if(ds.tables[0].rows[i][strwherecolumnname].tostring().trim().equals(strwherecolumnvalue))
{
//循环给找到行的各列赋新值
for(int j=0; j 〈 columns.length; j++)
{
ds.tables[0].rows[i][columns[j]] = columnvalue[j];
}
//更新dataset
ds.acceptchanges();
//重新写入xml文件
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
}
}
return false;
}
catch(exception)
{
return false;
}
}
#endregion
(6) 删除记录
为了方便,删除记录提供了三个方法,一个可以删除所有记录,一个删除符合条件的行,还有一个删除指定index值的行,该index值和记录在data
set中的index值对应。删除所有记录的方法为:
#region deletexmlallrows
/// 〈summary〉
/// 删除所有行
/// 〈/summary〉
/// 〈param name="strxmlpath"〉xml路径〈/param〉
/// 〈returns〉〈/returns〉
public static bool deletexmlallrows(string strxmlpath)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
//如果记录条数大于0
if(ds.tables[0].rows.count 〉 0)
{
//移除所有记录
ds.tables[0].rows.clear();
}
//重新写入,这时xml文件中就只剩根节点了
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
删除指定index值的行的方法为:
#region deletexmlrowbyindex
/// 〈summary〉
/// 通过删除dataset中ideleterow这一行,然后重写xml以实现删除指定行
/// 〈/summary〉
/// 〈param name="strxmlpath"〉〈/param〉
/// 〈param name="ideleterow"〉要删除的行在dataset中的index值〈/param〉
public static bool deletexmlrowbyindex(string strxmlpath,int ideleterow)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
if(ds.tables[0].rows.count 〉 0)
{
//删除符号条件的行
ds.tables[0].rows[ideleterow].delete();
}
ds.writexml(getxmlfullpath(strxmlpath));
return true;
}
catch(exception)
{
return false;
}
}
#endregion
这里说一下提供此方法的原因,有的时候将xml的内容读到dataset,然后绑定到datagrid后,由于datagrid中只有一个模板列,而模板列里又套了表格等许多控件,这就使得我们可能无法得到记录对应的id值,这个时候就可以先得到记录的index值(第一行为0,第二行为1,以此类推),然后将该index值传到方法中,就可以将该记录删掉。
注意:使用该方法的时候,绑定到datagrid上的dataset和删除时用的dataset要为同一个,也就是说index要相同,不能有排序,不然会误将记录。
有时候我们需要删除符合条件的多行,这个时候可以用以下方法实现:
#region deletexmlrows
/// 〈summary〉
/// 删除strcolumn列中值为columnvalue的行
/// 〈/summary〉
/// 〈param name="strxmlpath"〉xml相对路径〈/param〉
/// 〈param name="strcolumn"〉列名〈/param〉
/// 〈param name="columnvalue"〉strcolumn列中值为columnvalue的行均会被删除〈/param〉
/// 〈returns〉〈/returns〉
public static bool deletexmlrows(string strxmlpath,string strcolumn,string[] columnvalue)
{
try
{
dataset ds = new dataset();
ds.readxml(getxmlfullpath(strxmlpath));
//先判断行数
if(ds.tables[0].rows.count 〉 0)
{
//判断行多还是删除的值多,多的for循环放在里面
if(columnvalue.length 〉 ds.tables[0].rows.count)
{
for(int i=0; i 〈 ds.tables[0].rows.count; i++)
{
for(int j=0; j 〈 columnvalue.length; j++)
{
//找到符合条件的行if(ds.tables[0].rows[i][strcolumn].tostring().trim().equals(columnvalue[j]))
{
//删除行
ds.tables[0].rows[i].delete();
}
}
}
}
else
{
for(int j=0; j 〈 columnvalue.length; j++)
{
for(int i=0; i 〈 ds.tables[0].rows.count; i++)
{
//找到符合条件的行if(ds.tables[0].rows[i][strcolumn].tostring().trim().equals(columnvalue[j]))
{
//删除行
ds.tables[0].rows[i].delete();
}
}
}
}
ds.writexml(getxmlfullpath(strxmlpath));
}
return true;
}
catch(exception)
{
return false;
}
}
#endregion
3实例解析
(7) 读取xml
以下代码读取到一个没有排序和筛选的dataset。
datagrid1.datasource = operatexmlbydataset.getdatasetbyxml(@"xml/xml_xmldb.xml");
datagrid1.databind();
以下代码读到的数据是经过筛选和排序的:
datagrid1.datasource = operatexmlbydataset.getdataviewbyxml(
@"xml/xml_xmldb.xml", //xml文件路径
"name = 'asp.net'", //条件:name列值为asp.net
"peoplenum desc"); //按peoplenum列降序排列
datagrid1.databind();
(8) 添加记录
以下代码向xml文件中添加了一条记录,同时给7个列赋值:
bool b;
b = operatexmlbydataset.writexmlbydataset(
@"xml/xml_xmldb.xml", //xml文件地址
new string[]{
"name", //姓名字段
"peoplenum", //人数字段
"address", //地址字段
"description", //描述字段
"require", //需求字段
"deadline", //结束时间字段
"ismarried" //婚否字段
},
new string[]{
"asp.net程序员", //姓名字段值
"2", //人数字段值
"建国路", //地址字段值
"b/s结构程序", //描述字段值
"asp.net c#等", //需求字段值
datetime.now.toshortdatestring(), //结束时间字段值
"false" //婚否字段值
});
如果b返回值为true,表示添加成功,否则表示添加失败。以上的写法我用了些偷懒的方法,比如我把数组直接放在参数,而没有另外申明,事实上你可以另外申明一个数组,然后再传到方法中。
请注意字段在数组中的位置和值在数组中的位置的对应关系。
(9) 修改记录
以下代码将找到peoplenum列值为3的行,然后将行的name、peoplenum、、description和ismarried四个字段的值分别更新成kgdiwss、10、描述、true。
bool b;
b = operatexmlbydataset.updatexmlrow(
@"xml/xml_xmldb.xml",
new string[]{"name","peoplenum","description","ismarried"},
new string[]{"kgdiwss","10","描述","true"},
"peoplenum",
"3");
返回true表示修改成功,否则表示修改失败。
请特别注意,字段类型为逻辑型时,赋值用的是true和false,而不是0和1。
(10) 删除记录
以下代码实现删除name列值为数组中的值的行。
bool b;
b = operatexmlbydataset.deletexmlrows(
@"xml/xml_xmldb.xml", //xml文件路径
"name", //条件列
new string[]{
"值1", //条件值1
"值2", //条件值2
"值3" //条件值3
});
上面代码执行成功后,name列值为值1、值2、值3的行将被删除。
删除成功返回true,否则返回false。
另外两种删除的方法用法比较简单,这里就不介绍了。
以上就是操作xml的所有方法,相信可以满足很大一部份的使用了。然而,如果xml中的数据量比较大的话,使用以上方法效率可能不高,但话又说回来,如果数据量比较大的话,还是选择数据库比较好。
中国最大的web开发资源网站及技术社区,