开发一个网页分析程序,可以抓取特定网页的内容,加以分析之后将结果保存至数据库。
V1.5上线,保存至sqlite数据库用时2分13秒。
网页分析程序具体要求描述如下:
1. 使用http技术获取一个博客的首页http://blog.csdn.net/jiangsheng
2. 分析这个网页的内容,从中找到博客中每一篇文章的链接。
3. 通过这些链接,获取文章的正文网页,从内容中提取文章的标题和文章的内容。
4. 将文章的标题与内容分别保存至数据库。
5. 布局要求:提供一个列表框和一个多行文本框。列表框中显示从数据库中获取的文章标题列表;当点击列表框中的某一篇文章时,在文本框中显示该文章的内容。
获取源码
因为需多次调用获取源码功能,将它放入一个函数中。
1 | CString CGetWebDlg::DownloadCodes(CString path) |
编码问题
注意网页编码问题,因此需要格式转换,编写一个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void ConvertUtf8ToGBK(CString &strUtf8)
{
int len=MultiByteToWideChar(CP_UTF8, 0, (LPCTSTR)strUtf8, -1, NULL,0);
unsigned short * wszGBK = new unsigned short[len+1];
memset(wszGBK, 0, len * 2 + 2);
MultiByteToWideChar(CP_UTF8, 0, (LPCTSTR)strUtf8, -1, (LPWSTR)wszGBK, len);
len = WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)wszGBK, -1, NULL, 0, NULL, NULL);
char *szGBK=new char[len + 1];
memset(szGBK, 0, len + 1);
WideCharToMultiByte (CP_ACP, 0, (LPCWSTR)wszGBK, -1, szGBK, len, NULL,NULL);
strUtf8 = szGBK;
delete[] szGBK;
delete[] wszGBK;
}
有些类似网页爬虫的感觉。
由于我刚开始是用VC6.0创建项目,现在用VS2013打开,因此,提示报错:
error MSB8031: Building an MFC project for a non-Unicode character set is deprecated. You must change the project property to Unicode or download an additional library. See http://go.microsoft.com/fwlink/p/?LinkId=286820 for more information.
解决:用于多字节字符编码 (MBCS) 的 MFC 库 (DLL) 不再包含于 Visual Studio 中,但是可用作插件,可以在任何装有 Visual Studio Professional、Visual Studio Premium 或 Visual Studio Ultimate 的计算机上下载和安装。下载地址:https://www.microsoft.com/zh-cn/download/details.aspx?id=40770
解析
为了接下来的操作,我去学正则表达式了……
还是学习html解析库 htmlcxx
官方
HtmlCxx用户手册
HTMLCXX
下载htmlcxx库
http://sourceforge.net/projects/htmlcxx/
并解压。
编译
打开htmlcxx.vcproj,右键属性,配置属性-C/C++-代码生成-运行库:多线程调试 DLL (/ MDd)进行编译。编译会报错,将1
const char *signature = "";
改为1
const char *signature = "\xEF\xBB\xBF";
即可编译成功。
导入
把生成的htmlcxx.lib和html文件夹拷贝到所需的工程中。即:
在所开发项目文件夹中,新建”htmlcxx“文件,里面添加两个子文件夹”lib“和”include“。将编译好的htmlcxx.lib拷贝到lib文件夹,将html文件夹中所有的.h头文件和ParserSax.tcc添加到include文件夹。添加库文件htmlcxx.lib到项目中,具体说来:
在VS工程中,添加c/c++工程中外部头文件及库的基本步骤:
1、添加工程的头文件目录:工程—属性—配置属性—c/c++—常规—附加包含目录:加上头文件存放目录。
2、添加文件引用的lib静态库路径:工程—属性—配置属性—链接器—常规—附加库目录:加上lib文件存放目录。
然后添加工程引用的lib文件名:工程—属性—配置属性—链接器—输入—附加依赖项:加上lib文件名。
3、添加工程引用的dll动态库:把引用的dll放到工程的可执行文件所在的目录下。
注意:第一步可以不用,直接在工程里加入动态库的头文件,在使用代码处引用这个头文件。
所开发的项目的头文件中添加以下内容:1
2
3
4
5
6
7
using namespace std;
using namespace htmlcxx;
测试
官方测试代码: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
...
//Parse some html code
string html = "<html><body>hey</body></html>";
HTML::ParserDom parser;
tree<HTML::Node> dom = parser.parseTree(html);
//Print whole DOM tree
cout << dom << endl;
//Dump all links in the tree
tree<HTML::Node>::iterator it = dom.begin();
tree<HTML::Node>::iterator end = dom.end();
for (; it != end; ++it)
{
if (it->tagName() == "A")
{
it->parseAttributes();
cout << it->attributes("href");
}
}
//Dump all text of the document
it = dom.begin();
end = dom.end();
for (; it != end; ++it)
{
if ((!it->isTag()) && (!it->isComment()))
{
cout << it->text();
}
}
然而……
加入#include”iostream”头文件即可。
修改为:1
2
3
4
5
6if (it->tagName() == "A")
{
it->parseAttributes();
std::pair<bool, std::string> pa = it->attribute("href");
cout << pa.second;
}
编译通过。
还有其他的库也可以用,比如使用MSHTML解析HTML页面
比如LIBXML2库使用指南
还可以用正则表达式写库……
突然发现 原来的计划里有COM组件 XML和HTML 数据库访问技术
都没怎么接触过 补补补
在填坑的路上不能止步…
参考
C++ 使用Htmlcxx解析Html内容(VS编译库文件)
html与xml解析库htmlcxx使用过程中的若干问题及解决方案
c++ hmtlcxx 学习之旅
MSHTML
因为最近用过MSXML,就试试MSHTML。学有余力的话,htmlcxx之后还是想玩一下…
https://msdn.microsoft.com/en-us/library/aa741317(v=vs.85).aspx
蒋晟-关于MSHTML
https://msdn.microsoft.com/zh-cn/library/mshtml(v=vs.110).aspx
MSHTML导入
系统中自带了mshtml,和msxml一样,在C盘windows/system32中可找到。
先看MSDN……
MSDN-MSHTML
再看各种搜集的文章
http://bbs.csdn.net/topics/330214041
http://www.cnblogs.com/speedmancs/archive/2010/08/11/1797442.html
http://blog.csdn.net/jinyaba/article/details/17097323
https://social.msdn.microsoft.com/Search/zh-CN?query=MSHTML&pgArea=header&emptyWatermark=true&ac=4#refinementChanges=117&pageNumber=1&showMore=false
https://wenku.baidu.com/view/d571abc4ec3a87c24028c4bb.html
http://www.codeguru.com/cpp/i-n/ieprogram/article.php/c4385/Lightweight-HTML-Parsing-Using-MSHTML.htm
http://www.yesky.com/403/1938403.shtml?qq-pf-to=pcqq.c2c
http://www.bianceng.cn/Programming/vc/201411/46771.htm
https://wenku.baidu.com/view/299bba4a336c1eb91a375df5.html
思路:下载源码和获取链接是两个独立函数,会被多次调用。先获取首页源码,div id=”archive_list” 遍历该div获取各月份归档链接,再使用多线程(48个线程???)进入每个归档链接里下载源码,获取源码中h1的每篇文章标题,保存到数据库。
虽然有两种方法,一种通过归档获得链接,一种通过翻页获得链接,但根据本html特点,明显通过翻页要简洁方便一些,因为翻页的链接是有规律的,可通过循环搞定。每页5篇直接获得正文链接,比从归档获得少一层。两核4个逻辑处理器,所以是开2个线程好还是4个好呢……
获取每篇正文链接,下载源码,解析得正文,保存到数据库。最后从数据库中提取标题和正文显示到对应窗口(使用ADO)。
解析过程
创建
1.使用CoCreateInstance创建一个接口1
HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&pDoc);
2.创建一个COM中的数组,将HTML字符串写到数组中
a)SafeArrayCreateVector:函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这将它作为一个字符数组,所以是一个一维数组。
b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,操作这个缓冲区就相当于操作了这个数组。
c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,用来回收这个权限,并使对数组的操作生效。
- 调用接口的write方法,将接口与HTML字符串绑定
1
2
3
4
5
6
7
8SAFEARRAY* psa = SafeArrayCreateVector(VT_VARIANT, 0, 1);
VARIANT *param;
bstr_t bsData = (LPCTSTR)strHtml;
hr = SafeArrayAccessData(psa, (LPVOID*)¶m);
param->vt = VT_BSTR;
param->bstrVal = (BSTR)bsData;
hr = pDoc->write(psa);
hr = SafeArrayUnaccessData(psa);
目标:
1 | <span class="link_title"><a href="/jiangsheng/article/details/9870241"> |
整个 \<span> \</span> 是元素, \<span> 是标签,class是属性名,link_title是属性值,“选择剪贴板格式顺序”是文本。
元素遍历
至少两种方法:
法一:
获取了HTML文档的IID_IHTMLDocument2接口后,开始遍历:
1.get_all方法获取所有标签节点,这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针
2.用IHTMLElementCollection接口的get_length方法获取标签的总数量,据此写一个循环,在循环进行元素的遍历
3.循环中用IHTMLElementCollection接口的item方法进行迭代,获取各元素对应的IDispatch接口指针
4.调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各种信息
以下已能成功获取标题: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
40void CGetWebDlg::EnumElements(IHTMLDocument2* pDoc)
{
CComPtr<IHTMLElementCollection> pCollection;
pDoc->get_all(&pCollection);
if (NULL == pCollection)
{
return;
}
VARIANT varName;
CString strText;
long len = 0;
pCollection->get_length(&len);
for (int i = 0; i < len; i++)
{
varName.vt = VT_I4;
varName.lVal = i;
CComPtr<IHTMLElement> pElement;
CComPtr<IDispatch> pDisp;
pCollection->item(varName, varName, &pDisp);
if (NULL == pDisp)
{
continue;
}
pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement);
if (NULL != pElement)
{
BSTR bstrClass;
pElement->get_className(&bstrClass);
CString strClass = _com_util::ConvertBSTRToString(bstrClass);
if (strClass.Compare("link_title") == 0)
{
BSTR bstrText = NULL;
pElement->get_innerText(&bstrText);
strText = bstrText;
m_list.InsertItem(i, strText);
}
}
}
}
法二:
利用IHTMLDocument2将字符串形式的HTML转换为DOM对象,利用IHTMLDocument3的getElementByTagName等方法来操作DOM对象。
以下已能成功获取标题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24MSHTML::IHTMLDocument3Ptr pDoc3;
MSHTML::IHTMLElementCollectionPtr pCollection;
MSHTML::IHTMLElementPtr pElement;
pDoc3 = pDoc;
pCollection = pDoc3->getElementsByTagName(L"span");
for (long i = 0; i < pCollection->length; i++)
{
pElement = pCollection->item(i, (long)0);
if (pElement != NULL)
{
BSTR bstrClass;
pElement->get_className(&bstrClass);
CString strClass = _com_util::ConvertBSTRToString(bstrClass);
if (strClass.Compare("link_title") == 0)
{
BSTR bstrText = NULL;
pElement->get_innerText(&bstrText);
CString strText = bstrText;
m_list.InsertItem(i, strText);
}
}
}
其实两种方法大同小异,相较而言可能数据量大的话,法二效率高些吧。因为法一是直接遍历所有的元素寻找class相同的,而法二是先定位span,然后在span中找寻class。(getElementsByTagName只有IHTMLDocument3Ptr)
使用MSHTML解析HTML页面
变体VARIANT
MSDN-DOM https://msdn.microsoft.com/en-us/library/ms766487(v=vs.85).aspx
使用MSHTML接口获取链接
晚上几个小时做完了一半,抵了之前一两个月。
数据库这块没来得及做,没加多线程,很多细节还得调。但比起之前心有余而力不足的感觉还是好多了。 学过msxml后,学习mshtml确实强一点,比一个月前完全不知道怎么下手好很多了。
今晚总算做出来个半成品
正文解析
1 | void CGetWebDlg::ArticleParse(CString strArticle, Blog* blog, int iRow) |
建立了一个结构体用来存放每篇文章的标题和正文,方便点击列表控件项时读取对应的结构体。
1 | struct Blog |
列表控件和编辑框交互
列表控件初始化
1 | CRect rectLocal; |
添加多列后,单击只能选中第一列,这时需要修改风格 LVS_EX_FULLROWSELECT 表示整行。1
m_list.SetExtendedStyle(LVS_EX_FULLROWSELECT);
添加数据
单纯添加一项的话: m_list.InsertItem(项的索引, 数据);
但要指定列的话:
m_list.InsertItem(项的索引, “”);
m_list.SetItemText(行, 列, 数据);
OnLvnItemchangedList
试了多种方法,只有这一种成功了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void CGetWebDlg::OnLvnItemchangedList1(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
// m_edit.SetWindowText("");
// int index = m_list.GetNextItem(-1, LVNI_ALL | LVNI_SELECTED);
// Blog *pStructure = (Blog*)m_list.GetItemData(index);
// m_edit.SetWindowText(pStructure->article);
if (pNMLV->uNewState == (LVIS_SELECTED | LVIS_FOCUSED))
{
m_edit.SetWindowText("");
Blog *pStructure = (Blog*)m_list.GetItemData(pNMLV->iItem);
m_edit.SetWindowText(pStructure->article);
}
*pResult = 0;
}
实现列表和编辑框交互后,可以说这已经是一个可以完整运行的程序了,V1.0版本出炉。
实测解析时间:1分41秒
能够成功获取该博客111篇文章,能够正确显示正文(包括清晰显示代码)。现在的运行图:
该版本未添加多线程、未和数据库关联。
V2.0实现数据库操作
使用sqlite数据库
下载导入
官网下载sqlite
Source Code sqlite-amalgamation-3200000.zip 有三个东西 shell.c sqlite3.c sqlite3.h
根据VS2010下SQLite3生成lib库文件 文章方法生成sqlite3.lib
Precompiled Binaries for Windows sqlite-dll-win32-x86-3200000.zip 有一个所需 sqlite3.dll
将lib和dll放入项目工程里,在.cpp开头加上1
2
保存至sqlite数据库
1 | int CGetWebDlg::Database(std::vector<Blog*> &vecBlog) |
当然,也可以事先把数据库和表创建好……
因为sqlite数据库是UTF-8格式存储,于是,需要一个转换函数:
1 | CString ConvertUtf8ToGBK(CString &strUtf8) |
出现的问题
- 如图,数据少时并不会出现。
2.好像是运行了脚本
据查将“script”修改成别的字符便不会运行脚本,并没尝试成功。
效果
下载了一个SQLiteStudio可视化管理工具。可见数据已成功保存至数据库。
从sqlite数据库读取数据
新建工程。
主要用到该函数:1
2
3
4
5
6
7
8
9int sqlite3_get_table(
sqlite3 *db, /* An open database */
const char *zSql, /* SQL to be evaluated */
char ***pazResult, /* Results of the query */
int *pnRow, /* Number of result rows written here */
int *pnColumn, /* Number of result columns written here */
char **pzErrmsg /* Error msg written here */
);
void sqlite3_free_table(char **result);
1 | void CGetDataFromSqliteDlg::OnBnClickedBtnreaddata() |
同样地,注意格式转码。
成品
V1.5成功实现从sqlite数据库读取数据并反映在列表和编辑框中。
To be continued…