web實踐小項目<一>:簡單日程管理系統(涉及html/css,javascript,python,sql,日期處理)

暑假自學了些html/css,javascript和python,苦於學完無處練手幾乎過目即忘...最後在同窗的建議下作了個簡單日程管理系統。借初版完成之際,但願能將實踐期間犯過的錯誤和得到的新知進行整理,但願能給其餘初學者提供參考,也但願有大神在瀏覽我粗糙的開發過程當中能指出一些意見或建議。javascript

(閱讀如下內容須要有必定的html/css,javascript,python和sql基礎,and謝謝閱讀!)css

注:實踐中的環境爲ubuntu 14.04操做系統,python3.4(2.7實測也可行),firefox30.0html

1、簡單日程系統簡介前端

先上一張界面的清爽截圖(請原諒理工男的佈局和配色審美...)java

各個分區的功能應該比較明顯,左下的文本域用於顯示和修改被選中日期當天的日程安排。日曆中對於今天的日期突出字體顏色顯示,對當天有日程安排的日期突出背景色顯示,對月曆中非本月的部分進行虛化顯示。同時每月份的日曆是動態生成的,因此上述系統能夠顯示任意年份月份的日曆。python

同時鼠標在日曆上移動時有跟隨格背景色突出功能mysql

選取某一日期時跟隨格顏色跳變產生按鈕視覺效果,同時下方的修改按鈕解鎖。git

在文本域中輸入日程後點擊修改,會同步更新服務器端的數據庫,頁面中的日曆和右側的「最近14天內日程」提醒框。同時經過javascript的ajax實現頁面的局部更新而沒必要產生頁面刷新跳轉。github

修改時會自動判斷是建立一條新的日程安排存檔(以一天爲單位)仍是刪除(若是文本框爲空)抑或是更新。ajax

2、開發過程:

從界面佈局開始思考不知道這科學不,特別最後那個14天內日程提醒仍是由於最後發現右邊太空了的產物=  =||

而後功能上的初步設想就是實現一個hold住任意年份月份的日曆日程系統,同時提供對日程的增、刪、修改功能(恩,就是一個日程系統的基本功能),其它具體的視覺效果什麼的都是邊編碼邊想到補充的(不知道正確的開發方式和這差異多大求指教)。

前端

首先搞定了前端的大部分代碼(包括html/css和javascript的部分),這裏只列舉一些我的以爲有點意義的要點和處理思路,包括一些錯誤 > <

1.日曆的動態生成:

A.頁面中的日曆(使用<table>)

<table id="calendar">
<tr class="weekday">
<td class="head">星期日</td>
<td class="head">星期一</td>
<td class="head">星期二</td>
<td class="head">星期三</td>
<td class="head">星期四</td>
<td class="head">星期五</td>
<td class="head">星期六</td>
</tr>
<tr class="day">
<td id="begin">1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr class="day">
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr class="day">
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr class="day">
<td >1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr class="day">
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr class="day">
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td id="end">7</td>
</tr>
</table>

代碼上沒什麼特別的,特別<td>標籤裏的那些1234...能夠無視加載時確定要改(那時只是爲了先配合css樣式看看顯示效果)可是會注意到月曆的第一天和最後一天都給了個id,這樣方便後面給服務器端提供始末日期數據用以獲取當前月曆中的有日程安排日信息。其它的class屬性爲了方便css樣式表。

順便溫習下經過id獲取元素的js語法:document.getElementById("idname") 返回對元素的引用。

和經過標籤名獲取元素列表的語法:document.getElementsByTagName("tagname")  返回元素列表。注意這個Element後有s..被坑了幾回..

一開始打算每月份只顯示最少星期(即有可能5個星期)的日曆,結果發現這會增長一些工做量(好比須要不斷刪減建立新的表格行)因而看了眼操做系統中的日曆發現人家直接6個星期生活樂無憂=  =。

因而固定下來每月顯示42天后,因爲初始頁面要顯示今天所在月的月曆,因此直接js裏一個new Date()得到今天日期對象,而後算法上是經過求得今天所在月份的第一天的星期數,再倒推回去求本月曆第一天的日期,而後一個for循環爲日曆裏的每一個格賦值(修改其innerHTML改變顯示的文字)並將value屬性賦值爲日期信息(格式xxxx-xx-xx)方便後面向服務器端獲取日程安排內容時的後端腳本處理。

B.日期計算

而後就是略坑的js中的日期運算,沒有直接加個數字n就返回n天后的日期對象這種好事...因而只能每次在循環裏

new Date(firstday.getFullYear(),firstday.getMonth(),firstday.getDate()+i)一個一個弄,不過還好,js裏的Date對象支持日期超出(如new Date(2014,6,35))和日期負數(如Date(2014,6,-2))會自動往下個月和前個月轉換。注:getDate()得到日期,getDay()得到星期。

可是...注意Date對象是這樣的,w3school中的資料顯示月份是0~11,星期0~6,因而一開始我覺得星期中的0表明星期一,以此類推。結果後面測試發現1表明星期一,因而我覺得w3school錯了應該是1~7,最後debug許久發現原來星期日就是0 =  =...這個故事告訴咱們基礎知識必定要搞清楚...

另外,雖然月份是0~11,若是你直接拿Date去print的話會發現它是正常的1~12,只是在getMonth()方法時是0~11,而這個與你建立一個新的Date對象時傳入的參數是對應的,好比你想建立2014年8月3日,那麼傳入2014,7,3。

順便說一下,firefox瀏覽器右鍵->查看元素後能夠選取調試器用來調試js代碼,並且支持加斷點查看變量值!

這一部分的核心代碼以下:

var firstday=new Date();
    firstday.setFullYear(nowyear,nowmonth,1);//設置某一天要用setFullYear設置 直接傳數字 月份參數比實際少一(如傳入6) 但顯示出來和實際相符(如顯示7)
    var weekday=firstday.getDay();
    if( weekday!=0) {
    //weekday 0-6 0是週日 month 0~11   date 1~31 setDate超時會自動加到下個月負數會自動減到前個月...
    firstday.setDate(firstday.getDate()-weekday);
    
    }
    var table=document.getElementsByTagName("td");
    for(i=0;i<42;i++)
    {
    var date=new Date(firstday.getFullYear(),firstday.getMonth(),firstday.getDate()+i);
    table[7+i].innerHTML=date.getDate();
    tmpmon=((date.getMonth()+1)>=10)?(date.getMonth()+1):('0'+(date.getMonth()+1));
    tmpday=(date.getDate()>=10)?(date.getDate()):('0'+date.getDate());
    table[7+i].value=date.getFullYear()+'-'+tmpmon+'-'+tmpday;
    if(date.getFullYear()==nowdate.getFullYear()&&date.getMonth()==nowdate.getMonth()&&
    date.getDate()==nowdate.getDate())
    {
    table[7+i].className="today";
    rec14beg=table[7+i].value;
    }
    else {
        table[7+i].className="none";
        table[7+i].style.backgroundColor="#FFFFFF";
    }
    if(date.getMonth()==nowmonth)//控制月曆中不一樣部分的透明度
    table[7+i].style.opacity=1;
    else {
        table[7+i].style.opacity=0.5;
    }

 

C.動態生成

使用3個js全局變量(全局變量在<script>標籤的內容中定義,且不能包含在任何函數內)用於記錄今天的日期,當前顯示年份,當前顯示月份,每次點擊前/後一月/年時修改他們的值再把日曆中的42個格子從新刷新,同時每次這樣的刷新都會像服務器端發送始末日期信息以獲取當前顯示月曆中的有日程安排日信息用以突出背景色。

2.一些佈局或顯示技巧

A.使用padding屬性來控制文字的位置

某些時候單純的控制對齊方式不足以知足對顯示位置的要求,這時可使用padding屬性進行調整。

使用方式是直接使用css樣式表,或者是直接在標籤裏使用如<p style="padding-right:20px">

style="css樣式語法"是通行的一種定義css樣式的方法,若是懶得每次都在內/外聯css樣式表裏寫選擇器而某個樣式又不須要大規模應用並且你不太記得經過屬性怎麼寫(我標籤屬性目前只試過<p align:"xxx">寫對...)那麼直接用這種語法寫會方便得多(應該大多數html編輯器都支持對css樣式表語法提供提示(如bluefish))。

B.當你不但願兩個文字元素隔行而你又須要能經過id訪問某個元素時使用<span>標籤

好比界面裏右上角那個時鐘就是用下面的html語句:

<p align="right" style="padding-right:20px">如今是:<span id="clock"></span></p>

C.利用某側空間的技巧——float樣式

頁面裏的「14天內日程」提醒框是爲了佔據右邊豎直方向的空間,若是直接插進去就算改了align屬性仍是會成爲佈局中間的間隔物,爲了讓它知足佈局效果,樣式表中float:right就好了,而後太靠右的話再靠padding-right屬性來調。至於外面那圈虛線,是outline屬性的結果,有關樣式表條目:

.tips{float: right;width:50%;outline:#44AE9D dashed thin; margin-right:1%; }//使用百分比能應對更多的瀏覽器頁面大小狀況

D.使用<pre>來顯示須要保留空格和換行的字符串

和服務器端交互時拿到的字符串一般但願保留空格和換行,可是若是直接賦值給一個<p>標籤會自動把這些過濾掉,而<pre>就能夠保留。

3.ajax技術

w3school上的教程缺乏對POST方式的說明,個人實現中GET和POST兩種都用過,其中POST方法適合於數據傳輸量大的狀況(上限2M而GET聽說某些瀏覽器只有1K,所以上傳修改後的日程內容時使用了這一方法)。

GET方式比較普通:

var xmlhttp;
if (window.XMLHttpRequest)
  {// code for IE7+, Firefox, Chrome, Opera, Safari
      xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
      xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
  str="cgi-bin/getschedule.cgi?"+date;//建立get的url,其中包含QUERY_STRING內容 後端腳本直接解析它便可
  xmlhttp.open("GET",str,true);
  xmlhttp.send();
  xmlhttp.onreadystatechange=function () {//完成時的處理函數
      if (xmlhttp.readyState==4&&xmlhttp.status==200) {
             document.getElementById("schedule").value=xmlhttp.responseText;//responseText存放了服務器腳本返回的文本內容
      }
  }

 

POST方式,下面的代碼是用於向服務器發起對數據庫的增、刪、改請求時的函數:

function updatedb() {
    var xmlhttp;
if (window.XMLHttpRequest)
  {// code for IE7+, Firefox, Chrome, Opera, Safari
  xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
 if (tdchosen.className.indexOf("job")<0&& document.getElementById("schedule" ).value==0){
     
     return;
 }
  senddata='date='+document.getElementById("nowchoice").value+'&&schedule='+
document.getElementById("schedule" ).value+'&&method=';//與GET相似用&&隔開各個鍵=值對
if (tdchosen.className.indexOf("job")<0&&document.getElementById("schedule" ).value!=0) {
    tdchosen.className+="job";
    tdchosen.style.backgroundColor="#EBD44D";
    senddata+='0';//增長
}
else if (tdchosen.className.indexOf("job")>=0&&document.getElementById("schedule" ).value!=0) {
    senddata+='1';//修改
}
else if (tdchosen.className.indexOf("job")>=0&&document.getElementById("schedule" ).value==0) {
    tdchosen.className="none";
    tdchosen.style.backgroundColor="#FFFFFF";
    senddata+='2';//刪除
}

  xmlhttp.open("POST","cgi-bin/updatedb.cgi",true);//url裏不須要附上QUERY_STRING 須要發送的數據在後面的send函數裏發送
  xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");//該語句必須,用於告知服務器端腳本傳送的數據按鍵=值對格式
 
  xmlhttp.send( senddata);  
     xmlhttp.onreadystatechange=function (){
    if (xmlhttp.readyState==4&&xmlhttp.status==200) {
    document.getElementById("feedback").innerHTML=xmlhttp.responseText;
    }
    }
}

 

 

4.應避免的一個錯誤和一個須要注意的地方

A.咱們在修改頁面中的文本元素的顯示內容時習慣修改innerHTML屬性,可是對<textarea>這種用戶編輯其顯示內容會跟着修改其value屬性的元素,在後臺使用js時應對value屬性作修改。(開發期間曾經由於修改innerHTML而出現左下方文本域某些狀況下不顯示的bug)

B.js的ajax(異步方法)得到的字符串是utf-8格式的,爲了顯示不出現亂碼,須要注意服務器端傳回的字符串的編碼格式是否同爲utf-8。

後端

1.服務器和文件系統

用了python最簡單的一個支持cgi的簡單服務器類HTTPServer(初學者的本質暴露無疑)而後用CGIHTTPRequestHandler做爲處理程序類的基類。(理論上cgi一般指經過表單<form>標籤提交數據並返回要求頁面的動態頁面生成方式,它會使得頁面產生刷新跳轉,而ajax是局部刷新頁面無需跳轉,但在個人實現中,只要控制腳本返回的數據的表頭便可用這樣的方式實現ajax。固然,在這裏跪求正規的ajax服務器實現方式,歡迎評論留言交流或郵箱449339387@qq.com

 

import http.server
from http.server import HTTPServer
from http.server import CGIHTTPRequestHandler
def run(server_class=HTTPServer, handler_class=CGIHTTPRequestHandler):
    server_address=('',8001)
    httpd=server_class(server_address, handler_class)
    httpd.serve_forever()
if __name__ == '__main__':
    run()

 

而後就直接運行這個腳本它會一直運行到你用ctrl-C,期間能夠在終端看到運行過程當中信息包括後臺python腳本的錯誤信息

eg.

文件系統方面,注意使用上述cgi服務器時,假設服務器腳本在頂層目錄,則全部的url相對尋址以該腳本所在位置爲起始點。要求全部腳本文件必須在cgi-bin這個文件夾下(不然會是獲取這個源文件的內容而不是執行後的返回結果),同時你須要chmod u+x 腳本文件名以賦予它可被執行的權限。(腳本文件名後綴.cgi或.py結尾都可)

另外,注意數據庫文件與服務器腳本的目錄關係,若是在同一層目錄,後臺腳本中使用的相對路徑在被服務器調用時是以服務器所在位置爲起點的。但這爲腳本的單獨測試形成了不便,由於腳本文件是在cgi-bin目錄下不和服務器同一層級,若是隻有一個數據庫文件,須要自行修改訪問路徑,或者你本身拿個數據庫文件副本放cgi-bin下專門測試用(我本身開發過程當中就拿了個副本測試可是後來忘了兩個數據庫文件內容已經不同了因而對一些現象一直覺得是bug結果發現是目錄對應關係沒理解透..)

2.其它後端腳本

數據庫使用的是sqlite3,緣由是它是裝python自帶的,而後也是個關係型數據庫,對sql的語法支持夠用,恩,等哪天再研究mysql...

一開始須要建立數據庫中的表和索引(index,用於後續select時能夠根據該索引所對應的表項排序)

import os
import sqlite3
conn=sqlite3.connect('jobs_database')
cursor=conn.cursor()
cursor.execute('''
create table jobs(
    userid varchar,
    year integer,
    month integer,
    date integer,
    totalday integer,
    jobslist varchar)
''')
cursor.execute('''create index userid on jobs(userid)''')
cursor.execute('''create index years on jobs(year)''')
cursor.execute('''create index months on jobs(month)''')
cursor.execute('''create index dates on jobs(date)''')
cursor.execute('''create index totaldays on jobs(totalday)''')
conn.commit()
cursor.close()
conn.close()

sql的語法我本身還不太熟練,有興趣的能夠百度。

建表時主要考慮了後續的可擴展使用度以及編程或排序的方便

裏面那個totalday列是發現根據year,month,date很差用between子句來得到想要的內容,好比我想提取2014-7-27~2014-8-30這段時間內的日程安排信息,可是你用jobs.day between 27 and 30時明顯就不對了。因此把全部日期轉換成一個整數如20141128直接比較大小看起來纔是正道(一樣的,但願有人分享更正規的作法)。可是要注意月份和日期可能只有一位數,轉整數時須要給其補0再鏈接轉換成整數。這體如今後續的數據傳送的格式上。

獲取有日程安排日信息的腳本

#!/usr/bin/python3.4
import os
import sqlite3
from datetime import *
import time
query=os.environ['QUERY_STRING'].split('&')
begin=query[0].split('-')
end=query[1].split('-')

beday=int(begin[0]+begin[1]+begin[2])
endday=int(end[0]+end[1]+end[2])
conn=sqlite3.connect("jobs_database")
cursor=conn.cursor()
cursor.execute('''
    select jobs.year,jobs.month,jobs.date,jobs.jobslist from jobs where 
    jobs.totalday between ? and ? order by jobs.totalday asc''',(beday,endday))
result=cursor.fetchall()
cursor.close()
conn.close()
bedate=date(int(begin[0]),int(begin[1]),int(begin[2]))
response=str(result.__len__())+'&'
for item in result:
    response+=str((date(item[0],item[1],item[2])-bedate)).split()[0]+'&'
print('Content-type: text/plain\n')
print(response)

頁面傳回來的數據是一對當前月曆始末日期,格式「xxxx-xx-xx&xxxx-xx-xx」,因爲頁面已經處理好了補零的問題,後臺腳本就能夠簡單地鏈接轉成整數,查詢。

這裏返回數據時要注意兩點:

1.print('Content-type: text/plain\n')語句是必須的,它告訴瀏覽器送回來一個字符串而不是頁面(text/html)

2.是要傳送回一堆xxxx-xx-xx格式的日期呢仍是別的呢?

考慮到傳送回日期的話還要不斷去遍歷表格中的每一個格子判斷日期是否對上來進行背景色變化,複雜度就上去了。(固然,能夠本地js寫個映射表,但月曆一改表就得從新維護吃力不討好)因而這裏選擇後臺累一點直接返回對應格子的索引下標,同時第一個數字是總共當前月曆中有日程安排日的總數,方便後面js裏寫for循環的方便和準確。因而返回的格式是"總數&索引1&索引2&...&",有&這個間隔符後面js處理就方便了。並且相比於傳送日期這樣的網絡通訊量更少卻提供了所需的所有信息。(忽然發現這有點協議的意思了:))

另外,補充一句,實測後發現運行後臺腳本時使用的python解釋器的版本是根據開頭的#!/usr/bin/python3.4來肯定的,ubuntu系統中2.x和3.x版本的python都有裝,兩個版本在socket編程等地方有必定差別,所以某些狀況下須要注意這一句使用了哪一個版本(上面語句若是爲#!/usr/bin/python在ubuntu中對應使用的時python2.x

獲取被選中日的日程內容:

經過js代碼中的預先判斷決定是否須要發起該獲取請求(對於標記爲沒有日程安排的格子明顯不須要),減小網絡通訊量(網絡帶寬永遠是珍稀資源,雖然放在這個小項目裏形式上的意義更大=。=不過實測中使用ajax確實是有一些肉眼可見的延遲的)。

#!/usr/bin/python3.4
import os
import sqlite3
query=os.environ['QUERY_STRING'].split('-')
day=int(query[0]+query[1]+query[2])
conn=sqlite3.connect('jobs_database')
cursor=conn.cursor()
cursor.execute('''
    select    jobs.jobslist from jobs where jobs.totalday = ?
    ''',(day,))
result=cursor.fetchall()
cursor.close()
conn.close()
response=""
if result.__len__()!=0:
    response=result[0][0]
print('Content-type: text/plain\n')
print(response)

而後是最大頭的增刪改操做:

#!/usr/bin/python3.4
import cgi
import sqlite3
form=cgi.FieldStorage()
date=form.getfirst('date').split('-')
day=int(date[0]+date[1]+date[2])
jobslist=form.getfirst('schedule')
method=int(form.getfirst('method'))
conn=sqlite3.connect('jobs_database')
cursor=conn.cursor()
try:
    if method == 1:#update
        cursor.execute('''
    update jobs set jobslist = ? where jobs.totalday =?
    ''',(jobslist,day))
    elif method == 0:#insert
        cursor.execute('''
    insert into jobs(userid,year,month,date,jobslist,totalday)
    values(?,?,?,?,?,?)
   ''',    ('author',int(date[0]),int(date[1]),int(date[2]),jobslist,day))
    elif method == 2:#delete
        cursor.execute('''
    delete from jobs where jobs.totalday=?
    ''',(day,))
    conn.commit()
except:
    print('Content-type: text/plain\n')
    print('更新失敗,請刷新頁面後重試!')
else:
    print('Content-type: text/plain\n')
    print('操做成功')
finally:
    cursor.close()
    conn.close()

專門寫異常處理可是隻在這個腳本里寫只是爲了回顧寫這個小項目的初衷:把自學的內容儘量用一遍。全部有了上面的try except finally。注意包括else時異常處理的執行順序哦!

最後是獲取「14天內日程」提示框內容:

#!/usr/bin/python3.4
import os
import sqlite3
from datetime import *
import time
query=os.environ['QUERY_STRING'].split('&')
begin=query[0].split('-')

beday=int(begin[0]+begin[1]+begin[2])
delta=date(2014,6,15)-date(2014,6,1)
nowdate=date(int(begin[0]),int(begin[1]),int(begin[2]))
enddate=nowdate+delta
endtmpyear=str(enddate.year)
endtmpmon=enddate.month
endtmpday=enddate.day
if endtmpmon < 10:
    endtmpmon='0'+str(endtmpmon)
if endtmpday <10:
    endtmpday='0'+str(endtmpday)
endday=int(endtmpyear+str(endtmpmon)+str(endtmpday))
conn=sqlite3.connect("jobs_database")
cursor=conn.cursor()
cursor.execute('''
    select jobs.year,jobs.month,jobs.date,jobs.jobslist from jobs where 
    jobs.totalday between ? and ? order by jobs.totalday asc''',(beday,endday))
result=cursor.fetchall()
cursor.close()
conn.close()
print('Content-type: text/plain\n')
response=""
for item in result:
    print('<strong>距今'+str((date(item[0],item[1],item[2])-nowdate)).split()[0]+'天:\n'+item[3]+'</strong><br/>')

因爲想試試python的日期處理因此此次的14天后是後端本身算的,注意python中兩個date對象作差獲得的不是int而是datetime.timedelta類的對象,又不能直接+整數n獲得n天后了,只好用一個tricky的方法delta=date(2014,6,15)-date(2014,6,1)造一個14天的delta再給它往今天的date對象上加(再次跪求正規作法!!)。

而後要獲取距離多少天這又是個問題,用dir查了下好像沒啥轉成int的方法,網上介紹用獲取距某個默認時間的秒數再數學運算求的方法感受太麻煩了= =還好datetime.timedelta類對象str後獲得的字符串表示是「xxx days xx:xx:xx」,因此又是tricky的方法拿到字符串後split而後拿0號下標就是要的天數字符串!(再再次跪求正規作法!!)

3、Future work

項目的所有源碼因爲尚未所有補好註釋待我弄完再發上來(或者我終於良心發現去研究github怎麼用而後傳到github回這裏貼連接)

最後說下這個標題所示的不能免俗的話題:

1.雖然爲了能把所學的內容儘量用上的初衷用了服務器腳本和數據庫,不過好像一點網絡的效用都沒發揮(恩,maybe局域網)。從網絡角度考慮應該實現多用戶使用,這也是爲何數據庫的表裏面預留了userid這一項。後續能夠加個登錄頁面作個表單填帳號密碼數據庫里加個密碼錶作驗證。

2.而後就說到了安全性,初版的這個項目安全性幾乎沒有= =,待哪日學了信息安全的課程再說。

3.多用戶後對數據庫的多線程訪問問題也出現了,這塊還徹底不清楚,須要擇日專門學數據庫。

4.服務器的問題也但願能弄得更清楚,包括怎麼正確支持ajax等。

相關文章
相關標籤/搜索