本文阅读的代码为2020年11月1日下载的github的最新master。
如果代码后续更新了请以github为准。

1 CMakelist.txt 与 package.xml

1.1 package.xml

package.xml 里的依赖:除了ros的常规依赖外,还有cv_bridge, pcl_conversions, GTSAM 三者。

1.2 CMakelist.txt

CMaKelist.txt里还指明了其他的依赖。

find_package(OpenMP REQUIRED)
find_package(PCL REQUIRED QUIET)
find_package(OpenCV REQUIRED QUIET)
find_package(GTSAM REQUIRED QUIET)

没有写install部分。

还生成了一个msg,其内容如下

# Cloud Info
Header header int32[] startRingIndex
int32[] endRingIndexint32[]  pointColInd # point column index in range image
float32[] pointRange # point range int64 imuAvailable
int64 odomAvailable# Attitude for LOAM initialization
float32 imuRollInit
float32 imuPitchInit
float32 imuYawInit# Initial guess from imu pre-integration
float32 initialGuessX
float32 initialGuessY
float32 initialGuessZ
float32 initialGuessRoll
float32 initialGuessPitch
float32 initialGuessYaw# Point cloud messages
sensor_msgs/PointCloud2 cloud_deskewed  # 去畸变后的雷达数据 original cloud deskewed
sensor_msgs/PointCloud2 cloud_corner    # 提取处的角点特征 extracted corner feature
sensor_msgs/PointCloud2 cloud_surface   # 提取处的面特征 extracted surface feature

2 include/utility.h

这个文件的内容很简单,可以分为3个部分。
第一部分: 各种 #include
第二部分: 定义了一个类 ParamServer。

  • 类的成员变量就是 config/param.yml 里所有的参数。
  • 只有2个成员函数
    – 构造函数: 从ros的参数服务器里读取所有参数,并赋值给成员变量
    – imuConverter() : 将ros下的imu数据 根据imu的外参 进行数值变换。

第三部分: 将ros的imu数据解析处理的函数,发布ros topic的函数,以及2个计算点距离的函数。

3 src/imageProjection.cpp

3.1 首先声明了 2 个数据类型

// Velodyne
struct PointXYZIRT
{PCL_ADD_POINT4DPCL_ADD_INTENSITY;uint16_t ring; // 使用了 ring 信息,雷达数据位于第几条扫描线上float time;EIGEN_MAKE_ALIGNED_OPERATOR_NEW // Eigen的字段对齐
} EIGEN_ALIGN16;// 将 PointXYZIRT 在pcl中注册,其包括的字段为 x,y,z,intensity,ring,time
POINT_CLOUD_REGISTER_POINT_STRUCT (PointXYZIRT,  (float, x, x) (float, y, y) (float, z, z) (float, intensity, intensity)(uint16_t, ring, ring) (float, time, time)// Ouster 雷达的数据类型
struct PointXYZIRT {PCL_ADD_POINT4D;float intensity;uint32_t t;uint16_t reflectivity;uint8_t ring;uint16_t noise;uint32_t range;EIGEN_MAKE_ALIGNED_OPERATOR_NEW
}EIGEN_ALIGN16;POINT_CLOUD_REGISTER_POINT_STRUCT(PointXYZIRT,(float, x, x) (float, y, y) (float, z, z) (float, intensity, intensity)(uint32_t, t, t) (uint16_t, reflectivity, reflectivity)(uint8_t, ring, ring) (uint16_t, noise, noise) (uint32_t, range, range)
)

3.2 main()

可以看到,使用了3个线程,这3个线程是ros自己管理的,灵活调用,有可能一个函数同时占用2个线程,并不是为每个回调分配1个线程。

int main(int argc, char** argv)
{ros::init(argc, argv, "lio_sam");ImageProjection IP;ROS_INFO("\033[1;32m----> Image Projection Started.\033[0m");ros::MultiThreadedSpinner spinner(3);spinner.spin();return 0;
}

3.3 ImageProjection类

类的成员变量为

// imu deque 的数据长度
const int queueLength = 2000;class ImageProjection : public ParamServer
{private:std::mutex imuLock;std::mutex odoLock;ros::Subscriber subLaserCloud;ros::Publisher  pubLaserCloud;ros::Publisher pubExtractedCloud;ros::Publisher pubLaserCloudInfo;// 保存imu数据ros::Subscriber subImu;std::deque<sensor_msgs::Imu> imuQueue;// 保存 odom 数据ros::Subscriber subOdom;std::deque<nav_msgs::Odometry> odomQueue;// 保存雷达数据std::deque<sensor_msgs::PointCloud2> cloudQueue;sensor_msgs::PointCloud2 currentCloudMsg;double *imuTime = new double[queueLength];double *imuRotX = new double[queueLength];double *imuRotY = new double[queueLength];double *imuRotZ = new double[queueLength];int imuPointerCur;bool firstPointFlag;Eigen::Affine3f transStartInverse;pcl::PointCloud<PointXYZIRT>::Ptr laserCloudIn;pcl::PointCloud<PointType>::Ptr   fullCloud;    // 一帧雷达所有的数据pcl::PointCloud<PointType>::Ptr   extractedCloud;int deskewFlag;cv::Mat rangeMat;bool odomDeskewFlag;float odomIncreX;float odomIncreY;float odomIncreZ;// 自己定义的消息类型lio_sam::cloud_info cloudInfo;double timeScanCur;double timeScanEnd;std_msgs::Header cloudHeader;};

4 ImageProjection类的成员函数

4.1 构造函数

    ImageProjection() : deskewFlag(0){// 订阅topic, 指定hints到roscpp的传输层, 使用 使用没延迟的TCP通讯subImu        = nh.subscribe<sensor_msgs::Imu>(imuTopic, 2000, &ImageProjection::imuHandler, this, ros::TransportHints().tcpNoDelay());subOdom       = nh.subscribe<nav_msgs::Odometry>(odomTopic+"_incremental", 2000, &ImageProjection::odometryHandler, this, ros::TransportHints().tcpNoDelay());subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>(pointCloudTopic, 5, &ImageProjection::cloudHandler, this, ros::TransportHints().tcpNoDelay());pubExtractedCloud = nh.advertise<sensor_msgs::PointCloud2> ("lio_sam/deskew/cloud_deskewed", 1);pubLaserCloudInfo = nh.advertise<lio_sam::cloud_info> ("lio_sam/deskew/cloud_info", 1);allocateMemory();       // 为各个指针分配内存resetParameters();      // 为其他参数进行初始化与重置// pcl控制台输出error信息pcl::console::setVerbosityLevel(pcl::console::L_ERROR);}// 为3个指针分配内存void allocateMemory(){// 估计是考虑到这3个指针从头到尾都持续存在吧,所以并没有delete...laserCloudIn.reset(new pcl::PointCloud<PointXYZIRT>());fullCloud.reset(new pcl::PointCloud<PointType>());extractedCloud.reset(new pcl::PointCloud<PointType>());fullCloud->points.resize(N_SCAN*Horizon_SCAN);cloudInfo.startRingIndex.assign(N_SCAN, 0);cloudInfo.endRingIndex.assign(N_SCAN, 0);cloudInfo.pointColInd.assign(N_SCAN*Horizon_SCAN, 0);cloudInfo.pointRange.assign(N_SCAN*Horizon_SCAN, 0);// 怎么调用了2遍 resetParameters()resetParameters();}// 为其他参数进行初始化与重置void resetParameters(){laserCloudIn->clear();extractedCloud->clear();// 深度图像的初始化 reset range matrix for range image projectionrangeMat = cv::Mat(N_SCAN, Horizon_SCAN, CV_32F, cv::Scalar::all(FLT_MAX));imuPointerCur = 0;firstPointFlag = true;odomDeskewFlag = false;for (int i = 0; i < queueLength; ++i){imuTime[i] = 0;imuRotX[i] = 0;imuRotY[i] = 0;imuRotZ[i] = 0;}}

4.2 imu与odom的回调

    // imu的回调,先转换坐标系,再保存数据void imuHandler(const sensor_msgs::Imu::ConstPtr& imuMsg){// 将imu的数据 转换到 lidar 坐标系下 ???sensor_msgs::Imu thisImu = imuConverter(*imuMsg);std::lock_guard<std::mutex> lock1(imuLock);imuQueue.push_back(thisImu);    // 保存数据}// odom的回调,仅仅保存数据void odometryHandler(const nav_msgs::Odometry::ConstPtr& odometryMsg){std::lock_guard<std::mutex> lock2(odoLock);odomQueue.push_back(*odometryMsg);}

4.3 雷达点云的回调

    // 雷达点云的回调void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg){// 首先检查点云的队列是否少于等于2个// 之后将ros格式的点云 转成 自定义的pcl格式的点云,并获取雷达数据的起始和终止时间,以及检查是否存在nan// 当第三个雷达数据到达的时候,会检查第一个雷达数据的field,是否存在ring与time字段,这个检查只会进行一次if (!cachePointCloud(laserCloudMsg))return;// 获取当前点云持续时间的旋转平移// 旋转的角速度由imu积分得到,平移量由里程计得到if (!deskewInfo())return;// 将校正畸变后的点云转换成range image,并存入fullCloudprojectPointCloud();cloudExtraction();// 发布 cloud_deskewed 和 cloudInfopublishClouds();// 参数进行初始化与重置resetParameters();}

5

5.1 cachePointCloud()

    // 首先检查点云的队列是否少于等于2个// 之后将ros格式的点云 转成 自定义的pcl格式的点云,并获取雷达数据的起始和终止时间,以及检查是否存在nan// 当第三个雷达数据到达的时候,会检查第一个雷达数据的field,是否存在ring与time字段,这个检查只会进行一次bool cachePointCloud(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg){// cache point cloudcloudQueue.push_back(*laserCloudMsg);// 点云的个数少于等于2个,不做处理,没法匹配if (cloudQueue.size() <= 2)return false;// convert cloudcurrentCloudMsg = cloudQueue.front();cloudQueue.pop_front();// 将ros格式的点云 转成 自定义的pcl格式的点云pcl::fromROSMsg(currentCloudMsg, *laserCloudIn);// get timestampcloudHeader = currentCloudMsg.header;timeScanCur = cloudHeader.stamp.toSec(); // 认为ros中header的时间为这一帧雷达数据的起始时间timeScanEnd = timeScanCur + laserCloudIn->points.back().time; // Velodyne// timeScanEnd = timeScanCur + (float)laserCloudIn->points.back().t / 1000000000.0; // Ouster数据的时间格式与vlp不一样// check dense flag 点云数据不能包含 nanif (laserCloudIn->is_dense == false){ROS_ERROR("Point cloud is not in dense format, please remove NaN points first!");ros::shutdown();}// check ring channel 检查是否存在 ring 字段static int ringFlag = 0;if (ringFlag == 0){ringFlag = -1;for (int i = 0; i < (int)currentCloudMsg.fields.size(); ++i){if (currentCloudMsg.fields[i].name == "ring"){ringFlag = 1;break;}}if (ringFlag == -1){ROS_ERROR("Point cloud ring channel not available, please configure your point cloud data!");ros::shutdown();}}   // check point time 检查是否存在 time 字段if (deskewFlag == 0){deskewFlag = -1;for (int i = 0; i < (int)currentCloudMsg.fields.size(); ++i){if (currentCloudMsg.fields[i].name == timeField){deskewFlag = 1;break;}}if (deskewFlag == -1)ROS_WARN("Point cloud timestamp not available, deskew function disabled, system will drift significantly!");}return true;}

5.2 deskewInfo()

    // 获取当前点云持续时间的旋转平移// 旋转的角速度由imu积分得到,平移量由里程计得到bool deskewInfo(){std::lock_guard<std::mutex> lock1(imuLock);std::lock_guard<std::mutex> lock2(odoLock);// make sure IMU data available for the scan// imu数据队列的头尾的时间戳要在雷达数据的时间段之外if (imuQueue.empty() || imuQueue.front().header.stamp.toSec() > timeScanCur || imuQueue.back().header.stamp.toSec() < timeScanEnd){ROS_DEBUG("Waiting for IMU data ...");return false;}imuDeskewInfo();odomDeskewInfo();return true;}// 修剪imu的数据队列,直到imu的时间处于这帧点云的时间内// 之后对当前队列中的imu进行积分,角加速度乘时间,求得旋转的角速度void imuDeskewInfo(){// cloudInfo 中imu的数据队列正在处理,暂时不可用cloudInfo.imuAvailable = false;// 修剪imu的数据队列,直到imu的时间处于这帧点云的时间前0.01秒内while (!imuQueue.empty()){if (imuQueue.front().header.stamp.toSec() < timeScanCur - 0.01)imuQueue.pop_front();elsebreak;}if (imuQueue.empty())return;imuPointerCur = 0;for (int i = 0; i < (int)imuQueue.size(); ++i){sensor_msgs::Imu thisImuMsg = imuQueue[i];double currentImuTime = thisImuMsg.header.stamp.toSec();// get roll, pitch, and yaw estimation for this scan// imu的时间合适,转变成 rpy if (currentImuTime <= timeScanCur)imuRPY2rosRPY(&thisImuMsg, &cloudInfo.imuRollInit, &cloudInfo.imuPitchInit, &cloudInfo.imuYawInit);if (currentImuTime > timeScanEnd + 0.01)break;if (imuPointerCur == 0){imuRotX[0] = 0;imuRotY[0] = 0;imuRotZ[0] = 0;imuTime[0] = currentImuTime;++imuPointerCur;continue;}// get angular velocitydouble angular_x, angular_y, angular_z;imuAngular2rosAngular(&thisImuMsg, &angular_x, &angular_y, &angular_z);// integrate rotation// 对imu的角加速度进行积分,角加速度 * ( 当前帧imu的时间 - 上一帧imu的时间 )double timeDiff = currentImuTime - imuTime[imuPointerCur-1];imuRotX[imuPointerCur] = imuRotX[imuPointerCur-1] + angular_x * timeDiff;imuRotY[imuPointerCur] = imuRotY[imuPointerCur-1] + angular_y * timeDiff;imuRotZ[imuPointerCur] = imuRotZ[imuPointerCur-1] + angular_z * timeDiff;imuTime[imuPointerCur] = currentImuTime;++imuPointerCur;}--imuPointerCur;if (imuPointerCur <= 0)return;cloudInfo.imuAvailable = true;}// 获取这帧点云时间内的 第一个和最后一个里程计信息,将第一个odom当成初始值放入cloudInfo// 求得第一个和最后一个里程计间的平移量void odomDeskewInfo(){cloudInfo.odomAvailable = false;// 修剪odom的数据队列,直到odom的时间处于这帧点云的时间前0.01秒内while (!odomQueue.empty()){if (odomQueue.front().header.stamp.toSec() < timeScanCur - 0.01)odomQueue.pop_front();elsebreak;}if (odomQueue.empty())return;if (odomQueue.front().header.stamp.toSec() > timeScanCur)return;// get start odometry at the beinning of the scannav_msgs::Odometry startOdomMsg;// 获取odom的时间 大于等于 雷达当前时间的 odomfor (int i = 0; i < (int)odomQueue.size(); ++i){startOdomMsg = odomQueue[i];if (ROS_TIME(&startOdomMsg) < timeScanCur)continue;elsebreak;}tf::Quaternion orientation;tf::quaternionMsgToTF(startOdomMsg.pose.pose.orientation, orientation);double roll, pitch, yaw;tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);// Initial guess used in mapOptimizationcloudInfo.initialGuessX = startOdomMsg.pose.pose.position.x;cloudInfo.initialGuessY = startOdomMsg.pose.pose.position.y;cloudInfo.initialGuessZ = startOdomMsg.pose.pose.position.z;cloudInfo.initialGuessRoll  = roll;cloudInfo.initialGuessPitch = pitch;cloudInfo.initialGuessYaw   = yaw;cloudInfo.odomAvailable = true;// get end odometry at the end of the scanodomDeskewFlag = false;// odom最后一个数据的时间 比 雷达结束的时间早,不能覆盖点云的时间if (odomQueue.back().header.stamp.toSec() < timeScanEnd)return;// 获取第一个比 雷达结束的时间 晚的 odomnav_msgs::Odometry endOdomMsg;for (int i = 0; i < (int)odomQueue.size(); ++i){endOdomMsg = odomQueue[i];if (ROS_TIME(&endOdomMsg) < timeScanEnd)continue;elsebreak;}if (int(round(startOdomMsg.pose.covariance[0])) != int(round(endOdomMsg.pose.covariance[0])))return;Eigen::Affine3f transBegin = pcl::getTransformation(startOdomMsg.pose.pose.position.x, startOdomMsg.pose.pose.position.y, startOdomMsg.pose.pose.position.z, roll, pitch, yaw);tf::quaternionMsgToTF(endOdomMsg.pose.pose.orientation, orientation);tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);Eigen::Affine3f transEnd = pcl::getTransformation(endOdomMsg.pose.pose.position.x, endOdomMsg.pose.pose.position.y, endOdomMsg.pose.pose.position.z, roll, pitch, yaw);Eigen::Affine3f transBt = transBegin.inverse() * transEnd;// 通过 transBt 获取 odomIncreX等,每一帧点云数据的odom变化量float rollIncre, pitchIncre, yawIncre;pcl::getTranslationAndEulerAngles(transBt, odomIncreX, odomIncreY, odomIncreZ, rollIncre, pitchIncre, yawIncre);odomDeskewFlag = true;}

5.3 projectPointCloud()

    // 将校正畸变后的点云转换成range image,并存入fullCloudvoid projectPointCloud(){int cloudSize = laserCloudIn->points.size();// range image projectionfor (int i = 0; i < cloudSize; ++i){PointType thisPoint;thisPoint.x = laserCloudIn->points[i].x;thisPoint.y = laserCloudIn->points[i].y;thisPoint.z = laserCloudIn->points[i].z;thisPoint.intensity = laserCloudIn->points[i].intensity;// 求得当前点对应 range image 的 rangefloat range = pointDistance(thisPoint);if (range < lidarMinRange || range > lidarMaxRange)continue;// 求得当前点对应 range image 的 行索引int rowIdn = laserCloudIn->points[i].ring;if (rowIdn < 0 || rowIdn >= N_SCAN)continue;if (rowIdn % downsampleRate != 0)continue;// 求得点的水平角(deg)float horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;// 水平分辨率static float ang_res_x = 360.0/float(Horizon_SCAN);// 求得当前点对应 range image 的 列索引int columnIdn = -round((horizonAngle-90.0)/ang_res_x) + Horizon_SCAN/2;if (columnIdn >= Horizon_SCAN)columnIdn -= Horizon_SCAN;if (columnIdn < 0 || columnIdn >= Horizon_SCAN)continue;if (rangeMat.at<float>(rowIdn, columnIdn) != FLT_MAX)continue;// 对点云中的每个点进行 畸变校正thisPoint = deskewPoint(&thisPoint, laserCloudIn->points[i].time); // Velodyne// thisPoint = deskewPoint(&thisPoint, (float)laserCloudIn->points[i].t / 1000000000.0); // OusterrangeMat.at<float>(rowIdn, columnIdn) = range;// 并且 将这个点存到 fullCloud 中int index = columnIdn + rowIdn * Horizon_SCAN;fullCloud->points[index] = thisPoint;}}// 根据点云中某点的时间戳赋予其对应插值得到的旋转量void findRotation(double pointTime, float *rotXCur, float *rotYCur, float *rotZCur){*rotXCur = 0; *rotYCur = 0; *rotZCur = 0;// 找到在 pointTime 之后的imu数据int imuPointerFront = 0;while (imuPointerFront < imuPointerCur){if (pointTime < imuTime[imuPointerFront])break;++imuPointerFront;}// 如果上边的循环没进去或者到了最大执行次数,则只能近似的将当前的旋转 赋值过去if (pointTime > imuTime[imuPointerFront] || imuPointerFront == 0){*rotXCur = imuRotX[imuPointerFront];*rotYCur = imuRotY[imuPointerFront];*rotZCur = imuRotZ[imuPointerFront];} else {int imuPointerBack = imuPointerFront - 1;// 算一下该点时间戳在本组imu中的位置,线性插值double ratioFront = (pointTime - imuTime[imuPointerBack]) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);double ratioBack = (imuTime[imuPointerFront] - pointTime) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);// 按前后百分比赋予旋转量*rotXCur = imuRotX[imuPointerFront] * ratioFront + imuRotX[imuPointerBack] * ratioBack;*rotYCur = imuRotY[imuPointerFront] * ratioFront + imuRotY[imuPointerBack] * ratioBack;*rotZCur = imuRotZ[imuPointerFront] * ratioFront + imuRotZ[imuPointerBack] * ratioBack;}}// 如果运动慢,点云由于运动导致的矫正不如没有void findPosition(double relTime, float *posXCur, float *posYCur, float *posZCur){*posXCur = 0; *posYCur = 0; *posZCur = 0;// If the sensor moves relatively slow, like walking speed, positional deskew seems to have little benefits. Thus code below is commented.// if (cloudInfo.odomAvailable == false || odomDeskewFlag == false)//     return;// float ratio = relTime / (timeScanEnd - timeScanCur);// *posXCur = ratio * odomIncreX;// *posYCur = ratio * odomIncreY;// *posZCur = ratio * odomIncreZ;}// 对点云的每个点进行畸变校正PointType deskewPoint(PointType *point, double relTime){if (deskewFlag == -1 || cloudInfo.imuAvailable == false)return *point;double pointTime = timeScanCur + relTime;float rotXCur, rotYCur, rotZCur;findRotation(pointTime, &rotXCur, &rotYCur, &rotZCur);float posXCur, posYCur, posZCur;findPosition(relTime, &posXCur, &posYCur, &posZCur);// 点云中的第一个点 求 transStartInverse,之后在这帧数据畸变过程中不再改变if (firstPointFlag == true){transStartInverse = (pcl::getTransformation(posXCur, posYCur, posZCur, rotXCur, rotYCur, rotZCur)).inverse();firstPointFlag = false;}// transform points to startEigen::Affine3f transFinal = pcl::getTransformation(posXCur, posYCur, posZCur, rotXCur, rotYCur, rotZCur);// 该点相对第一个点的变换矩阵 = 第一个点在lidar世界坐标系下的变换矩阵的逆 × 当前点时lidar世界坐标系下变换矩阵Eigen::Affine3f transBt = transStartInverse * transFinal;// 根据lidar位姿变换,修正点云位置PointType newPoint;newPoint.x = transBt(0,0) * point->x + transBt(0,1) * point->y + transBt(0,2) * point->z + transBt(0,3);newPoint.y = transBt(1,0) * point->x + transBt(1,1) * point->y + transBt(1,2) * point->z + transBt(1,3);newPoint.z = transBt(2,0) * point->x + transBt(2,1) * point->y + transBt(2,2) * point->z + transBt(2,3);newPoint.intensity = point->intensity;return newPoint;}

5.4 publishClouds()

// 发布 cloud_deskewed 和 cloudInfo
void publishClouds()
{cloudInfo.header = cloudHeader;cloudInfo.cloud_deskewed  = publishCloud(&pubExtractedCloud, extractedCloud, cloudHeader.stamp, lidarFrame);pubLaserCloudInfo.publish(cloudInfo);
}sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud<PointType>::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame)
{sensor_msgs::PointCloud2 tempCloud;pcl::toROSMsg(*thisCloud, tempCloud);tempCloud.header.stamp = thisStamp;tempCloud.header.frame_id = thisFrame;if (thisPub->getNumSubscribers() != 0)thisPub->publish(tempCloud);return tempCloud;
}

总结一下:

这个类做了点云的畸变纠正,在速度慢的情况下只用imu进行畸变校正。要是用odom的话要把代码的注释取消掉。

之后进行range image的制作,并提取了有效点的索引,暂时还没用到。

将处理好的2个点云发布出去。

REERENCES

https://blog.csdn.net/unlimitedai/article/details/107378759#t6

LIO-SAM探秘第三章之代码解析(一) --- utility.h + imageProjection.cpp相关推荐

  1. python爬虫实战之旅( 第三章:数据解析(xpath法))

    上接:第三章:数据解析(bs4法) 下接:第四章:验证码识别 1.xpath解析简介 最常用且最便捷高效的一种解析方式.通用性很好 xpath解析原理 实例化一个etree的对象,且需要将被解析的页面 ...

  2. python爬虫实战之旅( 第三章:数据解析(bs4法))

    上接:第三章:数据解析(正则法) 下接:第三章:数据解析(xpath法) 1.数据解析步骤 标签定位 提取标签,标签属性中存储的数据值 2.bs4数据解析的原理 实例化一个BeautifulSoup对 ...

  3. 计算机组成原理译码器选择,计算机组成原理第三章习题参考解析.doc

    计算机组成原理第三章习题参考解析 第3章习题参考答案 1.设有一个具有20位地址和32位字长的存储器,问 (1) 该存储器能存储多少字节的信息? (2) 如果存储器由512K×8位SRAM芯片组成,需 ...

  4. 【GNSS】GREAT多频多系统GREAT-UPD开源代码-第4.1章 代码解读之gnss.h/gnss.cpp

    GREAT多频多系统GREAT-UPD开源代码-第4.1章 代码解读之gnss.h/gnss.cpp 第4.1章 代码解读之gnss.h/gnss.cpp 1. GNSS系统设定 ///< GN ...

  5. 第三章:数据解析---聚焦爬虫

    文章目录 第三章:数据解析---聚焦爬虫 注:本页示例所用的test.html文档 一.编码流程 二.数据解析分类 三.数据解析原理概述 四.bs4要点 1.bs4数据解析原理: 2.相关属性: 五. ...

  6. GRBL三:gcode代码解析

    GRBL三:gcode代码解析 1.G00X_Y_Z_    :快速定位指令,_代表具体数值                             可以同时针对X轴Y轴Z轴移动,只快速定位,不切削加 ...

  7. CodeCombat代码全记录(Python学习利器)--SARVEN沙漠(第三章)代码10

    毒气攻击 # 计算所有食人魔的总生命值.def sumHealth(enemies):# 创建一个变量,将它设为0后开始运算totalHealth = 0# 初始化循环索引为0enemyIndex = ...

  8. UNIX-LINUX编程实践教程-第三章-实例代码注解-ls2

    一 问题 对ls1的功能进行扩展,以达到类似ll命令的功能. 二 分析 在ls1中,我们利用readdir()函数和dirent结构体来获得目标文件夹内的文件名(dirent->d_name). ...

  9. c#面向对象与程序设计第三版第三章例题代码_C#程序设计教程 | 教与学(教学大纲)...

    <C#程序设计教程>课程教学大纲 执笔人:xxx,xxx,xxx 编写日期:年 月 一.课程基本信息 1.课程名称:C#程序设计教程 2.课程编号: 3.课程体系/类别: 4.课程性质: ...

  10. 整理 刘汝佳紫书第三章习题代码 未完待续

    还有一会儿要去剑道社训练...就整理整理好了... 习题3-1 得分 UVA1585 代码: #include <iostream> using namespace std; int ma ...

最新文章

  1. oracle io profile,ORACLE 中 PROFILE的管理
  2. 深入浅出设计模式原则之里氏代换原则(Liskov Substitution Principle)
  3. SpringBoot创建SpringBoot项目以及启动器讲解
  4. java时间日期工具类_java工具类--日期相关;
  5. 如何找到下一个快手头条?赚他个1000万!
  6. linux wget安装mysql_linux安装mysql
  7. 模板类的声明和定义要放在同一个文件
  8. RHCE盘点(5)—— 打印机
  9. android进入工程模式,安卓手机怎么进工程模式 安卓手机进工程模式教程【详解】...
  10. ubuntu 安装 SMPlayer
  11. 微信摇一摇里没有周边
  12. [精品毕设]基于Python实现的飞机票销售系统订票系统
  13. spark -- PCA
  14. 淘宝宝贝图片批量下载教程
  15. Hilbert曲线简单介绍及生成算法
  16. 阿龙的学习笔记---202107学习
  17. Python爬取二级页面(页面分析很重要)
  18. stm32和电机开发(从mcu到架构设计)
  19. windows10简单试用多图,连薛定谔的猫都杀死了
  20. c语言函数名大全dfun,C语言函数题库以及答案.docx

热门文章

  1. Windows系统命令整理-Win10
  2. normalize.css v2.1.2 翻译
  3. PyCharm 入手第一记
  4. 【 javascript 】.innerHTML属性定义
  5. MySQL5.6.10的安装
  6. 当手机访问时自动跳转到手机网站
  7. 使用.net开发并生成Windows服务安装包
  8. 向日葵Gantt支持的XML 数据结构
  9. 使用Java语言借助Quartz jar包实现定时器的方法
  10. TCP新手误区–粘包的处理