Python数据清洗80%的工作量,看这篇就够了

干净整洁的数据是后续进行研究和分析的基础。数据科学家们会花费大量的时间来清除数据集,毫不夸张地说,数据清洗会占据他们80%的工作时间,而真正用来分析数据的时间只占到20%左右。
所以,数据清洗究竟是在清洗些什么?
通常来说,你所获取到的原始数据不能直接用来分析,由于它们会有各种各样的问题,如包含无效信息,列名不规范、格式不一致,存在重复值,缺失值,异常值等…..
假如你在学习Python的过程当中有遇见任何疑问,可以加入我的python交流学习qq群:250933691,多多交流问题,互帮互助,群里有不错的学习教程和开发工具。学习python有任何疑问(学习方法,学习效率,如何就业),可以随时来咨询我
本文会给大家详情如何用Python中自带的Pandas和NumPy库进行数据清洗。在正式讲解之前,先简单详情一下这两个非常好用的库。
Pandas的名称来自于Panel data和Python数据分析data analysis,是Python的一个数据分析包,最初由AQR Capital Management于2008年4月开发,被作为金融数据分析工具,为时间序列分析提供了很好的支持,并于2009年底开源出来。
NumPy是Numeric Python的缩写,是Python的一种开源的数值计算扩展,可用来存储和解决大型矩阵matrix,比Python自身的嵌套列表结构要高效的多,提供了许多高级的数值编程工具,如:矩阵数据类型、矢量解决,以及精密的运算库,专为进行严格的数字解决而产生。
目录
一、理解数据
二、清洗数据
去除不需要的行、列
重新命名列
重新设置索引
用字符串操作规范列
用函数规范列
删除重复数据
填充缺失值
三、总结
【注】为了清晰直观地展现数据清洗操作,本文会用到几个不同的数据集,重点是方法的讲解。
【工具】Python 3
一、理解数据
拿到一个全新的数据集,应该从哪里入手?
没错,我们需要先理解数据,看看它长什么样子。这里用tushare.pro上面的日线行情数据进行展现,以浦发银行(600000.SH)为例。常用的方法和属性如下:
.head()
.tail()
.shape
.columns
.info()
.describe()
.value_counts()
首先,获取数据:
importpandasaspd
importnumpyasnp
importmatplotlib.pyplotasplt
importtushareasts
pd.set_option(‘display.max_columns’,100)#?设置显示数据的最大列数,防止出现省略号…,导致数据显示不全
pd.set_option(‘expand_frame_repr’,False)#?当列太多时不自动换行
pro?=?ts.pro_api()
df?=?pro.daily(ts_code=’600000.SH’,?start_date=’20190401′,?end_date=’20190430′)
.head()?查看前n行数据,默认值是5
df.head()
Out[1]:
ts_codetrade_dateopenhighlowclosepre_closechangepct_chgvolamount
0??600000.SH20190430??11.7012.0911.7011.9711.480.494.26831234747.381466714.710
1??600000.SH20190429??11.3511.5411.3411.4811.320.161.4134385869.38442046.727
2??600000.SH20190426??11.4311.5611.2811.3211.54-0.22-1.9064424695.81485267.261
3??600000.SH20190425??11.5611.6911.4811.5411.62-0.08-0.6885408761.29473973.527
4??600000.SH20190424??11.7611.7711.5111.6211.70-0.08-0.6838382011.08444929.313
.tail()?查看后n行数据,默认值是5
df.tail()
Out[2]:
ts_codetrade_dateopenhighlowclosepre_closechangepct_chgvolamount
16??600000.SH20190408??11.7911.9611.6511.7211.710.010.0854778703.73920513.531
17??600000.SH20190404??11.5511.7111.5411.7111.500.211.8261752325.27876099.547
18??600000.SH20190403??11.3711.5411.3411.5011.440.060.5245502710.29575799.446
19??600000.SH20190402??11.5011.5211.4111.4411.440.000.0000467147.10534896.810
20??600000.SH20190401??11.3611.5211.2911.4411.280.161.4184706374.05808657.530
.shape?查看数据维数
df.shape
Out[3]:?(21,?11)
.columns?查看所有列名
df.columns
Out[4]:
Index([‘ts_code’,’trade_date’,’open’,’high’,’low’,’close’,’pre_close’,
‘change’,’pct_chg’,’vol’,’amount’],
dtype=’object’)
.info()?查看索引、数据类型和内存信息
df.info()
RangeIndex:21entries,0to20
Data?columns?(total11columns):
ts_code21non-nullobject
trade_date21non-nullobject
open21non-nullfloat64
high21non-nullfloat64
low21non-nullfloat64
close21non-nullfloat64
pre_close21non-nullfloat64
change21non-nullfloat64
pct_chg21non-nullfloat64
vol21non-nullfloat64
amount21non-nullfloat64
dtypes:?float64(9),object(2)
memory?usage:1.9+?KB
.describe()?查看每列数据的基本统计值,包括计数值、均值、标准差、最小最大值、1/4、1/2、3/4分位数。
df.describe()
Out[7]:
openhighlowclosepre_closechangepct_chgvolamount
count21.00000021.00000021.00000021.00000021.00000021.00000021.0000002.100000e+01??2.100000e+01
mean11.63047611.77761911.52428611.63714311.6042860.0328570.2962525.734931e+05??6.704836e+05
std0.2153480.2289300.1848400.2075120.2067990.1932131.6710992.333355e+05??2.792896e+05
min11.35000011.52000011.28000011.32000011.280000-0.300000-2.4979002.627369e+05??3.017520e+05
25%????11.47000011.56000011.41000011.48000011.470000-0.060000-0.5199004.102754e+05??4.739735e+05
50%????11.56000011.75000011.48000011.54000011.5400000.0000000.0000005.027103e+05??5.757994e+05
75%????11.76000011.99000011.65000011.72000011.7100000.1000000.8396007.050917e+05??8.161270e+05
max12.02000012.20000011.88000012.01000012.0100000.4900004.2683001.234747e+06??1.466715e+06
.value_counts()?查看Series对象的唯一值和计数值
df[‘close’].value_counts(dropna=False)
Out[8]:
11.482
11.472
11.712
11.542
11.912
11.442
11.721
11.951
11.701
11.321
11.491
12.011
11.621
11.501
11.971
Name:close,?dtype:int64
假如上面这些操作还不够直观的话,就作图看看,需要先导入Python可视化库matplotlib, 为了规范代码书写,统一写在了最前面。
①?直方图
df[‘close’].plot(kind=’hist’,?rot=0)
plt.show()

②?箱型图
df.boxplot(column=’close’,by=’ts_code’,?rot=0)
plt.show()

③?散点图
df.plot(kind=’scatter’,?x=’close’,?y=’pre_close’,?rot=0)
plt.show()

二、清洗数据
理解数据集之后,我们即可以开始对数据集进行清洗了,前面提到通常要解决的问题有包含无效信息,列名不规范、格式不一致,存在重复值,缺失值,异常值等,下面我们一个一个来看。
01
去除不需要的行、列
在分析一个数据集的时候,很多信息其实是用不到的,因而,需要去除不必要的行或者列。这里以csv文件为例,在导入的时候即可以通过设置pd.read_csv()里面的参数来实现这个目的。
先来感受一下官方文档中给出的详细解释,里面的参数是相当的多,本文只详情比较常用的几个,感兴趣的话,可以好好研究一下文档,这些参数还是非常好用的,能省去很多导入后整理的工作。

【header】默认header=0,即将文件中的0行作为列名和数据的开头,但有时候0行的数据是无关的,我们想跳过0行,让1行作为数据的开头,可以通过将header设置为1来实现。
【usecols】根据列的位置或者名字,如[0,1,2]或者[‘a’, ‘b’, ‘c’],选出特定的列。
【nrows】要导入的数据行数,在数据量很大、但只想导入其中一部分时使用。
获取数据:
从NYC OpenData网站下载csv格式原始数据

数据样本如下:

导入数据,只选取前100行和特定几列。
subset_columns=?[‘Job?#’,’Doc?#’,’Borough’,’Initial?Cost’,’Total?Est.?Fee’]
df?=?pd.read_csv(‘文件路径’,?nrows=100,?usecols=subset_columns)
df.head()
Out[15]:
Job#??Doc?#???Borough?Initial?Cost?Total?Est.?Fee
04202917941QUEENS$2000.00$100.00
14202918011QUEENS$15000.00$151.50
23406441281BROOKLYN$44726.00$234.00
34216854391QUEENS$0.00$243.00
44216779742QUEENS$105000.00$1275.60
再看一下将header设置为1的效果,但这里其实不需要这么做,由于0行数据是有用的。
df=?pd.read_csv(‘文件路径’,?nrows=100,?header=1)
df.head()
Out[15]:
04202917941QUEENS$2000.00$100.00
14202918011QUEENS$15000.00$151.50
23406441281BROOKLYN$44726.00$234.00
34216854391QUEENS$0.00$243.00
44216779742QUEENS$105000.00$1275.60
假如在数据导入之后,还想删除某些行和列,可以用?.drop()?方法。
先创立一个列表list,把不需要的列名放进去,再调用.drop()?方法,参数axis为1时代表列,为0时代表行,参数inplace=True表示不创立新的对象,直接对原始对象进行修改。这里我们删除前两列。
to_drop=?[‘Job?#’,’Doc?#’]
df.drop(to_drop,?axis=1,?inplace=True)
df.head()
Out[22]:
Borough?Initial?Cost?Total?Est.?Fee
0QUEENS$2000.00$100.00
1QUEENS$15000.00$151.50
2BROOKLYN$44726.00$234.00
3QUEENS$0.00$243.00
4QUEENS$105000.00$1275.60
02
重新命名列
当原始数据的列名不好了解,或者者不够简洁时,可以用.rename()方法进行修改。这里我们把英文的列名改成中文,先创立一个字典,把要修改的列名定义好,而后调用rename()方法。
new_names?=?{‘Borough’:’区’,’Initial?Cost’:’初始成本’,’Total?Est.?Fee’:’总附加费用’}
df.rename(columns=new_names,?inplace=True)
df.head()
Out[23]:
区????????初始成本?????总附加费用
0????QUEENS$2000.00$100.00
1????QUEENS$15000.00$151.50
2??BROOKLYN$44726.00$234.00
3????QUEENS$0.00$243.00
4????QUEENS$105000.00$1275.60
03
重新设置索引
数据默认的索引是从0开始的有序整数,但假如想把某一列设置为新的索引,可以用.set_index()方法实现,在示例中我们把”区”这列设置为新索引。
df.set_index(‘区’,?inplace=True)
df.head()
Out[24]:
初始成本?????总附加费用
区
QUEENS$2000.00$100.00
QUEENS$15000.00$151.50
BROOKLYN$44726.00$234.00
QUEENS$0.00$243.00
QUEENS$105000.00$1275.60
04
用字符串操作规范列
字符串str操作是非常实用的,由于列中总是会包含不必要的字符,常用的方法如下:
lower()
upper()
capitalize()
replace()
strip()
split()
get()
contains()
find()
str.lower()?是把大写转换成小写,同理,str.upper()是把小写转换成大写,将示例中用大写字母表示的索引转换成小写,效果如下:
df.index?=?df.index.str.lower()
df.head()
Out[25]:
初始成本?????总附加费用
区
queens$2000.00$100.00
queens$15000.00$151.50
brooklyn$44726.00$234.00
queens$0.00$243.00
queens$105000.00$1275.60
str.capitalize()?设置首字母大写
df.index?=?df.index.str.capitalize()
df.head()
Out[26]:
初始成本?????总附加费用
区
Queens$2000.00$100.00
Queens$15000.00$151.50
Brooklyn$44726.00$234.00
Queens$0.00$243.00
Queens$105000.00$1275.60
str.replace(‘$’, ”)?替换特定字符。这里把列中的美元符号$去掉,替换成空字符。
df[‘初始成本’]?=?df[‘初始成本’].str.replace(‘$’,”)
df[‘总附加费用’]?=?df[‘总附加费用’].str.replace(‘$’,”)
df.head()
Out[27]:
初始成本????总附加费用
区
Queens2000.00100.00
Queens15000.00151.50
Brooklyn44726.00234.00
Queens0.00243.00
Queens105000.001275.60
str.strip()?去除字符串中的头尾空格、以及\n \t
df[‘初始成本’]?=?’???’?+?df[‘初始成本’]
df[‘初始成本’][0]
Out[28]:?’???2000.00′
df[‘初始成本’]?=?df[‘初始成本’].str.strip()
df[‘初始成本’][0]
Out[29]:?’2000.00′
str.split(‘x’)?使用字符串中的‘x’字符作为分隔符,将字符串分隔成列表。这里将列中的值以‘.’进行分割,效果如下:
df[‘总附加费用’]?=?df[‘总附加费用’].str.split(‘.’)
df.head()
Out[30]:
初始成本???????总附加费用
区
Queens2000.00[100,00]
Queens15000.00[151,50]
Brooklyn44726.00[234,00]
Queens0.00[243,00]
Queens105000.00[1275,60]
str.get()?选取列表中某个位置的值。接着上面分割后的结果,我们用str.get(0)取出列表中前一个位置的数值,生成新的一列“总附加费用_整数”,即取出金额中的整数部分。
df[‘总附加费用_整数’]?=?df[‘总附加费用’].str.get(0)
df.head()
Out[31]:
初始成本???????总附加费用?总附加费用_整数
区
Queens2000.00[100,00]100
Queens15000.00[151,50]151
Brooklyn44726.00[234,00]234
Queens0.00[243,00]243
Queens105000.00[1275,60]1275
str.contains()?判断能否存在某个字符,返回的是布尔值。这里判断一下”总附加费用_整数”列中能否包含字符’0’。
df[‘总附加费用_整数’].str.contains(‘0’)
Out[33]:
区
QueensTrue
QueensFalse
BrooklynFalse
QueensFalse
QueensFalse
str.find()检测字符串中能否包含子字符串str,假如是,则返回该子字符串开始位置的索引值。示例中的’0’字符最开始出现的位置是1。
df[‘总附加费用_整数’][0]
Out[13]:?’100′
df[‘总附加费用_整数’][0].find(‘0’)
Out[14]:?1
学完基本的字符串操作方法,我们来看一下如何结合NumPy来提高字符串操作的效率。
获取数据,这里我们用一个新的数据集,下载链接如下,里面包含两个csv文件和一个txt文件:
realpython/python-data-cleaning
① BL-Flickr-Images-Book.csv
② olympics.csv
③?university_towns.txt
导入csv文件①,先观察一下”Place of Publication”这一列。
df?=?pd.read_csv(‘文件路径’)
df[‘Place?of?Publication’].head(10)
Out[38]:
0London
1London;?Virtue?&?Yorston
2London
3London
4London
5London
6London
7pp.40.?G.?Bryan?&?Co:?Oxford,1898
8London]
9London
Name:?PlaceofPublication,?dtype:object
我们发现,这一列中的格式并不统一,比方1行中的London; Virtue & Yorston,London后面的部分我们不需要,还有7行的pp. 40. G. Bryan & Co: Oxford, 1898,有效信息只是Oxford。
再用.tail(10)方法观察这一列的最后十行:
df[‘Place?of?Publication’].tail(10)
Out[39]:
8277New?York
8278London
8279New?York
8280London
8281Newcastle-upon-Tyne
8282London
8283Derby
8284London
8285Newcastle?upon?Tyne
8286London
Name:?PlaceofPublication,?dtype:object
我们发现,8281行的Newcastle-upon-Tyne中间有连字符,但8285行却没有,这些都是要处理的格式不规范的问题。
为了清洗这一列,我们可以将Pandas中的.str()方法与NumPy的np.where函数相结合,np.where函数是Excel的IF()宏的矢量化形式,它的语法如下:
>>>?np.where(condition,then,else)
假如condition条件为真,则执行then,否则执行else。这里的condition条件可以是一个类数组的对象,也可以是一个布尔表达式,我们也可以利用np.where函数嵌套多个条件进行矢量化计算和判断。
>>>?np.where(condition1,?x1,
np.where(condition2,?x2,
np.where(condition3,?x3,?…)))
下面的这个实例,就是同时嵌套两个条件处理上面提到的那两个字符串问题。思路是,假如字符串里面包含’London’,就用’London’代替,这样可以去除其余冗余信息,否则,假如字符串里面包含’Oxford’,则用’Oxford’代替,同时假如字符串里面包含符号’-‘,则用空格代替。
pub?=?df[‘Place?of?Publication’]
london?=?pub.str.contains(‘London’)
oxford?=?pub.str.contains(‘Oxford’)
df[‘Place?of?Publication’]?=?np.where(london,’London’,
np.where(oxford,’Oxford’,
pub.str.replace(‘-‘,’?’)))
打印出前十行和后十行,结果如下,可以和整理前的数据进行比照。
df[‘Place?of?Publication’].head(10)
Out[42]:
0London
1London
2London
3London
4London
5London
6London
7Oxford
8London
9London
Name:?PlaceofPublication,?dtype:object
df[‘Place?of?Publication’].tail(10)
Out[43]:
8277New?York
8278London
8279New?York
8280London
8281Newcastle?upon?Tyne
8282London
8283Derby
8284London
8285Newcastle?upon?Tyne
8286London
Name:?PlaceofPublication,?dtype:object
05
用函数规范列
在某些情况下,数据不规范的情况并不局限于某一列,而是更广泛地分布在整个表格中。因而,自己设置函数并应用于整个表格中的每个元素会更加高效。用applymap()方法可以实现这个功能,它相似于内置的map()函数,只不过它是将函数应用于整个表格中的所有元素。
我们打开文件txt文件③,先观察一下数据:
$?head?Datasets/univerisity_towns.txt
Alabama[edit]
Auburn?(Auburn?University)[1]
Florence?(University?of?North?Alabama)
Jacksonville?(Jacksonville?State?University)[2]
Livingston?(University?of?West?Alabama)[2]
Montevallo?(University?of?Montevallo)[2]
Troy?(Troy?University)[2]
Tuscaloosa?(University?of?Alabama,?Stillman?College,?Shelton?State)[3][4]
Tuskegee?(Tuskegee?University)[5]
Alaska[edit]
观察发现,数据格式有如下特点:
州A[edit]
城市A(大学)
城市B(大学)
州B[edit]
城市A(大学)
城市B(大学)
……
我们可以利用这一数据格式,创立一个(州、市)元组列表,并将该列表转化成一个DataFrame。先创立一个列表,列表中包含州和城市(大学)信息。
university_towns?=?[]
withopen(‘D:/code/tushare?interpret?and?tech?team/python-data-cleaning-master/Datasets/university_towns.txt’)asfile:
forlineinfile:
if'[edit]’inline:#?该行有[edit]
state?=?line#?将改行信息赋值给“州”,记住这个“州”,直到找到下一个为止
else:
university_towns.append((state,?line))#?否则,改行为城市信息,并且它们都属于上面的“州”
university_towns[:5]
Out[44]:
[(‘Alabama[edit]\n’,’Auburn?(Auburn?University)[1]\n’),
(‘Alabama[edit]\n’,’Florence?(University?of?North?Alabama)\n’),
(‘Alabama[edit]\n’,’Jacksonville?(Jacksonville?State?University)[2]\n’),
(‘Alabama[edit]\n’,’Livingston?(University?of?West?Alabama)[2]\n’),
(‘Alabama[edit]\n’,’Montevallo?(University?of?Montevallo)[2]\n’)]
用pd.DataFrame()方法将这个列表转换成一个DataFrame,并将列设置为”State”和”RegionName”。Pandas将接受列表中的每个元素,并将元组左边的值传入”State”列,右边的值传入”RegionName”列。
towns_df?=?pd.DataFrame(university_towns,?columns=[‘State’,’RegionName’])
towns_df.head()
Out[45]:
State?????????????????????????????????????????RegionName
0Alabama[edit]\n????????????????????Auburn?(Auburn?University)[1]\n
1Alabama[edit]\n???????????Florence?(UniversityofNorth?Alabama)\n
2Alabama[edit]\n??Jacksonville?(Jacksonville?State?University)[2]\n
3Alabama[edit]\n???????Livingston?(UniversityofWest?Alabama)[2]\n
4Alabama[edit]\n?????????Montevallo?(UniversityofMontevallo)[2]\n
接下来就要对列中的字符串进行整理,”State”列中的有效信息是州名,”RegionName”列中的有效信息是城市名,其余的字符都可以删掉。当然,除了用之前提到的利用循环和.str()方法相结合的方式进行操作,我们还可以选择用applymap()方法,它会将传入的函数作用于整个DataFrame所有行列中的每个元素。
先定义函数get_citystate(item),功能是只提取元素中的有效信息。
defget_citystate(item):
if’?(‘initem:
returnitem[:item.find(‘?(‘)]
elif'[‘initem:
returnitem[:item.find(‘[‘)]
else:
returnitem
而后,我们将这个函数传入applymap(),并应用于towns_df,结果如下:
towns_df?=?towns_df.applymap(get_citystate)
towns_df.head()
Out[48]:?
State????RegionName
0??Alabama????????Auburn
1??Alabama??????Florence
2??Alabama??Jacksonville
3??Alabama????Livingston
4??Alabama????Montevallo
现在towns_df表格看起来是不是干净多了!
06
删除重复数据
重复数据会消耗不必要的内存,在解决数据时执行不必要的计算,还会使分析结果出现偏差。因而,我们有必要学习如何删除重复数据。
先看一个来自DataCamp的数据集,调用info()方法打印出每列数据的具体信息和内存信息,共有24092行数据,内存占用量是753.0+?KB。
tracks?=?billboard[[‘year’,’artist’,’track’,’time’]]
print(tracks.info())
RangeIndex:24092entries,0to24091
Data?columns?(total4columns):
year24092non-nullint64
artist24092non-nullobject
track24092non-nullobject
time24092non-nullobject
dtypes:?int64(1),object(3)
memory?usage:753.0+?KB
None
下面调用.drop_duplicates()函数删除重复数据。
In?[11]:?tracks_no_duplicates?=?tracks.drop_duplicates()
…?print(tracks_no_duplicates.info())
…
Int64Index:317entries,0to316
Data?columns?(total4columns):
year317non-nullint64
artist317non-nullobject
track317non-nullobject
time317non-nullobject
dtypes:?int64(1),object(3)
memory?usage:12.4+?KB
None
删完之后我们发现,数据量减少到了317个,内存占用缩减至12.4+?KB。
07
填充缺失值
数据集中经常会存在缺失值,学会正确解决它们很重要,由于在计算的时候,有些无法解决缺失值,有些则在默认情况下跳过缺失值。而且,理解缺失的数据,并思考用什么值来填充它们,对做出无偏的数据分析至关重要。
同样是来自DataCamp的一个存在缺失值的数据集:
In[3]:airquality.head(10)
Out[3]:
OzoneSolar.RWindTempMonthDay
0???41.0190.07.467??????5????1
1???36.0118.08.072??????5????2
2???12.0149.012.674??????5????3
3???18.0313.011.562??????5????4
4NaNNaN14.356??????5????5
5???28.0NaN14.966??????5????6
6???23.0299.08.665??????5????7
7???19.099.013.859??????5????8
8????8.019.020.161??????5????9
9NaN194.08.669??????5???10
以”Ozone”列为例,我们可以调用fillna()函数,用该列的均值.mean()填充NaN值。
oz_mean?=?airquality.Ozone.mean()
airquality[‘Ozone’]?=?airquality[‘Ozone’].fillna(oz_mean)
print(airquality.head(10))
Ozone??Solar.R??Wind??Temp??Month??Day
041.000000190.07.46751
136.000000118.08.07252
212.000000149.012.67453
318.000000313.011.56254
443.195402NaN14.35655
528.000000NaN14.96656
623.000000299.08.66557
719.00000099.013.85958
88.00000019.020.16159
943.195402194.08.669510
假如你在学习Python的过程当中有遇见任何疑问,可以加入我的python交流学习qq群:250933691,多多交流问题,互帮互助,群里有不错的学习教程和开发工具。学习python有任何疑问(学习方法,学习效率,如何就业),可以随时来咨询我
三、总结
理解如何进行数据清洗非常重要,由于它是数据科学的重要组成部分。好在Python提供了非常好用的Pandas和NumPy库来帮助我们清除数据集,本文详情的方法都是在实际中经常会用到的,希望大家能牢记于心。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Python数据清洗80%的工作量,看这篇就够了