在一个大文本文件中,如何读取指定行,并保存?

aiyou18 2007-07-25 11:23:45
我有一个类似于:
123
456222
7896
111
455
0
0
12
......
这样的一列文本文件,这个文本文件很大,有120多M。

我写了这么一个程序:
Dim a() As String


Private Sub Command1_Click()
Dim inputdata
CommonDialog1.Action = 1 'Action=1表示弹出“打开”对话框

Open CommonDialog1.FileName For Input As #1 'Commondialog1.fileName表示的是路径名,也就是commondialog中打开的文件名


Do While Not EOF(1) 'EOF返回值为1表示已经到达文件末尾
Line Input #1, inputdata
Data = Data + inputdata + vbCrLf


Loop
a = Split(Data, vbCrLf)
Close #1


End Sub
'////以上代码的作用是把文本文件转成一个数组,以便于定位指定行///

Private Sub Command2_Click()
Dim startPoint As Double 'startPoint定义为以秒为单位
Dim timeLong As Double '时间长度单位为周期个数
startPoint = Val(Text1.Text) * 5000 '5000意思是,一秒钟为5000个点
timeLong = Val(Text2.Text) * 100 '一个周期是100个点

Dim i As Long
i = 0
Open "c:\" & "起始时刻:" & Text1.Text & "秒-取样周期数:" & Text2.Text & "个循环" & ".txt" For Append As #2
Do While (i < timeLong)
Print #2, a(startPoint + i)
i = i + 1

Loop
End Sub
'////以上代码的作用是从startPoint开始摘取timeLong长度的数据////

我在小文本文件上通过测试,可是换到120M的文本文件,会占用大量内存,系统没有反应。我该如何处理这样大的文本文件的定位问题?
请高手指教!
...全文
3103 52 打赏 收藏 转发到动态 举报
写回复
用AI写文章
52 条回复
切换为时间正序
请发表友善的回复…
发表回复
赵4老师 2010-04-21
  • 打赏
  • 举报
回复
导入数据库
以后只操作数据库即可
superManXsj 2010-04-20
  • 打赏
  • 举报
回复
数据库设计中的14个技巧
of123 2007-08-06
  • 打赏
  • 举报
回复
其实对于这样只有数字的.txt 文件,不妨使用 Jet 数据库引擎来处理。数据库引擎不会一次将所有记录在入内存,而是根据需要来进行磁盘交换。对于人机界面的应用,应该感觉不到等待。

虽然 Jet 引擎是为数据库与文本文件之间的导入导出设计的,不鼓励将文本文件作为永久性数据源来使用。

'引用 Microsoft ActiveX Data Object 2.x Library (2.x >= 2.1)
Option Explicit
Dim cn As ADODB.Connection, rs As ADODB.Recordset

Private Sub Command1_Click()
'假定定位到第 3 行
rs.AbsolutePosition = 3
Debug.Print rs.AbsolutePosition, rs.Fields(0)
End Sub

Private Sub Form_Load()

Set cn = New ADODB.Connection
cn.CursorLocation = adUseClient
'打开数据库连接,把文本文件所在目录作为数据库对待。
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\txtFilesFolder\;Extended Properties=""text;HDR=No;FMT=Delimited"""

'打开记录集
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM [test.txt]", cn

End Sub

Private Sub Form_Unload(Cancel As Integer)
rs.Close
Set rs = Nothing
cn.Close
Set cn = Nothing
End Sub
  • 打赏
  • 举报
回复
老马失蹄~
aiyou18 2007-08-06
  • 打赏
  • 举报
回复
啊? 老马,你的指头怎么了?
嗷嗷叫的老马 2007-08-04
  • 打赏
  • 举报
回复
我在想,是否能直接通过内存文件映射来处理呢?

我最近在学WIN32汇编,里面读写文件都是这样的,可以很方便地处理文件

哎打字辛苦,少了几个指头,速度严重下降啊.........
嗷嗷叫的老马 2007-08-04
  • 打赏
  • 举报
回复
嘿,我再来顶一下

现在打字写代码都不太方便.......
KiteGirl 2007-08-03
  • 打赏
  • 举报
回复
我正在给你写一个叫做BVP的Class类模块,我那个根据Byte识别Long值的算法比较适合用类模块实现(因为用事件响应比较方便)。
首先要提醒你:这类逐个字节处理的程序必须编译后才能得到最佳性能。初步测试在Celeron 433MHz的破电脑上,编译后处理1.37MB的数据,需要1.9秒。
如果把你的120MB的文本文件分多次读,然后用BVP处理(当处理积累的Long数据达到一定数量,定期将它存盘,以减少内存用量)。这个速率在这种状态下是恒定的。如果是在我测试用的Celeron 433MHz破电脑,它应该在166秒内完成全部的数据转换。
完整的模块叫TCV,这个东西我找时间写出来。

测试窗体代码:
Public WithEvents pubBVP As clsByteValueProcessor

Private Sub Form_Load()
Set pubBVP = New clsByteValueProcessor
End Sub

Private Sub Command2_Click()
Dim tBytes() As Byte
Dim tBytes_Index As Long

'生成一个模拟的0-100000数字的文本。

Dim tIndex As Long
Dim tString As String
Dim tStrs() As String

ReDim tStrs(100000)

For tIndex = 0 To 100000
tStrs(tIndex) = tIndex
Next

tString = Join(tStrs(), vbCrLf)

Text1.Text = LenB(tString)

DoEvents

tBytes() = StrConv(tString, vbFromUnicode)

Dim tOnTimer As Double

tOnTimer = Timer

'逐个字节用BVP转换

For tBytes_Index = 0 To UBound(tBytes())
pubBVP.Code = tBytes(tBytes_Index)
Next

Dim tValues() As Long
Dim tValues_Index As Long

tValues() = pubBVP.Values

Text1.Text = Abs(tOnTimer - Timer)

End Sub



clsByteValueProcessor模块:

'Byte Value Processor B.V.P

Private WithEvents priBVC As clsByteValueCell '值处理单元
Private priVM() As Long '值数组
Private priVe As Long '值数组结尾
Private priVs As Long '值数组界限

'ValueChange事件 当遇到第一个非法值,结束一个完整的值识别,触发该事件。
'Code属性 只写输入端。输入Byte值。
'Values属性 只读输出端。输出Long数组。
'Count属性 只读属性。返回Long数组的元素数。

Public Event ValueChange()

Private Sub Class_Initialize()
Set priBVC = New clsByteValueCell
End Sub

Public Property Let Code(ByVal pCode As Byte)
priBVC.Code = pCode
End Property

Public Property Get Values() As Long()
Values = priVM()
End Property

Public Property Get Count() As Long
Count = priVe
End Property

Private Sub priBVC_ValueChange()
ReDim Preserve priVM(priVe)

priVM(priVe) = priBVC.Value
priVe = priVe + 1

RaiseEvent ValueChange
End Sub

clsByteValueCell类模块:

'Byte Value Cell B.V.C

'ValueChange事件 当遇到第一个非法值,结束一个完整的值识别,触发该事件。
' 该事件触发时,应及时从Value属性读取完整值。
'Code属性 输入端。输入Byte值。
'Value属性 输出端。输出Long类型。


Private priCVT() As Byte 'Code Value Table 编码值映射表
Private priCET() As Boolean 'Code Enables Table 编码逻辑映射表
Private priVt As Long 'Temp Value 寄存值
Private priAp As Boolean 'Append Mode 追加标志

Public Event ValueChange()

Private Sub Class_Initialize()

Dim tCTi As Long

ReDim priCVT(0 To 255)
ReDim priCET(0 To 255)

For tCTi = 48 To 57

priCVT(tCTi) = tCTi - 48
priCET(tCTi) = True

Next

End Sub

Public Property Let Code(ByVal pCode As Byte)

Dim tCE As Boolean 'Code Enabled 编码有效
Dim tCV As Byte '编码值

tCE = priCET(pCode)

If tCE Then '如果值有效(是0-9数字)

If Not priAp Then '如果不在追加状态

priVt = 0 '初始化寄存值
priAp = True '置为追加态

End If

'进位追加

If priVt < 200000000 Then 'Long取值溢出保护

priVt = priVt * 10 + priCVT(pCode)

End If

Else

If priAp Then

priAp = False

RaiseEvent ValueChange

End If

End If

End Property


Public Property Get Value() As Long

Value = priVt

End Property


aiyou18 2007-08-03
  • 打赏
  • 举报
回复
to clear_zero(clear_zero) :我之前是用二进制保存的数据,因为客户觉得二进制文件打开不方便,所以我就用的txt格式保存的试验数据。现在我是骑虎难下了,需要把这些120M的文本文件打开处理。以后我会考虑按二进制保存,并写一个二进制察看程序的。

to lele_china() 你的代码,我还没有来得及测试和消化。:)

to myjian(嗷嗷叫的老马--酒疯子版) :欢迎拍砖!欢迎凑热闹!

to of123():我刚学VB,所以对于一个一长列数据的文本文件,我只会用line input读入,然后连成数据串,然后用split转成数组的方式。就像学英语,因为我的词汇量有限,所以只能用这种憋足的方法来实现功能。确实还没有能力估计到代码的效率问题。你能否给个思路:如何把一列txt数据快捷的转成一个数组?

to 小仙妹:小仙妹来了,我真幸福!你的代码写的详细又人性化。很喜欢。估计你也是个细心而体贴的人吧。我会认真学习的...
吾非大神 2007-08-03
  • 打赏
  • 举报
回复
真是高人汇集
这帖子必须顶下
KiteGirl 2007-08-03
  • 打赏
  • 举报
回复
OK!终于有人顶帖子了。

根据楼主的需要,有了一个非常快的解决办法!用“直接转换法”。
根本就不用读取什么行、字符串的,而是一个一个字节地把Byte顺序地“填”进一个过程里,这个过程根据“填”进来字节进行响应,将其识别成long类型的数值,然后在这个数字段结束后将完整的数值添加到long数组里。整个过程完全是数学计算,根本不涉及字符串。
在编译为本机代码后,这个办法在CR433电脑上至少以1MB/S的速度将文本文件的字节直接识别、转换成long类型数字。120MB的文本文件在CR433电脑上识别出全部数值只要120秒左右。而且它能从“小仙妹是个好孩子123小仙妹是个好孩子12222小仙妹”中识别出123和12222,抗干扰能力特别强。

如果使用这个方法,楼主必须保证以下几点:

1、你这个120MB的文件有用的部分必须全是数字
类似:
123
0
1222
2222

2、你这些数字不能是小数,必须是整数。

3、数字必须是10进制的,不能是其他进制。

下面是程序的核心部分及测试代码。但是对于不熟悉这个程序原理的楼主来说恐怕有点难以理解。因此我会接着给楼主写一个完整的转换程序。

Public pubBytesCount As Long

Option Explicit

Private Sub Command1_Click()
Dim tString As String
Dim tBytes() As Byte
Dim tValues() As Long
Dim tTempValue As Long
Dim tEnCodeTable() As Byte
Dim tApp As Boolean

ReDim tValues(0)
tEnCodeTable() = EncodeCodeTable()

Dim tIndex As Long
Dim tIndex2 As Long

'For tIndex2 = 0 To 100

' For tIndex = 0 To 10
' tString = tString & vbCrLf & tIndex
' Next

' tBytes() = StrConv(tString, vbFromUnicode)
tBytes() = StrConv("小仙妹是个好孩子1223小仙妹是个好孩子222啊", vbFromUnicode)


ValueSendByCodes tValues(), tTempValue, tBytes(), tEnCodeTable(), tApp
'tString = ""

' tString = ""
'Next

'tString = ""

For tIndex = 0 To UBound(tValues())
tString = tString & vbCrLf & tValues(tIndex)
Next

Text1.Text = tString

End Sub

Public Sub ValueSendByCodes(ByRef pValues() As Long, ByRef pTempValue As Long, ByRef pCodes() As Byte, ByRef pEnCodeTable() As Byte, ByRef pApp As Boolean)
Dim tBytes_Length As Long
Dim tBytes_Index As Long

tBytes_Length = UBound(pCodes())

For tBytes_Index = 0 To tBytes_Length
ValueSendByCode pValues(), pTempValue, pCodes(tBytes_Index), pEnCodeTable(), pApp

pubBytesCount = pubBytesCount + 1

'If Not CBool(pubBytesCount Mod 100000) Then

' DoEvents
' Text1.Text = pubBytesCount & " " & UBound(pValues())

' If pubBytesCount > 120000000 Then End


'End If

Next
End Sub

Public Sub ValueSendByCode(ByRef pValues() As Long, ByRef pTempValue As Long, ByVal pCode As Byte, ByRef pEnCodeTable() As Byte, ByRef pApp As Boolean)
'填充编码
Dim tCodeEnabled As Boolean
tCodeEnabled = CBool(pEnCodeTable(pCode))
If tCodeEnabled Then
If Not pApp Then
pApp = True
pTempValue = 0
End If
If pTempValue < 200000000 Then
pTempValue = pTempValue * 10 + (pEnCodeTable(pCode) - 1)
End If
Else
If pApp Then
Dim tValuesIndex As Long
tValuesIndex = UBound(pValues())
pValues(tValuesIndex) = pTempValue
ReDim Preserve pValues(tValuesIndex + 1)
pApp = False
End If
End If
End Sub

Public Function EncodeCodeTable() As Byte()
'创建编码映射表
Dim tBytes() As Byte
Dim tBytes_Index As Long
ReDim tBytes(0 To 255)

For tBytes_Index = 48 To 57
tBytes(tBytes_Index) = tBytes_Index - 47
Next

EncodeCodeTable = tBytes()
End Function

of123 2007-08-03
  • 打赏
  • 举报
回复
读取文件那段代码真的很笨拙。
我们知道,VB 中字符串的处理是最慢的。代码中先将读入的数据连接成一个超大的字符串,然后再用 Split 切分。这不是折腾吗?
KiteGirl 2007-08-03
  • 打赏
  • 举报
回复
告诉楼主一下我上面那个BytesLoadByFile_MultiLoad的测试结果:
(顺便提醒:它有一个编写上的错误:如果你在参数里设置缓冲区参数,会出错)

在128MB内存的CR433电脑上,读到6.8MB就内存溢出了。
在2GB内存的Core 2 Duo E6600电脑;1GB的Pentium 4HT电脑上,都成功地读取了一个222MB的文件。但运行起来还是很吃力。

我写程序的标准是让它能运行在尽量低端的电脑上,因此这个方法连我自己都不满意。

因此你要等我写出分页建立索引的程序,这个程序在CR433档次的电脑上能以1MB/秒的速度为文本文件建立索引文件。对内存基本没有需求(取决于缓冲区的大小,如果你用默认开方算法确定缓冲区,为2GB文件建索引需要占用44MB的内存空间。但是你可以自己把缓冲区设置小一些)。

索引文件记录格式我是这样设计的:它记录了每一行的开始地址(LineStart)和长度(Length)。之后你根据索引文件,读文本文件指定地址、指定长度的字节。直接定位到文本文件里取行,其速度是相当快的。

但是其缺点也是显而易见的:如果文本文件任何一个部分被改动,你需要重新为它建立索引。
KiteGirl 2007-08-03
  • 打赏
  • 举报
回复
看看我新改的BytesLoadByFile_MultiLoad行不行?这个是分页读文件到Byte数组(前提是你有足够内存,或者缓存盘有足够空间)。如果这个办法还是不行,那么我会写分页索引。
(我电脑上最大的文本文件是1.8MB的《瑜伽师地论》)

测试部分:
Private Sub Command2_Click()
Dim tBytes() As Byte
Dim tText As String
Dim tStrings() As String
tBytes() = BytesLoadByFile_MultiLoad("Test.txt", , Text2)
'在Text2显示进度。
tText = StrConv(tBytes(), vbUnicode)
tStrings() = Split(tText, vbCrLf)
Text1.Text = tStrings(120) '在Text1显示120行的信息。
End Sub


模块部分:必须在模块里定义。

Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

Public Function BytesLoadByFile_MultiLoad(ByVal pFileName As String, Optional pBufferSize As Long = 0, Optional ByRef pTextBox As TextBox = Nothing) As Byte()

Dim tBytes() As Byte
Dim tBytesLength As Long

Dim tFile_Number As Integer '文件句柄
Dim tFile_Size As Long '文件长度

Dim tBuffer_Size As Long '缓冲区容量
Dim tBuffer_Size_UserSet As Boolean '用户方式缓冲区容量
Dim tBuffer_Page As Long '缓冲页数
Dim tBuffer_End As Long '尾缓冲页余数
Dim tBuffer_Index As Long '缓冲页索引
Dim tBuffer_Bytes() As Byte '缓冲字节

Dim tShell_View As Boolean

tShell_View = Not (pTextBox Is Nothing)

tFile_Number = FreeFile

Open pFileName For Binary As #tFile_Number

tFile_Size = LOF(tFile_Number)

tBuffer_Size_UserSet = pBufferSize > 0

'缓冲区定义:默认方式=开方值;设置方式=用户设置
tBuffer_Size = (tBuffer_Size_UserSet And pBuffer_Size) + (Not tBuffer_Size_UserSet And Sqr(tFile_Size))
tBuffer_Page = (tFile_Size \ tBuffer_Size)
tBuffer_End = (tFile_Size Mod tBuffer_Size)

ReDim tBuffer_Bytes(tBuffer_Size - 1)
tBytesLength = 0

For tBuffer_Index = 1 To tBuffer_Page

ReDim Preserve tBytes(tBytesLength + tBuffer_Size - 1)
Get #tFile_Number, , tBuffer_Bytes()
CopyMemory tBytes(tBytesLength), tBuffer_Bytes(0), tBuffer_Size
tBytesLength = UBound(tBytes)

DoEvents

If tShell_View Then
pTextBox = tBytesLength & "/" & tFile_Size
End If

Next

ReDim tBuffer_Bytes(tBuffer_End - 1)
ReDim Preserve tBytes(tBytesLength + tBuffer_End - 1)
Get #tFile_Number, , tBuffer_Bytes()
CopyMemory tBytes(tBytesLength), tBuffer_Bytes(0), tBuffer_End
tBytesLength = UBound(tBytes)

Close #tFile_Number

BytesLoadByFile_MultiLoad = tBytes()
End Function

KiteGirl 2007-08-03
  • 打赏
  • 举报
回复
我原来的代码是操作10MB以下规模文件的,是一次性读取。
如果你要处理120MB的文件,需要用“分页索引法了”。其实也就是我原来的代码改进一下。等我有时间会给你写一个“分页索引算法”的模块。至于它大概的原理是这样的:

1、先把文件按照一定的字节数分页,分多次去处理。
实际上我先前的代码并没有问题,只是你的文件太大了,不管怎么读,只要是把它读一次,都要那么多时间。如果我把代码改成分多次读,中间留个DoEvents,会令你更容易接受一些。

2、建立索引信息,使下一次读取更快。
由于文件太大,无论什么算法,如果每次检索行要重历遍文件都会很慢。因此将文件每一行的索引信息存储在一个数据文件里。这样第一次读虽然慢,但是下次读取会特别快。
aiyou18 2007-08-02
  • 打赏
  • 举报
回复
以下代码的作用是:打开一个大的文本文件,按照每100个作为一个周期,求这个周期内的尖峰值的平均值,并保存下来
问题是:报内存溢出 我不知道为什么?
Private Sub Command1_Click()
Dim lastData As Integer '定义为上一次的值
lastData = 0

Dim last2Data As Integer '定义为上一次的上一次的值
last2Data = 0

Dim CurData As String
CommonDialog1.Action = 1
Dim i As Long
i = 0
Open CommonDialog1.FileName For Input As #1
Do While Not EOF(1) 'EOF返回值为1表示已经到达文件末尾


Line Input #1, CurData

If i = 100 Then '按每100个数作为一个周期,把这个周期内的尖峰值都累加,然后求平均值
Open "c:\AVER.txt" For Append As #4

Print #4, Str(Data / j) '把平均值保存到AVER.txt文件中
Data = 0
j = 0
i = 0
Close #4
End If


If lastData > Val(CurData) And lastData > last2Data Then '找到尖峰值,也就是如果上一次的值高于当前值,并且高于上一次的上一次的值,那么认为上一次的值为尖峰值
Data = Data + lastData '把这些尖峰值累加
j = j + 1
End If

last2Data = lastData
lastData = Val(CurData)
i = i + 1




DoEvents

Loop




Close #1



End Sub
aiyou18 2007-07-30
  • 打赏
  • 举报
回复
以上测试是在startPoint与timeLong均为1的时候得出的结果。

更有意思的是,当我把startPoint设置为1,timeLong设置为2,也就是保存200个数的时候。发现print与write没有区别了。也就是在保存数为200时,没有点“停止运行”,print语句也把200个数写到#2文件里。

然后我继续测试,发现timeLong设置为1.27的时候,也就是保存为125个数的时候,print语句不在停止运行程序时,只会写入64个数。
当改成timeLong=1.28的时候,print语句在没有停止程序时,把128个数全部写入到#2号文件里。
127个数和128个数好像是个门槛。不晓得这是什么原因。
mmyyxx88 2007-07-30
  • 打赏
  • 举报
回复
应该使用 write.
print本来就是打印的意思,多用于保存便于打印固定格式的数据。
aiyou18 2007-07-30
  • 打赏
  • 举报
回复
经过我的测试,发现这么一个有趣的现象:
当我运行程序后,如果没有点击“停止运行”,那么使用print语句时,#2文件里只有64个数。当点击停止运行后,才把剩下的36个数,写到#2文件里。

而如果使用write语句,在没有点击“停止运行”时,#2文件里就有100个数了。

或许这就是print与write的区别,可我不明白为什么?高手解释一下:)
aiyou18 2007-07-30
  • 打赏
  • 举报
回复
奇怪了? Print #2,data 语句和Write #2,data语句居然出来不同的结果。




Private Sub Command1_Click()


Dim iCunt As Integer '每个文件的行数
Dim fileCunt As Integer '文件编号
Dim inputdata
CommonDialog1.Action = 1 'Action=1表示弹出“打开”对话框
Open CommonDialog1.FileName For Input As #1 'Commondialog1.fileName表示的是路径名,也就是commondialog中打开的文件名

iCunt = 0
fileCunt = 0
Data = vbNullString
Do While Not EOF(1) 'EOF返回值为1表示已经到达文件末尾
Line Input #1, inputdata
Data = Data + inputdata + vbCrLf
iCunt = iCunt + 1
If iCunt = 10000 Then '每10000行一个文件
iCunt = 0
Open "C:\FileTmp" & Trim(Str(fileCunt)) For Output As #8
Print #8, Data
Close #8

fileCunt = fileCunt + 1
Data = vbNullString
DoEvents '别忘了这条,不然就会出现你说的“系统没有反应”
End If
Loop

Open "C:\FileTmp" & Trim(Str(fileCunt)) For Output As #8
Print #8, Data
Close #8



Close #1
End Sub






Private Sub Command2_Click()

Dim startPoint As Double 'startPoint定义为以秒为单位
Dim timeLong As Double '时间长度单位为周期个数
startPoint = Val(Text1.Text) * 20 '这里我为了测试方便改成了20
timeLong = Val(Text2.Text) * 100 '一个周期是100个点

Dim i, j As Long
Dim FileNo, startLine As Integer
FileNo = startPoint \ 10000
startLine = startPoint Mod 10000
Data = vbNullString
i = 0
j = 0
abcdefghijk:
Open "C:\FileTmp" & Trim(Str(FileNo)) For Input As #9
Do While Not EOF(9) And (i < timeLong) 'EOF返回值为1表示已经到达文件末尾

j = j + 1
Line Input #9, inputdata
If j >= startLine Then
Data = Data + inputdata + vbCrLf
i = i + 1 'mmyyxx88() 的代码i=i+1应该改成放在这里
End If
Loop
Close #9

If i < timeLong Then
FileNo = FileNo + 1
j = 0
startLine = 0
End If

If i < timeLong Then GoTo abcdefghijk



Open "c:\" & "起始时刻:" & Text1.Text & "秒-取样周期数:" & Text2.Text & "个循环" & ".txt" For Append As #2



Print #2, Data '问题出在这里了!!!我用执行到这暂停,在立即窗口中查看data是100个数,可是执行print语句后,为什么保存的文件里只有64个数?!我把print改成write,执行后也是100个数? 难道print语句不是这么用的吗!?



'''Loop
End Sub
加载更多回复(32)

7,763

社区成员

发帖
与我相关
我的任务
社区描述
VB 基础类
社区管理员
  • VB基础类社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧