How can I parse Name: & Value text from within the tag with DIHtmlParser? I tried doing it with TCLHtmlParser from Clever Components but it failed. Second question is can DIHtmlParser parse individual tags for example loop through its sub tags. Its a total nightmare for such a simple problem.
<div class="tvRow tvFirst hasLabel tvFirst" title="example1">
<label class="tvLabel">Name:</label>
<span class="tvValue">Value</span>
<div class="clear"></div></div>
<div class="tvRow tvFirst hasLabel tvFirst" title="example2">
<label class="tvLabel">Name:</label>
<span class="tvValue">Value</span>
<div class="clear"></div></div>
You could use IHTMLDocument2 DOM to parse whatever elements you need from the HTML:
uses ActiveX, MSHTML;
const
HTML =
'<div class="tvRow tvFirst hasLabel tvFirst" title="example1">' +
'<label class="tvLabel">Name:</label>' +
'<span class="tvValue">Value</span>' +
'<div class="clear"></div>' +
'</div>';
procedure TForm1.Button1Click(Sender: TObject);
var
doc: OleVariant;
el: OleVariant;
i: Integer;
begin
doc := coHTMLDocument.Create as IHTMLDocument2;
doc.write(HTML);
doc.close;
ShowMessage(doc.body.innerHTML);
for i := 0 to doc.body.all.length - 1 do
begin
el := doc.body.all.item(i);
if (el.tagName = 'LABEL') and (el.className = 'tvLabel') then
ShowMessage(el.innerText);
if (el.tagName = 'SPAN') and (el.className = 'tvValue') then
ShowMessage(el.innerText);
end;
end;
I wanted to mention another very nice HTML parser I found today: htmlp (Delphi Dom HTML Parser and Converter). It's not as flexible as the IHTMLDocument2 obviously, but it's very easy to work with, fast, free, and supports Unicode for older Delphi versions.
Sample usage:
uses HtmlParser, DomCore;
function GetDocBody(HtmlDoc: TDocument): TElement;
var
i: integer;
node: TNode;
begin
Result := nil;
for i := 0 to HtmlDoc.documentElement.childNodes.length - 1 do
begin
node := HtmlDoc.documentElement.childNodes.item(i);
if node.nodeName = 'body' then
begin
Result := node as TElement;
Break;
end;
end;
end;
procedure THTMLForm.Button2Click(Sender: TObject);
var
HtmlParser: THtmlParser;
HtmlDoc: TDocument;
i: Integer;
body, el: TElement;
node: TNode;
begin
HtmlParser := THtmlParser.Create;
try
HtmlDoc := HtmlParser.parseString(HTML);
try
body := GetDocBody(HtmlDoc);
if Assigned(body) then
for i := 0 to body.childNodes.length - 1 do
begin
node := body.childNodes.item(i);
if (node is TElement) then
begin
el := node as TElement;
if (el.tagName = 'div') and (el.GetAttribute('class') = 'tvRow tvFirst hasLabel tvFirst') then
begin
// iterate el.childNodes here...
ShowMessage(IntToStr(el.childNodes.length));
end;
end;
end;
finally
HtmlDoc.Free;
end;
finally
HtmlParser.Free
end;
end;
Use a HTML Parser to work on your html files.
Maybe DIHtmlParser will do the job.
RegEx is not a parser and converting from HTML to JSON is not a wise option.
One can also use a combination of HTMLP parser with THtmlFormatter and OXml XPath parsing
uses
// Htmlp
HtmlParser,
DomCore,
Formatter,
// OXml
OXmlPDOM,
OXmlUtils;
function HtmlToXHtml(const Html: string): string;
var
HtmlParser: THtmlParser;
HtmlDoc: TDocument;
Formatter: THtmlFormatter;
begin
HtmlParser := THtmlParser.Create;
try
HtmlDoc := HtmlParser.ParseString(Html);
try
Formatter := THtmlFormatter.Create;
try
Result := Formatter.GetText(HtmlDoc);
finally
Formatter.Free;
end;
finally
HtmlDoc.Free;
end;
finally
HtmlParser.Free;
end;
end;
type
TCard = record
Store: string;
Quality: string;
Quantity: string;
Price: string;
end;
TCards = array of TCard;
function ParseCard(const Node: PXMLNode): TCard;
const
StoreXPath = 'div[1]/ax';
QualityXPath = 'div[3]';
QuantityXPath = 'div[4]';
PriceXPath = 'div[5]';
var
CurrentNode: PXMLNode;
begin
Result := Default(TCard);
if Node.SelectNode(StoreXPath, CurrentNode) then
Result.Store := CurrentNode.Text;
if Node.SelectNode(QualityXPath, CurrentNode) then
Result.Quality := CurrentNode.Text;
if Node.SelectNode(QuantityXPath, CurrentNode) then
Result.Quantity := CurrentNode.Text;
if Node.SelectNode(PriceXPath, CurrentNode) then
Result.Price := CurrentNode.Text;
end;
procedure THTMLForm.OpenButtonClick(Sender: TObject);
var
Html: string;
Xml: string;
FXmlDocument: IXMLDocument;
QueryNode: PXMLNode;
XPath: string;
NodeList: IXMLNodeList;
i: Integer;
Card: TCard;
begin
Html := System.IOUtils.TFile.ReadAllText(FileNameEdit.Text, TEncoding.UTF8);
Xml := HtmlToXHtml(Html);
Memo.Lines.Text := Xml;
// Parse with XPath
FXMLDocument := CreateXMLDoc;
FXMLDocument.WriterSettings.IndentType := itIndent;
if not FXMLDocument.LoadFromXML(Xml) then
raise Exception.Create('Source document is not valid');
QueryNode := FXmlDocument.DocumentElement;
XPath := '//div[#class="row pricetableline"]';
NodeList := QueryNode.SelectNodes(XPath);
for i := 0 to NodeList.Count -1 do
begin
Card := ParseCard(NodeList[i]);
Memo.Lines.Text := Memo.Lines.Text + sLineBreak +
Format('%0:s %1:s %2:s %3:s', [Card.Store, Card.Quality, Card.Quantity, Card.Price]);
end;
Memo.SelStart := 0;
Memo.SelLength := 0;
end;
Related
<h1 class="tt">example</h1></div><div class="bl_la_main"><div class="divtext">
I am trying to remove everything before <div class="bl_la_main"> but keep everything after it.
Any help would be appreciated. Thanks
P.S: Since I misunderstood the question, I first created the "take the before terms" function.
You can take it like this.
procedure TForm1.Button1Click(Sender: TObject);
var
HTML: string;
begin
HTML := '<h1 class="tt">example</h1></div><div class="bl_la_main"><div class="divtext">';
Delete(HTML, Pos('<div class="bl_la_main">', HTML) - 1, Length(HTML) - Pos('<div class="bl_la_main">', HTML));
ShowMessage(HTML);
end;
If we need to make it functional;
function parseHTML(html: string; substr: string): string;
begin
Delete(HTML, Pos(substr, HTML) - 1, Length(HTML) - Pos(substr, HTML));
Result := HTML;
end;
Use:
function parseHTML(html: string; substr: string): string;
begin
Delete(HTML, Pos(substr, HTML) - 1, Length(HTML) - Pos(substr, HTML));
Result := HTML;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
HTML: string;
begin
HTML := '<h1 class="tt">example</h1></div><div class="bl_la_main"><div class="divtext">';
ShowMessage(parseHTML(HTML, '<div class="bl_la_main">'));
end;
Result:
<h1 class="tt">example</h1></div">
I created it separately, in a functional way to take both before and after.
function parseHTMLAfter(html: string; substr: string): string;
begin
Delete(HTML, Pos(substr, HTML) - 1, Length(HTML) - Pos(substr, HTML));
Result := HTML;
end;
function parseHTMLBefore(html: string; substr: string): string;
begin
Delete(HTML, 1, Pos(substr ,html) - 1);
Result := HTML;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
HTML: string;
begin
HTML := '<h1 class="tt">example</h1></div><div class="bl_la_main"><div class="divtext">';
ShowMessage(parseHTMLBefore(HTML, '<div class="bl_la_main">'));
end;
Result:
<div class="bl_la_main"><div class="divtext">
I'm working to create an installer and I need to edit and retrieve values from the JSON file.
To retrieve and edit the values from the Section_2 works fine. The problem is to edit and retrieve values from the children sections of Section_1. Bellow we can see an example:
{
"Section_1": {
"children_1": {
"children_1_1": "value_1",
"children_1_2": "value_2"
},
"children_2": "blablabla"
},
"Section_2": {
"children_2_1": "value_1",
"children_2_2": "value_2"
}
}
[Files]
Source: "{#ProjectUrl}\JSONConfig.dll"; Flags: dontcopy
[Code]
var
FileName: WideString;
StrValue: WideString;
StrLength: Integer;
function JSONQueryString(FileName, Section, Key, Default: WideString;
var Value: WideString; var ValueLength: Integer): Boolean;
external 'JSONQueryString#files:jsonconfig.dll stdcall';
function JSONWriteString(FileName, Section, Key,
Value: WideString): Boolean;
external 'JSONWriteString#files:jsonconfig.dll stdcall';
function editAppSettingsJson(Section_1: String; Section_2:String): Boolean;
begin
FileName := '{#AppSettingsJsonFile}';
SetLength(StrValue, 16);
StrLength := Length(StrValue);
Result := True;
{ Does not work. How can I edit it? }
if not JSONWriteString(FileName, 'children_1', 'children_1_1',
Section_1) then
begin
MsgBox('JSONWriteString Section_1:children_1:children_1_1 failed!',
mbError, MB_OK);
Result := False;
end;
{ Works fine. }
if not JSONWriteString(FileName, 'Section_2', 'children_2_1', Section_2)
then
begin
MsgBox('JSONWriteString Section_2:children_2_1 failed!', mbError,
MB_OK);
Result := False;
end;
end;
procedure InitializeWizard;
var
value_1: String;
value_2: String;
begin
value_1:= 'value_2';
value_2:= 'value_3';
editAppSettingsJson(value_1, value_2);
end;
In advance thank you very much for your support.
Regards, Diego Via
I do not think that JSONConfig.dll supports nested structures.
You can use JsonParser library instead. It can parse nested structures. Though it's not as easy to use as JSONConfig.dll – well, because it's more versatile.
The following code will do:
var
JsonLines: TStringList;
JsonParser: TJsonParser;
JsonRoot, Section1Object, Children1Object: TJsonObject;
Child11Value: TJsonValue;
begin
JsonLines := TStringList.Create;
JsonLines.LoadFromFile(FileName);
if ParseJsonAndLogErrors(JsonParser, JsonLines.Text) then
begin
JsonRoot := GetJsonRoot(JsonParser.Output);
if FindJsonObject(JsonParser.Output, JsonRoot, 'Section_1', Section1Object) and
FindJsonObject(JsonParser.Output, Section1Object, 'children_1', Children1Object) and
FindJsonValue(JsonParser.Output, Children1Object, 'children_1_1', Child11Value) and
(Child11Value.Kind = JVKString) then
begin
Log(Format('children_1_1 previous value %s', [
JsonParser.Output.Strings[Child11Value.Index]]));
JsonParser.Output.Strings[Child11Value.Index] := 'new value';
JsonLines.Clear;
PrintJsonParserOutput(JsonParser.Output, JsonLines);
JsonLines.SaveToFile(FileName);
end;
end;
end;
The code uses functions from my answer to How to parse a JSON string in Inno Setup?
So I'm currently working on a school project with a bunch of binary, 2s complement and other base conversions. Part of what i'm working on is a binary flipper; 1 becomes 0 and 0 becomes 1. I've written it as a function taking in and returning a string.
function flipbit(inp:string) : string;
var
new : string;
x:integer;
begin
writeln('new: ',new);
writeln('inp: ',inp);
new := '';
writeln('new assigned');
for x:= 1 to length(inp) do;
begin
writeln('loop started');
if strtoint(inp[x]) = 1 then
begin
new := new + '0';
writeln('0 added');
end;
if strtoint(inp[x]) = 0 then
begin
new := new + '1';
writeln('1 added');
end
else
begin
writeln('Something went wrong');
end;
end;
result := new;
end;
Basically the code iterates through a string of 1s and 0s, and re-writes them in another string but flips the bits.
I've used strtoint of inp[x] (inp is the input variable) and compared it to 1 (As an integer not a string)
I originally just tried comparing inp[x] without integer conversion to '1'. The code compiled, but 'new' did not change at all.
When I tried the method I'm using right now, the code crashes.
I've used debug writeln statements to try and narrow down where exactly the code is going wrong and it seems to be the if statement.
If anyone could help, that would be much appreciated. Thank you.
Why don't you receive as an array of integers and operate a not operation in each position? It has to be a string?
You don't need to convert chars to integers. You don't need to declare a local variable since you already have the 'result' variable.
function flipbit(inp: string): string;
var
i: integer;
begin
result := inp;
for i := 1 to Length(result) do
if result[i] = '0' then
result[i] := '1'
else
if result[i] = '1' then
result[i] := '0';
end;
Another version:
function flipbit(inp: string): string;
const
OTHER_VALUE: array['0'..'1'] of char = ('1', '0');
var
i: integer;
begin
result := inp;
for i := 1 to Length(result) do
if result[i] in ['0', '1'] then
result[i] := OTHER_VALUE[result[i]];
end;
I have a "parent" class which has a generic function to load a JSON string into the instance's properties called loadVals. I have two children with their own properties, and one of these props is a record.
The function sets successfully all the props of the main instance, but fails on setting the values on the record's props, no errors, I can see it loops successfully through the record's props but doesn't set the values.
wrote a small test console app, where you can see the behavior.
uses
System.SysUtils, System.TypInfo, RTTI, Data.DBXJSON;
type
TFieldValLoader = reference to procedure (const new_val: TValue);
tRec1 = record
x: integer;
y: String;
end;
tRec2 = record
a: integer;
b: String;
c: integer;
end;
TMyParent = class(TObject)
procedure loadVals(json_obj: TJSONObject);
end;
TMyChild1 = class(TMyParent)
h: integer;
my_rec: tRec1;
end;
TMyChild2 = class(TMyParent)
j: string;
my_rec: tRec2;
end;
{ TMyParent }
procedure TMyParent.loadVals(json_obj: TJSONObject);
procedure loadObj(Obj : TObject; my_json_obj: TJSONObject); forward;
procedure loadRecord(Obj : TValue; my_json_obj: TJSONObject);forward;
Procedure loadField( my_json_val: TJSONPair; _val: TValue; _loader: TFieldValLoader );
Begin
case _val.TypeInfo.Kind of
tkInteger:
_loader( TValue.From<integer>(StrToInt(my_json_val.JsonValue.Value)));
tkWChar, tkUString, tkVariant:
_loader( TValue.From(my_json_val.JsonValue.Value));
tkRecord:
loadRecord(_val, my_json_val.JsonValue as TJSONObject);
end;
End;
procedure loadRecord(obj : TValue; my_json_obj: TJSONObject);
var
i: Integer;
json_pair: TJSONPair;
ctx: TRttiContext;
obj_type: TRttiType;
my_field: TRttiField;
begin
ctx := TRttiContext.Create;
obj_type := ctx.GetType(obj.TypeInfo);
for I := 0 to my_json_obj.Size - 1 do
Begin
json_pair := my_json_obj.get(i);
my_field := obj_type.GetField(json_pair.JsonString.value);
WriteLn(' - '+ my_field.Name);
loadField(json_pair, my_field.GetValue(obj.GetReferenceToRawData),
procedure( const new_val: TValue )
Begin
// This does not work. (no feedback)!!!!
my_field.SetValue(obj.GetReferenceToRawData, new_val);
End
);
End;
End;
procedure loadObj(Obj : TObject; my_json_obj: TJSONObject);
var
i: Integer;
json_pair: TJSONPair;
ctx: TRttiContext;
obj_type: TRttiType;
my_field: TRttiField;
begin
ctx := TRttiContext.Create;
obj_type := ctx.GetType(obj.ClassInfo);
for I := 0 to my_json_obj.Size - 1 do
Begin
json_pair := my_json_obj.get(i);
my_field := obj_type.GetField(json_pair.JsonString.value);
WriteLn('* '+ my_field.Name);
loadField(json_pair, my_field.GetValue(obj),
procedure( const new_val: TValue )
Begin
// This does work
my_field.SetValue(obj, new_val);
End
);
End;
End;
begin
WriteLn('Loading '+ self.ClassName);
loadObj(self, json_obj);
end;
{ main Test Procedure }
var
my_child1: TMyChild1;
my_child2: TMyChild2;
begin
try
my_child1:= TMyChild1.Create;
my_child2:= TMyChild2.Create;
try
// load the json objs
my_child1.loadVals(TJSONObject.ParseJSONValue('{"h": 2, "my_rec": {"x": 4, "y": "test"}}') as TJSONObject);
my_child2.loadVals(TJSONObject.ParseJSONValue('{"j": "some", "my_rec": {"a": 8, "b": "any", "c": 9}}') as TJSONObject);
// print the loaded values
WriteLn('child 1 vals are: h: '+ intToStr(my_child1.h) +' my_rec.y= "'+ my_child1.my_rec.y +'" should equal to "test"');
WriteLn('child 2 vals are: j: '+ my_child2.j +' my_rec.b= "'+ my_child2.my_rec.b +'" should equal to "any"');
finally
my_child1.Free;
my_child2.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
// don't close the window, wait for [Enter]
Readln;
end.
I know records are different than classes, and I cannot find a way of getting this function to work; I really appreciate any help. Thanks
Your problem is that a record is a value type.
This line
loadField(json_pair, my_field.GetValue(obj),
gets the value of the record field. Keep in mind, its a value type so we get a copy of it.
Now you are setting the properties/fields on that copy which works. But then you never assign that back to the fields of your object.
So what you are doing here is basically like this:
my_child1:= TMyChild1.Create;
my_rec1 := my_child1.my_rec;
my_rec1.x := 4;
my_rec1.y := 'test';
So you can see that my_child1.my_rec never gets the values set to my_rec1.
You need to fix loadField as follows:
procedure loadField( my_json_val: TJSONPair; _val: TValue; _loader: TFieldValLoader );
begin
case _val.TypeInfo.Kind of
tkInteger:
_loader( TValue.From<integer>(StrToInt(my_json_val.JsonValue.Value)));
tkWChar, tkUString, tkVariant:
_loader( TValue.From(my_json_val.JsonValue.Value));
tkRecord:
begin
loadRecord(_val, my_json_val.JsonValue as TJSONObject);
_loader( _val); // <- set the record back to the field
end;
end;
end;
Can you advice me how to get "Log On As" parameter of specific windows service?
I need to re-register service in our upgrade project and it needs to be run under the same account as it was set up originally.
I've found QueryServiceConfig in advapi32.dll with lpServiceStartName in returned structure but I am not able to make it work from Inno Setup.
You cannot use QueryServiceConfig function from InnoSetup script. To use this function, you would have to allocate buffer from heap and that's impossible in InnoSetup. Instead you can use WMI, or to be more specific, the Win32_Service WMI class, which contains the StartName property, that you've asked for. In InnoSetup script it might look like this:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
[Code]
function GetServiceStartName(const AServiceName: string): string;
var
WbemLocator: Variant;
WbemServices: Variant;
WbemObject: Variant;
WbemObjectSet: Variant;
begin;
Result := '';
WbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
WbemServices := WbemLocator.ConnectServer('localhost', 'root\CIMV2');
WbemObjectSet := WbemServices.ExecQuery('SELECT * FROM Win32_Service ' +
'WHERE Name = "' + AServiceName + '"');
if not VarIsNull(WbemObjectSet) and (WbemObjectSet.Count > 0) then
begin
WbemObject := WbemObjectSet.Item('Win32_Service.Name="' +
AServiceName + '"');
if not VarIsNull(WbemObject) then
Result := WbemObject.StartName;
end;
end;
procedure SvcStartNameTestButtonClick(Sender: TObject);
begin
MsgBox(GetServiceStartName('Netlogon'), mbInformation, MB_OK);
end;
procedure InitializeWizard;
var
SvcStartNameTestButton: TNewButton;
begin
SvcStartNameTestButton := TNewButton.Create(WizardForm);
SvcStartNameTestButton.Parent := WizardForm;
SvcStartNameTestButton.Left := 8;
SvcStartNameTestButton.Top := WizardForm.ClientHeight -
SvcStartNameTestButton.Height - 8;
SvcStartNameTestButton.Width := 175;
SvcStartNameTestButton.Caption := 'Get service start name...';
SvcStartNameTestButton.OnClick := #SvcStartNameTestButtonClick;
end;
Quite easier (and probably faster) would be to make an external library and call it from the script. If you have Delphi or Lazarus, you can use the following function, which uses the QueryServiceConfig function to get the lpServiceStartName member, that you asked for:
function GetServiceStartName(const AServiceName: string): string;
var
BufferSize: DWORD;
BytesNeeded: DWORD;
ServiceHandle: SC_HANDLE;
ServiceManager: SC_HANDLE;
ServiceConfig: PQueryServiceConfig;
begin
Result := '';
ServiceManager := OpenSCManager(nil, nil, SC_MANAGER_CONNECT);
if ServiceManager <> 0 then
try
ServiceHandle := OpenService(ServiceManager, PChar(AServiceName),
SERVICE_QUERY_CONFIG);
if ServiceHandle <> 0 then
try
if not QueryServiceConfig(ServiceHandle, nil, 0, BufferSize) and
(GetLastError = ERROR_INSUFFICIENT_BUFFER) then
begin
ServiceConfig := AllocMem(BufferSize);
try
if QueryServiceConfig(ServiceHandle, ServiceConfig, BufferSize,
BytesNeeded)
then
Result := ServiceConfig^.lpServiceStartName;
finally
FreeMem(ServiceConfig);
end;
end;
finally
CloseServiceHandle(ServiceHandle);
end;
finally
CloseServiceHandle(ServiceManager);
end;
end;
I didn't liked the idea of linking external library so I finally solved the problem this way:
function GetServiceLogonAs():string;
var
res : Integer;
TmpFileName, FileContent: String;
begin
TmpFileName := ExpandConstant('{tmp}') + '\Service_Info.txt';
Exec('cmd.exe', '/C sc qc "MyServiceName" > "' + TmpFileName + '"', '', SW_HIDE, ewWaitUntilTerminated, res);
if LoadStringFromFile(TmpFileName, FileContent) then
begin
Result := Trim(Copy(FileContent,Pos('SERVICE_START_NAME', FileContent)+20,Length(FileContent)-(Pos('SERVICE_START_NAME', FileContent)+21)));
DeleteFile(TmpFileName);
end
else
begin
ShowErrorMsg('Error calling: GetServiceLogonAs(" + MYSERVICE + ")', res);
Result := '';
end;
end;