背景
从iata 网页中爬取全球航空公司的相关信息,目标页面:iata查询页面
数据爬取目标分析
- 分析目标网页的结构
- 确定爬取的锚点
- 确定有效数据
上图是浏览器开发者工具中显示的网页结构。我们可以分析出要提取有效数据,可以先找到.airlinecodesearchblock 然后再在这个div中找到tr,再对每一个tr遍历抽出td中的数据,每一行的数据就是我们要找的有效数据。
程序流程
画出程序流程图之后可看出,打开页面、获取页面数据、写入数据库可以抽象成函数来复用
打开页面的函数
async function openPage(pageHandle, pageNumber) {
const _url = `${CHANGEURL}${pageNumber}&airline.search=`;
console.log(_url);
await pageHandle.goto(_url, {waitUntil: 'networkidle2'});
let elementPath = '';
elementPath = '.airlinecodessearchblock';
await pageHandle.waitForSelector(elementPath);
}
获取页面数据函数
async function getPageData(pageHandle, pageNumber) {
console.log(`开始解析第${pageNumber}的数据...`);
const parseResult = await pageHandle.evaluate(() => {
let flightList = [];
const airlineBlock = document.querySelector('div.airlinecodessearchblock');
const trItems = airlineBlock.querySelectorAll('tbody>tr');
if (trItems.length > 0) {
for (let i = 0; i < trItems.length; i++) {
let tdItems = trItems[i].querySelectorAll('td');
let data = {};
data.companyName = tdItems[0].innerText.replace('\'', '');
data.country = tdItems[1].innerText.replace('\'', '');
data.code2 = tdItems[2].innerText;
data.airlinePrefix = tdItems[3].innerText;
data.pax = tdItems[4].innerText;
flightList.push(data);
}
}
return flightList;
});
console.log(`完成解析第${pageNumber}的数据...`);
return parseResult;
}
写入数据库
async function writeDataToDB(airlineData, pageNumber) {
console.log(`开始写入第${pageNumber}的数据...`);
gDBHandle.connection = await gDBHandle.getConnection();
await gDBHandle.beginTransaction();
for (let i = 0; i < airlineData.length; i++) {
let _data = airlineData[i];
let dbRes;//数据库写入是否成功
const {code2} = airlineData[i];
dbRes = await gDBHandle.find({code2}, 1, ['id']);
if (dbRes.data.length > 0) {
const additionalData = {
updateTime: getNowTimeFormat(),
}
_data = {...airlineData[i], ...additionalData};
let res = await gDBHandle.update({id: dbRes.data[0].id}, _data);
if (!res.code) {
await gDBHandle.rollback();
gErrorArr.add(pageNumber);
break;
}
} else {
let res = await gDBHandle.add(_data);
if (!res.code) {
try {
await gDBHandle.rollback();
gErrorArr.add(pageNumber);
} catch (e) {
console.log(e);
}
break;
}
}
}
await gDBHandle.commit();
console.log(`完成写入第${pageNumber}的数据...`);
}
数据库使用的是mysql,使用了mysql的连接池和事务技术。每一次数据库写入前先从连接池中获取一个连接,然后创建事务,如果写入数据库出错就回滚,但是回滚仅仅是丢掉了当前页的数据,并且将当前页的页码记录在一个数组中,方便再次爬取。
总体流程
async function initPage() {
let gPageData = [];
let startTime = new Date();
globeBrowser = await puppeteer.launch(defaultConfig.config);
if (globeBrowser) {
const page = await globeBrowser.newPage();
page.setDefaultNavigationTimeout(100000);
await openPage(page, 1);
gDBHandle = new DBTransaction('crawler_train_plane', 'tb_airline');
const parseResult = await page.evaluate(() => {
const airlineBlock = document.querySelector('div.airlinecodessearchblock');
let pagination = airlineBlock.querySelector('div.pagination');
let linkItems = pagination.querySelectorAll('a');
let _pageCount = linkItems[linkItems.length - 2].innerText;
return _pageCount;
});
let currPageData = await getPageData(page, 1);
await writeDataToDB(currPageData, 1);
const pageCount = parseResult;
for (let i = 2; i <= pageCount; i++) {
await openPage(page, i);
let currPageData = await getPageData(page, i);
await writeDataToDB(currPageData, i);
let endTime = new Date();
let str = getTimeDiff("爬取数据", startTime, endTime);
console.log(str);
}
}
}
代码遗留问题
1.puppeteer大量使用async/await来实现同步代码,这样方便大家理解,但是爬取速度上有问题,没有发挥javascript异步的优势
2.程序中puppeteer仅仅打开了一个页面来进行爬取,可以预先打开多个页面来进行多页面爬取。