CallteFoot's blog

Victory belongs to the most persevering


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

android_download_manager使用

发表于 2017-06-19 | 分类于 Android

  从Android 2.3(Api level 9)开始Android用系统服务的方式提供了Download Manager来优化处理长时间的下载操作。Download Manager处理HTTP/HTTPS连接并监控连接中的状态变化以及重启来确保每一个下载任务顺利完成。
  在大多数涉及到下载的情况中使用Download Manager都是不错的选择,特别是当用户切换不同的应用以后下载需要在后台继续进行,以及当下载任务顺利完成非常重要的情况(DownloadManager对于断点续传功能支持很好)。
  要想使用Download Manager,使用getSystemService方法请求系统的DOWNLOAD_SERVICE服务,代码片段如下:

1
2
3
String serviceString = Context.DOWNLOAD_SERVICE;  
DownloadManager downloadManager;
downloadManager = (DownloadManager) getSystemService(serviceString);

下载文件

  需要一个请求下载操作,创建一个DownloadManager.Request对象,将要请求下载的文件的Uri传递给Download Manager的enqueue方法,代码片段如下所示:

1
2
3
Uri uri = Uri.parse("http://developer.android.com/shareables/icon_templates-v4.0.zip");  
DownloadManager.Request request = new Request(uri);
long reference = downloadManager.enqueue(request);

  在这里返回的reference变量是系统为当前的下载请求分配的一个唯一的ID,我们可以通过这个ID重新获得这个下载任务,进行一些自己想要进行的操作或者查询下载的状态以及取消下载等等。

  我们可以通过addRequestHeader方法为DownloadManager.Request对象request添加HTTP头,也可以通过setMimeType方法重写从服务器返回的mime type。

  我们还可以指定在什么连接状态下执行下载操作。setAllowedNetworkTypes方法可以用来限定在WiFi还是手机网络下进行下载,setAllowedOverRoaming方法可以用来阻止手机在漫游状态下下载。

  下面的代码片段用于指定一个较大的文件只能在WiFi下进行下载:

1
request.setAllowedNetworkTypes(Request.NETWORK_WIFI);

  Android API level 11 介绍了getRecommendedMaxBytesOverMobile类方法(静态方法),返回一个当前手机网络连接下的最大建议字节数,可以来判断下载是否应该限定在WiFi条件下。

  调用enqueue方法之后,只要数据连接可用并且Download Manager可用,下载就会开始。

  要在下载完成的时候获得一个系统通知(notification),注册一个广播接受者来接收ACTION_DOWNLOAD_COMPLETE广播,这个广播会包含一个
EXTRA_DOWNLOAD_ID信息在intent中包含了已经完成的这个下载的ID,代码片段如下所示:

1
2
3
4
5
6
7
8
9
10
11
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);       
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (myDownloadReference == reference) {
//do something
}
}
};
registerReceiver(receiver, filter);

  使用Download Manager的openDownloadedFile方法可以打开一个已经下载完成的文件,返回一个ParcelFileDescriptor对象。我们可以通过Download Manager来查询下载文件的保存地址,如果在下载时制定了路径和文件名,我们也可以直接操作文件。

  我们可以为ACTION_NOTIFICATION_CLICKED action注册一个广播接受者,当用户从通知栏点击了一个下载项目或者从Downloads app点击可一个下载的项目的时候,系统就会发出一个点击下载项的广播。
代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED);    
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String extraID = DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS;
long[] references = intent.getLongArrayExtra(extraID);
for (long reference : references)
if (reference == myDownloadReference) {
// Do something with downloading file.
}
}
};
registerReceiver(receiver, filter);

定制Download Manager Notifications的样式

  默认情况下,通知栏中会显示被Download Manager管理的每一个download每一个Notification会显示当前的下载进度和文件的名字。通过Download Manager可以为每一个download request定制Notification的样式,包括完全隐藏Notification。下面的代码片段显示了通过setTitle和setDescription方法来定制显示在文件下载Notification中显示的文字(下载的通知icon不可更改?)。

1
2
request.setTitle(“Earthquakes”);  
request.setDescription(“Earthquake XML”);

  request.setNotificationVisibility方法可以用来控制什么时候显示Notification,甚至隐藏该request的Notification。有以下几个参数:

  1. Request.VISIBILITY_VISIBLE:在下载进行的过程中,通知栏中会一直显示该下载的Notification,当下载完成时,该Notification会被移除,这是默认的参数值;
  2. Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED:在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,直到用户点击该Notification或者消除该Notification;
  3. Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION:只有在下载完成后该Notification才会被显示;
  4. Request.VISIBILITY_HIDDEN:不显示该下载请求的Notification。如果要使用这个参数,需要在应用的清单文件中加上DOWNLOAD_WITHOUT_NOTIFICATION权限。

指定下载保存地址

  默认情况下,所有通过Download Manager下载的文件都保存在一个共享下载缓存中,使用系统生成的文件名每一个Request对象都可以制定一个下载保存的地址,通常情况下,所有的下载文件都应该保存在外部存储中,所以我们需要在应用清单文件中加上WRITE_EXTERNAL_STORAGE权限:

1
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”/>

下面的代码片段是在外部存储中指定一个任意的保存位置的方法:

1
request.setDestinationUri(Uri.fromFile(f));   // f是一个File对象

  如果下载的这个文件是你的应用所专用的,你可能会希望把这个文件放在你的应用在外部存储中的一个专有文件夹中。注意这个文件夹不提供访问控制,所以其他的应用也可以访问这个文件夹。在这种情况下,如果你的应用卸载了,那么在这个文件夹也会被删除。
  下面的代码片段是指定存储文件的路径是应用在外部存储中的专用文件夹的方法:

1
2
request.setDestinationInExternalFilesDir(this,  
Environment.DIRECTORY_DOWNLOADS, “Bugdroid.png”);

  如果下载的文件希望被其他的应用共享,特别是那些你下载下来希望被Media Scanner扫描到的文件(比如音乐文件),那么你可以指定你的下载路径在外部存储的公共文件夹之下,下面的代码片段是将文件存放到外部存储中的公共音乐文件夹的方法:

1
2
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC,  
"Android_Rock.mp3");

  在默认的情况下,通过Download Manager下载的文件是不能被Media Scanner扫描到的,进而这些下载的文件(音乐、视频等)就不会在Gallery和Music Player这样的应用中看到。为了让下载的音乐文件可以被其他应用扫描到,我们需要调用Request对象的allowScaningByMediaScanner方法。如果我们希望下载的文件可以被系统的Downloads应用扫描到并管理,我们需要调用Request对象的setVisibleInDownloadsUi方法,传递参数true。

取消或删除下载

  Download Manager的remove方法可以用来取消一个准备进行的下载,中止一个正在进行的下载,或者删除一个已经完成的下载。remove方法接受若干个download 的ID作为参数,你可以设置一个或者几个你想要取消的下载的ID,如下代码段所示:

1
downloadManager.remove(REFERENCE_1, REFERENCE_2, REFERENCE_3);

   该方法返回成功取消的下载的个数,如果一个下载被取消了,所有相关联的文件,部分下载的文件和完全下载的文件都会被删除。

查询Download Manager

  你可以通过查询Download Manager来获得下载任务的状态,进度,以及各种细节,通过query方法返回一个包含了下载任务细节的Cursor。query方法传递一个DownloadManager.Query对象作为参数,通过DownloadManager.Query对象的setFilterById方法可以筛选我们希望查询的下载任务的ID。也可以使用setFilterByStatus方法筛选我们希望查询的某一种状态的下载任务,传递的参数是DownloadManager.STATUS常量,可以指定正在*进行、暂停、失败、完成四种状态。Download Manager包含了一系列COLUMN静态String常量,可以用来查询Cursor中的结果列索引。我们可以查询到下载任务的各种细节,包括*状态,文件大小,已经下载的字节数,标题,描述,URI,本地文件名和URI,媒体类型以及Media Provider download URI。

  下面的代码段是通过注册监听下载完成事件的广播接受者来查询下载完成文件的本地文件名和URI的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    @Override  
public void onReceive(Context context, Intent intent) {
long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (myDownloadReference == reference) {
Query myDownloadQuery = new Query();
myDownloadQuery.setFilterById(reference);
Cursor myDownload = downloadManager.query(myDownloadQuery);
if (myDownload.moveToFirst()) {
//文件名称索引
int fileNameIdx =
myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
//下载文件存放地址uri索引
int fileUriIdx =
myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
//下载状态
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
statusMsg = "STATUS_PAUSED";
case DownloadManager.STATUS_PENDING:
statusMsg = "STATUS_PENDING";
case DownloadManager.STATUS_RUNNING:
statusMsg = "STATUS_RUNNING";
break;
case DownloadManager.STATUS_SUCCESSFUL:
statusMsg = "STATUS_SUCCESSFUL";
break;
case DownloadManager.STATUS_FAILED:
statusMsg = "STATUS_FAILED";
break;

default:
statusMsg = "未知状态";
break;
}
String fileName = myDownload.getString(fileNameIdx);
String fileUri = myDownload.getString(fileUriIdx);
// TODO Do something with the file.
Log.d(TAG, fileName + " : " + fileUri);
}
myDownload.close();
}
}

  对于暂停和失败的下载,我们可以通过查询COLUMN_REASON列查询出原因的整数码。

  1. 对于STATUSPAUSED状态的下载,可以通过DownloadManager.PAUSED* 静态常量来翻译出原因的整数码,进而判断出下载是由于等待网络连接还是等待WiFi连接还是准备重新下载三种原因而暂停。
  2. 对于STATUSFAILED状态的下载,我们可以通过DownloadManager.ERROR*来判断失败的原因,可能是错误码(失败原因)包括没有存储设备,存储空间不足,重复的文件名,或者HTTP errors。
      下面的代码是如何查询出当前所有的暂停的下载任务,提取出暂停的原因以及文件名称,下载标题以及当前进度的实现方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // Obtain the Download Manager Service.  
    String serviceString = Context.DOWNLOAD_SERVICE;
    DownloadManager downloadManager;
    downloadManager = (DownloadManager)getSystemService(serviceString);
    // Create a query for paused downloads.
    Query pausedDownloadQuery = new Query();
    pausedDownloadQuery.setFilterByStatus(DownloadManager.STATUS_PAUSED);
    // Query the Download Manager for paused downloads.
    Cursor pausedDownloads = downloadManager.query(pausedDownloadQuery);
    // Find the column indexes for the data we require.
    int reasonIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_REASON);
    int titleIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TITLE);
    int fileSizeIdx =
    pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
    int bytesDLIdx =
    pausedDownloads.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
    // Iterate over the result Cursor.
    while (pausedDownloads.moveToNext()) {
    // Extract the data we require from the Cursor.
    String title = pausedDownloads.getString(titleIdx);
    int fileSize = pausedDownloads.getInt(fileSizeIdx);
    int bytesDL = pausedDownloads.getInt(bytesDLIdx);
    // Translate the pause reason to friendly text.
    int reason = pausedDownloads.getInt(reasonIdx);
    String reasonString = "Unknown";
    switch (reason) {
    case DownloadManager.PAUSED_QUEUED_FOR_WIFI :
    reasonString = "Waiting for WiFi"; break;
    case DownloadManager.PAUSED_WAITING_FOR_NETWORK :
    reasonString = "Waiting for connectivity"; break;
    case DownloadManager.PAUSED_WAITING_TO_RETRY :
    reasonString = "Waiting to retry"; break;
    default : break;
    }
    // Construct a status summary
    StringBuilder sb = new StringBuilder();
    sb.append(title).append("\n");
    sb.append(reasonString).append("\n");
    sb.append("Downloaded ").append(bytesDL).append(" / " ).append(fileSize);
    // Display the status
    Log.d("DOWNLOAD", sb.toString());
    }
    // Close the result Cursor.
    pausedDownloads.close();

DownloadManager原理

  通过介绍我们已经可以灵活的使用DownloadManaged为我们服务了,为了更好的使用这个工具,得先了解它的工作原理、工作流程。下面就是整个工作流程的时序图:
Download Manager时序图
  从上面的时序图我们可以大致了解整个流程。从添加请求,到最后开启下载线程进行文件的下载。为了更好的理解这个下载工具的思想,下面将从源码上对一些重要的函数进行分析。

  一开始,调用DownloadManager的enqueue()法进行下载请求的添加,然后就会调用DownloadProvider的insert()方法进行数据库的数据的插入,insert()不单单是把数据插入到数据库,还会启动DownloadService这个服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override  
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();

// note we disallow inserting into ALL_DOWNLOADS
if (pckg != null && (clazz != null || isPublicApi)) {
int uid = Binder.getCallingUid();
try {
if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
filteredValues.put(Downloads.COLUMN_NOTIFICATION_PACKAGE,
pckg);
if (clazz != null) {
filteredValues.put(Downloads.COLUMN_NOTIFICATION_CLASS,
clazz);
}
}
} catch (PackageManager.NameNotFoundException ex) {
/* ignored for now */
}
}
copyString(Downloads.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
copyString(Downloads.COLUMN_COOKIE_DATA, values, filteredValues);
copyString(Downloads.COLUMN_USER_AGENT, values, filteredValues);
copyString(Downloads.COLUMN_REFERER, values, filteredValues);
if (getContext().checkCallingPermission(
Downloads.PERMISSION_ACCESS_ADVANCED) == PackageManager.PERMISSION_GRANTED) {
copyInteger(Downloads.COLUMN_OTHER_UID, values, filteredValues);
}
filteredValues.put(Constants.UID, Binder.getCallingUid());
if (Binder.getCallingUid() == 0) {
copyInteger(Constants.UID, values, filteredValues);
}
copyStringWithDefault(Downloads.COLUMN_TITLE, values, filteredValues,
"");
copyStringWithDefault(Downloads.COLUMN_DESCRIPTION, values,
filteredValues, "");
filteredValues.put(Downloads.COLUMN_TOTAL_BYTES, -1);
filteredValues.put(Downloads.COLUMN_CURRENT_BYTES, 0);


Context context = getContext();
context.startService(new Intent(context, DownloadService.class));

long rowID = db.insert(DB_TABLE, null, filteredValues);
if (rowID == -1) {
Log.d(Constants.TAG, "couldn't insert into downloads database");
return null;
}

insertRequestHeaders(db, rowID, values);
context.startService(new Intent(context, DownloadService.class));
notifyContentChanged(uri, match);
return ContentUris.withAppendedId(Downloads.CONTENT_URI, rowID);
}

  可以看到上面的代码很长,但是我们只需要关注一些核心的代码。

1
long rowID = db.insert(DB_TABLE, null, filteredValues);

  这行代码的作用主要是把下载的任务信息保存到数据库中,包括下载的URL、下载的控制状态、下载状态、总的文件大小、已下载的文件大小等默认的数据更新到数据库中。

1
context.startService(new Intent(context, DownloadService.class));

  这行代码的作用就是启动DownloadService服务。

  当我们启动DownloadService之后,DownloadService服务的onCreate()数就会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void onCreate() {  
super.onCreate();
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onCreate");
}

if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(this);
}

mObserver = new DownloadManagerContentObserver();
getContentResolver().registerContentObserver(
Downloads.ALL_DOWNLOADS_CONTENT_URI, true, mObserver);

mNotifier = new DownloadNotification(this, mSystemFacade);
mSystemFacade.cancelAllNotifications();

updateFromProvider();
}

  可以看到,在onCreate()数中,会注册一个数据库变化监听器DownloadManagerContentObserver,就是说Downloads.ALL_DOWNLOADS_CONTENT_URI这个数据库的数据发生变化的时候,该监听器的监听函数onChange()会被调用。

1
2
3
4
5
6
7
public void onChange(final boolean selfChange) {  
if (Constants.LOGVV) {
Log.v(Constants.TAG,
"Service ContentObserver received notification");
}
updateFromProvider();
}

  可以看到onChange()函数会调用updateFromProvider()这个函数,从上面可以看到,onCreate()函数也会调到这个函数。

1
2
3
4
5
6
7
8
9
private void updateFromProvider() {  
synchronized (this) {
mPendingUpdate = true;
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
mSystemFacade.startThread(mUpdateThread);
}
}
}

  可以看到,updateFormProvider()函数其实就是会启动UpdateThread()这个线程。

   下面就进入到UpdateThread这个线程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public void run() {  
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

trimDatabase();
removeSpuriousFiles();

boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
long wakeUp = Long.MAX_VALUE;
for (;;) {
synchronized (DownloadService.this) {
if (mUpdateThread != this) {
throw new IllegalStateException(
"multiple UpdateThreads in DownloadService");
}
if (!mPendingUpdate) {
mUpdateThread = null;
if (!keepService) {
stopSelf();
}
if (wakeUp != Long.MAX_VALUE) {
scheduleAlarm(wakeUp);
}
return;
}
mPendingUpdate = false;
}

long now = mSystemFacade.currentTimeMillis();
keepService = false;
wakeUp = Long.MAX_VALUE;
Set<Long> idsNoLongerInDatabase = new HashSet<Long>(
mDownloads.keySet());

Cursor cursor = getContentResolver().query(
Downloads.ALL_DOWNLOADS_CONTENT_URI, null, null, null,
null);
if (cursor == null) {
continue;
}
try {
DownloadInfo.Reader reader = new DownloadInfo.Reader(
getContentResolver(), cursor);
int idColumn = cursor.getColumnIndexOrThrow(Downloads._ID);

for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor
.moveToNext()) {
long id = cursor.getLong(idColumn);
idsNoLongerInDatabase.remove(id);
DownloadInfo info = mDownloads.get(id);
if (info != null) {
updateDownload(reader, info, now);
} else {
info = insertDownload(reader, now);
}
if (info.hasCompletionNotification()) {
keepService = true;
}
long next = info.nextAction(now);
if (next == 0) {
keepService = true;
} else if (next > 0 && next < wakeUp) {
wakeUp = next;
}
}
} finally {
cursor.close();
}

for (Long id : idsNoLongerInDatabase) {
deleteDownload(id);
}

// is there a need to start the DownloadService? yes, if there
// are rows to be deleted.

for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted) {
keepService = true;
break;
}
}

mNotifier.updateNotification(mDownloads.values());

// look for all rows with deleted flag set and delete the rows
// from the database
// permanently
for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted) {
Helpers.deleteFile(getContentResolver(), info.mId,
info.mFileName, info.mMimeType);
}
}
}
}

   可以看到UpdateThread这个线程也是很长,我们大概分析一下它的作用。

   可以看到这里有一个for(;;)的死循环,它的作用是保证数据库中的下载任务都会被加载出来,然后启动所有的下载任务,同时会更新下载任务,包括更新下载任务的状态,删除一些下载任务。

   它会从数据库中取出所有的下载任务,然后根据id从mDownloads集合中找到对应的下载任务,如果没找到就会新建一个下载任务DownloadInfo。然后就会调用updateDown()和insertDown(),启动下载任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info,  
long now) {
int oldVisibility = info.mVisibility;
int oldStatus = info.mStatus;

reader.updateFromDatabase(info);

boolean lostVisibility = oldVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& info.mVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& Downloads.isStatusCompleted(info.mStatus);
boolean justCompleted = !Downloads.isStatusCompleted(oldStatus)
&& Downloads.isStatusCompleted(info.mStatus);
if (lostVisibility || justCompleted) {
mSystemFacade.cancelNotification(info.mId);
}

info.startIfReady(now);
}

   可以看到该函数调用startIfReady()进行下载任务的启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void startIfReady(long now) {  
if (!isReadyToStart(now)) {
return;
}

if (Constants.LOGV) {
Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
}
if (mHasActiveThread) {
throw new IllegalStateException("Multiple threads on same download");
}
if (mStatus != Downloads.STATUS_RUNNING) {
mStatus = Downloads.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Downloads.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
return;
}
DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
mHasActiveThread = true;
mSystemFacade.startThread(downloader);
}

   该函数是一个挺重要的函数,它会根据不同的情况判断下载任务是否需要启动。判断函数是isReadyToStart。这个函数十分关键,在我们要实现暂停下载,继续下载这个功能,都是在这里起作用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private boolean isReadyToStart(long now) {  
if (mHasActiveThread) {
// already running
return false;
}
if (mControl == Downloads.CONTROL_PAUSED) {
// the download is paused, so it's not going to start
return false;
}
switch (mStatus) {
case 0: // status hasn't been initialized yet, this is a new download
case Downloads.STATUS_PENDING: // download is explicit marked as ready to start
case Downloads.STATUS_RUNNING: // download interrupted (process killed etc) while
// running, without a chance to update the database
return true;

case Downloads.STATUS_WAITING_FOR_NETWORK:
case Downloads.STATUS_QUEUED_FOR_WIFI:
return checkCanUseNetwork() == NETWORK_OK;

case Downloads.STATUS_WAITING_TO_RETRY:
// download was waiting for a delayed restart
return restartTime(now) <= now;
}
return false;
}

   可以看到,当下载任务正在进行,或者下载任务状态为暂停状态,或者网络状态是否正常,这时会返回false,就是没有准备好,就不会启动下载任务。当返回true的时候,就会把当前下载任务的状态刷新为Downloads.STATUS_RUNNING,同时会启动DownloadThread下载线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public void run() {  
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

State state = new State(mInfo);
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;

try {
PowerManager pm = (PowerManager) mContext
.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
Constants.TAG);
wakeLock.acquire();

if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
}


boolean finished = false;
while (!finished) {
Log.i(Constants.TAG, "Initiating request for download "
+ mInfo.mId);


Request.Builder requestBuilder = new Request.Builder();
InnerState innerState = new InnerState();
setupDestinationFile(state, innerState);
addRequestHeaders(innerState, requestBuilder);
requestBuilder.url(state.mRequestUri);

Request request = requestBuilder.build();
Call call = mOkHttpClient.newCall(request);
try {
executeDownload(innerState, state, call);
finished = true;
} catch (RetryDownload exc) {
// fall through
} finally {
call.cancel();
}
}

if (Constants.LOGV) {
Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
}
finalizeDestinationFile(state);
finalStatus = Downloads.STATUS_SUCCESS;
} catch (StopRequest error) {
// remove the cause before printing, in case it contains PII
Log.w(Constants.TAG, "Aborting request for download " + mInfo.mId
+ ": " + error.getMessage());
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { // sometimes the socket code throws unchecked
// exceptions
Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
if (mOkHttpClient != null) {
mOkHttpClient.cancel(null);
}
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(finalStatus, state.mCountRetry,
state.mRetryAfter, state.mGotData, state.mFilename,
state.mNewUri, state.mMimeType);
mInfo.mHasActiveThread = false;
}
}

其中

1
setupDestinationFile(state, innerState);

这个方法是实现断点续传的关键点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void setupDestinationFile(State state, InnerState innerState)  
throws StopRequest {
if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already
// run a thread for this
// download
if (!Helpers.isFilenameValid(state.mFilename)) {
// this should never happen
throw new StopRequest(Downloads.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from
// scratch
f.delete();
state.mFilename = null;
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
f.delete();
throw new StopRequest(Downloads.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
try {
state.mStream = new FileOutputStream(state.mFilename,
true);
} catch (FileNotFoundException exc) {
throw new StopRequest(Downloads.STATUS_FILE_ERROR,
"while opening destination for resuming: "
+ exc.toString(), exc);
}
innerState.mBytesSoFar = (int) fileLength;
if (mInfo.mTotalBytes != -1) {
innerState.mHeaderContentLength = Long
.toString(mInfo.mTotalBytes);
}
innerState.mHeaderETag = mInfo.mETag;
innerState.mContinuingDownload = true;
}
}
}

if (state.mStream != null
&& mInfo.mDestination == Downloads.DESTINATION_EXTERNAL) {
closeDestination(state);
}
}

   方法的流程大概是:先根据文件名建立一个文件对象,判断文件对象是否存在,如果存在再判断文件的大小,当文件大小为0的时候,把文件删除。同时,会把当前的文件的输出流保存到state.mStream,把当前文件的长度、要下载文件的总长度、文件继续下载状态保存到innerState中。
   再分析addRequestHeader()方法,该方法也是实现断点续传的关键。

1
2
3
4
5
6
7
8
9
10
11
12
private void addRequestHeaders(InnerState innerState, Request.Builder requestBuilder) {  
for (Pair<String, String> header : mInfo.getHeaders()) {
requestBuilder.addHeader(header.first, header.second);
}

if (innerState.mContinuingDownload) {
if (innerState.mHeaderETag != null) {
requestBuilder.addHeader("If-Match", mInfo.mETag);
}
requestBuilder.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
}
}

   该方法会把请求头添加到请求中,最重要的是:如果是断点续传的话,会把当前的文件大小也放到请求头中,这样服务器就会知道当前的文件已经下载了多少。

   下面来分析最重要的executeDownload方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void executeDownload(InnerState innerState, State state, Call call) throws StopRequest, RetryDownload, IOException {  
byte data[] = new byte[Constants.BUFFER_SIZE];


// check just before sending the request to avoid using an invalid
// connection at all
checkConnectivity(state);

Response response = call.execute();
handleExceptionalStatus(state, innerState, response);

if (Constants.LOGV) {
Log.v(Constants.TAG, "received response for " + mInfo.mUri);
}

processResponseHeaders(state, innerState, response);
InputStream entityStream = openResponseEntity(state, response);
transferData(state, innerState, data, entityStream);
}

   可以看到这个下载工具用到okhttp这个开源库进行网络的请求。使用okhttp得到了Response对象。

  • 先分析processResponseHeaders()这个方法,这个方法中会获取Http请求的header,同时,根据这次下载是否为断点下载,如果是则返回,如果不是,则会把要下载的文件的输入流对象保存到state.mStream变量中。
  • 再分析openResponseEntity()这个方法。
1
2
3
4
5
6
7
8
9
10
private InputStream openResponseEntity(State state, Response response)  
throws StopRequest {
try {
return response.body().byteStream();
} catch (IOException ex) {
logNetworkState();
throw new StopRequest(getFinalStatusForHttpError(state),
"while getting entity: " + ex.toString(), ex);
}
}

   该方法是获取Response对象的输出流变量,最后是transferData()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void transferData(State state, InnerState innerState, byte[] data,  
InputStream entityStream) throws StopRequest {
for (; ; ) {
int bytesRead = readFromResponse(state, innerState, data,
entityStream);
if (bytesRead == -1) { // success, end of stream already reached
handleEndOfStream(state, innerState);
return;
}

state.mGotData = true;
writeDataToDestination(state, data, bytesRead);
innerState.mBytesSoFar += bytesRead;
reportProgress(state, innerState);

if (Constants.LOGVV) {
Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar
+ " for " + mInfo.mUri);
}

checkPausedOrCanceled(state);
}
}

   可以看到,这是一个for(;;)死循环,用于读取下载的文件流。

  • 先调用readFromResponse()函数,从文件输出流中读取数据,保存到data字节数组中。
  • 然后调用writeDataToDestination()数,把data字节数组中的数据写到本地的文件中。
  • 然后调用reportProgress()数,把已下载的文件的大小更新到数据库中。用于更新进度条的显示。

   可以看到checkPauseOrCanceled()数。这是实现暂停下载的关键函数。

1
2
3
4
5
6
7
8
9
10
11
12
private void checkPausedOrCanceled(State state) throws StopRequest {  
synchronized (mInfo) {
if (mInfo.mControl == Downloads.CONTROL_PAUSED) {
throw new StopRequest(Downloads.STATUS_PAUSED_BY_APP,
"download paused by owner");
}
}
if (mInfo.mStatus == Downloads.STATUS_CANCELED) {
throw new StopRequest(Downloads.STATUS_CANCELED,
"download canceled");
}
}

  可以分析,这里会根据下载任务的当前状态进行判断,如果当前的任务状态被更改为Downloads.CONTROL_PAUSED时,就会抛出StopRequest的异常,当前的文件下载就会被终止,这样就可以实现暂停下载了。

到此为止,DownloadManager下载的整个流程就分析完了。

拓展

  通过上面的分析,我们几乎理解了DownloadManager的整个工作流程。在我们下载文件的时候,我们几乎都是需要暂停下载和继续下载还有断点续传的功能。DownloadProvider代码是可以让我们能够实现这个功能了。

  实现断点续传的原理其实就是我们每次添加下载任务,都会把任务的信息保存到数据库中,包括下载的URL,已下载的文件大小,总的文件大小。下次我们再进行下载的时候,把已下载的大小传到服务器中,就可以从上一次已下载的文件的基础上继续下载,就可以实现断点下载了。

  暂停下载和继续下载的实现,其实只需要更新下载任务的状态就可以实现了。因为从上面的下载可以知道,在下载文件的过程中,都会检验当前的下载任务的状态,若是暂停状态,就会停止下载,跳出死循环。当我们再次改变状态为继续下载时,下载任务会被再次启动。

来源:
http://blog.csdn.net/carrey1989/article/details/8060155
http://blog.csdn.net/garment1991/article/details/54178557
http://www.trinea.cn/android/android-downloadmanager-pro/

rxjava2基础

发表于 2017-06-03 | 分类于 Java

RxJava2基础

  RxJava核心思想是观察者模式和响应式编程,因此其核心的东西主要有两个:Observable(被观察者) 和 Observer(观察者),Observable可以发出一系列的 事件(例如网络请求、复杂计算、数据库操作、文件读取等),事件执行结束后交给Observer的回调处理。

  1. RxJava2 的观察者模式

  观察者模式是对象的行为模式,也叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

  1. RxJava2 响应式编程结构

  什么是响应式编程?举个栗子,a = b + c; 这句代码将b+c的值赋给a,而之后如果b和c的值改变了不会影响到a,然而,对于响应式编程,之后b和c的值的改变也动态影响着a,意味着a会随着b和c的变化而变化。
响应式编程的组成为Observable/Operator/Subscriber,RxJava在响应式编程中的基本流程如下:

  • Observable发出一系列事件,他是事件的产生者;
  • Subscriber负责处理事件,他是事件的消费者;
  • Operator是对Observable发出的事件进行修改和变换;
  • 若事件从产生到消费不需要其他处理,则可以省略掉中间的Operator,从而流程变为Obsevable -> Subscriber;
  • Subscriber通常在主线程执行,所以原则上不要去处理太多的事务,而这些复杂的处理则交给Operator;

  这个流程,可以简单的理解为:Observable -> Operator1 -> …….-> OperatorN -> Subscriber。

  1. 背压

  背压简单理解即生产者的生产速度大于消费者的消费速度带来的问题,这个并非新概念,只是在RxJava2中新增了新的实现者Flowable及其子类:

初步体验

  使用前需要添加相应的依赖:

1
2
compile 'io.reactivex.rxjava2:rxjava:2.0.7'
compile 'org.reactivestreams:reactive-streams:1.0.0'

注意:不同的版本在使用上可能略微有差异。

RxJava2的使用主要有三个步骤:

  1. 创建一个Observable
1
2
3
4
5
6
7
8
9
//被观察者
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> e) throws Exception {
e.onNext(1);
e.onNext(2);
e.onComplete();
}
});
  1. 创建一个Observer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//观察者,观察者中onError和onComplete两个方法是互斥的,只有其中一个会执行。
Observer<Integer> observer = new Observer<Integer>() {
//该实例用于解除订阅关系
private Disposable disposable;

@Override
public void onSubscribe(Disposable d) {
System.out.println("subscribe");
disposable = d;
}

@Override
public void onNext(Integer value) {
System.out.println("next" + value);
if (value > 3) {
// >3 时为异常数据,解除订阅
disposable.dispose();
}
}

@Override
public void onError(Throwable e) {
System.out.println("error:" + e.getMessage());
}

@Override
public void onComplete() {
System.out.println("onComplete");
}
};
  1. 建立订阅关系
1
observable.subscribe(observer);

  通过以上可以看出,首先,创建Observable时,回调的是ObservableEmitter,字面意思即发射器,用于发射数据(onNext)和通知(onError/onComplete);其次,创建的Observer中多了一个回调方法onSubscrible,传递参数为Disposable,Disposable用于解除订阅关系。

包解析和类说明

操作符

  目前只解析标准包中的操作符。对于扩展包,待查阅相应资料再补上。

创建操作符:用于创建Observable

  1. create:使用OnSubscribe从头创建一个Observable。需要注意的是,使用该方法创建时,建议在ObservableOnSubscribe#subscribe方法中检查订阅状态,以便及时停止发射数据或者运算。

    1
    2
    3
    4
    5
    6
    Observable.create(new ObservableOnSubscribe<String >() {
    @Override
    public void subscribe(ObservableEmitter<String> observableEmitter) throws Exception {

    }
    });
  2. from*: 将一个Iterable, 一个Future, 或者一个数组,内部通过代理的方式转换成一个Observable。Future转换为OnSubscribe是通过OnSubscribeToObservableFuture进行的,Iterable转换通过OnSubscribeFromIterable进行。数组通过OnSubscribeFromArray转换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    List<String > list=new ArrayList<>();
    list.add("abc");
    Observable.fromArray(list).subscribe(new Observer<List<String>>() {
    @Override
    public void onSubscribe(Disposable disposable) {

    }

    @Override
    public void onNext(List<String> strings) {

    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onComplete() {

    }
    });
    Future<String> future = Executors.newSingleThreadExecutor().submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
    return "abc";
    }
    });
    Observable.fromFuture(future).subscribe(new Observer<String>() {
    @Override
    public void onSubscribe(Disposable disposable) {

    }

    @Override
    public void onNext(String s) {

    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onComplete() {

    }
    });
  3. just: 将一个或多个对象转换成发射这个或这些对象的一个Observable。如果是单个对象,内部创建的是ScalarSynchronousObservable对象。如果是多个对象,则是调用了from方法创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    List<String> strings = new ArrayList<>();
    Observable.just(strings).subscribe(new Observer<List<String>>() {
    @Override
    public void onSubscribe(Disposable disposable) {

    }

    @Override
    public void onNext(List<String> strings) {

    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onComplete() {

    }
    });
  4. empty: 创建一个什么都不做直接通知完成的Observable。

  5. error: 创建一个什么都不做直接通知错误的Observable。
  6. never: 创建一个什么都不做的Observable。

    1
    2
    3
    Observable.empty();  //直接调用onCompleted。
    Observable.error(new Throwable("adb"));//直接调用onError。这里可以自定义异常
    Observable.never();//啥都不做
  7. timer: 创建一个在给定的延时之后发射数据项为0的Observable,内部通过OnSubscribeTimerOnce工作。

1
2
3
4
5
6
Observable.timer(1000, TimeUnit.MILLISECONDS).subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {

}
});
  1. interval:创建一个按照给定时间间隔发射从0开始的整数序列的Observable,内部通过ObservableInterval工作。

    1
    2
    3
    4
    5
    6
    Observable.interval(2,TimeUnit.MILLISECONDS).subscribe(new Consumer<Long>() {
    @Override
    public void accept(Long aLong) throws Exception {

    }
    });
  2. range:创建一个发射指定范围的整数序列的Observable。

    1
    2
    3
    4
    5
    6
    Observable.range(3,9).subscribe(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) throws Exception {

    }
    });
  3. defer:只有当订阅者订阅才创建Observable,为每个订阅者创建一个新的Observable。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Observable.defer(new Callable<ObservableSource<String >>() {
    @Override
    public ObservableSource<String > call() throws Exception {
    return null;
    }
    }).subscribe(new Consumer<String>() {
    @Override
    public void accept(String s) throws Exception {

    }
    });

合并操作

  用户组合多个Observable。
注意 为了使结构更加清晰以及缩小代码量,之后例子都使用Lamdda表达式。

  1. concat:按顺序链接多个Observable,需要注意的是Observable。concat(a,b)等价与a.concatWith(b),内部调用的是concatArray。

    1
    2
    3
    4
    Observable<Integer> observable1=Observable.just(1,2,3,5);
    Observable<Integer> observable2=Observable.just(2,5,6,8);
    Observable.concat(observable1,observable2).subscribe(item -> System.out.println(item));
    observable1.concatWith(observable2);
  2. startWith:在数据序列的开头增加一个数据项,其内部也是调用concatArray。

    1
    2
    3
    Observable.just(1,2,3,4,5)
    .startWith(1)
    .subscribe(integer -> System.out.print(integer));
  3. merge:将多个Observable合并为一个,不同于concat,merge不是按照添加顺序链接,而是按照时间线来链接。其中mergeDelayError将异常延迟到其它没有错误的Observable发送完毕后才发射。而merge则是一遇到异常将停止发射数据,发送onError通知。
    Alt text

    1
    2
    3
    Observable<String > observable1=Observable.just("4","3","1");
    Observable<String > observable2=Observable.just("4","3","1");
    Observable.merge(observable1,observable2).subscribe(s -> System.out.println(s));
  4. zip:使用一个函数组合多个Observab发射的数据集合,然后发射这个结果。然后再发射这个结果。如果多个Observable发射的数据量不一样,则以最少的Observable为标准进行压合。内部通过OperatorZip进行压合。

    1
    2
    3
    4
    5
    6
    7
    8
    Observable<String > observable1=Observable.just("4","3","1");
    Observable<String > observable2=Observable.just("4","3","1");
    Observable.zip(observable1, observable2, new BiFunction<String, String, String >() {
    @Override
    public String apply(String s, String s2) throws Exception {
    return s+" and "+s2;
    }
    }).subscribe(item -> System.out.println(item));

Alt text

过滤操作

  1. filter:过滤数据,内部通过ObservableFilter所虑数据。

    1
    2
    3
    Observable.just(3, 4, 5, 6)
    .filter(integer -> integer > 3)
    .subscribe(integer -> System.out.println(integer));
  2. ofType:过滤制定的类型。

    1
    2
    3
    Observable.just(3, 4, 5, "aad")
    .ofType(Integer.class)
    .subscribe(integer -> System.out.println(integer));
  3. take:只发射开始的N想数据或者一定时间内的数据,内部通过ObservableTake和ObservableTakeUntil过滤数据。

    1
    2
    3
    4
    Observable.just(3, 4, 5, "aad")
    .take(3)
    .take(100,TimeUnit.MILLISECONDS)
    .subscribe(integer -> System.out.println(integer));
  4. takeLast:只发射最后的N项数据或者一定时间内的数据。

    1
    2
    3
    4
    Observable.just(3, 4, 5)
    .takeLast(2)
    .take(100,TimeUnit.MILLISECONDS)
    .subscribe(integer -> System.out.println(integer));
  5. last/lastOrError:只发射最后一项数据。

  6. first/firstOrError:只发射第一项数据。
  7. skip:跳过开始的N项数据或者一定时间内的数据。
  8. skiplast:跳过最后的N想数据或者一定时间内的数据。
  9. elementAt/elementAtOrError:发射某项数据,如果超过了范围可以指定默认值。
  10. ignoreElements:丢弃所有数据,只发射错误或者正确终止的通知。
  11. distinct:过滤重复数据。
  12. distinctUntilChanged:过滤掉连续重复的数据。
  13. throttleFirst:定期发射Observable发射的第一项数据。
  14. throttleWithTimeout/debounce:发射数据时,如果两次数据的发射间隔小于指定时间,就会丢弃前一次的数据,直到指定时间内都没有新数据发射时才进行发射。
  15. sample/throttleLast:定期发射Observable最近的数据。
  16. timeout:如果原始Observable过了指定的一段时长没有发射数据,就发射一个异常或者使用备用的Observable。

条件或者布尔操作

  1. all :判断所有的数据项是否满足某个条件,内部通过ObservableAllSingle实现。

    1
    2
    3
    Observable.just(3, 4, 5)
    .all(integer -> integer>3)
    .subscribe(integer -> System.out.println(integer));//得到boolean值
  2. contains:判断在发射的所有数据项中是否包含指定的数据。

    1
    2
    3
    Observable.just(3, 4, 5)
    .contains(3)
    .subscribe(integer -> System.out.println(integer)); //得到boolean值
  3. sequenceEqual:用于判断两个Observable发射的数据是否相同(数据,发射顺序,终止状态)。

    1
    2
    3
    4
    Observable<String> observable1 = Observable.just("4", "3", "1");
    Observable<String> observable2 = Observable.just("4", "3", "1");
    Observable.sequenceEqual(observable1,observable2)
    .subscribe(integer -> System.out.println(integer)); //得到boolean值
  4. isEmpty:用于判断Observable发射完毕时,有没有发射数据。有数据false,如果只收到了onComplete通知则为true。

    1
    2
    3
    Observable.just(3,2,5,4)
    .isEmpty()
    .subscribe(integer -> System.out.println(integer)); //得到boolean值
  5. amb:给定多个Observable,只让第一个发射数据的Observable发射全部数据,其他的Observable将会被忽略。

    1
    2


  6. ambArray:给定多个Observable,只让第一个发射数据的Observable发射全部数据,其他的Observable将会被忽略。

    1
    2
    3
    4
    Observable<Integer> observable1 = Observable.just(4,3,2,1);
    Observable<Integer> observable2 = Observable.just(6,7,8,9);
    Observable.ambArray(observable1,observable2)
    .subscribe(integer -> System.out.println(integer));
  7. switchIfEmpty:如果原始Observable正常终止后仍然没有发射任何的数据,就使用备用的Observable。

    1
    2
    3
    Observable.empty()
    .switchIfEmpty(Observable.just(2, 3, 4))
    .subscribe(integer -> System.out.println(integer));
  8. defaultIfEmpty:如果原始Observable正常终止后仍然没有发射任何数据,就发射一个默认值,内部调用的switchIfEmpty。

    1
    2
    3
    Observable.empty()
    .defaultIfEmpty(Observable.just(2, 3, 4))
    .subscribe(integer -> System.out.println(integer));
  9. takeUtil:当发射的数据满足某个条件后(包含该数据),或者第二个Observable发送完毕,终止第一个Observable发送数据。

    1
    2
    3
    Observable.just(2,3,4,5)
    .takeUntil(integer -> integer==4)
    .subscribe(integer -> System.out.println(integer)); //得到2,3,4
  10. takeWhile:当发射的数据满足某个条件时(不包含该数据),Observab终止发射数据。

    1
    2
    3
    Observable.just(2,3,4,5)
    .takeWhile(integer -> integer==4)
    .subscribe(integer -> System.out.println(integer));
  11. skipUntil:丢弃Observable发射的数据,直到第二个Observable发送数据。(丢弃条件数据)

  12. skipWhile:丢弃Observable发射的数据,直到一个指定的条件不成立(不丢弃条件数据)。

聚合操作

  1. reduce/reduceInto:对序列使用reduce()函数并发射最终的结果。

    1
    2
    3
    Observable.just(2,3,4,5)
    .reduce((integer, integer2) -> integer+integer2)
    .subscribe(integer -> System.out.println(integer)); //得到14
  2. collect:使用collect收集数据到一个可变的数据结构。

    1
    2
    3
    4
    Observable.just(2,3,4,5)
    .collect((Callable<List<Integer>>) () -> new ArrayList<>(),
    (s, integer) -> s.add(integer))
    .subscribe(integer -> System.out.println(integer)); //得到列表[2, 3, 4, 5]
  3. count:计算发射的数量。

转换操作

  1. toList:收集原始Observable发射的所有数据到一个列表,然后返回列表。

    1
    2
    3
    Observable.just(2,3,4,5)
    .toList()
    .subscribe(integer -> System.out.println(integer)); //得到列表[2, 3, 4, 5]
  2. toSortedList:收集原始Observable发射的所有数据到一个有序的列表,然后返回这个列表。

    1
    2
    3
    Observable.just(8,3,6,5)
    .toSortedList((o1, o2) -> o1-o2)
    .subscribe(integer -> System.out.println(integer));
  3. toMap:将序列数据转换为一个Map,可以根据数据项生成key和生成value。

    1
    2
    3
    4
    Observable.just(8,3,6,5)
    //根据数据项生成map的key,//根据数据项生成map的kvalue
    .toMap(integer -> integer+"-", integer -> integer+"+")
    .subscribe(integer -> System.out.println(integer)); //得到{8-=8+, 6-=6+, 5-=5+, 3-=3+}
  4. toMultiMap:类似toMap,不同的地方在于map的value是一个集合。

变换操作

  1. map:对Observable发射的每一项数据都应用一个函数来变换。

    1
    2
    3
    Observable.just(8,3,6,5)
    .map(integer -> "item"+integer)
    .subscribe(integer -> System.out.println(integer)); //得到item8,item3,item6,item5
  2. cast:在发射之前将Observable发射的所有数据转换为指定类型。

  3. flatMap:将Observable发射的数据变换为Observable集合,然后将这些Observable发射的数据平坦化的放进一个单独的Observable,内部采用merge合并。

    1
    2
    3
    4
    5
    6
    Observable.just(8,3,6,5)
    .flatMap((Function<Integer, ObservableSource<?>>) integer -> Observable.create((ObservableOnSubscribe<String>) observableEmitter -> {
    observableEmitter.onNext(integer+"");
    observableEmitter.onComplete();
    }))
    .subscribe(integer -> System.out.println(integer)); //得到8,3,6,5
  4. flatMapIterable:和flatMap的作用一样,只不过生产的是Iterable而不是Observable。

    1
    2
    3
    4

    Observable.just(8,3,6,5)
    .flatMapIterable(integer -> Arrays.asList("item"+integer))
    .subscribe(integer -> System.out.println(integer));
  5. concatMap:类似于flatMap,由于内部使用concat合并,所以是按照顺序连接发射。

  6. SwitchMap:和flatMap很像,将Observable发射的数据变换为Observable集合,将原始Observable发射一个新的数据(Observable)时,它将取消订阅前一个Observable。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {
    for(int i=0;i<7;i++){
    observableEmitter.onNext(i);
    Thread.sleep(500);
    }
    observableEmitter.onComplete();
    })
    .switchMap((Function<Integer, ObservableSource<?>>) integer -> Observable.create(new ObservableOnSubscribe<Integer>() {
    @Override
    public void subscribe(ObservableEmitter<Integer> observableEmitter) throws Exception {
    observableEmitter.onNext(integer*10);
    Thread.sleep(500);
    observableEmitter.onComplete();
    }
    }))
    .subscribe(integer -> System.out.println(integer)); //得到 0 10 20
  7. scan:与reduce很像,对Observable发射的每一项数据应用一个函数,然后按顺序一次发射每个值。

    1
    2
    3
    Observable.just(2,3,4)
    .scan((integer, integer2) -> integer+integer2)
    .subscribe(integer -> System.out.println(integer)); //得到 2 5 9
  8. groupBy:将Observab分拆为Observable集合,将原始Observable发射的数据按key分组,每一个Observable发射一组不同的数据。

    1
    2
    3
    4
    5
    6
    7
      Observable.just(2,3,4)
    .groupBy(integer -> integer%2==0?"偶数":"奇数")
    .subscribe(stringIntegerGroupedObservable -> stringIntegerGroupedObservable.subscribe(integer ->
    System.out.println(stringIntegerGroupedObservable.getKey()+":"+integer)));//得到
    //偶数:2
    //奇数:3
    //偶数:4
  9. buffer:定期从Observable收集数据到一个集合,然后把这些数据集合打包发射,而不是一次发射一个。

    1
    2
    3
    Observable.just(2,3,4)
    .buffer(2)
    .subscribe(integers -> System.out.println(integers)); //得到 [2, 3] [4]
  10. window:定期将来自Observable的数据分拆成一些Observable窗口,饭后发射这些窗口数据,而不是每次发射一项。

    1
    2
    3
    Observable.just(2,3,4)
    .window(2)
    .subscribe(integerObservable -> integerObservable.subscribe(integer -> System.out.println(integer)));

错误处理/重试机制

  1. onErrorResumeNext:当原始Observable在遇到错误时,使用备用的Observable。

    1
    2
    3
    4
    Observable.just(2,"3",4)
    .cast(Integer.class)
    .onErrorResumeNext(Observable.just(5,6,7))
    .subscribe(integer -> System.out.println(integer)); //得到 2,5,6,7
  2. onErrorReturn:当原始Observable在遇到错误时发射一个特定的数据。

    1
    2
    3
    4
    Observable.just(2,"3",4)
    .cast(Integer.class)
    .onErrorReturn(throwable -> 0)
    .subscribe(integer -> System.out.println(integer)); //得到2,0
  3. retry:当原始Observable在遇到错误时进行重试。

    1
    2
    3
    4
    Observable.just(2,"3",4)
    .cast(Integer.class)
    .retry(3)
    .subscribe(integer -> System.out.println(integer),throwable ->System.out.println("error"));
  4. retryWhen:当原始Observable在遇到错误时,将错误传递给另一个Observable类决定是否要重新订阅这个Observable。

    1
    2
    3
    4
    Observable.just(2,"3",4)
    .cast(Integer.class)
    .retryWhen(throwableObservable -> throwableObservable.retry(1))
    .subscribe(integer -> System.out.println(integer),throwable ->System.out.println("error"));

链接操作

  ConnectableObservable与普通的Observable差不多,但是可连接的Observable在被订阅时并不开始发射数据,只有在它的connect()被调用时才开始。用这种方法,你可以等所有的潜在订阅者都订阅了这个Observable之后才开始发射数据。
  ConnectableObservable.connect()指示一个可连接的Observable开始发射数据。
  Observable.publish()将一个Observable转换为一个可连接的Observable 。
  Observable.replay()确保所有的订阅者看到相同的数据序列的ConnectableObservable,即使它们在Observable开始发射数据之后才订阅。
  ConnectableObservable.refCount()让一个可连接的Observable表现得像一个普通的Observable。

1
2
3
4
ConnectableObservable co=  ConnectableObservable.just(1,2,3)
.publish();
co.subscribe(integer -> System.out.println(integer));
co.connect();

阻塞操作

  BlockingObservable是一个阻塞的Observable。普通的Observable 转换为 BlockingObservable,可以使用 Observable.blocking( )方法。内部通过CountDownLatch实现了阻塞操作。
  以下的操作符可以用于BlockingObservable,如果是普通的Observable,务必使用Observable.blocking
()转为阻塞Observable后使用,否则达不到预期的效果。

  1. blockingForEach:对BlockingObservable发射的每一项数据调用一个方法,会阻塞到Observable完成。
    1
    2
    3
    Observable.just(2, 3, 4)
    .observeOn(Schedulers.newThread())
    .blockingForEach(integer ->System.out.println(integer) );

工具集

  1. materialize:将Observable转换成一个通知列表。

    1
    2
    3
    Observable.just(2, 3, 4)
    .materialize()
    .subscribe(integerNotification ->System.out.println(integerNotification.isOnNext()) );
  2. dematerialize:与上面的作用相反,将通知逆转回一个Observable。?

  3. timestamp:给Observable发射的每个数据项添加一个时间戳。

    1
    2
    3
    Observable.just(2, 3, 4)
    .timestamp()
    .subscribe(integerTimed ->System.out.println( integerTimed.time()+" "+integerTimed.value()));
  4. timeInterval:给Observable发射的两个数据项间添加一个时间差,实现在OperatorTimeInterval中。?

  5. serialize:强制Observable按次序发射数据并且要求功能是完好的。
  6. cache: 缓存Observable发射的数据序列并发射相同的数据序列给后续的订阅者。
  7. observeOn: 指定观察者观察Observable的调度器。
  8. subscribeOn: 指定Observable执行任务的调度器。
  9. doOnEach: 注册一个动作,对Observable发射的每个数据项使用

    1
    2
    3
    Observable.just(2, 3, 4)
    .doOnEach(integerNotification -> integerNotification.getValue())
    .subscribe(integer -> System.out.println(integer));
  10. doOnCompleted: 注册一个动作,对正常完成的Observable使用。

  11. doOnError: 注册一个动作,对发生错误的Observable使用。
  12. doOnTerminate:注册一个动作,对完成的Observable使用,无论是否发生错误。

    1
    2
    3
    Observable.just(2, 3, 4)
    .doOnTerminate(() -> System.out.println("do torminate"))
    .subscribe(integer -> System.out.println(integer));
  13. doOnSubscribe: 注册一个动作,在观察者订阅时使用。内部由OperatorDoOnSubscribe实现。

    1
    2
    3
    Observable.just(2, 3, 4)
    .doOnSubscribe(disposable -> disposable.isDisposed())
    .subscribe(integer -> System.out.println(integer));
  14. doOnUnsubscribe: 注册一个动作,在观察者取消订阅时使用。内部由OperatorDoOnUnsubscribe实现,在call中加入一个解绑动作。

  15. doFinally: 注册一个动作,在Observable完成时使用。

    1
    2
    3
    Observable.just(2, 3, 4)
    .doFinally(() -> {})
    .subscribe(integer -> System.out.println(integer));
  16. delay: 延时发射Observable的结果。即让原始Observable在发射每项数据之前都暂停一段指定的时间段。效果是Observable发射的数据项在时间上向前整体平移了一个增量(除了onError,它会即时通知)。

  17. delaySubscription: 延时处理订阅请求。实现在OnSubscribeDelaySubscription中

    1
    2
    3
    Observable.just(2, 3, 4)
    .delaySubscription(200,TimeUnit.MILLISECONDS)
    .subscribe(integer -> System.out.println(integer));
  18. single/singleOrError: 强制返回单个数据,否则抛出异常或默认数据。

参考:

  • https://mcxiaoke.gitbooks.io/rxdocs/
  • http://www.sohu.com/a/138462889_468731
  • http://blog.csdn.net/maplejaw_/article/details/52442065
  • http://blog.csdn.net/maplejaw_/article/details/52396175
  • http://www.cnblogs.com/dragonfei/p/6263253.html

python爬虫

发表于 2017-05-20 | 分类于 Python
  1. urllib2的运用

  urllib2获取网页内容时可以设定超时时间,防止运行过程中假死;在获取网页时需要做异常处理,防止部分网页访问不到程序终止。

1
2
3
4
5
6
7
8
9
#coding:utf-8
import urllib2
try:
url = "http://www.baidu.com"
f = urllib2.urlopen(url, timeout=0) #timeout设置超时的时间
result = f.read()
print result
except Exception,e:
print 'a',str(e)

结果:

1
a <urlopen error timed out>

异常处理
  如果想在代码中处理URLError和HTTPError有两种方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /usr/bin/env python
#coding=utf-8
import urllib2
url="xxxxxx" #需要访问的URL
try:
response=urllib2.urlopen(url)
except urllib2.HTTPError,e: #HTTPError必须排在URLError的前面
print "The server couldn't fulfill the request"
print "Error code:",e.code
print "Return content:",e.read()
except urllib2.URLError,e:
print "Failed to reach the server"
print "The reason:",e.reason
else:
#something you should do
pass #其他异常的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /usr/bin/env python
#coding=utf-8
import urllib2
url="http://xxx" #需要访问的URL
try:
response=urllib2.urlopen(url)
except urllib2.URLError,e:
if hasattr(e,"reason"):
print "Failed to reach the server"
print "The reason:",e.reason
elif hasattr(e,"code"):
print "The server couldn't fulfill the request"
print "Error code:",e.code
print "Return content:",e.read()
else:
pass #其他异常的处理

  相比较而言,第二种异常处理方法更优。

  1. re正则的运用

字符匹配过成功中,re匹配模式很难定义,主要还是不熟;主要用到以下几种:

1
2
p = re.compile(r'''<a href="http://wiki.jikexueyuan.com/project/start-learning-python/(.+?).html" >.+?</a>''')
matchs = p.findall(contents)

  这个主要匹配出所有的网页名称出来,结果是得到一个包含list的tuple列表;

1
2
linkpatten = re.compile(r'''href="/.+?/css''')
contents = re.sub(linkpatten, r'href="./css', contents)

  这个主要是将contents中的href=”/.+?/css字符串替换成./css形式,所有的哦。

1
2
linkpatten = re.compile(r'''href="/assets/(.+?)\.ico"''')
contents = re.sub(linkpatten, r'href="./images/\g<1>.ico"', contents)

  这个主要是将contents中的href=”/assets/(.+?).ico”字符串并提取ico的名称用来后台替换,替换成href=”./images/\g<1>.ico”形式,其中\g<1>部分或用提取出来的名称替换,同样是所有的哦.

小结
  正则中()代表需要提取出来的内容;re.sub(linkpatten, r’href=”./images/\g<1>.ico”‘, contents)解析;反斜杠加g以及中括号内一个名字,即:\g,对应着命了名的组,named group;文件保存推荐使用

1
2
with open(filename, 'w') as cssfile:
cssfile.write(req.read())

  同样需要注意的是在保存图片文件(png或jpg)时,写入模式是“w+b”(以二进制形式保存),但是有的网站图片可能是压缩过的,保存下来是看不了的,可以通过如下格式保存:

1
2
3
4
with open(filename,"wb") as code:
decompresser = zlib.decompressobj(16+zlib.MAX_WBITS)
data = decompresser.decompress(f.read())
code.write(data)

  目前不知道如何判断图片是否压缩及压缩格式,有知道的可以交流下。

  1. 贪婪 vs 不贪婪
      当重复一个正则表达式时,如用 a*,操作结果是尽可能多地匹配模式。当你试着匹配一对对称的定界符,如 HTML 标志中的尖括号时这个事实经常困扰你。匹配单个 HTML 标志的模式不能正常工作,因为 .* 的本质是“贪婪”的
1
2
3
4
5
6
7
8
#!python
>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print re.match('<.*>', s).span()
(0, 32)
>>> print re.match('<.*>', s).group()
<html><head><title>Title</title>

  RE 匹配 在 ““ 中的 “<”,.* 消耗掉字符串的剩余部分。在 RE 中保持更多的左,虽然 > 不能匹配在字符串结尾,因此正则表达式必须一个字符一个字符地回溯,直到它找到 > 的匹配。最终的匹配从 ““ 中的 “>”,这并不是你所想要的结果。

  在这种情况下,解决方案是使用不贪婪的限定符 *?、+?、?? 或 {m,n}?,尽可能匹配小的文本。在上面的例子里, “>” 在第一个 “<” 之后被立即尝试,当它失败时,引擎一次增加一个字符,并在每步重试 “>”。这个处理将得到正确的结果:

1
2
3
#!python
>>> print re.match('<.*?>', s).group()
<html>

###最后附上最近写的原始版的极客python教程内容爬取的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# -*- coding:utf-8 -*-
import re
import urllib2
import logging
import time

import zlib

logging.basicConfig(level=logging.INFO)
"""
"""
main_dir = r'D:\mugwort\book\python\base_python'
main_url = r'http://wiki.jikexueyuan.com/project/start-learning-python/'


def sava_css(contents):
p = re.compile(r'''<link rel="stylesheet" type="text/css" href="(.+?)">''')
matchs = p.findall(contents)
for match in matchs:
logging.info(match)
if not match.startswith("//"):
match = "http://wiki.jikexueyuan.com" + match
else:
match = "http:" + match
logging.info("format:" + match)
req = urllib2.urlopen(match)
contents = req.read()
names = re.compile(r'''.+?/(.*?)\.css''').findall(match)
name = names.pop(len(names) - 1)
logging.info(name)
logging.info("rindex %d length %d " % (name.rindex("/") + 1, len(name)))
logging.info("name format:" + name[name.rindex("/") + 1:len(name)])
filename = main_dir + '\\css\\' + name[name.rindex("/") + 1:len(name)] + ".css"
if filename:
logging.info(filename)
with open(filename, 'w') as cssfile:
cssfile.write(contents)


def sava_js(contents):
p = re.compile(r'''<script type="text/javascript" src="(.+?)">''')
matchs = p.findall(contents)
for match in matchs:
logging.info(match)
if not match.startswith("//"):
match = "http://wiki.jikexueyuan.com" + match
else:
match = "http:" + match
logging.info("format:" + match)
req = urllib2.urlopen(match)
contents = req.read()
names = re.compile(r'''.+?/(.*?)\.js''').findall(match)
logging.info(names)
name = names.pop(len(names) - 1)
logging.info(name)
logging.info("rindex %d length %d " % (name.rindex("/") + 1, len(name)))
logging.info("name format:" + name[name.rindex("/") + 1:len(name)])
filename = main_dir + '\\js\\' + name[name.rindex("/") + 1:len(name)] + ".js"
if filename:
logging.info(filename)
with open(filename, 'w') as cssfile:
cssfile.write(contents)


def sava_img(contents):
p = re.compile(r'''<img src="(.+?)".+?>|<link.+?href="(.+?)".+?type="image/x-icon".+?>''')
matchs = p.findall(contents)
for matchT in matchs:
logging.info(matchT)
match = matchT[0]
if not match:
match = matchT[1]
logging.info(match)
if not match.startswith("//"):
match = "http://wiki.jikexueyuan.com" + match
else:
match = "http:" + match
logging.info("format:" + match)
logging.info("rindex %d length %d " % (match.rindex(r"."), len(match)))
subfix = match[match.rindex("."):len(match)]
logging.info("subfix:" + subfix)
try:
req = urllib2.urlopen(match)
names = re.compile(r'''.+?/(.*?)\.png|.+?/(.*?)\.jpg|.+?/(.*?)\.ico''').findall(match)
logging.info(names)
nameT = names.pop(0)
name = nameT[0]
if not name:
name = nameT[1]
if not name:
name = nameT[2]
logging.info(name)
logging.info("rindex %d length %d " % (name.rindex("/") + 1, len(name)))
logging.info("name format:" + name[name.rindex("/") + 1:len(name)])
filename = main_dir + '\\images\\' + name[name.rindex("/") + 1:len(name)] + subfix
if filename:
logging.info(filename)
# 对于png文件需要解压且以二进制保存
if ".png" == subfix:
with open(filename, 'w+b') as cssfile:
cssfile.write(req.read())
# 对于jpg文件需要以二进制写入
elif ".jpg" == subfix:
with open(filename, 'w+b') as cssfile:
cssfile.write(req.read())
else:
with open(filename, 'w') as cssfile:
cssfile.write(req.read())
except urllib2.URLError, e:
if hasattr(e, "reason"):
print "The reason:", e.reason
elif hasattr(e, "code"):
print "Error code:", e.code
print "Return content:", e.read()
else:
pass # 其他异常处理


def save_html(urlname):
# 打开并保存hmtl
url = main_url + urlname + '.html'
file_name = main_dir + '\\' + urlname + '.html'
req = urllib2.urlopen(url)
contents = req.read()
sava_css(contents)
sava_js(contents)
sava_img(contents)
# 替换掉地址信息,让其变为本地
# 规范css文件保存
contents = re.sub(main_url, r"", contents)
linkpatten = re.compile(r'''href="/.+?/css''')
contents = re.sub(linkpatten, r'href="./css', contents)
# 规范js文件保存
linkpatten = re.compile(r'''src=".*?/js/(.+?)\.js.*?"''')
contents = re.sub(linkpatten, r'src="./js/\g<1>.js"', contents)
linkpatten = re.compile(r"src='.*?/js/(.+?)\.js.*?'")
contents = re.sub(linkpatten, r'src="./js/\g<1>.js"', contents)
# 规范js文件保存
linkpatten = re.compile(r'''src=".*?/src/(.+?)\.js.*?"''')
contents = re.sub(linkpatten, r'src="./js/\g<1>.js"', contents)
# 规范img文件保存
linkpatten = re.compile(r'''src=".*?/images/(.+?)\.png"''')
contents = re.sub(linkpatten, r'src="./images/\g<1>.png"', contents)
linkpatten = re.compile(r'''src=".*?/images/(.+?)\.jpg"''')
contents = re.sub(linkpatten, r'src="./images/\g<1>.jpg"', contents)
linkpatten = re.compile(r'''href="/assets/(.+?)\.ico"''')
contents = re.sub(linkpatten, r'href="./images/\g<1>.ico"', contents)

with open(file_name, 'w') as urlfile:
urlfile.write(contents)


req = urllib2.urlopen(r'http://wiki.jikexueyuan.com/project/start-learning-python/')
p = re.compile(r'''<a href="http://wiki.jikexueyuan.com/project/start-learning-python/(.+?).html" >.+?</a>''')
contents = req.read().decode("utf-8")
matchs = p.findall(contents)
logging.info(len(matchs))
logging.info(time.time())
for row in matchs:
save_html(row)
logging.info(time.time())

参考地址:

  • 正则速查表 http://www.jb51.net/shouce/jquery1.82/regexp.html
  • urllib2异常处理 http://wangxiaoxu.iteye.com/blog/1844989
  • Python正则表达式操作指南 http://wiki.ubuntu.org.cn/Python%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%93%8D%E4%BD%9C%E6%8C%87%E5%8D%97
  • re.sub使用示例 http://www.programcreek.com/python/example/21/re.sub
  • python替换函数 http://blog.csdn.net/zcmlimi/article/details/47709049
  • re.sub 五个参数解析 http://www.crifan.com/python_re_sub_detailed_introduction/
  • 压缩问题 http://stackoverflow.com/questions/3122145/zlib-error-error-3-while-decompressing-incorrect-header-check

raspberryWithPython

发表于 2017-05-10 | 分类于 Raspberry

树莓派gpio接口操作

  1. 基础知识介绍

  General Purpose Input Output (通用输入/输出)简称为GPIO,或总线扩展器,人们利用工业标准I2C、SMBus或SPI接口简化了I/O口的扩展。当微控制器或芯片组没有足够的I/O端口,或当系统需要采用远端串行通信或控制时,GPIO产品能够提供额外的控制和监视功能。(摘自百度)
  树莓派gpio接口示意图树莓派针脚图,图片来源。

  1. 使用python操作gpio接口
    ##安装python gpio库
    1
    2
    3
    4
    5
    6
    7
    mkdir gpio
    cd gpio
    wget https://pypi.python.org/packages/source/R/RPi.GPIO/RPi.GPIO-0.5.7.tar.gz
    #或者到这里下载最新版本:https://pypi.python.org/pypi/RPi.GPIO
    tar xvzf RPi.GPIO-*.tar.gz
    cd RPi.GPIO-*/
    sudo python setup.py install

安装的时候出现错误:

1
source/py_gpio.c:23:20: fatal error: Python.h: No such file or directory

缺少Python.h文件,没安装python编译环境:

1
sudo apt-get install python-dev

再次安装:

1
sudo python setup.py install

点亮led灯

先测试下输出,新建个led.py文件:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python

import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BOARD)
GPIO.setup(11,GPIO.OUT)
while True:
GPIO.output(11,True)
time.sleep(1)
GPIO.output(11,False)
time.sleep(1)

  注意: 这里使用GPIO.BOARD模式,所以对于引脚号的排序,是按26个pin的顺序,不是gpio1这样的.也就是说pin1就是板子上的3V3. 把led的负极接到板子上的pin11.正极接一个3K3的电阻,在接到3V3上,防止烧坏. 特别注意python的缩进.

终端下运行:

1
sudo python led.py

如果LED出现一闪一闪就表示成功了.

python第三方库beatifulSoup使用

发表于 2017-05-08 | 分类于 Python

python中BeautifulSoup使用

  BeautifulSoup是一个可以从Html或xml文件中提取数据的python库,它能够通过不同的转换器实现惯用的文档导航、查找、修改文档的方式。
  首先看看它的一些基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from BeautifulSoup import BeautifulSoup

html_doc = """<html><head><title>The Dormouse's story</title></head>

<body>

<p class="title"><b>The Dormouse's story</b></p>



<p class="story">Once upon a time there were three little sisters; and their names were

<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,

<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and

<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;

and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body>
</html>"""
soup = BeautifulSoup(html_doc)
# 通过prettify方法格式化文档
# print(soup.prettify())
# 输出文件对象的title标签内容
print soup.title
# 输出标签的名字
print soup.title.name
# 输出标签的文本内容
print soup.title.string
# 输出标签title的父节点名字
print soup.title.parent.name
# 输出标签的属性class的值,!前提是该标签有定义了该属性,要不会报错
print soup.p["class"]
# 输出文档中所有a标签的节点
print soup.findAll("a")
# 输出节点带有id属性且id属性值为"link3"的节点
print soup.find(id="link3")
# 从文档中找到所有a标签的链接
for link in soup.findAll("a"):
print(link.get("href"))
# 从文档中获取所有文字内容,text属性是调用getText()方法,getText()方法可以添加分隔符,有的本版是这个方法名:get_text()
print(soup.text)

  大致浏览了下BeautifulSoup用法,接着我们看看如何在中用。

#首先是安装

  1. ubuntu或者Debain系统安装

    1
    $ apt-get install Python-bs4
  2. 直接命令安装,Beautiful Soup 4 通过PyPi发布,所以如果你无法使用系统包管理安装,那么也可以通过 easy_install 或 pip 来安装。包的名字是 beautifulsoup4 ,这个包兼容Python2和Python3。

    1
    2
    c:\> easy_install beautifulsoup4
    c:\> pip install beautifulsoup4

  注意在PyPi中还有一个名字是 BeautifulSoup 的包,但那可能不是你想要的,那是 Beautiful Soup3 的发布版本,因为很多项目还在使用BS3, 所以 BeautifulSoup 包依然有效。但是如果你在编写新项目,那么你应该安装的 beautifulsoup4。

  1. 通过下载源码安装,解压进入源码包,然后运行:

    1
    python setup.py install
  2. 通过ide辅助安装,在代码中输入一下内容:

    1
    2
    # 版本 3.x.x引入方式
    from BeautifulSoup import BeautifulSoup

或者

1
2
# 版本4以上含,引入方式
from bs4 import BeautifulSoup

  然后ide会提示BeautifulSoup找不到,再根据提示安装即可。

安装完成后可能遇到的问题


  1. 代码中抛出ImportError的异常:“No module named HTMLParser”,这是因为你在Python3版本中执行Python2版本的代码。
  2. 代码抛出了 ImportError 的异常: “No module named html.parser”, 这是因为你在Python2版本中执行Python3版本的代码.
      如果遇到上述2种情况,最好的解决方法是重新安装BeautifulSoup4。

#安装解析器
  Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,其中一个是 lxml .根据操作系统不同,可以选择下列方法来安装lxml:

1
2
3
$ apt-get install Python-lxml
$ easy_install lxml
$ pip install lxml

  另一个可供选择的解析器是纯Python实现的 html5lib , html5lib的解析方式与浏览器相同,可以选择下列方法来安装html5lib:

1
2
3
$ apt-get install Python-html5lib
$ easy_install html5lib
$ pip install html5lib

python中主要的一些解析器和各自优缺点:

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, “html.parser”) Python的内置标准库;执行速度适中;文档容错能力强 Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
lxml HTML 解析器 BeautifulSoup(markup, “lxml”) 速度快;文档容错能力强 需要安装C语言库
lxml XML 解析器 BeautifulSoup(markup, [“lxml-xml”])
BeautifulSoup(markup, “xml”)
速度快;唯一支持XML的解析器 需要安装C语言库
html5lib BeautifulSoup(markup, “html5lib”) 最好的容错性;以浏览器的方式解析文档;生成HTML5格式的文档 速度慢;不依赖外部扩展

  推荐使用lxml作为解析器,因为效率更高. 在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必须安装lxml或html5lib, 因为那些Python版本的标准库中内置的HTML解析方法不够稳定.

  提示 如果一段HTML或XML文档格式不正确的话,那么在不同的解析器中返回的结果可能是不一样的,查看 [解析器之间的区别] (http://doc.iplaypy.com/bs4/#id49)了解更多细节。

#使用
  将一段文档传入BeautifulSoup 的构造方法,就能得到一个文档的对象, 可以传入一段字符串或一个文件句柄。

1
2
3
4
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))
soup = BeautifulSoup("<html>data</html>")

  首先文档被转换成Unicode,并且HTML的实例都被转换成Unicode编码,然后,Beautiful Soup选择最合适的解析器来解析这段文档,如果手动指定解析器那么Beautiful Soup会选择指定的解析器来解析文档。

##对象的种类
  Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种: Tag ,NavigableString ,BeautifulSoup,Comment 。

###Tag
  Tag 对象与XML或HTML原生文档中的tag相同:

1
2
3
4
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>

  Tag有很多方法和属性,在遍历文档树和搜索文档树中有详细解释。现在介绍一下tag中最重要的属性: name和attributes

  1. Name:每个tag都有自己的名字,通过 .name 来获取:

    1
    2
    tag.name
    # u'b'
  2. Attributes:一个tag可能有很多个属性.。tag 有一个 “class” 的属性,值为 “boldest” 。 tag的属性的操作方法与字典相同:

    1
    2
    tag['class']
    # u'boldest'

也可以直接”点“取属性,比如:.attrs:

1
2
tag.attrs
# {u'class': u'boldest'}

  tag的属性可以被添加,删除或修改。tag的属性操作方法与字典一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

tag['class']
# KeyError: 'class'

print(tag.get('class'))
# None

  1. 多值属性:HTML 4定义了一系列可以包含多个值的属性。在HTML5中移除了一些,却增加更多。最常见的多值的属性是 class (一个tag可以有多个CSS的class). 、。还有一些属性 rel , rev , accept-charset , headers , accesskey 。在Beautiful Soup中多值属性的返回类型是list:
    1
    2
    3
    4
    5
    6
    7
    css_soup = BeautifulSoup('<p class="body strikeout"></p>')
    css_soup.p['class']
    # ["body", "strikeout"]

    css_soup = BeautifulSoup('<p class="body"></p>')
    css_soup.p['class']
    # ["body"]

  如果某个属性看起来好像有多个值,但在任何版本的HTML定义中都没有被定义为多值属性,那么Beautiful Soup会将这个属性作为字符串返回。

1
2
3
id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'

  将tag转换成字符串时,多值属性会合并为一个值。

1
2
3
4
5
6
7
rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']

rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>

  如果转换的文档是XML格式,那么tag中不包含多值属性。

1
2
3
4
xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')

xml_soup.p['class']
# u'body strikeout'

###NavigableString:可以遍历的字符串
  字符串常被包含在tag内。Beautiful Soup用 NavigableString 类来包装tag中的字符串:

1
2
3
4
5
tag.string
# u'Extremely bold'

type(tag.string)
# <class 'bs4.element.NavigableString'>

  一个 NavigableString 字符串与Python中的Unicode字符串相同,并且还支持包含在 遍历文档树 和 搜索文档树 中的一些特性. 通过 unicode() 方法可以直接将 NavigableString 对象转换成Unicode字符串:

1
2
3
4
5
6
7
unicode_string = unicode(tag.string)

unicode_string
# u'Extremely bold'

type(unicode_string)
# <type 'unicode'>

  tag中包含的字符串不能编辑,但是可以被替换成其它的字符串,用 replaceWith() 方法:

1
2
3
4
tag.string.replaceWith("No longer bold")

tag
# <blockquote>No longer bold</blockquote>

  NavigableString 对象支持 遍历文档树 和 搜索文档树 中定义的大部分属性, 并非全部。尤其是一个字符串不能包含其它内容(tag能够包含字符串或是其它tag),字符串不支持 .contents 或 .string 属性或 find() 方法。
  如果想在Beautiful Soup之外使用 NavigableString 对象,需要调用 unicode() 方法,将该对象转换成普通的Unicode字符串,否则就算Beautiful Soup的方法已经执行结束,该对象的输出也会带有对象的引用地址,这样会浪费内存。

###BeautifulSoup
  BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象,它支持遍历文档树 和 搜索文档树 中描述的大部分的方法。
  因为 BeautifulSoup 对象并不是真正的HTML或XML的tag,所以它没有name和attribute属性。但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个值为 “[document]” 的特殊属性 .name。

1
2
soup.name
# u'[document]'

###Comment
  注释及特殊字符串,Tag , NavigableString , BeautifulSoup 几乎覆盖了html和xml中的所有内容,但是还有一些特殊对象。容易让人担心的内容是文档的注释部分:

1
2
3
4
5
6
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string

type(comment)
# <class 'bs4.element.Comment'>

  Comment 对象是一个特殊类型的 NavigableString 对象:

1
2
comment
# u'Hey, buddy. Want to buy a used parser'

  但是当它出现在HTML文档中时, Comment 对象会使用特殊的格式输出:

1
2
3
4
print(soup.b.prettify())
# <b>
# <!--Hey, buddy. Want to buy a used parser?-->
# </b>

  Beautiful Soup中定义的其它类型都可能会出现在XML的文档中: CData , ProcessingInstruction , Declaration , Doctype 。与 Comment 对象类似,这些类都是 NavigableString 的子类,只是添加了一些额外的方法的字符串独享。下面是用CDATA来替代注释的例子:

1
2
3
4
5
6
7
8
from bs4 import CData
cdata = CData("A CDATA block")
comment.replaceWith(cdata)

print(soup.b.prettify())
# <b>
# <![CDATA[A CDATA block]]>
# </b>

##遍历文档树
  还拿开头的html_doc文档来做例子

###子节点
  一个Tag可能包含多个字符串或其它的Tag,这些都是这个Tag的子节点.Beautiful Soup提供了许多操作和遍历子节点的属性.
注意: Beautiful Soup中字符串节点不支持这些属性,因为字符串没有子节点。

  1. tag的名字

  操作文档树最简单的方法就是告诉它你想获取的tag的name.如果想获取 标签,只要用 soup.head :

1
2
3
4
5
soup.head
# <head><title>The Dormouse's story</title></head>

soup.title
# <title>The Dormouse's story</title>

  这是个获取tag的小窍门,可以在文档树的tag中多次调用这个方法。下面的代码可以获取标签中的第一个标签:

1
2
soup.body.b
# <b>The Dormouse's story</b>

  通过点取属性的方式只能获得当前名字的第一个tag:

1
2
soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

  如果想要得到所有的标签,或是通过名字得到比一个tag更多的内容的时候,就需要用到 Searching the tree 中描述的方法,比如: find_all()

1
2
3
4
5
soup.find_all('a')

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. .contents 和 .children

  tag的 .contents 属性可以将tag的子节点以列表的方式输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>

head_tag.contents
[<title>The Dormouse's story</title>]

title_tag = head_tag.contents[0]
title_tag
# <title>The Dormouse's story</title>

title_tag.contents
# [u'The Dormouse's story']

  BeautifulSoup 对象本身一定会包含子节点,也就是说标签也是 BeautifulSoup 对象的子节点:

1
2
3
4
len(soup.contents)
# 1
soup.contents[0].name
# u'html'

  字符串没有 .contents 属性,因为字符串没有子节点:

1
2
3
text = title_tag.contents[0]
text.contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

  通过tag的 .children 生成器,可以对tag的子节点进行循环:

1
2
3
for child in title_tag.children:
print(child)
# The Dormouse's story

  1. .descendants

  .contents 和 .children 属性仅包含tag的直接子节点。例如,标签只有一个直接子节点<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">head_tag.contents</span><br><span class="line"><span class="comment"># [<title>The Dormouse's story</title>]</span></span><br></pre></td></tr></table></figure>

  但是标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串 “The Dormouse’s story”也属于<head>标签的子孙节点. .descendants 属性可以对所有tag的子孙节点进行递归循环 :<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> child <span class="keyword">in</span> head_tag.descendants:</span><br><span class="line"> print(child)</span><br><span class="line"> <span class="comment"># <title>The Dormouse's story</title></span></span><br><span class="line"> <span class="comment"># The Dormouse's story</span></span><br></pre></td></tr></table></figure></head>

  1. .string

  如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点:

1
2
title_tag.string
# u'The Dormouse's story'

  如果一个tag仅有一个子节点,那么这个tag也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同:

1
2
3
4
5
head_tag.contents
# [<title>The Dormouse's story</title>]

head_tag.string
# u'The Dormouse's story'

  如果tag包含了多个子节点,tag就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None :

1
2
print(soup.html.string)
# None

  1. .stings和stipped_strings
      如果tag中包含多个字符串 ,可以使用 .strings 来循环获取:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    for string in soup.strings:
    print(repr(string))

    # u"The Dormouse's story"
    # u'\n\n'
    # u"The Dormouse's story"
    # u'\n\n'
    # u'Once upon a time there were three little sisters; and their names were\n'
    # u'Elsie'
    # u',\n'
    # u'Lacie'
    # u' and\n'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'\n\n'
    # u'...'
    # u'\n'

  输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容:

1
2
3
4
5
6
7
8
9
10
11
12
for string in soup.stripped_strings:
print(repr(string))
# u"The Dormouse's story"
# u"The Dormouse's story"
# u'Once upon a time there were three little sisters; and their names were'
# u'Elsie'
# u','
# u'Lacie'
# u'and'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'...'

  全部是空格的行会被忽略掉,段首和段末的空白会被删除。

###父节点
  每个tag或字符串都有父节点:被包含在某个tag中.

  1. .parent

  通过 .parent 属性来获取某个元素的父节点.在例子“爱丽丝”的文档中,标签是标签的父节点:<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">title_tag = soup.title</span><br><span class="line">title_tag</span><br><span class="line"><span class="comment"># <title>The Dormouse's story</title></span></span><br><span class="line"></span><br><span class="line">title_tag.parent</span><br><span class="line"><span class="comment"># <head><title>The Dormouse's story</title></head></span></span><br></pre></td></tr></table></figure>

  文档title的字符串也有父节点:标签<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">title_tag.string.parent</span><br><span class="line"><span class="comment"># <title>The Dormouse's story</title></span></span><br></pre></td></tr></table></figure>

  文档的顶层节点比如的父节点是 BeautifulSoup 对象:

1
2
3
html_tag = soup.html
type(html_tag.parent)
# <class 'bs4.BeautifulSoup'>

  BeautifulSoup 对象的 .parent 是None:

1
2
print(soup.parent)
# None

  1. .parents

  通过元素的 .parents 属性可以递归得到元素的所有父辈节点,下面的例子使用了 .parents 方法遍历了a标签到根节点的所有节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
for parent in link.parents:
if parent is None:
print(parent)
else:
print(parent.name)
# p
# body
# html
# [document]
# None

###兄弟节点
  节点同属于同一个元素的子节点,那么这些节点可以被称为兄弟节点。一段文档以标准格式输出时,兄弟节点有相同的缩进级别。在代码中也可以使用这种关系。

  1. .next_sibling和.previous_sibling

  在文档树中,使用 .next_sibling 和 .previous_sibling 属性来查询兄弟节点:

1
2
3
4
5
6
7
sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")

print sibling_soup.b.next_sibling
# <c>text2</c>

print sibling_soup.c.previous_sibling
# <b>text1</b>

  b标签有 .next_sibling 属性,但是没有 .previous_sibling 属性,因为b标签在同级节点中是第一个.同理,c标签有 .previous_sibling 属性,却没有 .next_sibling 属性:

1
2
3
4
5
print(sibling_soup.b.previous_sibling)
# None

print(sibling_soup.c.next_sibling)
# None

  例子中的字符串“text1”和“text2”不是兄弟节点,因为它们的父节点不同:

1
2
3
4
5
sibling_soup.b.string
# u'text1'

print(sibling_soup.b.string.next_sibling)
# None

  1. .next_siblings 和 .previous_siblings

  通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for sibling in soup.a.next_siblings:
print(repr(sibling))

# u',\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u' and\n'
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
# u'; and they lived at the bottom of a well.'
# None


for sibling in soup.find(id="link3").previous_siblings:
print(repr(sibling))

# ' and\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u',\n'
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# u'Once upon a time there were three little sisters; and their names were\n'
# None

###回退和前进

  1. .next_element 和 .previous_element
      .next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的.
      这是“爱丽丝”文档中最后一个a标签,它的 .next_sibling 结果是一个字符串,因为当前的解析过程 [2] 因为当前的解析过程因为遇到了a标签而中断了:
    1
    2
    3
    4
    5
    6
    last_a_tag = soup.find("a", id="link3")
    print last_a_tag
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

    print last_a_tag.next_sibling
    # '; and they lived at the bottom of a well.'

  但这个a标签的 .next_element 属性结果是在a标签被解析之后的解析内容,不是a标签后的句子部分,应该是字符串”Tillie”:

1
2
print last_a_tag.next_element
# u'Tillie'

  这是因为在原始文档中,字符串“Tillie” 在分号前出现,解析器先进入a标签,然后是字符串“Tillie”,然后关闭a标签,然后是分号和剩余部分.分号与a标签在同一层级,但是字符串“Tillie”会被先解析.

  .previous_element 属性刚好与 .next_element 相反,它指向当前被解析的对象的前一个解析对象:

1
2
3
4
5
print last_a_tag.previous_element
# u' and\n'

print last_a_tag.previous_element.next_element
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

  1. .next_elements 和 .previous_elements

  通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样:

1
2
3
4
5
6
7
8
9
for element in last_a_tag.next_elements:
print(repr(element))
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# <p class="story">...</p>
# u'...'
# u'\n'
# None

###搜索文档树
  Beautiful Soup定义了很多搜索方法,这里着重介绍2个: find() 和 find_all() ,其它方法的参数和用法类似,请读者举一反三。

  1. 过滤器

  介绍 find_all() 方法前,先介绍一下过滤器的类型 ,这些过滤器贯穿整个搜索的API。过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中。

  • 字符串:最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的b标签:
    1
    2
    soup.find_all('b')
    # [<b>The Dormouse's story</b>]

  如果传入字节码参数,Beautiful Soup会当作UTF-8编码,可以传入一段Unicode 编码来避免Beautiful Soup解析编码出错。

  • 正则表达式:如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容。下面例子中找出所有以b开头的标签,这表示body和b标签都应该被找到:
    1
    2
    3
    4
    5
    6
    import re

    for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
    # body
    # b

下面代码找出所有名字中包含”t”的标签:

1
2
3
4
5
for tag in soup.find_all(re.compile("t")):
print(tag.name)

# html
# title

  1. 列表

  如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有a标签和b标签:

1
2
3
4
5
6
soup.find_all(["a", "b"])

# [<b>The Dormouse's story</b>,
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. True

  True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for tag in soup.find_all(True):
print(tag.name)

# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

  1. 方法

  如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False。
  下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True:

1
2
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')

  将这个方法作为参数传入 find_all() 方法,将得到所有p标签:

1
2
3
4
5
soup.find_all(has_class_but_no_id)

# [<p class="title"><b>The Dormouse's story</b></p>,
# <p class="story">Once upon a time there were...</p>,
# <p class="story">...</p>]

  返回结果中只有p标签没有a标签,因为a标签还定义了”id”,没有返回html和head,因为html和head中没有定义”class”属性.
  通过一个方法来过滤一类标签属性的时候, 这个方法的参数是要被过滤的属性的值, 而不是这个标签. 下面的例子是找出 href 属性不符合指定正则的 a 标签:

1
2
3
4
5
6
7
def not_lacie(href):
return href and not re.compile("lacie").search(href)

soup.find_all(href=not_lacie)

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  标签过滤方法可以使用复杂方法. 下面的例子可以过滤出前后都有文字的标签.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from bs4 import NavigableString

def surrounded_by_strings(tag):

return (isinstance(tag.next_element, NavigableString)

and isinstance(tag.previous_element, NavigableString))

for tag in soup.find_all(surrounded_by_strings):
print tag.name

# p
# a
# a
# a
# p

##find_all()方法剖析
find_all( name , attrs , recursive , string , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件。

  1. name参数

  name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉。
  重申: 搜索 name 参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True 。

  1. keyword 参数

  如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 id 的参数,Beautiful Soup会搜索每个tag的”id”属性:

1
2
soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

  如果传入 href 参数,Beautiful Soup会搜索每个tag的”href”属性:

1
2
3
soup.find_all(href=re.compile("elsie"))

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

  重申:搜索指定名字的属性时可以使用的参数值包括 字符串 , 正则表达式 , 列表, True 。
  使用多个指定名字的参数可以同时过滤tag的多个属性:

1
2
3
soup.find_all(href=re.compile("elsie"), id='link1')

# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

  1. attrs
      有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:
    1
    2
    3
    data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
    data_soup.find_all(data-foo="value")
    # SyntaxError: keyword can't be an expression

  但是可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的tag:

1
2
data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]

  1. 按CSS搜索

  按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字 class 在Python中是保留字,使用 class 做参数会导致语法错误.从Beautiful Soup的4.1.1版本开始,可以通过 class_ 参数搜索有指定CSS类名的tag:

1
2
3
4
5
6
7
soup.find_all("a", class_="sister")

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

重申:class_ 参数同样接受不同类型的 过滤器 ,字符串,正则表达式,方法或 True :

1
2
3
4
5
6
7
8
9
10
11
12
soup.find_all(class_=re.compile("itl"))
# [<p class="title"><b>The Dormouse's story</b></p>]

def has_six_characters(css_class):
return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  tag的 class 属性是 多值属性 。按照CSS类名搜索tag时,可以分别搜索tag中的每个CSS类名:

1
2
3
4
5
6
7
8
css_soup = BeautifulSoup('<p class="body strikeout"></p>')

css_soup.find_all("p", class_="strikeout")

# [<p class="body strikeout"></p>]
css_soup.find_all("p", class_="body")

# [<p class="body strikeout"></p>]

搜索 class 属性时也可以通过CSS值完全匹配:

1
2
css_soup.find_all("p", class_="body strikeout")
# [<p class="body strikeout"></p>]

完全匹配 class 的值时,如果CSS类名的顺序与实际不符,将搜索不到结果:

1
2
3
4
5
6
7
soup.find_all("a", attrs={"class": "sister"})

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

  1. string 参数

  通过 string 参数可以搜搜文档中的字符串内容。与 name 参数的可选值一样,,重申string 参数接受 字符串 , 正则表达式 , 列表, True . 看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
soup.find_all(string="Elsie")

# [u'Elsie']

soup.find_all(string=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(string=re.compile("Dormouse"))

[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):

""Return True if this string is the only child of its parent tag.""
return (s == s.parent.string)

soup.find_all(string=is_the_only_string_within_a_tag)
# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

  虽然 string 参数用于搜索字符串,还可以与其它参数混合使用来过滤tag。Beautiful Soup会找到 .string 方法与 string 参数值相符的tag.下面代码用来搜索内容里面包含“Elsie”的a标签:

1
2
3
soup.find_all("a", string="Elsie")

# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

  1. limit 参数

  find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢;如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量。效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果。
  文档树中有3个tag符合搜索条件,但结果只返回了2个,因为我们限制了返回数量:

1
2
3
4
5
soup.find_all("a", limit=2)

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

  1. recuresive参数
      调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False 。
    1
    2
    3
    4
    5
    soup.html.find_all("title")
    # [<title>The Dormouse's story</title>]

    soup.html.find_all("title", recursive=False)
    # []

注意Beautiful Soup 提供了多种DOM树搜索方法。这些方法都使用了类似的参数定义。 比如这些方法: find_all(): name, attrs, text, limit. 但是只有 find_all() 和 find() 支持 recursive 参数。

  1. 像调用 find_all() 一样调用tag

  find_all() 几乎是Beautiful Soup中最常用的搜索方法,所以我们定义了它的简写方法. BeautifulSoup 对象和 tag 对象可以被当作一个方法来使用,这个方法的执行结果与调用这个对象的 find_all() 方法相同,下面两行代码是等价的:

1
2
3
4
5
soup.find_all("a")
soup("a")

soup.title.find_all(string=True)
soup.title(string=True)

##find()方法剖析
find( name , attrs , recursive , string , **kwargs )

  find_all() 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个标签,那么使用 find_all() 方法来查找body标签就不太合适, 使用 find_all 方法并设置 limit=1 参数不如直接使用 find() 方法。下面两行代码是等价的:

1
2
3
4
5
soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]

soup.find('title')
# <title>The Dormouse's story</title>

  唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果。
  find_all() 方法没有找到目标是返回空列表,find() 方法找不到目标时,返回 None 。

##find_parents() 和 find_parent()
find_parents( name , attrs , recursive , string , kwargs )
find_parent( name , attrs , recursive , string ,
kwargs )

  我们已经用了很大篇幅来介绍 find_all() 和 find() 方法,Beautiful Soup中还有10个用于搜索的API.它们中的五个用的是与 find_all() 相同的搜索参数,另外5个与 find() 方法的搜索参数类似.区别仅是它们搜索文档的不同部分.

  记住: find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等. find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容. 我们从一个文档中的一个叶子节点开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a_string = soup.find(string="Lacie")

a_string

# u'Lacie'



a_string.find_parents("a")

# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]



a_string.find_parent("p")

# <p class="story">Once upon a time there were three little sisters; and their names were

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,

# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and

# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;

# and they lived at the bottom of a well.</p>



a_string.find_parents("p", class="title")

# []

  文档中的一个a标签是是当前叶子节点的直接父节点,所以可以被找到.还有一个p标签,是目标叶子节点的间接父辈节点,所以也可以被找到.包含class值为”title”的p标签不是不是目标叶子节点的父辈节点,所以通过 find_parents() 方法搜索不到.

  find_parent() 和 find_parents() 方法会让人联想到 .parent 和 .parents 属性.它们之间的联系非常紧密.搜索父辈节点的方法实际上就是对 .parents 属性的迭代搜索.

##find_next_siblings()和find_next_sibling()
find_next_siblings( name , attrs , recursive , string , **kwargs )

find_next_sibling( name , attrs , recursive , string , **kwargs )

  这2个方法通过 .next_siblings 属性对当tag的所有后面解析 的兄弟tag节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点, find_next_sibling() 只返回符合条件的后面的第一个tag节点.

##find_previous_siblings() 和 find_previous_sibling()
find_previous_siblings( name , attrs , recursive , string , kwargs )
find_previous_sibling( name , attrs , recursive , string ,
kwargs )

  这2个方法通过 .previous_siblings 属性对当前tag的前面解析 的兄弟tag节点进行迭代, find_previous_siblings() 方法返回所有符合条件的前面的兄弟节点, find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
last_link = soup.find("a", id="link3")

last_link
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_link.find_previous_siblings("a")

# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# <p class="title"><b>The Dormouse's story</b></p>

##find_all_next() 和 find_next()
  这2个方法通过 .next_elements 属性对当前tag的之后的 tag和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点:

1
2
3
4
5
6
7
8
9
10
first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_next(string=True)
# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")
# <p class="story">...</p>

  第一个例子中,字符串 “Elsie”也被显示出来,尽管它被包含在我们开始查找的a标签的里面.第二个例子中,最后一个p标签也被显示出来,尽管它与我们开始查找位置的a标签不属于同一部分.例子中,搜索的重点是要匹配过滤器的条件,并且在文档中出现的顺序而不是开始查找的元素的位置.

##find_all_previous() 和 find_previous()
find_all_previous( name , attrs , recursive , string , kwargs )
find_previous( name , attrs , recursive , string ,
kwargs )

  这2个方法通过 .previous_elements 属性对当前节点前面 的tag和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous() 方法返回第一个符合条件的节点.

1
2
3
4
5
6
7
8
9
10
first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_previous("p")
# [<p class="story">Once upon a time there were three little sisters; ...</p>,
# <p class="title"><b>The Dormouse's story</b></p>]

first_link.find_previous("title")
# <title>The Dormouse's story</title>

  find_all_previous(“p”) 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,p标签包含了我们开始查找的a标签.不要惊讶,这段代码的功能是查找所有出现在指定a标签之前的p标签,因为这个p标签包含了开始的a标签,所以p标签一定是在a之前出现的.

##CSS选择器
Beautiful Soup支持大部分的CSS选择器 , 在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数, 即可使用CSS选择器的语法找到tag:

1
2
3
4
5
soup.select("title")
# [<title>The Dormouse's story</title>]

soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]

通过tag标签逐层查找:

1
2
3
4
5
6
7
8
soup.select("body a")

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("html head title")
# [<title>The Dormouse's story</title>]

找到某个tag标签下的直接子标签 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
soup.select("head > title")
# [<title>The Dormouse's story</title>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("body > a")
# []

找到兄弟节点标签:

1
2
3
4
5
6
soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过CSS的类名查找:

1
2
3
4
5
6
7
8
9
10
11
通过CSS的类名查找:

soup.select(".sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("[class~=sister]")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过tag的id查找:

1
2
3
4
5
soup.select("#link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("a#link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

同时用多种CSS选择器查询元素:

1
2
3
soup.select("#link1,#link2")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

通过是否存在某个属性来查找:

1
2
3
4
5
soup.select('a[href]')

# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

通过属性的值来查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
soup.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select('a[href^="http://example.com/"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href*=".com/el"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

通过语言设置来查找:

1
2
3
4
5
6
7
8
9
10
11
12
multilingual_markup = """
<p lang="en">Hello</p>
<p lang="en-us">Howdy, y'all</p>
<p lang="en-gb">Pip-pip, old fruit</p>
<p lang="fr">Bonjour mes amis</p>
"""

multilingual_soup = BeautifulSoup(multilingual_markup)
multilingual_soup.select('p[lang|=en]')
# [<p lang="en">Hello</p>,
# <p lang="en-us">Howdy, y'all</p>,
# <p lang="en-gb">Pip-pip, old fruit</p>]

返回查找到的元素的第一个:

1
2
3
soup.select_one(".sister")

# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

  对于熟悉CSS选择器语法的人来说这是个非常方便的方法.Beautiful Soup也支持CSS选择器API, 如果你仅仅需要CSS选择器的功能,那么直接使用 lxml 也可以, 而且速度更快,支持更多的CSS选择器语法,但Beautiful Soup整合了CSS选择器的语法和自身方便使用API。

##修改文档树
BeautifuSoup的强项是文档树的搜索,当同时也可以方便的修改文档树。

  1. 修改tag的名称和属性

  在 Attributes 的章节中已经介绍过这个功能,但是再看一遍也无妨. 重命名一个tag,改变属性的值,添加或删除属性:

1
2
3
4
5
6
7
8
9
10
11
12
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

  1. 修改.string

  给tag的 .string 属性赋值,就相当于用当前的内容替代了原来的内容:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.string = "New link text."
tag
# <a href="http://example.com/">New link text.</a>

注意 如果当前的tag包含了其它tag,那么给它的 .string 属性赋值会覆盖掉原有的所有内容包括子tag。

  1. append()
      Tag.append() 方法想tag中添加内容,就好像Python的列表的 .append() 方法:

    1
    2
    3
    4
    5
    6
    soup = BeautifulSoup("<a>Foo</a>")
    soup.a.append("Bar")
    soup
    # <html><head></head><body><a>FooBar</a></body></html>
    soup.a.contents
    # [u'Foo', u'Bar']
  2. NavigableString() 和 .new_tag()

  如果想添加一段文本内容到文档中也没问题,可以调用Python的 append() 方法 或调用 NavigableString 的构造方法:

1
2
3
4
5
6
7
8
9
soup = BeautifulSoup("<b></b>")
tag = soup.b
tag.append("Hello")
new_string = NavigableString(" there")
tag.append(new_string)
tag
# <b>Hello there.</b>
tag.contents
# [u'Hello', u' there']

  如果想要创建一段注释,或 NavigableString 的任何子类, 只要调用 NavigableString 的构造方法:

1
2
3
4
5
6
7
from bs4 import Comment
new_comment = soup.new_string("Nice to see you.", Comment)
tag.append(new_comment)
tag
# <b>Hello there<!--Nice to see you.--></b>
tag.contents
# [u'Hello', u' there', u'Nice to see you.']

这是Beautiful Soup 4.2.1 中新增的方法

  • 创建一个tag最好的方法是调用工厂方法 BeautifulSoup.new_tag() :
1
2
3
4
5
6
7
8
9
soup = BeautifulSoup("<b></b>")
original_tag = soup.b
new_tag = soup.new_tag("a", href="http://www.example.com")
original_tag.append(new_tag)
original_tag
# <b><a href="http://www.example.com"></a></b>
new_tag.string = "Link text."
original_tag
# <b><a href="http://www.example.com">Link text.</a></b>

  第一个参数作为tag的name,是必填,其它参数选填。

  1. insert()

  Tag.insert() 方法与 Tag.append() 方法类似,区别是不会把新元素添加到父节点 .contents 属性的最后,而是把元素插入到指定的位置.与Python列表总的 .insert() 方法的用法下同:

1
2
3
4
5
6
7
8
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.insert(1, "but did not endorse ")
tag
# <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
tag.contents
# [u'I linked to ', u'but did not endorse', <i>example.com</i>]

  1. insert_before()和insert_after()

insert_before() 方法在当前tag或文本节点前插入内容:

1
2
3
4
5
6
soup = BeautifulSoup("<b>stop</b>")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# <b><i>Don't</i>stop</b>

insert_after() 方法在当前tag或文本节点后插入内容:

1
2
3
4
5
soup.b.i.insert_after(soup.new_string(" ever "))
soup.b
# <b><i>Don't</i> ever stop</b>
soup.b.contents
# [<i>Don't</i>, u' ever ', u'stop']

  1. clear()

Tag.clear() 方法移除当前tag的内容:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a
tag.clear()
tag
# <a href="http://example.com/"></a>

  1. extract()

PageElement.extract() 方法将当前tag移除文档树,并作为方法结果返回:

1
2
3
4
5
6
7
8
9
10
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
i_tag = soup.i.extract()
a_tag
# <a href="http://example.com/">I linked to</a>
i_tag
# <i>example.com</i>
print(i_tag.parent)
None

这个方法实际上产生了2个文档树: 一个是用来解析原始文档的 BeautifulSoup 对象,另一个是被移除并且返回的tag.被移除并返回的tag可以继续调用 extract 方法:

1
2
3
4
5
6
7
my_string = i_tag.string.extract()
my_string
# u'example.com'
print(my_string.parent)
# None
i_tag
# <i></i>

  1. decompose()

Tag.decompose() 方法将当前节点移除文档树并完全销毁:

1
2
3
4
5
6
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
soup.i.decompose()
a_tag
# <a href="http://example.com/">I linked to</a>

  1. replace_with()

PageElement.replace_with() 方法移除文档树中的某段内容,并用新tag或文本节点替代它:

1
2
3
4
5
6
7
8
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a
new_tag = soup.new_tag("b")
new_tag.string = "example.net"
a_tag.i.replace_with(new_tag)
a_tag
# <a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 方法返回被替代的tag或文本节点,可以用来浏览或添加到文档树其它地方.

  1. wrap()

    PageElement.wrap() 方法可以对指定的tag元素进行包装 ,并返回包装后的结果:

    1
    2
    3
    4
    5
    soup = BeautifulSoup("<p>I wish I was bold.</p>")
    soup.p.string.wrap(soup.new_tag("b"))
    # <b>I wish I was bold.</b>
    soup.p.wrap(soup.new_tag("div"))
    # <div><p><b>I wish I was bold.</b></p></div>

注意该方法在 Beautiful Soup 4.0.5 中添加

  1. unwra()
    Tag.unwrap() 方法与 wrap() 方法相反.将移除tag内的所有tag标签,该方法常被用来进行标记的解包:
    1
    2
    3
    4
    5
    6
    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup)
    a_tag = soup.a
    a_tag.i.unwrap()
    a_tag
    # <a href="http://example.com/">I linked to example.com</a>

与 replace_with() 方法相同, unwrap() 方法返回被移除的tag

##输出

  1. 格式化输出

  prettify() 方法将Beautiful Soup的文档树格式化后以Unicode编码输出,每个XML/HTML标签都独占一行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n <a href="http://example.com/">\n...'
print(soup.prettify())
# <html>
# <head>
# </head>
# <body>
# <a href="http://example.com/">
# I linked to
# <i>
# example.com
# </i>
# </a>
# </body>
# </html>

BeautifulSoup 对象和它的tag节点都可以调用 prettify() 方法.

  1. 压缩输出

  如果只想得到结果字符串,不重视格式,那么可以对一个 BeautifulSoup 对象或 Tag 对象使用Python的 unicode() 或 str() 方法:

1
2
3
4
str(soup)
# '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'
unicode(soup.a)
# u'<a href="http://example.com/">I linked to <i>example.com</i></a>'

  str() 方法返回UTF-8编码的字符串,可以指定 编码 的设置.还可以调用 encode() 方法获得字节码或调用 decode() 方法获得Unicode.

  1. 输出格式

Beautiful Soup输出是会将HTML中的特殊字符转换成Unicode,比如“&lquot;”:

1
2
3
soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'

如果将文档转换成字符串,Unicode编码会被编码成UTF-8.这样就无法正确显示HTML特殊字符了:

1
2
str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'

  1. get_text()

如果只想得到tag中包含的文本内容,那么可以调用 get_text() 方法,这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回:

1
2
3
4
5
6
markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)
soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'

可以通过参数指定tag的文本内容的分隔符:

1
2
# soup.get_text("|")
u'\nI linked to |example.com|\n'

还可以去除获得文本内容的前后空白:

1
2
# soup.get_text("|", strip=True)
u'I linked to|example.com'

或者使用 .stripped_strings 生成器,获得文本列表后手动处理列表:

1
2
[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']

##解析器

  1. 指定文档的解析器

  如果仅是想要解析HTML文档,只要用文档创建 BeautifulSoup 对象就可以了.Beautiful Soup会自动选择一个解析器来解析文档.但是还可以通过参数指定使用那种解析器来解析当前文档.

  BeautifulSoup 第一个参数应该是要被解析的文档字符串或是文件句柄,第二个参数用来标识怎样解析文档.如果第二个参数为空,那么Beautiful Soup根据当前系统安装的库自动选择解析器,解析器的优先数序: lxml, html5lib, Python标准库.在下面两种条件下解析器优先顺序会变化:

  • 要解析的文档是什么类型: 目前支持, “html”, “xml”, 和 “html5”
  • 指定使用哪种解析器: 目前支持, “lxml”, “html5lib”, 和 “html.parser”
      安装解析器 章节介绍了可以使用哪种解析器,以及如何安装.

  如果指定的解析器没有安装,Beautiful Soup会自动选择其它方案.目前只有 lxml 解析器支持XML文档的解析,在没有安装lxml库的情况下,创建 beautifulsoup 对象时无论是否指定使用lxml,都无法得到解析后的对象。

  1. 解析器之间的区别

  Beautiful Soup为不同的解析器提供了相同的接口,但解析器本身时有区别的.同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档.区别最大的是HTML解析器和XML解析器,看下面片段被解析成HTML结构:

1
2
BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>

  因为空标签b不符合HTML标准,所以解析器把它解析成b>/b
  同样的文档使用XML解析如下(解析XML需要安装lxml库).注意,空标签b 依然被保留,并且文档前添加了XML头,而不是被包含在html标签内:

1
2
3
BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>

  HTML解析器之间也有区别,如果被解析的HTML文档是标准格式,那么解析器之间没有任何差别,只是解析速度不同,结果都会返回正确的文档树.

  但是如果被解析文档不是标准格式,那么不同的解析器返回结果可能不同.下面例子中,使用lxml解析错误格式的文档,结果p标签被直接忽略掉了:

1
2
BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>

  使用html5lib库解析相同的文档会得到不同的结果

1
2
BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>

  html5lib库没有忽略掉p标签,而是自动补全了标签,还给文档树添加了head标签.使用pyhton内置库解析结果如下:

1
2
BeautifulSoup("<a></p>", "html.parser")
# <a></a>

  与lxml库类似的,Python内置库忽略掉了p标签,与html5lib库不同的是标准库没有尝试创建符合标准的文档格式或将文档片段包含在body标签内,与lxml不同的是标准库甚至连html标签都没有尝试去添加.
  因为文档片段“a/p”是错误格式,所以以上解析方式都能算作”正确”,html5lib库使用的是HTML5的部分标准,所以最接近”正确”.不过所有解析器的结构都能够被认为是”正常”的.
  不同的解析器可能影响代码执行结果,如果在分发给别人的代码中使用了 BeautifulSoup ,那么最好注明使用了哪种解析器,以减少不必要的麻烦.

##编码
  任何HTML或XML文档都有自己的编码方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文档都被转换成了Unicode:

1
2
3
4
5
6
markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'

  这不是魔术(但很神奇),Beautiful Soup用了 编码自动检测 子库来识别当前文档编码并转换成Unicode编码. BeautifulSoup 对象的 .original_encoding 属性记录了自动识别编码的结果:

1
2
soup.original_encoding
'utf-8'

  编码自动检测 功能大部分时候都能猜对编码格式,但有时候也会出错.有时候即使猜测正确,也是在逐个字节的遍历整个文档后才猜对的,这样很慢.如果预先知道文档编码,可以设置编码参数来减少自动检查编码出错的概率并且提高文档解析速度.在创建 BeautifulSoup 对象的时候设置 from_encoding 参数.
  下面一段文档用了ISO-8859-8编码方式,这段文档太短,结果Beautiful Soup以为文档是用ISO-8859-7编码:

1
2
3
4
5
6
markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
# 'ISO-8859-7'

通过传入 from_encoding 参数来指定编码方式:

1
2
3
4
5
soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>םולש</h1>
soup.original_encoding
# 'iso8859-8'

&esnp; 如果仅知道文档采用了Unicode编码, 但不知道具体编码. 可以先自己猜测, 猜测错误(依旧是乱码)时, 可以把错误编码作为 exclude_encodings 参数, 这样文档就不会尝试使用这种编码了解码了. 译者备注: 在没有指定编码的情况下, BS会自己猜测编码, 把不正确的编码排除掉, BS就更容易猜到正确编码.

1
2
3
4
5
soup = BeautifulSoup(markup, exclude_encodings=["ISO-8859-7"])
soup.h1
<h1>םולש</h1>
soup.original_encoding
# 'WINDOWS-1255'

&esnp; 猜测结果是 Windows-1255 编码, 猜测结果可能不够准确, 但是 Windows-1255 编码是 ISO-8859-8 的扩展集, 所以猜测结果已经十分接近了, 并且不影响使用. (exclude_encodings 参数是 4.4.0版本的新功能)

&esnp; 少数情况下(通常是UTF-8编码的文档中包含了其它编码格式的文件),想获得正确的Unicode编码就不得不将文档中少数特殊编码字符替换成特殊Unicode编码,“REPLACEMENT CHARACTER” (U+FFFD, �) [9] . 如果Beautifu Soup猜测文档编码时作了特殊字符的替换,那么Beautiful Soup会把 UnicodeDammit 或 BeautifulSoup 对象的 .contains_replacement_characters 属性标记为 True .这样就可以知道当前文档进行Unicode编码后丢失了一部分特殊内容字符.如果文档中包含�而 .contains_replacement_characters 属性是 False ,则表示�就是文档中原来的字符,不是转码失败.

  1. 输出编码

&esnp; 通过Beautiful Soup输出文档时,不管输入文档是什么编码方式,输出编码均为UTF-8编码,下面例子输入文档是Latin-1编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
markup = b'''
<html>
<head>
<meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
</head>
<body>
<p>Sacr\xe9 bleu!</p>
</body>
</html>
'''
soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
# <head>
# <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
# </head>
# <body>
# <p>
# Sacré bleu!
# </p>
# </body>
# </html>

&esnp; 注意,输出文档中的meta标签的编码设置已经修改成了与输出编码一致的UTF-8.

&esnp; 如果不想用UTF-8编码输出,可以将编码方式传入 prettify() 方法:

1
2
3
4
5
print(soup.prettify("latin-1"))
# <html>
# <head>
# <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...

  还可以调用 BeautifulSoup 对象或任意节点的 encode() 方法,就像Python的字符串调用 encode() 方法一样:

1
2
3
4
soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'
soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'

  如果文档中包含当前编码不支持的字符,那么这些字符将呗转换成一系列XML特殊字符引用,下面例子中包含了Unicode编码字符SNOWMAN:

1
2
3
markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b

  SNOWMAN字符在UTF-8编码中可以正常显示(看上去像是☃),但有些编码不支持SNOWMAN字符,比如ISO-Latin-1或ASCII,那么在这些编码中SNOWMAN字符会被转换成“&#9731”:

1
2
3
4
5
6
print(tag.encode("utf-8"))
# <b>☃</b>
print tag.encode("latin-1")
# <b>&#9731;</b>
print tag.encode("ascii")
# <b>&#9731;</b>

  1. Unicode, Dammit! (乱码, 靠!)

  译者备注: UnicodeDammit 是BS内置库, 主要用来猜测文档编码.

  编码自动检测 功能可以在Beautiful Soup以外使用,检测某段未知编码时,可以使用这个方法:

1
2
3
4
5
6
from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'

  如果Python中安装了 chardet 或 cchardet 那么编码检测功能的准确率将大大提高. 输入的字符越多,检测结果越精确,如果事先猜测到一些可能编码, 那么可以将猜测的编码作为参数,这样将优先检测这些编码:

1
2
3
4
5
dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'

  1. 智能引导

使用Unicode时,Beautiful Soup还会智能的把引号 转换成HTML或XML中的特殊字符:

1
2
3
4
5
markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

也可以把引号转换为ASCII码:

1
2
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'

很有用的功能,但是Beautiful Soup没有使用这种方式.默认情况下,Beautiful Soup把引号转换成Unicode:

1
2
UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'

  1. 矛盾的编码

  有时文档的大部分都是用UTF-8,但同时还包含了Windows-1252编码的字符,就像微软的智能引号 一样. 一些包含多个信息的来源网站容易出现这种情况. UnicodeDammit.detwingle() 方法可以把这类文档转换成纯UTF-8编码格式,看个简单的例子:

1
2
3
snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")

  这段文档很杂乱,snowmen是UTF-8编码,引号是Windows-1252编码,直接输出时不能同时显示snowmen和引号,因为它们编码不同:

1
2
3
4
5
print(doc)
# ☃☃☃�I like snowmen!�

print(doc.decode("windows-1252"))
# ☃☃☃“I like snowmen!”

  如果对这段文档用UTF-8解码就会得到 UnicodeDecodeError 异常,如果用Windows-1252解码就回得到一堆乱码. 幸好, UnicodeDammit.detwingle() 方法会吧这段字符串转换成UTF-8编码,允许我们同时显示出文档中的snowmen和引号:

1
2
3
new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”

  UnicodeDammit.detwingle() 方法只能解码包含在UTF-8编码中的Windows-1252编码内容,但这解决了最常见的一类问题.

  在创建 BeautifulSoup 或 UnicodeDammit 对象前一定要先对文档调用 UnicodeDammit.detwingle() 确保文档的编码方式正确.如果尝试去解析一段包含Windows-1252编码的UTF-8文档,就会得到一堆乱码,比如: ☃☃☃“I like snowmen!”.
  UnicodeDammit.detwingle() 方法在Beautiful Soup 4.1.0版本中新增

###比较对象是否相同
  两个 NavigableString 或 Tag 对象具有相同的HTML或XML结构时, Beautiful Soup就判断这两个对象相同. 这个例子中, 2个 b标签在 BS 中是相同的, 尽管他们在文档树的不同位置, 但是具有相同的表象: “b pizza /b”

1
2
3
4
5
6
7
markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
soup = BeautifulSoup(markup, 'html.parser')
first_b, second_b = soup.find_all('b')
print first_b == second_b
# True
print first_b.previous_element == second_b.previous_element
# False

如果想判断两个对象是否严格的指向同一个对象可以通过 is 来判断

1
2
print first_b is second_b
# False

###复制Beautiful Soup对象
copy.copy() 方法可以复制任意 Tag 或 NavigableString 对象

1
2
3
4
import copy
p_copy = copy.copy(soup.p)
print p_copy
# <p>I want <b>pizza</b> and more <b>pizza</b>!</p>

复制后的对象跟与对象是相等的, 但指向不同的内存地址

1
2
3
4
print soup.p == p_copy
# True
print soup.p is p_copy
# False

  源对象和复制对象的区别是源对象在文档树中, 而复制后的对象是独立的还没有添加到文档树中. 复制后对象的效果跟调用了 extract() 方法相同.

1
2
print p_copy.parent
# None

这是因为相等的对象不能同时插入相同的位置

###解析部分文档
  如果仅仅因为想要查找文档中的a>标签而将整片文档进行解析,实在是浪费内存和时间.最快的方法是从一开始就把a>标签以外的东西都忽略掉. SoupStrainer 类可以定义文档的某段内容,这样搜索文档时就不必先解析整篇文档,只会解析在 SoupStrainer 中定义过的文档. 创建一个 SoupStrainer 对象并作为 parse_only 参数给 BeautifulSoup 的构造方法即可

  1. SoupStrainer
      SoupStrainer 类接受与典型搜索方法相同的参数:name , attrs , recursive , string , **kwargs 。下面举例说明三种 SoupStrainer 对象:
    1
    2
    3
    4
    5
    6
    from bs4 import SoupStrainer
    only_a_tags = SoupStrainer("a")
    only_tags_with_id_link2 = SoupStrainer(id="link2")
    def is_short_string(string):
    return len(string) < 10
    only_short_strings = SoupStrainer(string=is_short_string)

再拿“爱丽丝”文档来举例,来看看使用三种 SoupStrainer 对象做参数会有什么不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
# <a class="sister" href="http://example.com/elsie" id="link1">
# Elsie
# </a>
# <a class="sister" href="http://example.com/lacie" id="link2">
# Lacie
# </a>
# <a class="sister" href="http://example.com/tillie" id="link3">
# Tillie
# </a>
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
# <a class="sister" href="http://example.com/lacie" id="link2">
# Lacie
# </a>
print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
# Elsie
# ,
# Lacie
# and
# Tillie
# ...
#

还可以将 SoupStrainer 作为参数传入 搜索文档树 中提到的方法.这可能不是个常用用法,所以还是提一下:

1
2
3
4
soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
# u'\n\n', u'...', u'\n']

###常见问题

  1. 代码诊断

  如果想知道Beautiful Soup到底怎样处理一份文档,可以将文档传入 diagnose() 方法(Beautiful Soup 4.2.0中新增),Beautiful Soup会输出一份报告,说明不同的解析器会怎样处理这段文档,并标出当前的解析过程会使用哪种解析器:

1
2
3
4
5
6
7
8
9
10
11
12
from bs4.diagnose import diagnose
data = open("bad.html").read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug 1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...

  diagnose() 方法的输出结果可能帮助你找到问题的原因,如果不行,还可以把结果复制出来以便寻求他人的帮助.

  1. 文档解析错误

  文档解析错误有两种.一种是崩溃,Beautiful Soup尝试解析一段文档结果却抛除了异常,通常是 HTMLParser.HTMLParseError;一种异常情况,是Beautiful Soup解析后的文档树看起来与原来的内容相差很多.

  这些错误几乎都不是Beautiful Soup的原因,这不是因为Beautiful Soup得代码写的太优秀,而是因为Beautiful Soup没有包含任何文档解析代码.异常产生自被依赖的解析器,如果解析器不能很好的解析出当前的文档,那么最好的办法是换一个解析器.更多细节查看 安装解析器 章节.

  最常见的解析错误是 HTMLParser.HTMLParseError: malformed start tag 和 HTMLParser.HTMLParseError: bad end tag .这都是由Python内置的解析器引起的,解决方法是 安装lxml或html5lib

  最常见的异常现象是当前文档找不到指定的Tag,而这个Tag光是用眼睛就足够发现的了. find_all() 方法返回 [] ,而 find() 方法返回 None .这是Python内置解析器的又一个问题: 解析器会跳过那些它不知道的tag.解决方法还是 安装lxml或html5lib

  1. 版本错误

;

  • SyntaxError: Invalid syntax (异常位置在代码行: ROOT_TAG_NAME = u’[document]’ ),因为Python2版本的代码没有经过迁移就在Python3中窒息感
  • ImportError: No module named HTMLParser 因为在Python3中执行Python2版本的Beautiful Soup
  • ImportError: No module named html.parser 因为在Python2中执行Python3版本的Beautiful Soup
  • ImportError: No module named BeautifulSoup 因为在没有安装BeautifulSoup3库的Python环境下执行代码,或忘记了BeautifulSoup4的代码需要从 bs4 包中引入
  • ImportError: No module named bs4 因为当前Python环境下还没有安装BeautifulSoup4

###解析成XML

  默认情况下,Beautiful Soup会将当前文档作为HTML格式解析,如果要解析XML文档,要在 BeautifulSoup 构造方法中加入第二个参数 “xml”:

1
soup = BeautifulSoup(markup, "xml")

当然,还需要 安装lxml
解析器的错误

  • 如果同样的代码在不同环境下结果不同,可能是因为两个环境下使用不同的解析器造成的.例如这个环境中安装了lxml,而另一个环境中只有html5lib, 解析器之间的区别 中说明了原因.修复方法是在 BeautifulSoup 的构造方法中中指定解析器
  • 因为HTML标签是 大小写敏感 的,所以3种解析器再出来文档时都将tag和属性转换成小写.例如文档中的 TAG会被转换为 tag.如果想要保留tag的大写的话,那么应该将文档 解析成XML .

杂项错误

  • UnicodeEncodeError: ‘charmap’ codec can’t encode character u’\xfoo’ in position bar (或其它类型的 UnicodeEncodeError )的错误,主要是两方面的错误(都不是Beautiful Soup的原因),第一种是正在使用的终端(console)无法显示部分Unicode,参考 Python wiki ,第二种是向文件写入时,被写入文件不支持部分Unicode,这时只要用 u.encode(“utf8”) 方法将编码转换为UTF-8.
  • KeyError: [attr] 因为调用 tag[‘attr’] 方法而引起,因为这个tag没有定义该属性.出错最多的是 KeyError: ‘href’ 和 KeyError: ‘class’ .如果不确定某个属性是否存在时,用 tag.get(‘attr’) 方法去获取它,跟获取Python字典的key一样
  • AttributeError: ‘ResultSet’ object has no attribute ‘foo’ 错误通常是因为把 find_all() 的返回结果当作一个tag或文本节点使用,实际上返回结果是一个列表或 ResultSet 对象的字符串,需要对结果进行循环才能得到每个节点的 .foo 属性.或者使用 find() 方法仅获取到一个节点
  • AttributeError: ‘NoneType’ object has no attribute ‘foo’ 这个错误通常是在调用了 find() 方法后直节点取某个属性 .foo 但是 find() 方法并没有找到任何结果,所以它的返回值是 None .需要找出为什么 find() 的返回值是 None .

如何提高效率

  Beautiful Soup对文档的解析速度不会比它所依赖的解析器更快,如果对计算时间要求很高或者计算机的时间比程序员的时间更值钱,那么就应该直接使用 lxml .
  换句话说,还有提高Beautiful Soup效率的办法,使用lxml作为解析器.Beautiful Soup用lxml做解析器比用html5lib或Python内置解析器速度快很多.
  安装 cchardet 后文档的解码的编码检测会速度更快
  解析部分文档 不会节省多少解析时间,但是会节省很多内存,并且搜索时也会变得更快.

需要的解析器
  Beautiful Soup 3曾使用Python的 SGMLParser 解析器,这个模块在Python3中已经被移除了.Beautiful Soup 4默认使用系统的 html.parser ,也可以使用lxml或html5lib扩展库代替.查看 安装解析器 章节

  因为解析器 html.parser 与 SGMLParser 不同. BS4 和 BS3 处理相同的文档会产生不同的对象结构. 使用lxml或html5lib解析文档的时候, 如果添加了 html.parser 参数, 解析的对象又回发生变化. 如果发生了这种情况, 只能修改对应的处文档结果处理代码了.

方法名的变化

  • renderContents -> encode_contents
  • replaceWith -> replace_with
  • replaceWithChildren -> unwrap
  • findAll -> find_all
  • findAllNext -> find_all_next
  • findAllPrevious -> find_all_previous
  • findNext -> find_next
  • findNextSibling -> find_next_sibling
  • findNextSiblings -> find_next_siblings
  • findParent -> find_parent
  • findParents -> find_parents
  • findPrevious -> find_previous
  • findPreviousSibling -> find_previous_sibling
  • findPreviousSiblings -> find_previous_siblings
  • nextSibling -> next_sibling
  • previousSibling -> previous_sibling

Beautiful Soup构造方法的参数部分也有名字变化:

  • BeautifulSoup(parseOnlyThese=…) -> BeautifulSoup(parse_only=…)
  • BeautifulSoup(fromEncoding=…) -> BeautifulSoup(from_encoding=…)

为了适配Python3,修改了一个方法名:

  • Tag.has_key() -> Tag.has_attr()

修改了一个属性名,让它看起来更专业点:

  • Tag.isSelfClosing -> Tag.is_empty_element

  修改了下面3个属性的名字,以免与Python保留字冲突.这些变动不是向下兼容的,如果在BS3中使用了这些属性,那么在BS4中这些代码无法执行.

  • UnicodeDammit.Unicode -> UnicodeDammit.Unicode_markup``
  • Tag.next -> Tag.next_element
  • Tag.previous -> Tag.previous_element

生成器
  将下列生成器按照PEP8标准重新命名,并转换成对象的属性:

  • childGenerator() -> children
  • nextGenerator() -> next_elements
  • nextSiblingGenerator() -> next_siblings
  • previousGenerator() -> previous_elements
  • previousSiblingGenerator() -> previous_siblings
  • recursiveChildGenerator() -> descendants
  • parentGenerator() -> parents

  BS3中有的生成器循环结束后会返回 None 然后结束.这是个bug.新版生成器不再返回 None .

  BS4中增加了2个新的生成器, .strings 和 stripped_strings . .strings 生成器返回NavigableString对象, .stripped_strings 方法返回去除前后空白的Python的string对象.

XML

  BS4中移除了解析XML的 BeautifulStoneSoup 类.如果要解析一段XML文档,使用 BeautifulSoup 构造方法并在第二个参数设置为“xml”.同时 BeautifulSoup 构造方法也不再识别 isHTML 参数.

  Beautiful Soup处理XML空标签的方法升级了.旧版本中解析XML时必须指明哪个标签是空标签. 构造方法的 selfClosingTags 参数已经不再使用.新版Beautiful Soup将所有空标签解析为空元素,如果向空元素中添加子节点,那么这个元素就不再是空元素了.

python的List与tuple操作方法详解

发表于 2017-05-07 | 分类于 Python

  列表是Python中最基本的数据结构,列表是最常用的Python数据类型,列表的数据项不需要具有相同的类型。列表中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是0,第二个索引是1,依此类推。
  Python有6个序列的内置类型,但最常见的是列表和元组,因为列表和元组操作基本相似,只是tuple一旦初始化就不能修改,這里只介绍列表的。序列都可以进行的操作包括索引,切片,加,乘,检查成员。此外,Python已经内置确定序列的长度以及确定最大和最小的元素的方法。

  1. 创建一个列表
      只要把逗号分隔的不同的数据项使用方括号括起来即可。如下所示:
    1
    2
    3
    list1 = ['physics', 'chemistry', 1997, 2000];
    list2 = [1, 2, 3, 4, 5 ];
    list3 = ["a", "b", "c", "d"];

  与字符串的索引一样,列表索引从0开始。列表可以进行截取、组合等。

  1. 访问列表中的值
      使用下标索引来访问列表中的值,同样你也可以使用方括号的形式截取字符, 如下所示:
    1
    2
    3
    4
    5
    6
    #!/usr/bin/python

    list1 = ['physics', 'chemistry', 1997, 2000];
    list2 = [1, 2, 3, 4, 5, 6, 7 ];
    print "list1[0]: ", list1[0]
    print "list2[1:5]: ", list2[1:5]

以上实例输出结果:

1
2
list1[0]:  physics
list2[1:5]: [2, 3, 4, 5]

  1. 更新列表
      你可以对列表的数据项进行修改或更新,你也可以使用append()方法来添加列表项,如下所示:
    1
    2
    3
    4
    5
    6
    7
    #!/usr/bin/python
    list = ['physics', 'chemistry', 1997, 2000];
    print "Value available at index 2 : "
    print list[2];
    list[2] = 2001;
    print "New value available at index 2 : "
    print list[2];

以上实例输出结果:

1
2
3
4
Value available at index 2 :
1997
New value available at index 2 :
2001

  1. 删除列表元素
      可以使用 del 语句来删除列表的的元素,如下实例:
    1
    2
    3
    4
    5
    6
    #!/usr/bin/python
    list1 = ['physics', 'chemistry', 1997, 2000];
    print list1;
    del list1[2];
    print "After deleting value at index 2 : "
    print list1;

以上实例输出结果:

1
2
3
['physics', 'chemistry', 1997, 2000]
After deleting value at index 2 :
['physics', 'chemistry', 2000]

  1. Python列表脚本操作符
      列表对 + 和 的操作符与字符串相似。+ 号用于组合列表, 号用于重复列表。如下所示:
Python 表达式 结果 描述
len([1, 2, 3]) 3 长度
[1, 2, 3] + [4, 5, 6] [1, 2, 3, 4, 5, 6] 组合
[‘Hi!’] * 4 [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] 重复
3 in [1, 2, 3] True 元素是否存在于列表中
for x in [1, 2, 3]: print x, 1 2 3 迭代
  1. Python列表截取或切片
    Python的列表截取与字符串操作类型,如下所示:
    1
    L = ['spam', 'Spam', 'SPAM!']

操作:

Python 表达式 结果 描述
L[2] ‘SPAM!’ 读取列表中第三个元素
L[-2] ‘Spam’ 读取列表中倒数第二个元素
L[1:] [‘Spam’, ‘SPAM!’] 从第二个元素开始截取列表
  1. Python列表操作的函数和方法
    列表操作包含以下函数:
  • cmp(list1, list2):比较两个列表的元素
  • len(list):列表元素个数
  • max(list):返回列表元素最大值
  • min(list):返回列表元素最小值
  • list(seq):将元组转换为列表
    列表操作包含以下方法:
  • list.append(obj):在列表末尾添加新的对象
  • list.count(obj):统计某个元素在列表中出现的次数
  • list.extend(seq):在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
  • list.index(obj):从列表中找出某个值第一个匹配项的索引位置,注意这个有个前提,obj必须在list中不然会抛异常
  • list.insert(index, obj):将对象插入列表
  • list.pop(obj=list[-1]):移除列表中的一个元素(默认最后一个元素),并且返回该元素的值
  • list.remove(obj):移除列表中某个值的第一个匹配项
  • list.reverse():反向列表中元素
  • list.sort([func]):对原列表进行排序

参考资料:

  • http://www.jb51.net/article/47978.htm
  • http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819318453af120e8751ea4d2696d8a1ffa5ffdfd7000

python编码中问题

发表于 2017-05-06 | 分类于 Python

1.mysql 模块集成:
  通过pycharm IDE引入mysql模块时会报错mysql引入错误,此时到mysql conenctor下载对应系统的mysql 模块安装包即可。

  1. 关于BeautifulSoup使用时引入问题:
      网上普遍的引入方法是:
    1
    from bs4 import BeautifulSoup

最近在使用是发现此方法报错,因此尝试使用:

1
from BeautifulSoup import BeautifulSoup

  发下该方法才正确,My god。
注意具体那个版本使用那种形式暂时不是很清楚,希望有人告知。

  1. python中import x和from x import y的区别
      简单解释:
    import x:把x给我;from x import y:从x里把y拿出来给我。
    举例子:
    1
    2
    import datetime
    print(datetime.datetime.now())

  引入的是整个datetime包

1
2
from datetime import datetime
print(datetime.now())

  只引入datetime包里的datetime类
注意两者的意义何在?:主要是引入内存中的东西大小不一样。

python切片

发表于 2017-05-06 | 分类于 Python

原理上分析切片运算:
  list的切片,内部是调用getitem,setitem,delitem和slice函数。而slice函数又是和range()函数相关的,给切片传递的键是一个特殊的slice对象。该对象拥有可描述所请求切片方位的属性,例如:

1
2
3
4
a = [ 1, 2, 3, 4, 5, 6 ]
x = a [ 1 : 5 ] # x = a.__getitem__( slice ( 1, 5, None ) )
a [ 1 : 3 ] = [10, 11, 12 ]# a.__setitem__( slice ( 1, 3, None ), [ 10, 11, 12 ] )
del a [ 1 : 4 ] # a.__delitem__( slice ( 1, 4, None ) )

Python 的切片功能实际上比很多程序员认为的更强大,其形式如下:

1
a=m[start:end:step]   #  带步进的切片(步进值=step)

可见,列表的下标有三个参数:start(起始下标),end(终止下标),step(变化量),简单解释如下:

  • 当step大于0时,beg默认为0,end默认为最末之后;
  • 当step小于0时,start默认为len(array)-1,end默认为开头之前;
  • 当step未给出时:start默认为1;

比较复杂的解释如下:
  注意:步进值为step

  1. 当step > 0 时切片从 start(含start)处开始,到end(不含end)处结束,从左往右,每隔(step-1)(索引之间的差仍为step,但相隔的元素是step-1个)个元素进行一次截取。这时,start 指向的位置应该在end指向的位置的左边,否则返回值为空;
  2. 当step < 0 时,切片从 start(含start)处开始,到end(不含end)处结束,从右往左,每隔(step-1)(索引之间的差仍为step,但相隔的元素是step-1个)个元素进行一次截取。这时,start 指向的位置应该在end指向的位置的右边,否则返回值为空;

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
str="abcdefh"
print str[0:3] #截取第一位到第三位的字符
print str[:] #截取字符串的全部字符
print str[6:] #截取第七个字符到结尾
print str[:-3] #截取从头开始到倒数第三个字符之前
print str[2] #截取第三个字符
print str[-1] #截取倒数第一个字符
print str[::-1] #创造一个与原字符串顺序相反的字符串
print str[-3:-1] #截取倒数第三位与倒数第一位之前的字符
print str[-3:] #截取倒数第三位到结尾
print str[:-5:-3] #逆序截取,从末尾到倒数第五个,每三步截取一个;
print str[:-5:-3] #逆序截取,从末尾到开始,每三步截取一个;

参考资料:

  1. http://www.cnblogs.com/weidiao/p/6428681.html
  2. http://www.pythontab.com/html/2013/pythonjichu_0128/182.html

python字符串连接的N种方式

发表于 2017-05-06 | 分类于 Python

  python中有很多字符串连接方式,今天在写代码,顺便总结一下:

  • 最原始的字符串连接方式:str1 + str2
  • python 新字符串连接语法:str1, str2
  • 奇怪的字符串方式:str1 str2
  • % 连接字符串:‘name:%s; sex: ’ % (‘tom’, ‘male’)
  • 字符串列表连接:str.join(some_list)

第一种  想必只要是有编程经验的人,估计都知道,直接用 “+” 来连接两个字符串:

1
'Jim' + 'Green' #结果:'JimGreen'

第二种  比较特殊,如果两个字符串用“逗号”隔开,那么这两个字符串将被连接,但是,字符串之间会多出一个空格:

1
'Jim', 'Green' #结果: 'Jim Green'

第三种   python 独有的,只要把两个字符串放在一起,中间有空白或者没有空白:两个字符串自动连接为一个字符串:

1
2
'Jim''Green'   #结果: 'JimGreen'
'Jim' 'Green' #结果: 'JimGreen'

第四种  功能比较强大,借鉴了C语言中 printf 函数的功能,如果你有C语言基础,看下文档就知道了。这种方式用符号“%”连接一个字符串和一组变量,字符串中的特殊标记会被自动用右边变量组中的变量替换:

1
'%s, %s' % ('Jim', 'Green') #结果: 'Jim, Green'

第五种  就属于技巧了,利用字符串的函数 join 。这个函数接受一个列表,然后用字符串依次连接列表中每一个元素:

1
2
3
var_list = ['tom', 'david', 'john']
a = '###'
a.join(var_list) #结果: 'tom###david###john'

其实  python 中还有一种字符串连接方式,不过用的不多,就是字符串乘法,如:

1
2
a = 'abc'
a * 3 #结果 'abcabcabc'

python中@的作用

发表于 2017-05-06 | 分类于 Python

  在python2.4以上的函数中经常看到函数定义上一行有@functionName的修饰,注意下这个语法细节,有点像c语言中带参数的宏操作,当解释器读到这样的修饰之后,会先解析@后的内容,直接把@下一行的函数或者类作为@后边的函数的参数,然后将返回值赋值给下一行修饰的函数对象。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# !/usr/bin/env python
# -*- coding: utf-8 -*-

def funa(a):
print "funa"


def funb(b):
print "funB"

@funa
@funb
def func(c):
print "funC"


if __name__ == "__main__":
func

  最后得到的结果是:

1
2
funB  
funA

  通过结果我们可以认为:return funa(funb(func)),也即第一个函数修饰符开始,自下而上做参数传递,这样实际是使用了约定的函数修饰符达到函数嵌套的目的。记得没错Java中SSH也有这样的用法哦。
其他例子:
1.

1
2
3
4
5
6
7
8
9
10
def spamrun(fn):
def sayspam(*args):
print "spam,spam,spam"
return sayspam

@spamrun
def useful(a,b):
print a**2+b**2

useful(3,4)

结果:

1
spam,spam,spam

  解释:只是将函数useful传递给spamrun,并没用使用,因此最终返回的是spamrun函数的结果。
2.

1
2
3
4
5
6
def spamrun(fn):
print "spam,spam,spam"

@spamrun
def useful(a,b):
print a**2+b**2

结果:

1
spam,spam,spam

  解释:只是将函数useful传递给spamrun,并没用使用,因此最终返回的是spamrun函数的结果。
3.

1
2
3
4
5
6
7
8
9
10
11
def decorator(fn):
def test(*args):
print "My god!"*3
return fn(*args)
return test
@decorator
def other(a,b):
print a**2+b**2
if __name__=="__main__":
other(4,3)
other(3,4)

结果:

1
2
3
4
My god!My god!My god!
25
My god!My god!My god!
25

注释掉//return fn(*args),结果:

1
2
My god!My god!My god!
My god!My god!My god!

  解释:要想使other函数能正常运行(即调用other函数并将其参数继续往下传),必须加返回值,@decorator是一个statement,会将other函数当作参数传入来执行test方法。

  总结:’@’符号用作函数修饰符是python2.4新增加的功能,修饰符必须出现在函数定义前一行,不允许和函数定义在同一行。也就是说@A def f(): 是非法的。 只可以在模块或类定义层内对函数进行修饰,不允许修修饰一个类。一个修饰符就是一个函数,它将被修饰的函数做为参数,并返回修饰后的同名函数或其它可调用的东西。

  感悟:@的作用有点像代理模式,即@functionName修饰的函数是由functionName函数管理。

1…456

CallteFoot

The blog from a Android coder

56 日志
23 分类
71 标签
© 2020 CallteFoot
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4