chongqing_2018_price_crawler.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /**
  2. * @author vian
  3. * 重庆材料信息价爬虫
  4. * 由于headless chrome “puppeteer”占用资源比较大,且材料信息价的数据是ssr的静态内容,因此不需要使用puppeteer。
  5. * 数据获取使用cheerio(解析html,可用类jquery语法操作生成的数据)
  6. */
  7. module.exports = {
  8. crawlData,
  9. };
  10. const axios = require('axios');
  11. const querystring = require('querystring');
  12. const v1 = require('uuid/v1');
  13. const mongoose = require('mongoose');
  14. const _ = require('lodash');
  15. const priceInfoLibModel = mongoose.model('std_price_info_lib');
  16. const priceInfoClassModel = mongoose.model('std_price_info_class');
  17. const priceInfoItemModel = mongoose.model('std_price_info_items');
  18. const priceInfoAreaModel = mongoose.model('std_price_info_areas');
  19. const defaultAreas = [
  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. '荣昌县1',
  67. '荣昌县2',
  68. '彭水县1',
  69. '彭水县2',
  70. '彭水县3',
  71. '潼南区',
  72. '荣昌区1',
  73. '荣昌区2',
  74. '武隆区1',
  75. '武隆区2',
  76. '武隆区3',
  77. '武隆区4',
  78. '武隆区5',
  79. '武隆区6',
  80. ];
  81. const subAreaMap = {
  82. '渝西区': [
  83. '涪陵区',
  84. '长寿区',
  85. '永川区',
  86. '江津区',
  87. '合川区',
  88. '大足区',
  89. '綦江区',
  90. '南川区',
  91. '荣昌区1',
  92. '荣昌区2',
  93. '铜梁区',
  94. '璧山区',
  95. '潼南区',
  96. '双桥经开区',
  97. '万盛经开区',
  98. ],
  99. '渝东北区': [
  100. '万州区',
  101. '开州区',
  102. '梁平区',
  103. '丰都县',
  104. '垫江县',
  105. '忠县',
  106. '云阳县',
  107. '奉节县',
  108. '巫山县',
  109. '巫溪县',
  110. '城口县',
  111. ],
  112. '渝东南区': [
  113. '黔江区',
  114. '武隆区1',
  115. '武隆区2',
  116. '武隆区3',
  117. '武隆区4',
  118. '武隆区5',
  119. '武隆区6',
  120. '石柱县',
  121. '彭水县1',
  122. '彭水县2',
  123. '彭水县3',
  124. '酉阳县',
  125. '秀山县',
  126. ],
  127. }
  128. const TIME_OUT = 60000;
  129. // 创建axios实例
  130. const axiosInstance = axios.create({
  131. baseURL: 'http://www.cqsgczjxx.org/',
  132. timeout: TIME_OUT,
  133. /* proxy: {
  134. host: "127.0.0.1", port: "8888" // Fiddler抓包,需要打开Fiddler否则会报connect error
  135. }, */
  136. headers: {
  137. 'Cache-Control': 'max-age=0',
  138. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  139. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
  140. 'Accept': 'application/json, text/javascript, */*; q=0.01',
  141. 'X-Requested-With': 'XMLHttpRequest',
  142. 'Accept-Encoding': 'gzip, deflate',
  143. 'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6',
  144. // 'Cookie': 'ASP.NET_SessionId=uozdrp0hep5x344vq153muju'
  145. },
  146. // responseType: 'json'
  147. });
  148. // 响应拦截器
  149. axiosInstance.interceptors.response.use(function (response) {
  150. return response;
  151. }, function (error) {
  152. // 对响应错误做点什么
  153. if (error.message.includes('timeout')) {
  154. return Promise.reject(`目标网络超时,请稍后再试。(${TIME_OUT}ms)`);
  155. } else {
  156. return Promise.reject(error);
  157. }
  158. });
  159. // 将月期数转换为季度期数
  160. function month2quarter(period) {
  161. if (!period || typeof period !== 'string') {
  162. return null;
  163. }
  164. const quarter1 = /(01|02|03)月/;
  165. const quarter2 = /(04|05|06)月/;
  166. const quarter3 = /(07|08|09)月/;
  167. const quarter4 = /(10|11|12)月/;
  168. if (quarter1.test(period)) {
  169. return period.replace(quarter1, '1季度');
  170. }
  171. if (quarter2.test(period)) {
  172. return period.replace(quarter2, '2季度');
  173. }
  174. if (quarter3.test(period)) {
  175. return period.replace(quarter3, '3季度');
  176. }
  177. if (quarter4.test(period)) {
  178. return period.replace(quarter4, '4季度');
  179. }
  180. return period;
  181. }
  182. function setTimeoutSync(handle, time) {
  183. return new Promise((resolve, reject) => {
  184. setTimeout(() => {
  185. if (handle && typeof handle === 'function') {
  186. handle();
  187. }
  188. resolve();
  189. }, time);
  190. });
  191. }
  192. // 目标网站做了反爬虫处理,请求需要携带cookie(sessionID),否则会报500错误
  193. let curCookie = '';
  194. async function getCookie() {
  195. const indexRes = await axiosInstance.get('/Pages/CQZJW/index.aspx', null, { responseType: 'document' });
  196. const cookies = indexRes.headers['set-cookie'];
  197. return Object.prototype.toString.call(cookies) === '[object Array]'
  198. ? cookies[0].split(';')[0]
  199. : cookies || 'ASP.NET_SessionId=cbwzceh5pxzim13gyesho5af';
  200. }
  201. async function post(url, body) {
  202. const cookie = curCookie ? curCookie : await getCookie();
  203. curCookie = cookie;
  204. const extendConfig = { headers: { Cookie: cookie } };
  205. const serviceUrl = `/Service/MaterialPriceQuerySvr.svrx${url}`
  206. let res = await axiosInstance.post(serviceUrl, querystring.stringify(body), extendConfig);
  207. while (res && typeof res.data === 'string' && /<!doctype html>/.test(res.data)) {
  208. // 有时候请求会返回302,需要重新发请求
  209. await setTimeoutSync(null, 500);
  210. res = await axiosInstance.post(serviceUrl, querystring.stringify(body), extendConfig);
  211. }
  212. if (typeof res.data === 'string' && /<!doctype html>/.test(res.data)) {
  213. console.log(serviceUrl);
  214. console.log(body);
  215. console.log(res.data);
  216. console.log('==================================')
  217. }
  218. return res;
  219. }
  220. // 获取材料价格
  221. async function queryPrice(period, area, groupType, classify) {
  222. const body = {
  223. period: period.replace('-', ''),
  224. area: area || '',
  225. groupType: groupType || '',
  226. classify: classify || '',
  227. priceType: '',
  228. searchParam: '',
  229. pageIndex: 1,
  230. pageSize: 10000,
  231. option: 0,
  232. token: ''
  233. };
  234. const res = await post('/QueryInfoPrice', body);
  235. return res && res.data && res.data.Data && res.data.Data._Items || [];
  236. }
  237. // 获取地区信息
  238. async function queryArea(period, groupType) {
  239. const body = {
  240. groupType,
  241. period: period.replace('-', ''),
  242. token: ''
  243. };
  244. const res = await post('/QueryArea', body);
  245. const areaData = res && res.data && res.data.Data && res.data.Data._Items || [];
  246. return areaData.map(item => item.Area);
  247. }
  248. // 获取分类信息
  249. async function queryKind(groupType, period) {
  250. const body = {
  251. groupType,
  252. period: period.replace('-', ''),
  253. token: ''
  254. }
  255. const res = await post('/QueryKind', body);
  256. return res && res.data && res.data.Data || [];
  257. }
  258. // 爬取人工价格
  259. async function crawlLabour(period) {
  260. const groupType = '人工信息价';
  261. const quater = month2quarter(period);
  262. const areas = await queryArea(quater, groupType);
  263. const rst = [];
  264. for (const rootArea of areas) {
  265. const priceItems = await queryPrice(quater, rootArea, groupType);
  266. priceItems.forEach(item => item.Unit = '工日');
  267. const subAreas = subAreaMap[rootArea];
  268. if (subAreas) {
  269. subAreas.forEach(area => {
  270. rst.push({ area, data: [{ classify: '人工', priceItems }] });
  271. });
  272. } else {
  273. rst.push({ area: rootArea, data: [{ classify: '人工', priceItems }] });
  274. }
  275. }
  276. return rst;
  277. }
  278. // 爬取地方材料信息价
  279. async function crawlLocaleMaterial(period) {
  280. const groupType = '区县材料价格';
  281. const areas = await queryArea(period, groupType);
  282. const rst = [];
  283. for (const area of areas) {
  284. const priceItems = await queryPrice(period, area, groupType);
  285. const item = { area, data: [{ classify: '地方材料信息价', priceItems }] };
  286. rst.push(item);
  287. }
  288. return rst;
  289. }
  290. // 爬取预拌砂浆息价
  291. async function crawlBetonMaterial(period) {
  292. const groupType = '预拌砂浆价格';
  293. const areas = await queryArea(period, groupType);
  294. const rst = [];
  295. for (const area of areas) {
  296. const priceItems = await queryPrice(period, area, groupType);
  297. const item = { area, data: [{ classify: '预拌商品砂浆', priceItems }] };
  298. rst.push(item);
  299. }
  300. return rst;
  301. }
  302. // 爬取建安工程材料
  303. async function crawlBuldingMaterial(period) {
  304. const groupType = '建筑工程材料价格';
  305. // 根据期数获取建安工程材料分类
  306. const kinds = await queryKind(groupType, period);
  307. if (!kinds || !kinds.length) {
  308. return [];
  309. }
  310. const rst = [];
  311. for (const kind of kinds) {
  312. const priceItems = await queryPrice(period, '', groupType, kind);
  313. rst.push({ classify: kind, priceItems, subClass: [] });
  314. }
  315. return rst;
  316. }
  317. // 爬取园林绿化工程材料
  318. async function crawlGardenMateiral(period) {
  319. const priceItems = await queryPrice(period, '', '城市园林绿化工程材料价格', '');
  320. // 数据 高度(CM) | 干径(CM) | 冠径(CM) | 分枝高(CM) | 不含税价(元) = ‘’ | 14-17 | 大于400 | 200-300 | 430-780
  321. // 则此数据需要分为:
  322. // 1. { name: 名称-最低价, specs: 干径14-17CM 冠径大于400CM 分枝高200-300CM, noTaxPrice: 430 }
  323. // 2. { name: 名称-最高价, specs: 干径14-17CM 冠径大于400CM 分枝高200-300CM, noTaxPrice: 780 }
  324. if (!priceItems.length) {
  325. return [];
  326. }
  327. const rootClass = { classify: '苗木', subClass: [] };
  328. const rst = [rootClass];
  329. const groupedData = _.groupBy(priceItems, 'Family');
  330. const unit = 'CM';
  331. const duplicateReg = /-/;
  332. Object
  333. .entries(groupedData)
  334. .forEach(([kind, items]) => {
  335. const classItem = { classify: kind, priceItems: [], subClass: [] };
  336. rootClass.subClass.push(classItem);
  337. items.forEach(item => {
  338. // 拼接规格型号
  339. const specsList = [];
  340. if (item.Height) {
  341. specsList.push(`高度${item.Height}${unit}`);
  342. }
  343. if (item.TrunkDiameter) {
  344. specsList.push(`干径${item.TrunkDiameter}${unit}`);
  345. }
  346. if (item.TopDiameter) {
  347. specsList.push(`冠径${item.TopDiameter}${unit}`);
  348. }
  349. if (item.BranchHeight) {
  350. specsList.push(`分枝高${item.BranchHeight}${unit}`);
  351. }
  352. item.Model = specsList.join(' ');
  353. const isDuplicate = duplicateReg.test(item.TaxPrice) || duplicateReg.test(item.NoTaxPrice);
  354. if (isDuplicate) {
  355. // 分成最高低价最高价数据
  356. const taxPriceList = item.TaxPrice ? item.TaxPrice.split('-') : [''];
  357. const noTaxPriceList = item.NoTaxPrice ? item.NoTaxPrice.split('-') : [''];
  358. const minItem = {
  359. ...item,
  360. Name: `${item.Name}-最低价`,
  361. TaxPrice: taxPriceList[0],
  362. NoTaxPrice: noTaxPriceList[0]
  363. };
  364. const maxItem = {
  365. ...item,
  366. Name: `${item.Name}-最高价`,
  367. TaxPrice: taxPriceList[1] || '',
  368. NoTaxPrice: noTaxPriceList[1] || ''
  369. };
  370. classItem.priceItems.push(minItem, maxItem);
  371. } else {
  372. classItem.priceItems.push(item);
  373. }
  374. });
  375. });
  376. return rst;
  377. }
  378. // 爬取绿色、节能建筑工程材料
  379. async function crawlEnergyMateiral(period) {
  380. const groupType = '绿色、节能建筑材料价格';
  381. // 获取分类
  382. const kinds = await queryKind(groupType, period);
  383. if (!kinds || !kinds.length) {
  384. return [];
  385. }
  386. const rootClass = { classify: '绿色、节能建筑工程材料', subClass: [] };
  387. const rst = [rootClass];
  388. for (const kind of kinds) {
  389. const priceItems = await queryPrice(period, '', groupType, kind);
  390. rootClass.subClass.push({ classify: kind, priceItems, subClass: [] });
  391. }
  392. return rst;
  393. }
  394. // 爬取装配式建筑工程成品构件
  395. async function crawlPrefabricatedMateiral(period) {
  396. const groupType = '装配式建筑材料价格';
  397. // 获取分类
  398. const quater = month2quarter(period);
  399. const kinds = await queryKind(groupType, quater);
  400. if (!kinds || !kinds.length) {
  401. return [];
  402. }
  403. const rootClass = { classify: '装配式建筑工程成品构件', subClass: [] };
  404. const rst = [rootClass];
  405. for (const kind of kinds) {
  406. const priceItems = await queryPrice(quater, '', groupType, kind);
  407. rootClass.subClass.push({ classify: kind, priceItems, subClass: [] });
  408. }
  409. return rst;
  410. }
  411. // 爬取轨道材料
  412. async function crawlTrackMateiral(period) {
  413. const groupType = '轨道材料价格';
  414. // 获取分类
  415. const quater = month2quarter(period);
  416. const kinds = await queryKind(groupType, quater);
  417. if (!kinds || !kinds.length) {
  418. return [];
  419. }
  420. const rootClass = { classify: '城市轨道交通工程材料', subClass: [] };
  421. const rst = [rootClass];
  422. for (const kind of kinds) {
  423. const priceItems = await queryPrice(quater, '', groupType, kind);
  424. rootClass.subClass.push({ classify: kind, priceItems, subClass: [] });
  425. }
  426. return rst;
  427. }
  428. // 爬取主要材料信息价
  429. async function crawlGeneralMaterial(period) {
  430. const area = '通用';
  431. const buildingMaterial = await crawlBuldingMaterial(period);
  432. const gardenMaterial = await crawlGardenMateiral(period)
  433. const energyMaterial = await crawlEnergyMateiral(period)
  434. const prefMaterial = await crawlPrefabricatedMateiral(period)
  435. const trackMaterial = await crawlTrackMateiral(period);
  436. return [{ area, data: [...buildingMaterial, ...gardenMaterial, ...energyMaterial, ...prefMaterial, ...trackMaterial] }];
  437. }
  438. /**
  439. * 获取期数数据
  440. * @param {String} from - 从哪一期开始 eg: 2020-01
  441. * @param {String} to - 从哪一期结束 eg: 2020-05
  442. * @return {Array<string> | null} ['2020年12月']
  443. */
  444. function getPeriodData(from, to) {
  445. if (from > to) {
  446. return null;
  447. }
  448. // 根据区间获取期数列表
  449. const reg = /(\d+)-(\d+)/;
  450. const fromMatch = from.match(reg);
  451. const fromYear = +fromMatch[1];
  452. const fromMonth = +fromMatch[2];
  453. const toMatch = to.match(reg);
  454. const toYear = +toMatch[1];
  455. const toMonth = +toMatch[2];
  456. let curYear = fromYear;
  457. let curMonth = fromMonth;
  458. const list = [];
  459. const monthMap = {
  460. '1': '01月',
  461. '2': '02月',
  462. '3': '03月',
  463. '4': '04月',
  464. '5': '05月',
  465. '6': '06月',
  466. '7': '07月',
  467. '8': '08月',
  468. '9': '09月',
  469. '10': '10月',
  470. '11': '11月',
  471. '12': '12月',
  472. };
  473. while (curYear <= toYear && curMonth <= toMonth) {
  474. list.push(`${curYear}年-${monthMap[curMonth]}`);
  475. if (curMonth === 12) {
  476. curYear++;
  477. curMonth = 1;
  478. } else {
  479. curMonth++;
  480. }
  481. }
  482. return list;
  483. }
  484. // 地区serialNo补丁
  485. async function areaPatch(compilationID) {
  486. const areaData = await priceInfoAreaModel.find({ compilationID, serialNo: null }).lean();
  487. const bulks = [];
  488. areaData.forEach(areaItem => {
  489. const serialNo = defaultAreas.indexOf(areaItem.name) + 1;
  490. bulks.push({
  491. updateOne: {
  492. filter: { ID: areaItem.ID },
  493. update: { serialNo }
  494. }
  495. });
  496. });
  497. if (bulks.length) {
  498. await priceInfoAreaModel.bulkWrite(bulks);
  499. }
  500. }
  501. function transformPriceItems(period, compilationID, libID, areaID, classID, items) {
  502. return items.map(item => ({
  503. period,
  504. compilationID,
  505. libID,
  506. areaID,
  507. classID,
  508. ID: v1(),
  509. code: '',
  510. name: item.Name,
  511. specs: item.Model,
  512. unit: item.Unit,
  513. taxPrice: item.TaxPrice,
  514. noTaxPrice: item.NoTaxPrice,
  515. remark: item.Remark,
  516. }));
  517. }
  518. // 转换爬取出来的原始数据并入库
  519. async function save(allData, period, compilationID) {
  520. // 将各部分数据按照地区进行合并
  521. const areaMap = {};
  522. allData.forEach(({ area, data }) => {
  523. const areaName = `重庆市-${area}`;
  524. (areaMap[areaName] || (areaMap[areaName] = [])).push(...data);
  525. });
  526. const libData = { period, compilationID, ID: v1(), name: `信息价(${period})`, createDate: Date.now() };
  527. const curAreas = await priceInfoAreaModel.find({ compilationID }).sort({ serialNo: 1 }).lean();
  528. let maxSerialNo = curAreas.length ? curAreas[curAreas.length - 1].serialNo || 0 : 0;
  529. const areaData = [];
  530. const classData = [];
  531. const priceData = [];
  532. for (const area in areaMap) {
  533. let curArea = curAreas.find(cArea => cArea.name === area);
  534. if (!curArea) {
  535. curArea = { compilationID, ID: v1(), serialNo: ++maxSerialNo, name: area };
  536. areaData.push(curArea);
  537. }
  538. const data = areaMap[area];
  539. // class最多只有两层
  540. data.forEach((item, index) => {
  541. item.classObj = { libID: libData.ID, areaID: curArea.ID, ID: v1(), ParentID: '-1', NextSiblingID: '-1', name: item.classify };
  542. classData.push(item.classObj);
  543. const pre = data[index - 1];
  544. if (pre) {
  545. pre.classObj.NextSiblingID = item.classObj.ID;
  546. }
  547. if (item.priceItems) {
  548. priceData.push(...transformPriceItems(period, compilationID, libData.ID, curArea.ID, item.classObj.ID, item.priceItems));
  549. }
  550. if (item.subClass && item.subClass.length) {
  551. item.subClass.forEach((child, cIndex) => {
  552. child.classObj = { libID: libData.ID, areaID: curArea.ID, ID: v1(), ParentID: item.classObj.ID, NextSiblingID: '-1', name: child.classify };
  553. classData.push(child.classObj);
  554. const preChild = item.subClass[cIndex - 1];
  555. if (preChild) {
  556. preChild.classObj.NextSiblingID = child.classObj.ID;
  557. }
  558. if (child.priceItems) {
  559. priceData.push(...transformPriceItems(period, compilationID, libData.ID, curArea.ID, child.classObj.ID, child.priceItems));
  560. }
  561. });
  562. }
  563. });
  564. }
  565. if (areaData.length) {
  566. await priceInfoAreaModel.insertMany(areaData);
  567. }
  568. if (classData.length) {
  569. await priceInfoClassModel.insertMany(classData);
  570. }
  571. if (priceData.length) {
  572. await priceInfoItemModel.insertMany(priceData);
  573. }
  574. await priceInfoLibModel.insertMany([libData]);
  575. }
  576. /**
  577. * 爬取数据
  578. * @param {String} from - 从哪一期开始 eg: 2020-01
  579. * @param {String} to - 从哪一期结束 eg: 2020-05
  580. * @param {String} compilationID - 费用定额ID
  581. * @return {Object}
  582. */
  583. async function crawlData(from, to, compilationID) {
  584. let curPeriod;
  585. try {
  586. const periods = getPeriodData(from, to);
  587. if (!periods || !periods.length) {
  588. throw '无效的期数区间。';
  589. }
  590. // 地区补丁
  591. await areaPatch(compilationID);
  592. // 一期一期爬取数据
  593. for (const period of periods) {
  594. const labourData = await crawlLabour(period);
  595. const localeData = await crawlLocaleMaterial(period);
  596. const betonData = await crawlBetonMaterial(period);
  597. const generalData = await crawlGeneralMaterial(period);
  598. const allData = [...labourData, ...localeData, ...betonData, ...generalData];
  599. if (!allData.length) {
  600. throw `${period}无有效数据`;
  601. }
  602. await save(allData, period, compilationID);
  603. curPeriod = period;
  604. }
  605. } catch (err) {
  606. console.log(err);
  607. // 错误时提示已经成功爬取的期数
  608. let errTip = '';
  609. if (curPeriod) {
  610. errTip += `\n成功爬取期数为:${periods[0]}到${curPeriod}`;
  611. }
  612. const errStr = String(err) + errTip;
  613. console.log(`err`);
  614. console.log(errStr);
  615. throw errStr;
  616. }
  617. }